Core
The best, most hardened and generally useful libraries in CanJS.
Use
CanJS’s core libraries are the best, most hardened and generally useful modules.
Each module is part of an independent package, so you
should install the ones you use directly:
npm install can-value can-component can-realtime-rest-model can-define can-route can-route-pushstate --save
Let’s explore each module a bit more.
can-value
can-values represent an observable value. A compute can contain its own value and notify listeners of changes like:
import value from 'can-value';
let name = value.fromValue("Justin");
// read the value
name.value //-> "Justin"
name.on(function(newVal, oldVal){
newVal //-> "Matthew"
oldVal //-> "Justin"
});
name.value = "Matthew";
More commonly, a compute derives its value from other observables. The following
info
compute derives its value from a person
map, hobbies
list, and age
compute:
import DefineMap from 'can-define/map/map';
import DefineList from 'can-define/list/list';
import value from 'can-value';
let person = new DefineMap({first: "Justin", last: "Meyer"}),
hobbies = new DefineList(["js","bball"]),
age = value.fromValue(33);
let info = value.returnedBy(function(){
return person.first +" "+ person.last+ " is "+age()+
"and like "+hobbies.join(", ")+".";
});
info.value //-> "Justin Meyer is 33 and likes js, bball."
info.on(function(ev, newVal){
newVal //-> "Justin Meyer is 33 and likes js."
});
hobbies.pop();
can-define
can-define/map/map and can-define/list/list allow you to create observable
maps and lists with well-defined properties. You can
define a property’s type initial value, enumerability, getter-setters and much more.
For example, you can define the behavior of a Todo
type and a TodoList
type as follows:
import DefineMap from 'can-define/map/map';
import DefineList from 'can-define/list/list';
const Todo = DefineMap.extend({ // A todo has a:
name: "string", // .name that’s a string
complete: { // .complete that’s
type: "boolean", // a boolean
default: false // initialized to false
},
dueDate: "date", // .dueDate that’s a date
get isPastDue(){ // .pastDue that returns if the
return new Date() > this.dueDate; // dueDate is before now
},
toggleComplete: function(){ // .toggleComplete method that
this.complete = !this.complete; // changes .complete
}
});
const TodoList = DefineList.extend({ // A list of todos:
"#": Todo, // has numeric properties
// as todos
get completeCount(){ // has .completeCount that
return this.filter("complete") // returns # of
.length; // complete todos
}
});
This allows you to create a Todo, read its properties, and call back its methods like:
const dishes = new Todo({
name: "do dishes",
// due yesterday
dueDate: new Date() - 1000 * 60 * 60 * 24
});
dishes.name //-> "do dishes"
dishes.isPastDue //-> true
dishes.complete //-> false
dishes.toggleComplete()
dishes.complete //-> true
And it allows you to create a TodoList
, access its items and properties
like:
const todos = new TodoList( dishes, {name: "mow lawn", dueDate: new Date()});
todos.length //-> 2
todos[0].complete //-> true
todos.completeCount //-> 1
These observables provide the foundation for data connection (models), view-models and even routing in your application.
can-set
[can-set] models a service layer’s behavior as a [can-set.Algebra set.Algebra]. Once modeled, other libraries such as can-connect or can-fixture can add a host of functionality like: real-time behavior, performance optimizations, and simulated service layers.
A todosAlgebra
set algebra for a GET /api/todos
service might look like:
import set from 'can-set';
let todosAlgebra = new set.Algebra(
// specify the unique identifier property on data
set.prop.id("_id"),
// specify that completed can be true, false or undefined
set.prop.boolean("complete"),
// specify the property that controls sorting
set.prop.sort("orderBy")
)
This assumes that the service:
- Returns data where the unique property name is
_id
:GET /api/todos -> [{_id: 1, name: "mow lawn", complete: true}, {_id: 2, name: "do dishes", complete: false}, ...]
- Can filter by a
complete
property:GET /api/todos?complete=false -> [{_id: 2, name: "do dishes", complete: false}, ...]
- Sorts by an
orderBy
property:GET /api/todos?orderBy=name -> [{_id: 2, name: "do dishes", complete: false}, {_id: 1, name: "mow lawn", complete: true}]
In the next section will use todoAlgebra
to build a model with can-connect.
can-connect
can-connect connects a data type, typically a DefineMap
and its DefineList
,
to a service layer. This is often done via the
can-connect/can/base-map/base-map module which bundles many common behaviors
into a single api:
import baseMap from 'can-connect/can/base-map/base-map';
import DefineMap from 'can-define/map/map';
import DefineList from 'can-define/list/list';
import set from 'can-set';
const Todo = DefineMap.extend({
// ...
});
const TodosList = DefineMap.extend({
"#": Todo,
// ...
});
const todosAlgebra = new set.Algebra({
// ...
});
const connection = baseMap({
url: "/api/todos",
Map: Todo,
List: TodoList,
algebra: todosAlgebra,
name: "todo"
});
baseMap
extends the Map
type, in this case, Todo
, with
the ability to make requests to the service layer.
- Get a list of Todos
Todo.getList({complete: true}).then(function(todos){})
- Get a single Todo
Todo.get({_id: 6}).then(function(todo){})
- Create a Todo
const todo = new Todo({name: "do dishes", complete: false}) todo.save().then(function(todo){})
- Update an already created Todo
todo.complete = true; todo.save().then(function(todo){})
- Delete a Todo
todo.destroy().then(function(todo){})
can-connect is also middleware, so custom connections can be assembled too:
import base from 'can-connect/base/base';
import dataUrl from 'can-connect/data-url/data-url';
import constructor from 'can-connect/constructor/constructor';
import map from 'can-connect/can/map/map';
const options = {
url: "/api/todos",
Map: Todo,
List: TodoList,
algebra: todosAlgebra,
name: "todo"
}
const connection = map(constructor(dataUrl(base(options))));
can-stache
can-stache provides live binding mustache and handlebars syntax. While
templates should typically be loaded with a module loader like steal-stache,
you can create a template programmatically that lists out todos within a
promise loaded from Todo.getList
like:
import stache from 'can-stache';
// Creates a template
let template = stache(`
<ul>
{{#if(todos.isPending)}}<li>Loading…</li>{{/if}}
{{#if(todos.isResolved)}}
{{#for(todo of todos.value)}}
<li class="{{#todo.complete}}complete{{/}}">{{todo.name}}</li>
{{else}}
<li>No todos</li>
{{/for}}
{{/if}}
</ul>
`);
// Calls the template with some data
let fragment = template({
todos: Todo.getList({})
});
// Inserts the result into the page
document.body.appendChild(fragment);
can-stache templates use magic tags like {{}}
to control what
content is rendered. The most common forms of those magic tags are:
- {{key}} - Insert the value at
key
in the page. Ifkey
is a function or helper, run it and insert the result. - {{#key}}...{{/key}} - Render the content between magic tags based on some criteria.
can-stache templates return document fragments that update whenever their source data changes.
can-component
can-component creates custom elements with unit-testable view models. It combines a view model created by can-define/map/map with a template created by can-stache.
import Component from 'can-component';
import DefineMap from 'can-define/map/map';
import stache from 'can-stache';
// Defines the todos-list view model
let TodosListVM = DefineMap.extend({
// An initial value that is a promise containing the
// list of all todos.
todos: {
default: function(){
return Todo.getList({});
}
},
// A method that toggles a todo’s complete property
// and updates the todo on the server.
toggleComplete: function(todo){
todo.complete = !todo.complete;
todo.save();
}
});
Component.extend({
tag: "todos-list",
ViewModel: TodosVM,
view: `
<ul>
{{#if(todos.isPending)}}<li>Loading…</li>{{/if}}
{{#if(todos.isResolved)}}
{{#for(todo of todos.value)}}
"<li class='{{#todo.complete}}complete{{/}}'>{{todo.name}}</li>
{{else}}
<li>No todos</li>
{{/for}}
{{/if}}
</ul>`
});
can-stache-bindings
can-stache-bindings provides custom attributes for can-stache event and data bindings.
Bindings look like:
on:event="key()"
for event binding.prop:from="key"
for one-way binding to a child.prop:to="key"
for one-way binding to a parent.prop:bind="key"
for two-way binding.
Event binding examples:
<!-- calls `toggleComplete` when the li is clicked -->
<li on:click="toggleComplete(.)"/>
<!-- calls `resetData` when cancel is dispatched on `my-modal`’s view model -->
<my-modal on:cancel="resetData()"/>
One-way to child examples:
<!-- updates input’s `checked` property with the value of complete -->
<input type="checkbox" checked:from="complete"/>
<!-- updates `todo-lists`’s `todos` property with the result of `getTodos`-->
<todos-list todos:from="getTodos(complete=true)"/>
One-way to parent examples:
<!-- updates `complete` with input’s `checked` property -->
<input type="checkbox" checked:to="complete"/>
<!-- updates `todosList` with `todo-lists`’s `todos` property -->
<todos-list todos:to="todosList"/>
Two-way examples:
<!-- Updates the input’s `value` with `name` and vice versa -->
<input type="text" value:bind="name"/>
<!-- Updates `date-picker`’s `date` with `dueDate` and vice versa -->
<date-picker date:bind="dueDate"/>
can-route and can-route-pushstate
can-route connects a DefineMap
’s properties to values in the
url. Create a map type, [canjs/doc/can-route.map connect it to the url], and [can-route.ready begin routing] like:
import route from 'can-route';
import DefineMap from 'can-define/map/map';
const AppViewModel = DefineMap.extend({
seal: false
},{
// Sets the default type to string
"#": "string",
todoId: "string",
todo: {
get: function(){
if(this.todoId) {
return Todo.get({_id: this.todoId})
}
}
}
});
const appViewModel = new AppViewModel();
route.map(appViewModel);
route.start();
When the url changes, to something like #!&todoId=5
, so will the
appViewModel
’s todoId
and todo
property:
appViewModel.todoId //-> "5"
appViewModel.todo //-> Promise<Todo>
Similarly, if appViewModel
’s todoId
is set like:
appViewModel.todoId = 6;
The hash will be updated:
window.location.hash //-> "#!&todoId=6"
The route
function can be used to specify pretty routing rules that
translate property changes to a url and a url to property changes. For example,
// a route like:
route.register("todo/{todoId}");
// and a hash like:
window.location.hash = "#!todo/7";
// produces an appViewModel like:
appViewModel.serialize() //-> {route: "todo/{todoId}", todoId: "7"}
can-route-pushstate adds pushstate support. It mixes in this behavior so you just need to import the module:
import route from 'can-route';
import 'can-route-pushstate';
Want to learn more?
If you haven’t already, check out the Guides page on how to learn CanJS. Specifically, you’ll want to check out the Chat Guide and TodoMVC Guide to learn the basics of using CanJS’s core libraries. After that, check out the Reading the API Docs on how to use and learn from these API docs.