title | tags | author | lead | layout |
---|---|---|---|---|
CanJS 2.0 Release Notes |
open-source canjs |
justinbmeyer |
Today we're happy to announce the latest major release of CanJS. |
post |
CanJS 2.0 is out! It packs new features that advance CanJS' mission to make JavaScript application development even easier. This article will introduce the changes. It brings:
- Two way binding -
<input can-value="person.name"/>
- In-template declarative event handlers -
<button can-click="destroy">X</button>
- Custom elements -
<ui-grid>
- Mustache and observable performance improvements.
All of this is packaged in a new module that makes it easy to make modular, testable, state-driven widgets and even easier to use them: can.Component.
To get CanJS 2.0, go to the downloads page. Also, check out the release notes, migration guide, and changelog.
Lets see what can.Component can do!
can.Component adds two killer capabilities to UI widget development:
- The three major components of a widget (template, event handlers, data model) are packaged under a single module
- Non-developers (like designers) can add your components to the page without knowing JavaScript, but just simple HTML
For example, a tabs widget can be added to the page like:
<tabs>
<panel title="Fruits">oranges, apples</panel>
<panel title="Breads">pasta, cereal</panel>
<panel title="Sweets">ice cream, candy</panel>
</tabs>
And if they know some mustache, a dynamic tabs widget looks like:
<tabs>
{{#each foodTypes}}
<panel title='title'>{{content}}</panel>
{{/each}}
</tabs>
If you add too foodTypes
a new panel is added automagially:
can.Component is largely motivated by the WebComponents spec. The WebComponents spec is not supported in any browsers, but several of the ideas there motivated the design of can.Component, namely adding rich behavior to custom elements.
can.Component conveniently assembles the common features of modern widget construction:
- A template
- An observable scope
- Mustache helpers
- Event bindings
And it adds things like two-way live binding declarative binding in templates. Lets checkout the basics with some examples.
The following simply inserts <h1>Hello World</h1>
whenever it finds
a <hello-world></hello-world>
element:
can.Component.extend({
tag: "hello-world",
template: "<h1>Hello World</h1>"
})
<iframe>hello world</iframe>
A component's template is inserted directly within the custom element. For example, even if the source html looked like:
<hello-world> Keep Me!!! </hello-world>
The template would overwrite the source content within <hello-world>
, resulting in:
<hello-world><h1>Hello World</h1></hello-world>
But we can place that source content within the component's template with the <content>
tag like:
can.Component.extend({
tag: "hello-world",
template: "<h1><content/></h1>"
})
Now the html ends up as:
<hello-world><h1> Keep Me!!! </h1></hello-world>
Provide default content within the <content>
tag that gets used if there's
no source content. The following says "Hello World" if no source content is provided:
can.Component.extend({
tag: "hello-world",
template: "<h1><content>Hello World</content></h1>"
})
To make these templates powerful, they need some
observable data. The template is rendered with the
component's scope. The following makes
<todo-list>
elements show a list of todo elements:
can.Component.extend({
tag: "todo-list",
template:
"<ul>"+
"{{#each todos}}"+
"<li>{{name}}</li>"+
"{{/todos}}"+
"</ul>",
scope: {
todos: new can.List([
{name: "Take out trash", completed: true},
{name: "Mow Lawn", completed: false}
])
}
})
<iframe>list</iframe>
The scope defines an observable [can.Map] that is used to render the component's template. In the example above, the scope object is used to define a [can.Map] constructor function like:
TodoListMap = can.Map.extend({
todos: new can.List([
{name: "Take out trash", completed: true},
{name: "Mow Lawn", completed: false}
])
})
When a <todo-list>
element is found, a new TodoListMap
is
created and used to render the template. This means that todos
is actually a default value and can be overwritten. To overwrite default
values or add a property to the todo-list
's scope, add an attribute
to the element specifying the value.
For example, if a template is passed a list of critical todos like:
var template = can.view.mustache("<todo-list todos='criticalTodos'></todo-list>")
var criticalTodos = new can.List([
{name: "Learn can.Component", completed: false},
{name: "Tweet about this article", completed: false}
])
template({
criticalTodos: criticalTodos
})
Specify the todos property on todos-list
's scope like:
<todo-list todos='criticalTodos'></todo-list>
Or more commonly, criticalTodos
is part of the parent scope of another component:
var template = can.view.mustache(
"<todo-app>"+
"<todo-list todos='criticalTodos'></todo-list>"+
"</todo-app>");
can.Component.extend({
tag: "todo-app",
scope: {
criticalTodos: new can.List([
{name: "Learn can.Component", completed: false},
{name: "Tweet about this article", completed: false}
])
}
})
template()
A list of todos is rarely static. Instead, it might be
retrieved by can.Model.findAll
when <todo-app>
is inserted into the document:
can.Component.extend({
tag: "todo-app",
scope: {
criticalTodos: []
},
events: {
"inserted": function(){
var scope = this.scope;
Todo.findAll({critical: true},function(todos){
scope.attr("criticalTodos", todos)
})
}
}
})
Add can-value="key"
, where key
is a reference to an observable value,
to an input, select, or textarea to cross-bind the value of that input
to the observable value. The following
cross-binds the input's value to the person's name:
var person = new can.Map({name: "Sarah"})
var template = can.view.mustache(
'<h2>{{name}}</h2>'+
'<input can-value="name"/>'
)
$("#out").html( template(person) )
<iframe>name</iframe>
When the input changes, it will update person's name
property. If you update
person's name
property like:
person.attr('name',"Sarah Brzezinski");
The input's value will update.
We can use this in todo-list
's template to update the completed
property of each todo by adding a checkbox with can-value
set
to the completed property:
can.Component.extend({
tag: "todo-list",
template:
"<ul>"+
"{{#each todos}}"+
"<li class='{{#completed}}completed{{/completed}}'>"+
"<input type='checkbox' can-value='completed'/> "+
"{{name}}"+
"</li>"+
"{{/todos}}"+
"</ul>"
})
That's a mouthful, but this allows you to bind functions in Mustache's scope to DOM events like click, keypress, etc.
The following makes <hello-world>
toggle what's in the <h1>
everytime
someone clicks it.
can.Component.extend({
tag: "hello-world",
template:
"<h1 can-click='toggle'>"+
"{{#visible}}"+
"<content>Hello World</content>"+
"{{/visible}}"+
"</h1>",
scope: {
visible: true,
toggle: function(){
this.attr("visible", !this.attr("visible"))
}
}
})
We can use this to add a delete buttons to <todo-list>
that destroy the todo
on the server:
can.Component.extend({
tag: "todo-list",
template:
"<ul>"+
"{{#each todos}}"+
"<li class='{{#completed}}completed{{/completed}}'>"+
"<input type='checkbox' can-value='completed'/> "+
"{{name}}"+
"<button can-click='destroy'>X</button>"+
"</li>"+
"{{/todos}}"+
"</ul>"
})
This works because click event's are wired to each todo's [destroy] event.
As Mustache is now included in core by default, we've been working on improving rendering performance.
[PERFORMANCE INFO HERE]
- can.Component's docs for a bunch of examples.
- Our roadmap
- A can.Component TodoMVC example
A huge thanks to the Bitovi crew for helping build the new features, bug fixes, and getting this out the door.