DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Migrating to CanJS 5

  • Edit on GitHub

This guide walks you through the process to upgrade a 4.x app to CanJS 5.x.

Why Upgrade

CanJS 5.0:

  • Is easy to upgrade to! Only the model layer has breaking changes and there's a compatibility api that eliminates most breaking changes.
  • Supports named exports from the "can" package! The following is a "hello world":
    import {Component} from "can";
    Component.extend({
      tag: "hello-world",
      view: `{{message}} World!`,
      ViewModel: {  message: {default: "Hello"} }
    });
    
  • Simplifies the model layer!
    • Use nicely documented, pre-built models for common scenarios:

      • can-rest-model - A simple restful connection without the need to configure a can-query-logic.
      • can-realtime-rest-model - A simple restful connection with automatic list management.
      • can-super-model - A restful connection with most of can-connect’s features already configured.
    • More easily configure can-query-logic. If your service layer matches can-query-logic’s expectations, all configuration comes from your DefineMap. The following defines a Todo type and connects it to a restful service. No need to create a new set.Algebra()!

      import {DefineMap, DefineList, realtimeRestModel} from "can";
      
      const Todo = DefineMap.extend({
          // Configures `id` as the unique property.
          id: {identity: true, type: "number"},
      
          // Configures `name` as allowing null, undefined, or string values
          name: "string",
      
          // Configures `complete` as allowing true, false, null, or undefined
          complete: "boolean",
      
          // Configures `dueDate` as allowing null, undefined, or date values
          // Also will support queries like:
          // {filter: {dueDate: {$gt: "Thu Jun 07 2018 10:00:00 GMT-0500 (CDT)"}}}
          dueDate: "date"
      });
      
      Todo.List = DefineList.extend({"#": Todo})
      
      realtimeRestModel({
          Map: Todo,
          url: "/todos/{id}"
      })
      
    • Automatic list management (also known as real-time) and can-fixture support MongoDB-style comparison operators like $in, $ne, $lte, etc. The following shows simulating a service that supports filtering with MongoDB-style comparison operators and makes a request for todos using those comparison operators:

      import {fixture} from "can-fixture";
      
      const todoStore = fixture.store([
          { id: 1, name: "Do the dishes", complete: true, dueDate: "2018-06-01" },
          { id: 2, name: "Walk the dog", complete: false, dueDate: "2018-06-28" } ], Todo);
      
      fixture("/todos/{id}", todoStore);
      
      // Request todos after June 7th
      Todo.getList({
          filter: {
              dueDate: { $gt: "Thu Jun 07 2018 10:00:00 GMT-0500 (CDT)" }
          }
      }).then(function(todos){
          // Only receive todos after June 7th
          todos //-> Todo.List[ Todo{id: 2, ...} ]
      })
      
    • If your service layer does not match can-query-logic’s expectations, it’s far easier to configure than can-set. For example, if your service layer uses orderBy instead of sort, you just need to provide functions that translate back and forth between your servers parameters and can-query-logic’s query format.

      import {DefineMap, DefineList, realtimeRestModel, QueryLogic} from "can";
      
      const Todo = DefineMap.extend({
          // Configures `id` as the unique property.
          id: {identity: true, type: "number"},
      
          // Configures `name` as allowing null, undefined, or string values
          name: "string",
      
          // Configures `complete` as allowing true, false, null, or undefined
          complete: "boolean",
      
          // Configures `dueDate` as allowing null, undefined, or date values
          // Also will support queries like:
          // {filter: {dueDate: {$gt: "Thu Jun 07 2018 10:00:00 GMT-0500 (CDT)"}}}
          dueDate: "date"
      });
      
      Todo.List = DefineList.extend({"#": Todo});
      
      const todoQueryLogic = new QueryLogic(Todo,{
          toQuery(params){
              let query = {...params};
              if(query.orderBy != null) {
                  query.sort = query.orderBy;
                  delete query.orderBy;
              }
              return query;
          },
          toParams(query){
              let params = {...query};
              if(params.orderBy != null) {
                  params.sort = params.orderBy;
                  delete params.orderBy;
              }
              return params;
          }
      });
      
      realtimeRestModel({
          Map: Todo,
          url: "/todos/{id}",
          queryLogic: todoQueryLogic
      })
      

      There's detailed documentation on how to configure a new QueryLogic() for any circumstances.

  • Includes guides on HTML, Routing, Service Layer, Testing and Logic.
  • Includes hundreds of other bug fixes and new features, including:
    • New stache helpers for logic: and(), or(), and not().
    • #let stache helper for creating block-level variables.
    • #for(of) stache helper for looping through lists without creating new contexts.
    • #portal stache helper for inserting a section of a template into another element.
    • scope/key syntax for scope walking within a stache template.
    • Component elements now have a .viewModel property.
    • Slots are able to pass individual values.
    • Support for setting properties within event handlers in stache (e.g. on:click="this.prop = value").
    • Security: XSS vulnerability fix in v5.13.0.
    • New packages: can-map-compat and can-route-mock.
    • Internet Explorer 11 support

