Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@moschel
Forked from justinbmeyer/2.0.markdown
Last active January 1, 2016 17:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moschel/8179331 to your computer and use it in GitHub Desktop.
Save moschel/8179331 to your computer and use it in GitHub Desktop.
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

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:

<iframe width="100%" height="300" src="http://jsfiddle.net/qYdwR/1019/embedded/" allowfullscreen="allowfullscreen" frameborder="0">demo</iframe>

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.

Ease of development

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)
      })
    }
  }
})

Two Way Binding

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>"
})  

In template declarative event handlers

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.

Performance improvements

As Mustache is now included in core by default, we've been working on improving rendering performance.

[PERFORMANCE INFO HERE]

Other things to checkout

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment