These notes are based on the cujo.js TodoMVC.js example, which can be found here.
Bower offers a simple way to handle the downloading and managing of required packages. It is controlled via the bower.json
file. That file needs to specify name and version for our app, and to list the dependencies and their versions.
You can use:
bower install
once the json file is in place, to download the required packages. These packages are placed into a folder named bower_components
. You still need to link these into your application, more on that described later.
curl is an AMD loader. It can also load regular js files with the "js!" plugin. We should make sure to build in the domReady module.
For deployment, we use cram to package javascript files together.
Basic usage:
curl(['dep1', 'dep2', 'dep3' /* etc */], callback);
Loads the dependencies, then calls the callback function.
curl(['domReady!', 'dep1', 'dep2', 'dep3' /* etc */])
.then(callback, errorback);
Promises based version, allows for an error callback too if something goes wrong with the loading. Use the 'domReady!' part if we need to wait for the DOM to be ready before executing the callback.
For an example of use in the project, see app/run.js. This uses another curl form, with a config object as its first and only argument. This form augments curl's configuration settings. You can see this page for details on the possible settings. Standard settings include:
baseUrl
: The base url relative to which all other paths are resolved. For instance it might make sense to setbaseUrl: 'js'
if all the javascript files are under a js folder.paths
: Ways to resolve certain paths. Can be useful for introducing a "codename" for the path with all vendor files, for example. You can find more about paths here.packages
: An array of package definitions with their locations. The TODOMVC example uses this heavily.preloads
: An array of modules to load before the others. Useful for things like jquery, or any other things the other modules might depend on. The TODOMVC example uses this to loadpoly/string
andpoly/array
, which enhance the available methods on string and array objects.
The TODOMVC examples uses two more settings, main
and locale
. In general main
is a package specific setting, which specifies the path to that package's main function. It is used heavily inside the packages
setting. Settings on curl will be inherited by packages, so this provides a main for packages that don't need to specify their own main, so to speak.
These configurations can be accessed from within an object using module.config()
.
Back to our project, the file app/run.js basically does the following:
- Tells curl to preload common string and array methods from the
poly
package. - Tells curl where to find the key cujo.js packages.
- Tells curl that it should locate the "main" app by using the
wire
package on theapp/main.js
file. This effectively starts the application.
How things are put together is determined by wire.js, and resides in app/main.js. Wire.js uses what is called "Inversion of Control" or also "Dependency Injection". In practice what this means is a very clear separation between the components and their use. In our example, app/main.js provides the necessary directives for which components are to be used and exactly how they will be used. The important aspect of this is that this "connecting" happens in a "non-invasive" way. This will hopefully become more clear as we dig deeper into the files. Here's a good read on "Inversion of Control".
app/main.js
is effectively just a json object, and it is being passed to wire in app/run.js
with the main: 'wire!app/main'
line. These json objects are in this context referred to as "wiring specs".
Each property in that wiring spec defines something. These can then be referred to from the contents of other properties. The values of these properties could be even a simple string, but most of the time they will be objects themselves, whose properties have special significance.
For example, one of the properties, on line 102, is todoController
. This section defines an object called todoController
, which will be create
d using the module located in app/controller.js
. This create
property could also have a different form which we will look at later. The remaining properties link that controller object to various other objects.
Other objects defined in the wiring spec have render
and insert
methods. These correspond to view components with direction on how they are to be rendered. More on those later as well.
Finally, at the end of the file is a plugins
array. This specifies some key modules that enhance wire.js's functionality. For instance the wire/dom
plugin enables linking to specific dom elements and actions, wire/dom/render
allows specifying how to render forms etc.
A lot of the components use a $ref
syntax. This syntax refers to another component, with possibly some resolving steps along the way. They have the general form $ref: 'resolver!identifier'
. For instance the todosController
has a todos: { $ref: 'todos' }
as part of its properties. This links the todos
property of that controller with the todos
object defined on line 82. For some more examples, the line $ref: 'dom!todoapp'
sets up a references to the DOM object with id todoapp
, which is a <section>
html tag defined in index.html
. The reference $ref: 'dom.first!form'
on the other hand refers to the first dom element that happens to be a form element.
At the end of the day, what wire does is to take all these specifications, and create a set of fully realized and inter-connected objects corresponding to these specifications. It actually places all these together under a parent "context" object, which can then be used to manage them and when the time comes tear them down. This can be very useful in separating out reusable pieces of the architecture.
The components go through a 6 phase lifecycle, create/configure/initialize/connect/ready/destroy. "Facets" allow us to embed parts and code at any one of those phases. For instance the properties
facet sets properties during the create phase, the connect
facet activates during the connect phase and allows linking this component to other components, the ready
facet invokes methods during the ready state and so on.
Components are identified by the presence of one of the following properties, called (factories):
module
for loading a standard AMD module.create
for loading via a constructor method. Another property calledargs
is expected for any arguments to be passed to the constructor.compose
allows for function composition. No example currently.literal
creates literal objects. Useful for configuration options/shared static objects. Can also be declared directly if it doesn't have a property named after one of the factories.wire
recursively creates a wire spec. It accepts adefer
property that enables delaying the formation of this context until it is needed. See examples here. And a more detailed example of using thedefer
method here, where it is used to bring up user preferences.
More about these components here.
There are 4 built-in connection types, but more can be created. See here for details. In brief, these types are:
Dependency Injection
: simply use a $ref to another property. Assign it either to a key specified inproperties
, or pass it asargs
to your constructor in thecreate
portion.DOM Events
: need thewire/on
orwire/jquery/on
plugins. Connects components methods to individual dom events. They are placed under aon
property. These can be placed either in the component that implements the method, or on the dom node that triggers the event. You can see examples of the former on line 117 ofapp/main.js
.Javascript to Javascript
: Usingwire/connect
, and placed inside aconnect
property. You can specify that when a method on one component is called, then a method in another component will be called with the same arguments. Again, connections can be made in either direction. Line 133 ofapp/main.js
has examples, as well as line 22.Aspect Oriented Programming
, usingwire/aop
. Similar to the previous one, but allows more flexible connections. In particular, you can specify when the method is to be called, relative to the one it is linked to. This way uses properties likebefore
,after
,afterReturning
,afterThrowing
etc. Line 91 ofapp/main.js
has an example of this. More examples here. You cans also useafterFulfilling
orafterRejecting
for promise-based aop connections.after
is already promise-aware. Look intomeld.js
for [even more on this exciting technique](https://github.com/cujojs/meld/blob/master/docs/reference.md https://github.com/cujojs/meld/blob/master/docs/reference.md).- Finally, we can use composition of functions via the pipeline syntax "|" as part of the connection. Examples of this are in lines 92, 107, 138, 139. This is usually referred to as "transforming the connection". Also see here for an illustrative example.
Dome elements can be referenced to via the 'dom!idr'
, 'dom.all!selector'
and 'dom.first!selector'
resolvers. For instance:
$ref: 'dom!todoapp'
gets hold of the element with id todoapp
. While a line like:
element: { $ref: 'dom.first!form', at: 'createView' }
in line 21 links to the first form element inside the element referred to in createView. Lines 111-115 have more examples.
DOM elements can be created via the element
, clone
or render
factories. They can then be insert
ed using one of the following options: last
, first
, after
, before
, at
. Details here.
For example, line 15 in app/main.js
introduces the createView as the first child of the root dom object, while line 33 adds the listView as a sibling following the createView.
The render
factory allows the rendering of arbitrary html templates. It can also be used to render single nodes by just specifying the tag. It accepts template
and css
properties, amongst others. You can use replace
to replace some strings if needed (internationalization).
These templates are logic-less. For data-driven views, cola.js
is provided as a data-binding library and would be used instead. And transformations can be handled in the wirespec via aop instead.
We can still use template engines like handlebars or mustache.
Functions can be used as components in a number of ways.
- The simplest is that a module actually returns a function. It will then be used with the
module
factory. - Another is a case where the module returns a function that is then meant to create other functions. Use the
create
factory in this case. - Functions can be composed using the
compose
factory. It expects and array of functions. The array can contain direct references to functions, or even create/module factories.
In addition to the above, there are 4 properties of components that allow customization at various points along the component's lifecycle. More details here.
properties
property. Already discussed previously. These properties can be essentially anything.init
property. One or more methods that will be invoked once the properties are set.ready
property. For methods to be called after connections have been established to the other components.destroy
property. For methods to be called when a component is being destroyed.
We will now go through app/main.js
and look at each component created there. But first off notice the folder structure under app
. There is one folder for all the components involved in creating a new todo, one folder for all the components involved in listing the available todos and so on. But how we structure these is at the end of the day entirely up to us.
Also notice how most of the other js files are actually quite small. This is part of the idea, to create tiny components that are good at just one simple thing, then bind them together.
theme
(line 5) is meant to be a reference to a css file meant for non-structural style components. Any other variable name could have been used. Notice thecss!
plugin directive, that identifies the file as a css file.theme/base.css
is the relative path to the file.root
(line 8) refers to the root dom element that the wire is meant to be based on. A lot of the other components will refer to it for their placement.createView
(lines 11-17) is the part of the dom where new todo elements are being formed. It is formed byrender
ing thetemplate
located inapp/create/template.html
, and some string replacement takes place based onapp/create/strings.js
. Notice that thetemplate.html
file has expressions like${todo.placeholder}
. These will exactly be replaced based on the strings file. It is not really implemented here, but this can be used for locale-specific bundles. Finally, wire is instructed to insert this new html element as the first child of the root dom element.createForm
(lines 20-23) is the form that is inside the createView, which is what theelement
property specifies. theconnect: { 'todos.onAdd': 'reset' }
line specifies that when the onAdd event of the todos object happens, the form should reset itself. We are referring here to the standardreset
dom method that html form elements have.listView
(lines 27-45) is the view that shows the list of todos. In itsrender
part we further specify a css file. Note that the HTML does not actually contain the list of items. theinsert
directive specifies that this template should be placed right after the createView as a sibling. Thebind
directive is related to data binding coming fromcola.js
. This component is currently in flux. They are working on a backbone binding. But the current cola is safe and stable, just not well documented. Anyway, moving along with it: Bind first off has ato
component, identifying the data component that is linked to, namelytodos
. Acomparator
offers a way to order the values, and can be set by either a function or a string property to be used. In this case, it is set to creation date, effectively sorting the values chronologically. Thetext
property of the data is linked to both thelabel
and the.edit
dom items, while thecomplete
boolean property is linked to the css class 'toggle' (actually not sure on that one), and in addition the classList attribute will alter based on the handler functionsetCompletedClass.js
. Might revisit this after seeing the todos model object.controlsView
(lines 49-56) renders the 'controls' area that is below the list view. Standardrender
+insert
. Notice that it comes with its own css for structural aspects of these controls (when they should be visible etc).footerView
(lines 60-66) standard render + insert.todoStore
(lines 72-80) sets up local storage for the todos. It relies on thecola/adapter/LocalStorage
adapter, simply providing it the name under which it should be saved, and it binds itself to thetodos
model.todos
(lines 82-95) is the model. It uses cola'scola/Collection
component to manage a collection of items. Notice that it is passed a custom 'validator' as part of the strategy options. Looking at that method, it makes sure that the 'todo' to be added exists, has a text property, and further trims the text and makes sure it's a nonempty string. The result seems to need to contain a 'valid' boolean property, and an optional 'errors' field if it was not valid. Finally,todos
hooks up some methods to be called before certain actions are taken. Before a new todo is added, it is filtered throughcleanTodo
andgenerateMetadata
, both in theapp/create
folder. cleanTodo simply trims the 'text' and ensures that 'completed' is a boolean. generateMetadata adds 'id' and 'dateCreated'.todoController
(lines 102-141) the main controller. Mostly redirects things around. First of all, the controller is created by theapp/controller
module. As you can see there, most methods return nothing. This is because their implementation are actually injected by the wiring spec. Notice however the 'self-optimizing' functionupdateRemainingCount
that replaces itself based on what properties the browser has. Looking at the controller's properties, it has a reference to a 'todos' object, acreateTodo
that pipes the form's values into thetodos
's add function, and delegatesremoveTodo
andupdateTodo
to the model. The 'querySelector' property is used by the controller in itsbeginEditTodo
method. A couple more links to dom objects follow. Theon
property ties numerous form events to controller methods. For instance, submitting the form in the createView is tied to thecreateTodo
method. Notice also how a lot of listView events are tied directly into methods of thetodos
object. All these events have the form: 'action:selector' where selector is a description of the dom element we want this event attached to. Finally,connect
is being used to tie some of the controller's methods to transforms inwire/dom/transform
. These are worth looking into more.