Breaking Changes

The following is a list of changes you must make to use CanJS 5.0.

Replace can-set with can-set-legacy

The biggest change to CanJS 5.0 from 4.0 was the replacement of can-set with can-query-logic. If you are using can-set, it’s likely you can upgrade by using can-set-legacy instead of can-set. So if you had code like:

import set from "can-set";
import DefineMap from "can-define/map/map";
import superMap from "can-connect/can/super-map/super-map";

const Todo = DefineMap.extend({ /* ... */ });

todoAlgebra = new set.Algebra(
    set.props.id("_id"),
    set.props.enum("status",["new","pending","resolved"]),
    set.props.boolean("complete")
);

superMap({
    Map: Todo,
    algebra: todoAlgebra,
    // ...
})

You can simply use can-set-legacy instead like:

import set from "can-set-legacy";
import DefineMap from "can-define/map/map";
import DefineList from "can-define/list/list";
import superMap from "can-connect/can/super-map/super-map";

const Todo = DefineMap.extend({ /* ... */ });
Todo.List = DefineList.extend({"#": Todo, /* ... */});

todoAlgebra = new set.Algebra(
    set.props.id("_id"),
    set.props.enum("status",["new","pending","resolved"]),
    set.props.boolean("complete")
);

superMap({
    Map: Todo,
    algebra: todoAlgebra,
    // ...
})

can-set-legacy is highly compatible with can-set. It returns an instance of can-query-logic so it is not perfectly compatible, but it should be enough for the vast majority of applications to upgrade without problems.

If you'd like to upgrade to avoid using can-set-legacy, the above code could be replaced with the can-query-logic equivalent:

import DefineMap from "can-define/map/map";
import superModel from "can-super-model";
import QueryLogic from "can-query-logic";

const Todo = DefineMap.extend({
    _id: {identity: true, type: "number"},
    name: "string",
    complete: QueryLogic.makeEnum([true, false]),
    status: QueryLogic.makeEnum(["new","pending","resolved"])
});
Todo.List = DefineList.extend({"#": Todo, /* ... */});

let todoQueryLogic = new QueryLogic(Todo,{
    toQuery(params){
        return {filter: params};
    },
    toParams(query){
        return query.filter;
    }
})

superModel({
    Map: Todo,
    queryLogic: todoQueryLogic,
    // ...
})

Note: If the service layer can be re-written to match can-query-logic’s format, configuring a queryLogic instance isn't necessary. Read more about this format in can-rest-model’s documentation.

Create a list type on all connections

Previously, can-connect/can/map/map would create a default list type for connections if one was not supplied. For example, the following only provides a Map setting, but a List is created:

import connect from "can-connect";
import dataUrl from "can-connect/data/url/url";
import constructor from "can-connect/constructor/constructor";
import canMap from "can-connect/can/map/map";
import DefineMap from "can-define/map/map";

const Todo = DefineMap.extend({
    id: "number",
    // ...
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos"
});

todoConnection.List //-> DefineList

In CanJS 5.0, you must provide a List type yourself:

import connect from "can-connect";
import dataUrl from "can-connect/data/url/url";
import constructor from "can-connect/constructor/constructor";
import canMap from "can-connect/can/map/map";
import DefineMap from "can-define/map/map";
import DefineList from "can-define/list/list";

const Todo = DefineMap.extend({
    id: "number",
    // ...
});
const TodoList = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    List: TodoList,
    url: "/services/todos"
});

The List type can also be on the Map:

import connect from "can-connect";
import dataUrl from "can-connect/data/url/url";
import constructor from "can-connect/constructor/constructor";
import canMap from "can-connect/can/map/map";
import DefineMap from "can-define/map/map";
import DefineList from "can-define/list/list";

const Todo = DefineMap.extend({
    id: "number",
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos"
});

All model types, for example can-rest-model and can-realtime-rest-model, must also be passed a List type.

Create, update, and delete requests must return all properties or set updateInstanceWithAssignDeep to true

In 4.0, can-connect/can/map/map performed assignment with the data returned from the server. For example, if a todo was created as follows:

const todo = new Todo({
    name: "laundry",
    complete: false
}).save();

If the server returned the following JSON (note the missing complete property):

{
    id: 5, name: "Laundry"
}

The todo would have the following properties:

todo //-> Todo{id: 5, name: "Laundry", complete: false}

In 4.0, the response value was "assigned" to the instance. Missing properties were not deleted.

In 5.0, the response is merged into the instance. Missing properties will be deleted. This means that in 5.0, the above response would delete the complete property, resulting in a todo like:

todo //-> Todo{id: 5, name: "Laundry"}

Note: The 5.0 merge behavior is quite powerful when dealing with nested data. You can read more about the behavior on its documentation page: can-diff/merge-deep/merge-deep.

The solution is to either change your services to return all properties or set your connection's updateInstanceWithAssignDeep property to true:

import connect from "can-connect";
import dataUrl from "can-connect/data/url/url";
import constructor from "can-connect/constructor/constructor";
import canMap from "can-connect/can/map/map";
import DefineMap from "can-define/map/map";
import DefineList from "can-define/list/list";

const Todo = DefineMap.extend({
    id: "number",
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos",
    updateInstanceWithAssignDeep: true
});

Replace idProp configuration with an identity configuration

In 4.0, it was possible to configure your the property that defined your identity with an idProp. For example, the following uses it to specify the idProp as _id:

import connect from "can-connect";
import dataUrl from "can-connect/data/url/url";
import constructor from "can-connect/constructor/constructor";
import canMap from "can-connect/can/map/map";
import DefineMap from "can-define/map/map";
import DefineList from "can-define/list/list";

const Todo = DefineMap.extend({
    _id: "number",
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos",
    idProp: "_id"
});

idProp is no longer supported.

In 5.0, there are multiple ways to configure the identity property. The identity property comes from the can-query-logic created by or passed to the todoConnection. For example, you can specify the identity properties on the schema passed to new QueryLogic(schema):

const Todo = DefineMap.extend({
    _id: "number",
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos",
    queryLogic: new QueryLogic({
        identity: ["_id"]
    })
});

Schemas are available on DefineMaps. So you could also use the identity property behavior to specify that _id is the identity property:

const Todo = DefineMap.extend({
    _id: {type: "number", identity: true}
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos",
    queryLogic: new QueryLogic(Todo)
});

Finally, if queryLogic is only being configured by the same type that is being passed as Map, no queryLogic is needed as this will happen by default. The following is equivalent to the previous example:

const Todo = DefineMap.extend({
    _id: {type: "number", identity: true}
    // ...
});
Todo.List = DefineList.extend({
    "#": Todo
});

const todoConnection = connect( [ dataUrl, constructor, canMap ], {
    Map: Todo,
    url: "/services/todos"
});

Replace __listSet with with the can.listQuery symbol

In CanJS 4.0, the default listQueryProp was __listSet. It is now a can.listQuery symbol. If you were setting __listSet on an List like the following:

const ClassRoom = DefineMap.extend({
    // ...
    students: {
        type: Student.List
        set(students) {
            students.__listSet = {filter: {classRoomId: this._id}}
            return students;
        }
    }
})

You should do it as follows:

const ClassRoom = DefineMap.extend({
    // ...
    students: {
        type: Student.List
        set(students) {
            students[Symbol.for("can.listQuery")] = {filter: {classRoomId: this._id}}
            return students;
        }
    }
})

Note: use can-symbol if you want IE11 support.

Set urlData when using can-route-pushstate

In can-route-pushstate 4.X it would automatically register itself as the default binding with can-route. In order to reduce the amount of side-effectual packages CanJS has, we changed this in can-route-pushstate 5.0 so that you must explicitly register it.

To do this you need to:

  • import the RoutePushstate constructor function.
  • Create a new instance.
  • Set it to the route.urlData property.
import RoutePushstate from 'can-route-pushstate';
import route from 'can-route';

route.urlData = new RoutePushstate();
route.register('{page}', { page: 'home' });
route.start();

Don’t parse error responses with can-ajax

This is a common pattern with can-ajax 1:

import ajax from 'can-ajax';

ajax().then(function() {
    // Handle a successful response…
}, function(xhr) {
    const error = JSON.parse(xhr.responseText);
    // Do something with error…
});

With can-ajax 2, you no longer need to parse the responseText:

import ajax from 'can-ajax';

ajax().then(function() {
    // Handle a successful response…
}, function(error) {
    // Do something with error…
});

Recommended Changes

The following are suggested changes to make sure your application is compatible beyond 5.0.

Use new models instead of old models

can-connect 2.X had some pre-configured modules that created a connection of multiple behaviors:

  • can-connect/can/base-map/base-map
  • can-connect/can/super-map/super-map

These have been moved into their own packages:

  • can-realtime-rest-model
  • can-super-model

Replace can-set-legacy with can-query-logic

Instead of using can-set-legacy, use can-query-logic directly to configure your query behavior. Read through the documentation on how to customize can-query-logic's documentation here.

In short:

  • set.props.id is replaced by the identity property behavior on a DefineMap.
  • set.props.boolean is replaced by {type: "boolean"} on a DefineMap.
  • set.props.enum is replaced by makeEnum.
  • offsetLimit, rangeInclusive, and sort are replaced by options.toQuery and options.toParams values passed to can-query-logic.

