SpaceMagic is a full-stack javascript web development framework which allows you to build fast, real-time web applications. It allows you to write only one app and share it between the client and the server.
SpaceMagic is not yet production-ready. We are working very hard to get it there, but the API is unstable and subject to change without notice.
Sorry, IE users. Support for your browser is coming very soon.
npm install -g SpaceMagic
spacemagic init myProject
cd myProject
npm install
You need to have mongodb installed, if you don't, go here MongoDB QuickStart Guide
If you're already running MongoDB on localhost on the default port, you're done. But if you need to configure your database connection info goto the file:
vim config/database.js
SpaceMagic is an MV* framework. Its main components are:
Returns name of view.
Defines new view class with name name
and optional constructor constructor
.
Loads a template into the view. template
is a string containing the URL of the template.
When view is created, callback
is called with first parameter element where element is the jQuery result of selector
.
actionSelector
is a string containing an event followed by a selector. (e.g.
"click .close" or "keypress .nameField") callback
is called when the action
is performed on the selector.
The first argument for callback is the event object, the second parameter is the element.
Changes the browser title to title
when view is loaded.
Specifies a view class which will be loaded as a sub view with selector
indicating the template.
Model that was passed into view.
LiveView instance generated from template.
Parent view of the current view.
SingleView has all the methods of view but, in addition, binds all key in model to the view.
When key
is loaded or changes on the model, calls mapper
with the key and
the value associated with the key, and a set
function as a parameter.
.map("votes", function(key, value, set) {
set("plusTenVotes", value + 10)
})
Runs fn
before a new item is added to the list.
fn
's arguments are: model
, view
, and done
. Once done
is called, view
is appended to the DOM.
Runs fn
before a new item is removed from the list.
fn
's arguments are: model
, view
, and done
. Once done
is called, view
is removed from the DOM.
view
is the class to use for each view in the list.
Runs fn
before form input is saved.
Runs fn
after form input is saved.
If called, this view will automatically save the model after any changes.
Displays an errorMessage when model invalidates the field field
.
If called, this view will automatically save the model after timeout
milliseconds without new changes.
Constructs a new live view from a template. The template
may be a template
url, css selector, or html. data
refers to the data with which the live view
is initialized. scope
is an array of strings that describe the scope of the
live view variables. If omitted, LiveView will assume the default scope, "main."
Changes the template used by the live view to the one specified. This can be used to hotswap templates during development.
Returns elements that match selector. Finds elements in current view as well as elements in any polymorphic subviews.
Sets the value or values of an element name
to value
. If a single object is
passed, set each key in JSONObject
to its value.
<div class="post"></div>
LiveView.set("post", "An angry comment about your taste in movies.")
If live view was constructed with scopes, you can do the following:
<div class = "title"></div>
<div class = "postTitle"></div>
var view = new LiveView("template.html", {})
view.set("title", "Another post.")
This will set both .title and .postTitle, this can be used to disambiguate classes in subViews.
Removes the live view, as well as all associated events, from the document.
Removes the live view from the document while leaving events intact.
Attaches live view to a particular container.
Appends a special loading element to the end of the collection if any such element exists.
Removes the special loading element from the collection.
Returns the view at index id
in the collection.
Returns the number of LiveViews in the collection.
Removes a live view from the collection. Appends special empty view (if one is defined) if last view is removed.
Removes all live views from the collection, appends special empty view if one exists.
Inserts a new view at the index
or, if no index
is specified, the end of
the live view collection. Returns a new live view when completed.
Inserts a view or each of an array of views into the live view collection.
Model Definition Methods are the methods that you use to define the schema and other properties of your documents. These methods allow you to describe keys on your documents, as well as validations and associations.
To add properties to your document, use the key
method. The name
is a
String
which describes a property that will exist in instances of this
Document
.
module.exports = Document.define("Post")
.key("title")
.key("body", {required: false})
The following are the optional properties of a key
required
: Determines whether a user must set a value for this key in
order for the document to save.
default
: The default value of this key if none is set.
You can also specify validations for a key. To see a list of validations, and how to make your own custom validations, see the validations page.
The timestamps
method will give your document createdAt
and updatedAt
timestamps. The timestamps will be created and updated automatically.
The format of a timestamp is a unix timestamp, specifically the result
of calling (new Date).getTime()
To access the timestamps you can call get("createdAt") or get("updatedAt")
module.exports = Document.define("Post")
//define the rest of your keys
.timestamps()
The getKey
method defines a dynamic key whose value is determined by the
return value of valueFn.
A change event "change:name" will be called when any of the fields listed in watchFields changes.
User = Document.define("User")
.getKey("fullName", ["first", "last"], function () {
return this.get("first") + " " + this.get("last")
})
The setKey method allows you to define behavior when a key is set.
The following example sets the first and last name property when
a user calls the .set
method.
module.exports = Document.define("Post")
//define the rest of your keys
.setKey("name", function(name) {
name = name.split(" ")
this.set("first", name[0])
this.set("last", name[1])
})
These methods register a handler to be called before or after some action is performed on a document.
In each of these methods, fn
takes two arguments:
instance
- The instance of the document
done
- A function to be called when your handler is finished. If anything is
passed to done in a before filter, the document will not save and whatever was
passed to done will be emitted as the first argument to the "error"
event.
module.exports = Document.define("Post")
.beforeSave(function(post, done) {
post.isUnique("title", function(isUnique) {
if(!isUnique) {
done(new Error("title isn't unique"))
} else {
done()
}
})
})
This is called before updating or creating a document. It is called on the client and the server.
This is called before updating or creating a document. It is called on the server.
This is called before updating or creating a document. It is called on the client.
This is called before creating a document on the both client and the server.
This is called before creating a document on the the server.
This is called before creating a document on the the client.
This is called after creating or updating a document on the both client and the server.
This is called after creating or updating a document on the server.
This is called after removing a document on the client or server.
This is called after removing a document on the server.
Associations provide a way to model relationships between your documents.
For example, blogPosts
might have many comments
and a blogPost
might
belongTo an author
or an author
might have one profile
.
NOTE: A quick note about cyclic dependencies. If two documents have each other as associations, i.e. one belongs to another, then we need to do the following:
//comment.js
var Comment = Document.define("Comment")
, Post = require("./post")
Comment
.belongsTo(Post)
var Post = Document.define("Post")
, Comment = require("./comment")
Post
.many(Comment)
A many
association describes a one-to-many association between
documents.
The associatedModel
argument is the Document which we are associating.
var Comment = require("./comment")
module.exports = Document.define("Post")
.many(Comment)
Opts accepts the following options:
dependent
- Specifies whether the associated document should be
deleted when this document is deleted, defaults to false
foreignKey
- The foreignKey on the associated document. Defaults
to documentNameId
(e.g. a comment with a post will have the key, postId)
as
- The name of the association, if you want to refer to a post's
comments as messages
, you would pass { as: "message" }
.
conditions
- An list of conditions the associated documents will be
queried with.
A one
association describes a one-to-one association between two
documents.
associatedModel is the Document which we are associating.
var Profile = require("./profile")
module.exports = Document.define("User")
.one(Profile)
Opts accepts the following options:
dependent
- Specifies whether the associated document should be
deleted when this document is deleted, defaults to false
foreignKey
- The foreignKey on the associated document. Defaults
to documentNameId
(e.g. an author with a profile will have the key, authorId)
as
- The name of the association, if you want to refer to a user's
profile as bio
, you would pass { as: "bio" }
.
A belongsTo
association describes a one-to-one or one-to-many association
where the document belongs to (has the foriegn key of) another document. For
example, comments
belong to a blogPost
, a profile
belongs to a user
.
It specifies the reverse association of one
and many
.
var Post = require("./post")
module.exports = Document.define("Comment")
.belongsTo(Post)
The find
method takes in a query and returns a Collection
.
A query is simply a mongodb query object.
See this page for more on queries.
Here is an example that will find all posts with 100 or more votes.
//post.js
module.exports = Document.define("Post")
.key("votes")
var post = Post.find({votes: { $gt: 100 })
findOne
takes in an _id
, and finds a document with that id. The document is
returned immediatly, however none of it's properties are actually set. However
you can, and should, pass it to the view immediatly. The document will fire
events as it's state changes (i.e. when it loads, or
If you do for some reason need to wait for the post to load first, you can
pass in a callback as the second argument to findOne, or pass a callback to the
.load
method on the returned instance.
//post.js
module.exports = Document.define("Post")
//myCode.js
var Post = require("./post")
var post = Post.findOne("4f7580a64e822029b7000001")
Finds the document in which key
equals value
, there should
only be one document where key
equals value
. i.e. value
should be unique.
//user.js
module.exports = Document.define("User")
.key("email")
var user = User.findByKey("email", "x.coder.zach[At]gmail.com")
This example finds a user by email address.
This is simply a utility method for the following:
module.exports = Document.define("User")
.key("name")
.key("email")
var user = User.create({ name: "Zach", email: "x.coder.zach[At]gmail.com" })
//this is equivelent to
var user = new User({ name: "Zach", email: "x.coder.zach[At]gmail.com" })
user.save()
Constructs a new document instance.
Options:
validate - defaults to true
. Specifies whether to value the contents of the JSONDocument.
noId - defaults to false
. If noId
is true
, constructor does not generate an id.
Sets the value of key
to value
. If the first parameter is an object, sets the value of each key
in the document to its value
.
Gets the value of the key
.
Returns associated document or collection. callback
is called once association is loaded.
Returns a list of the keys this document has.
If the document does not exist, creates the document. If the document already exists, saves the document.
Removes the document from the DB. Callback is called once document has been removed successfully.
Returns a list of the many associations this document has.
module.exports = Document.define("Post")
.many("comments")
var post = new Post()
post.manyAssocs() // ["comments"]
This is the names of one assocs, as well as belongsTo assocs, since they both point to one document.
module.exports = Document.define("Post")
.one("thing")
.belongsTo("author")
var post = new Post()
post.oneAssocs() // ["thing", "author"]
Returns a list of the names of all associations for this document.
module.exports = Document.define("Post")
.many("comments")
.belongsTo("author")
var post = new Post()
post.assocs() // ["comments", "author"]
Forces the document to validate. This method is called automatically
everytime a value is changed with .set(). This will emit valid
or
invalid
events.
var post = new Post()
post.on("invalid", function() {
})
post.validate(function(post, invalidFields) {
})
Validates the contents of a specified field
. callback
is called when finished. If the field is invalid, the second parameter to callback
will be a list of failed validations.
Options: * silent - If true, doesn't emit an event.
Calls callback with true
as the first argument if the field is
unique, otherwise it calls it with false
Post.isUnique("email", "x.coder.zach{At}gmail.com", function(isUnique) {
isUnique //true if is unique, false otherwise.
})
You can Define your own instance methods like so:
//Every type of document will get this method
Document.prototype.myMethod = function() {
}
//Just Posts will get this method
Post.prototype.myMethod = function() {
}
There are a lot of events a document can emit as it's state changes. Here's a list of all of them in one place, so you don't have to hunt around.
The first argument for all event callbacks is the instance itself, all event handlers are bound to the instance.
Emitted when the document starts saving.
Emitted when the document finishes saving.
Emitted once the document has loaded.
Emitted when a field on the document changes. The second parameter is a list of fields that changed.
Emitted when field, field
, on the document changes.
The second parameter is the new value of the field,
the third parameter is the old value.
Emitted when the document is deleted.
Emitted when the document in the database that this document refers to doesn't actually exist.
Emitted when a document becomes invalid, this happens as soon as set is called with invalid data, in order to do responsive real-time error reporting.
Emitted when a field, field
in the document becomes invalid, this happens
as soon as set is called with invalid data, in order to do responsive
real-time error reporting.
Emitted when new data is validated and is in fact valid.
Emitted when a particular field is validated and is in fact valid.
Emitted when there is an error with the document, right now this only happens when an error is passed into the done method of after save.
A collection is a list of documents, usually returned by the find() method.
This example returns a collection of all tasks in the database.
var tasks = Task.find()
Returns the Document at index index
.
var firstTask = tasks.at(0)
This isn't a method, but collections have a length property, the length of the collection
This returns the document with _id equal to id.
Sorts collection by field
. If reverse is true, then it's in reverse order.
Reverse order is descending. Normal order is ascending.
Filters the collection by fn, if fn returns true, the document is added to the collection.
collection.filter(function(document) {
return document.get("votes") > 100
})
Called once the collection is loaded, unless it already is, then it's called immediately.
base
is a string the URL over which the controller acts. (ex. "/posts")
scope
is a function in which routes are defined.
controller = new LiveController("/posts", function(app) {
app.get("/show/:id", function() {
//do stuff
})
})
Navigates to the specified url
.
Options:
method - accepts strings push
and reload
. push
is used by default.
push - (default) use history.pushState to navigate without refreshing the page.
* refresh - uses document.location to navigate by refreshing page, included for older browser support.
route
is the route for a particular view. callback
is the function in which
the view is created.
LiveController.get("/", function() {
//do stuff
})
fn
is run before the controller gets a route.
fn
is run after the controller gets a route.
Returns a new script pipeline.
Returns a new image pipeline.
Returns a new stylesheet pipeline.
Returns a new html pipeline.
root
is the root directory from which the assets will be served. Used to translate URLs to file names.
extension
refers to the file extension of the assets which will be served.
Pipe
.fileExtension("coffee")
This will serve CoffeeScript files.
extension
refers to the file extension on the requested URL.
Pipe
.fileExtension("coffee")
.urlExtension("js")
.process(coffee)
This will serve CoffeeScript files compiled as JavaScript.
file
is a string. If file
contains a filename, that filename will be served. If it contains a directory name, all files within that directory, and all subdirectories, will be served.
processor
is a function that processes assets accessed through the pipeline in some way. (e.g. processing CoffeeScript to JavaScript or less into css.)
There are several built-in processors:
This processor compiles CoffeeScript into JavaScript.
Pipe
.process(require("AssetPipeline/lib/processors/coffeescript"))
This processor parses LESS into CSS. By default, the LESS processor includes bootstrap in its paths, so you can @import any default bootstrap stylesheets.
Pipe
.process(require("AssetPipeline/lib/processors/less"))
This processor parses Stylus into CSS. It includes stylus-blueprint and nib by default.
Pipe
.process(require("AssetPipeline/lib/processors/stylus"))
This processor wraps javascript files in commonjs modules.
module = require("AssetPipeline/lib/processors/modules")
Pipe
.process(module(baseURL, aliases))
baseURL
refers to the root URL from which modules will be requirable.
aliases
refers to a map of module name aliases.
This processor strips arguments out of function calls where the function name begins with server
.
Pipe
.process(require("AssetPipeline/lib/processors/strip_code"))