CanJS 2.3 Release

Justin Meyer by Justin Meyer

CanJS 2.3 Release

Justin Meyer CanJS 2.3 is out. Actually, it's been out for a while. But now it's stable, documented, and ready for you to upgrade to. Read on to find out what you CAN do with this release.

posted in Open Source on January 22, 2016 by Justin Meyer

 

Today, we're announcing the release of CanJS 2.3. Here's the 2.3.11 download, its npm page, and its changelog. This release includes over 800 commits, a bunch of bug fixes, performance improvements, and new features that improve the expressive power of the framework.

This article highlights 2.3's biggest new features:

  • {{#each}} diffing - Perform minimal DOM modifications if #each is passed different list instances.
  • Call Expressions - Call methods in stache templates like: {{ capitalize( pluralize('person',2) ) }}.
  • Binding Syntaxes - Control the direction of data bindings and listen to viewModel events in stache.
  • Key Operators - Pass functions in stache or use template variables.
  • Performance Improvements - Faster computes and live binding.

For information on 3.0 and beyond, checkout 3.0's Roadmap Discussion.

{{#each}} diffing

With the introduction of can.compute in CanJS 1.1 and the define plugin in CanJS 2.1, one of CanJS's best features has been its ability to derive new values from other values. This pattern has taken hold in the React community and for good reason - these types of apps feel like mathematical equations.

For example, in TodoMVC, the displayed todos are derived from the todos loaded from the server and the route's filter value:

displayList: function () {
    var filter = route.attr('filter');
    return this.attr('todos').filter(function (todo) {
        if (filter === 'completed') {
            return todo.attr('complete');
        }
        else if (filter === 'active') {
            return !todo.attr('complete');
        }
        else {
            return true;
        }
    });
}

The todos returned by displayList are converted into a list of <li> elements with {{#each}}:

{{#each displayList}}
   <li> .... </li>
{{/each}}

Prior to 2.3, when a new todo is created, displayList would be re-calculated and every <li> would be removed and re-created. With 2.3, {{#each}} performs a diff of the new list and old list, and adds only a single <li> for the new todo.

Checkout the difference in performance between 2.2 without the diff and 2.3 with the diff by adding a new todo to a list of 1000 todos:

2.2 Demo

JS Bin on jsbin.com

2.3 Demo

JS Bin on jsbin.com

With 2.3, you can now take full advantage of CanJS's ability to derive values from other values without worrying about performance. In future releases, with the aid of can-derive, we should be able to speed this up even more (from a linear diff to a logarithmic binary tree insert).

Call Expressions

2.3 adds call expressions. These look similar to normal JavaScript function calls. For example:

{{ pluralize(type, ages.length) }}

Call expressions have three advantageous over normal helper functions:

  • They are passed values instead of computes
  • They can be nested
  • They have more easily understood scope lookup rules

Call expressions pass values instead of computes

By default, stache and mustache helpers are passed computes that represent an observable argument. For example, the pluralize helper in the following fiddle needs to get the value from the type() and count() compute arguments:

JS Bin on jsbin.com

Passing computes was a legacy decision that has been confusing with hindsight. Fortunately, call expressions are here to simplify things. They pass the value of the observable argument. In the following example, notice how pluralize simply uses the type and count values:

JS Bin on jsbin.com

Nested call expressions

Call expressions can be nested as part of other call expressions or other helper expressions like:

{{ capitalize( pluralize(type, args.length) ) }}
{{#each statsForTeam( teamId.id ) }}

This makes it easier to compose behaviors. In the following example, statsForTeam is able to get a list of scores for a team. Using #each's diffing, the list of scores updates with minimal DOM changes if the source list of scores changes. Notice that the yellow elements stay in place as stats are added:

JS Bin on jsbin.com

Scope lookup rules

Call expression methods are found in in the Scope first and only if nothing is found, the HelperOptions scope is searched. This is in contrast to helper expression methods that find methods in the following order:

  1. search the current context
  2. search the helper scope
  3. search the scope

Notice how the pluralize helper expression calls the pluralize helper despite there being a pluralize method in the scope:

JS Bin on jsbin.com

Notice how the pluralize call expression calls the pluralize method in the scope.

JS Bin on jsbin.com

Call expressions make calling functions from stache more simple and straightforward.

Binding Syntaxes

CanJS 2.3 supports new binding syntaxes that:

  • enable fine grained binding behavior,
  • support dynamic bindings.

This section will go through the new binding syntaxes and how they accomplish these goals. But first, lets take a refresher on "bindings" and CanJS's older binding syntaxes.

2.2 Binding Syntaxes

CanJS 2.2 supports two main types of binding syntaxes:

  • Event bindings - used to listen to an event and call a method in the scope.
  • Data bindings - used to tie a scope value to a value on either the viewModel of a component or a property or attribute of a DOM element.

These bindings expressed themselves in three different syntaxes:

Type Example
Event
DOM
<my-comp can-click="scopeMethod"/>
Data - 2 way
scope to viewModel
<my-comp vm-prop="{scopeProp}"/>
Data - 2 way
scope to DOM
<input can-value="{scopeProp}"/>
 

The 2.2 syntaxes have multiple issues.

First, they are incompatible with dynamic bindings. Dynamic bindings are binding behaviors that change as attributes are added and removed to a component. For example:

<my-component vm-prop="{ {{keyName}} }"{{/value}}/>
<my-component {{#if value}}vm-prop="{key}"{{/value}}/>

It's possible that someone wants to use magic tags to dynamically control the bindings applied to a component. The 2.2 syntaxes made this difficult or outright impossible.

Second, while two-way bindings are useful for establishing cross communication between components, they can sometimes make debugging more difficult. For example, a parent component might want to pass a value to a child component, but not immediately get updates as the child changes its value. A common scenario might be an autocomplete that should only retrieve suggestions after a user has entered more than two characters. To make situations like these easier to code and reason about, our new syntaxes enable more fine-grained control of binding behavior.

New Syntaxes

In 2.3, bindings use the following syntax rules:

  • {left}="right" - update left with the value of right.
  • (left)="right" - listen to the left event and do right when it happens.
  • {(left)}="right" - two way bind left and right.
  • $ - act upon the element's events, attributes, or properties instead of the viewModel's.
  • ^ - reverse the direction of the binding.

The following lists all the combinations of those syntax rules:

Type Example
event
viewModel
<my-comp (vm-event)="scopeMethod()"/>

Listens to vmEvent on the component's viewModel.

event
DOM
<element ($click)="scopeMethod()"/>

Listens to click on the element.

1 way
scope to viewModel
<my-comp {vm-prop}="scopeProp"/>

Updates the viewModel's vmProp property with the value of scopeProp.

<my-comp {vm-prop}="callExpression()"/>

Updates the viewModel's vmProp property with the value of a callExpression.

1 way
viewModel to scope
<my-comp {^vm-prop}="scopeProp"/>

Updates scopeProp with the viewModel's vmProp property.

1 way
scope to attribute
<element {$el-prop}="scopeProp"/>

Updates the element's el-prop attribute or property with the value of scopeProp. This is equivalent to el-prop="{{scopeProp}}".

1 way
attribute to scope
<input {^$value}="name"/>

Updates name in the scope with the the element's value property.

2 way
scope to viewModel
<my-comp {(vm-prop)}="scopeProp"/>

Two way binds the viewModel’s vmProp with the value of scopeProp.

2 way
scope to attribute
<element {($el-prop)}="scopeProp"/>

Two way binds the element’s el-prop attribute or property with the value of scopeProp.

2 way
attribute to viewModel
<my-comp vm-prop="value"/>

Sets the viewModel's vmProp to the value of the attribute.

Fine Grained Control

Lets take a look at what these new bindings can do!

Example 1: DOM event bindings and two way bindings

The following example creates a <my-input> element that behaves similar to a native input element. It uses a DOM event binding to update its value property as the user types.

($keydown)='updateValue(%event)'

The example cross binds the <my-input>'s value and a native input element's value to a person's name property.

<my-input {(value)}="name"/>
<input {($value)}="name"/>

Notice how when one element's value changes, the other element's value will change.

JS Bin on jsbin.com

Example 2: ViewModel event bindings

One way bindings and viewModel event handlers can be used to setup one way data flows between components.

The following example only updates name when there is an even number of characters. It does this by first defining an updateNameOnEven method that takes a new name and only updates <person-edit>'s viewModel when name has an even number of characters:

updateNameOnEven: function(newName){
  if(newName.length % 2 === 0) {
    this.attr("name", newName);
  }
}

Next, it listens to when <my-input>'s value changes and calls updateNameOnEven, passing it the value of <my-input>:

 (value)="updateNameOnEven( %viewModel.value )"

Finally, it listens to the <input>'s oninput event and calls updateNameOnEven, passing it the value of the <input>:

 ($input)="updateNameOnEven( %element.value )"

Notice how the name only changes on every other character:

JS Bin on jsbin.com

Example 3: Custom ViewModel event bindings

The viewModel events you can listen to are not limited to the named events produced whenever a property changes. In the following example, the <person-edit> module produces "saved" events when the save button is clicked using can.trigger:

can.trigger(self,"saved");

<person-page> listens to these events and calls its addMessage method with:

(saved)="addMessage('Saved')"

Notice that when save is clicked, "Saved" messages will temporarily appear:

JS Bin on jsbin.com

Key Operators

Stache adds the following three new key operators:

  • @key - at operator - Return whatever value is at key. Pass the function or compute at key instead of its return value.
  • ~key - compute operator - Pass a compute instead of the value at key.
  • *key - template variable operator - Reference a local template variable.

These key operators can be used everywhere keys are used:

  • helper expressions - The arguments passed to stache helpers like {{myHelper key1 key2}}.
  • call expressions - The arguments passed to call expressions like {{myHelper( key1, key2 )}}.
  • data bindings - The new binding syntaxes like {vm-prop}="key".
  • event bindings - The arguments passed to an (event) binding like ($click)="method( key )".

At operator (@key)

The At operator is used to pass a function or compute as an argument instead of the function or compute's return value. This can be a great way of isolating concerns between components.

For example, the following passes <person-page>'s save method to <person-edit> with:

{onsave}="@save"

This allows <person-edit> to be able to call onsave without having to define the save behavior itself. Notice how the <person-edit> disables the form elements when save is clicked.

JS Bin on jsbin.com

When the At operator is used in helper expressions, a compute can still be passed. Notice how method is a compute that returns the func function:

JS Bin on jsbin.com

The @ symbol can be used multiple times within a key reference. For example, if models was a collection of can.Models like:

var models = {
  Task: can.Model.extend({resource: "/tasks"},{}),
  ...
}

To pass the Task.findAll method, you might do:

<my-grid {get-list}="models@Task@findAll"/>

The first @ symbol makes sure that the Task function is not called and the second @ makes sure that findAll is not called.

Compute operator (~key)

In all key uses other than helper expressions (call expressions, data and event bindings), values are passed instead of computes. Generally speaking, values are what you want. However, sometimes it's nice to get a compute that lets you read, update, and listen to changes in that value. The Compute operator lets you pass a compute for that value instead of the value itself.

The most important place to be aware of this functionality is calling {{#each}} helper with a call expression. The {{#each}} helper needs a compute to perform diffing. If you call {{#each}} as helper expression like:

{{# each statsForTeam( teamId.id ) }}

... the results of statsForTeam will be passed as a compute. However, if you call {{#each}} as a call expression like:

{{# each( statsForTeam( teamId.id ) ) }}

... the results of statsForTeam will be passed as list. The solution for this is to use the compute operator as follows:

{{# each( ~statsForTeam( teamId.id ) ) }}

In the following example each is in the scope, so the alias eachOf is used instead:

JS Bin on jsbin.com

Template variable operator

In 2.3, you can create variables local to a template. Templates have a special context that can be used to store observable values. These observable values are useful for connecting values across components without the need to create a value in the parent component.

Template variables are indicated with *. The following example uses an *editing variable to connect the <driver-select> and <edit-plate> components. <driver-select> exports the selected driver to *editing with:

<driver-select {^selected}="*editing"/>

<edit-plate> two way binds to the *editing.licensePlate with:

<edit-plate {(plate-name)}="*editing.licensePlate"/>

Click a driver and edit their plate number:

JS Bin on jsbin.com

Performance Improvements

Finally, the last improvement we made is improving the performance of computes. As of 2.3.10, the spinning circle demo is about 20% faster than 2.2.9:

2.3

JS Bin on jsbin.com

2.2

JS Bin on jsbin.com

Create better web applications. We’ll help. Let’s work together.