So instead of:

const todoAlgebra = new set.Algebra(

    // specify the unique identifier on data
    set.props.id( "_id" ),

    // specify that completed can be true, false or undefined
    set.props.boolean( "completed" ),

    set.props.enum("status",["new","assigned","complete"])

    // specify properties that define pagination
    set.props.offsetLimit( "offset", "limit" ),

    // specify the property that controls sorting
    set.props.sort( "orderBy" ),
);

You would have:

const Todo = DefineMap.extend({
    _id: {type: "string", identity: true},
    complete: {type: "boolean"},
    status: QueryLogic.makeEnum(["new","assigned","complete"])
});

const todoQueryLogic = new QueryLogic(Todo,{
    toQuery(params){
        let query = {...params};
        if(query.orderBy != null) {
            query.sort = query.orderBy;
            delete query.orderBy;
        }
        if(("offset" in query) || ("limit" in query)) {
            query.page = {};
        }
        if("offset" in query) {
            query.page.start = query.offset;
            delete query.offset;
        }
        if("limit" in query) {
            query.page.end = (query.page.start || 0 ) + query.limit - 1;
            delete query.limit;
        }
        return query;
    },
    toParams(query){
        let params = {...query};
        if(params.orderBy != null) {
            params.sort = params.orderBy;
            delete params.orderBy;
        }
        if(params.page) {
            params.offset = params.page.start;
            params.limit = (params.page.end - params.page.start) + 1;
            delete params.page;
        }
        return params;
    }
})

Space out your stache

We've begun formatting our can-stache templates as follows:

{{# if(app.session) }}
    {{# if(app.session.isAdmin) }}
        <li {{# is(app.page, 'users') }}class='active'{{/ is }}>
            <a href="{{ routeUrl(page='users') }}">Users</a>
        </li>
    {{/ if }}
    <li {{# is(app.page, 'account') }}class='active'{{/ is }}>
        <a href="{{ routeUrl(page='account') }}">Account</a>
    </li>
    <li>
        <a href="javascript://" on:click="scope.vm.logout()">Logout</a>
    </li>
{{ else }}

{{/ if }}

The following regular expressions and substitutions can be useful to clean up your stache code:

  • {{([^ #\/]) => {{ $1 - Replaces {{foo with {{ foo.
  • {{([#\/\^])([^ ]) => {{$1 $2 - Replaces {{#foo with {{# foo.
  • ([^ ])}} => $1 }} - Replaces foo}} with foo }}.

Also, the following regular expression can help you find helper expressions like: {{ foo bar }} and update them to call expressions like {{ foo(bar) }}:

  • {{\s*([#\/\^])\s*\w+\s+[\w\.]+

Use for(of), let, and this

In short, CanJS is migrating away from "context" lookup and to variable lookup. This section talks about what this means and how to migrate your code.

can-stache was originally based off Mustache and Handlebars. As CanJS evolved, we recognized that their implicit scope walking was a source of numerous bugs. For example, the following might look up name on a todo or on the ViewModel:

Component.extend({
    view: `
        {{#each todosPromise.value}}
            <li on:click="edit(this)">{{name}}</li>
        {{/each}}
    `,
    ViewModel: {
        get todosPromise(){ return Todo.getList(); },
        name: { type: "string", default: "ViewModel" },
        edit(todo) { /* ... */ }
    }
})

For CanJS 4.0, we made scope walking explicit. If edit should be read on the ViewModel, it must be looked up with ../ as follows:

Component.extend({
    view: `
        {{#each todosPromise.value}}
            <li on:click="../edit(this)">{{name}}</li>
        {{/each}}
    `,
    ViewModel: {
        get todosPromise(){ return Todo.getList(); },
        name: { type: "string", default: "ViewModel" },
        edit(todo) { /* ... */ }
    }
})

While this explicitness prevents errors, it confusing to users. In fact, context-based lookup is confusing to users altogether. Values seem to come from nowhere. It works more like JavaScript's with. Instead, we are migrating towards a variable-based lookup approach. Thus, we've:

  • Created two new helpers (for(of) and let) that create variables.
  • Started using {{this.key}} to refer to values on the ViewModel instead of {{key}}.

The component above should be updated to:

Component.extend({
    view: `
        {{# for todo of this.todosPromise.value }}
            <li on:click="this.edit(todo)">{{ todo.name }}</li>
        {{/ for }}
    `,
    ViewModel: {
        get todosPromise(){ return Todo.getList(); },
        name: { type: "string", default: "ViewModel" },
        edit(todo) { /* ... */ }
    }
})

Notice that this remains the ViewModel because for(of) doesn't change the context, it only creates a todo variable. Writing stache templates like this makes what's going on immediately clear.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.0.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news