Skip to content

Instantly share code, notes, and snippets.

@skiadas
Created June 10, 2013 16:59
Show Gist options
  • Save skiadas/5750398 to your computer and use it in GitHub Desktop.
Save skiadas/5750398 to your computer and use it in GitHub Desktop.
A somewhat detailed description on Cujo.js's implementation of the TodoMVC.js example.

These notes are based on the cujo.js TodoMVC.js example, which can be found here.

Required package specification using bower

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.

Initialization using curl.js

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 set baseUrl: '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 load poly/string and poly/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 the app/main.js file. This effectively starts the application.

Application Composition is distinct from Component Implementation

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 created 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.

Component Types

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 called args 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 a defer property that enables delaying the formation of this context until it is needed. See examples here. And a more detailed example of using the defer method here, where it is used to bring up user preferences.

More about these components here.

Connection Types

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 in properties, or pass it as args to your constructor in the create portion.
  • DOM Events: need the wire/on or wire/jquery/on plugins. Connects components methods to individual dom events. They are placed under a on 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 of app/main.js.
  • Javascript to Javascript: Using wire/connect, and placed inside a connect 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 of app/main.js has examples, as well as line 22.
  • Aspect Oriented Programming, using wire/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 like before, after, afterReturning, afterThrowing etc. Line 91 of app/main.js has an example of this. More examples here. You cans also use afterFulfilling or afterRejecting for promise-based aop connections. after is already promise-aware. Look into meld.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.

Linking to DOM elements

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 inserted 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 as components

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.

Configuring Components

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.

A complete run-through of main.js

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.

  1. 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 the css! plugin directive, that identifies the file as a css file. theme/base.css is the relative path to the file.
  2. 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.
  3. createView (lines 11-17) is the part of the dom where new todo elements are being formed. It is formed by rendering the template located in app/create/template.html, and some string replacement takes place based on app/create/strings.js. Notice that the template.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.
  4. createForm (lines 20-23) is the form that is inside the createView, which is what the element property specifies. the connect: { '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 standard reset dom method that html form elements have.
  5. listView (lines 27-45) is the view that shows the list of todos. In its render part we further specify a css file. Note that the HTML does not actually contain the list of items. the insert directive specifies that this template should be placed right after the createView as a sibling. The bind directive is related to data binding coming from cola.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 a to component, identifying the data component that is linked to, namely todos. A comparator 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. The text property of the data is linked to both the label and the .edit dom items, while the complete 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 function setCompletedClass.js. Might revisit this after seeing the todos model object.
  6. controlsView (lines 49-56) renders the 'controls' area that is below the list view. Standard render + insert. Notice that it comes with its own css for structural aspects of these controls (when they should be visible etc).
  7. footerView (lines 60-66) standard render + insert.
  8. todoStore (lines 72-80) sets up local storage for the todos. It relies on the cola/adapter/LocalStorage adapter, simply providing it the name under which it should be saved, and it binds itself to the todos model.
  9. todos (lines 82-95) is the model. It uses cola's cola/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 through cleanTodo and generateMetadata, both in the app/create folder. cleanTodo simply trims the 'text' and ensures that 'completed' is a boolean. generateMetadata adds 'id' and 'dateCreated'.
  10. todoController (lines 102-141) the main controller. Mostly redirects things around. First of all, the controller is created by the app/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' function updateRemainingCount that replaces itself based on what properties the browser has. Looking at the controller's properties, it has a reference to a 'todos' object, a createTodo that pipes the form's values into the todos's add function, and delegates removeTodo and updateTodo to the model. The 'querySelector' property is used by the controller in its beginEditTodo method. A couple more links to dom objects follow. The on property ties numerous form events to controller methods. For instance, submitting the form in the createView is tied to the createTodo method. Notice also how a lot of listView events are tied directly into methods of the todos 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 in wire/dom/transform. These are worth looking into more.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment