Migrating to CanJS 6
This guide walks you through the process to upgrade a 5.x app to CanJS 6.x.
Why Upgrade
CanJS 6.0 is a major step forward for CanJS, fully embracing JavaScript classes, web components and other modern features. The highlights of the release include:
- ObservableObject and ObservableArray as new simplified replacements for DefineMap and DefineList based on JavaScript class syntax. These new types use proxies so they can react to changes even if properties are not predefined.
import { ObservableObject } from "can"; class Person extends ObservableObject { get fullName() { return this.first + " " + this.last; } } let person = new Person(); person.on("fullName", ({ value }) => { console.log("Full name is", value); }); person.first = "Ada"; person.last = "Lovelace";
class HelloWorld extends StacheElement {
static view = `Hello {{ this.name }}!`;
}
customElements.define("hello-world", HelloWorld);
let el = new HelloWorld();
el.name = "world";
document.body.append(el);
```
New package can-type brings a high level of flexibility to defining property types on can-observable-object and can-stache-element. It allows defining strict types, types that can be null/undefined and more.
import { ObservableObject, type } from "can"; class Person extends ObservableObject { static props = { age: type.maybe(Number) }; } let person = new Person({ age: null }); console.log(person.age); // null person.age = 11; console.log(person.age); // 11 person.age = "14"; // throws!!
Internet Explorer 11 support (still!)
Although StacheElement, ObservableObject, and ObservableArray use features such as classes and proxies that are not supported in IE11, you can continue to use Component and DefineMap and DefineList if your application needs to be compatible with IE11.
See the Setup Guide for more details.
Using Codemods
This guide will help you understand all of the changes in CanJS 6. We recommend you read it in full before starting the migration process.
Once you have read this guide, using codemods is a good way to take care of many of the changes described below. If you haven't already, read the Using Codemods guide, which explains what codemods are and how the can-migrate CLI tool works.
Even if you have already installed can-migrate
in the past, you need to upgrade to a version that supports CanJS 6.
npm install -g can-migrate@2
Once installed, you can run all of the CanJS 6 codemods on your code:
can-migrate '**/*.js' --can-version 6 --apply
Breaking Changes
The following are the breaking changes in CanJS 6.
Removal of can-view-nodelist
The package can-view-nodelist was used in previous versions of CanJS primarily for tracking when DOM nodes were removed from the page and doing necessary cleanup (such as removing event listeners). Using nodeLists was cumbersome, so in 6.0 we made it a priority to remove the need for them.
nodeLists have been completely removed from CanJS, but because several packages depended on them in the past these changes represent a breaking change. However it should have no affect on your codebase, and simply upgrading all of your packages is all you need to do.
route.data is now an ObservableObject
In 5.0 we changed route.data to be a DefineMap that was automatically connected to the route's properties. This meant you could use dot notation to listen to changes in route properties like so:
import { DefineMap, route } from "can";
const ApplicationViewModel = DefineMap.extend("ApplicationViewModel", {
page: {
get() {
return route.data.page;
}
}
});
route.register("{page}", { page: "home" });
route.ready();
Now route.data
is instead an ObservableObject. Unless you are using methods only available on a DefineMap like set this change will most likely not be noticed.
If you want to continue to use a DefineMap you can set route.data
before any calls to register:
import { DefineMap, route } from "can/everything";
const ApplicationViewModel = DefineMap.extend("ApplicationViewModel", {
page: {
get() {
return route.data.page;
}
}
});
route.data = new DefineMap();
route.register("{page}", { page: "home" });
route.ready();
beforeremove event removed
The event beforeremove was deprecated as part of CanJS 4.0 and is no longer available at all. This event was fired synchronously before the element was removed from the page.
In 6.0 this event was removed. Instead use connectedCallback with a return function. This function will be called when the ViewModel is torn down.
import { Component } from "can";
Component.extend({
tag: "my-component",
view: `
<p>Name changed: {{ this.nameChanged }}</p>
<p>Name: <input value:bind="this.name"/></p>
`,
ViewModel: {
nameChanged: { type: "number", default: 0 },
name: "string",
connectedCallback(element) {
this.listenTo("name", () => {
this.nameChanged++;
});
const disconnectedCallback = this.stopListening.bind( this );
return disconnectedCallback;
}
}
});
can-connect/can/tag moved to an ecosystem package
The module can-connect/can/tag
has been moved to its own package at can-connect-tag. You can import and use it like so:
import { connectTag, restModel, DefineMap, DefineList } from "can/everything";
const Todo = DefineMap.extend({
id: { identity: true, type: "number" },
name: "string"
});
Todo.List = DefineList.extend({ "#": Todo });
Todo.connection = restModel({
url: "/todos/{id}",
Map: Todo
});
connectTag("todo-model", Todo.connection);
Component / DefineMap / DefineList moved to legacy
can-component, can-define/map/map, and can-define/list/list are no longer part of core, but are still available as legacy packages. This will only affect you if you were using the core.mjs
or dist/global/core.js
bundles. Use either everything.mjs
or dist/globale/everything.js
instead.
inserted/removed event
Starting with CanJS 4, the inserted and removed events are no longer available by default.
If you still need to listen to those events on an element, you can write the following:
import { domEvents, domMutateDomEvents } from "can";
domEvents.addEvent(domMutateDomEvents.inserted);
domEvents.addEvent(domMutateDomEvents.removed);
Recommended Changes
The following are suggested changes to make sure your application is compatible beyond 6.0.
Map, List in connections renamed
The Map
and List
properties which are used to configure the instance and list types to create, have been renamed. Most likely this is used in configuration of can-rest-model or can-realtime-rest-model, but it also might be used with can-connect directly. These have been renamed to ObjectType
and ArrayType
, respectively. This is to keep in line with the new class-based can-observable-object and can-observable-array types.
Todo.connection = restModel({
List: TodoList,
Map: Todo,
url: "/api/todos/{id}"
});
becomes:
Todo.connection = restModel({
ArrayType: TodoList,
ObjectType: Todo,
url: "/api/todos/{id}"
});
Migrate to ObservableObject and ObservableArray for models
In CanJS 3.0 the DefineMap and DefineList were added as the preferred ways to build models. This allowed CanJS observables to work with the dot operator.
In 6.0 we are taking the next big step, by allowing JavaScript classes to be used as models through the new ObservableObject and ObservableArray base classes. You can extend them using the extends keywork like so:
import { ObservableObject } from "can";
class Todo extends ObservableObject {
}
Below are the major differences between ObservableObject / ObservableArray and DefineMap / DefineList.
Primitive constructors instead of string types
In DefineMaps you used string type names like so:
const Todo = DefineMap.extend("Todo", {
dueDate: "date",
label: "string",
complete: "boolean"
});
With ObservableObject you instead use the primitive constructors to convey the same information.
class Todo extends ObservableObject {
static props = {
dueDate: Date,
label: String,
complete: Boolean
};
}
Strict typing is the default
In addition to using primitive constructors, ObservableObject also differs in how it does type conversion. By default types defined for properties are strictly checked. That is, this scenario will throw:
class Person extends ObservableObject {
static props = {
name: String,
age: Number
};
}
let person = new Person({ name: "Theresa", age: "4" }); // throws!
For more control over type conversion the new can-type package was built. The above can be made to be loose using convert like so:
class Person extends ObservableObject {
static props = {
name: String,
age: type.convert(Number)
};
}
let person = new Person({ name: "Theresa", age: "4" }); // throws!
Asynchronous getters
In DefineMap you can make a getter be asynchronous by using the resolve
argument like so:
const ViewModel = DefineMap.extend("TodoList", {
todosPromise: {
get() {
return Todo.getList();
}
},
todos: {
get(lastSet, resolve) {
this.todosPromise.then(resolve);
}
}
});
In ObservableObject this behavior still exists but is part of its own behavior. The above would be written as:
class ViewModel extends ObservableObject {
static props = {
todosPromise: {
get() {
return Todo.getList();
}
},
todos: {
async(resolve) {
this.todosPromise.then(resolve);
}
}
};
}
get default instead of default function
In DefineMap you could have a default value be an object by making default
be a function like so:
const Configuration = DefineMap.extend("Configuration", {
data: {
default() {
return { admin: false };
}
}
});
However this makes it less ergonomic to have the default value of a property be a function itself. With ObservableObject a default value can be a function. So in order to have the default value be an object, you can use get default() like so:
class Configuration extends ObservableObject {
static props = {
data: {
get default() {
return { admin: false };
}
}
}
}
Migrate to StacheElement
CanJS 6 includes a new base class for creating web components, StacheElement. This class shares the same API as ObservableObject for defining properties.
Here are some of the major differences between can-stache-element and can-component:
Based on JavaScript classes
Like with can-observable-object you create elements using class Component extends
rather than extend. Because of this you need to use the separate customElements.define call to register the class as a custom element.
import { StacheElement } from "can";
class MyElement extends StacheElement {
}
customElements.define("my-element", MyElement);
Replaces:
import { Component } from "can";
Component.extend({
tag: "my-element"
});
No events object
In can-component an events object can be used to attach event listeners. This was deprecated in 4.0 and StacheElement doesn't support it.
It's recommended to instead use on:event in the template or listenTo in the element's connected lifecycle hook.
import { Component } from "can";
Component.extend({
tag: "my-element",
view: `<button>Increment {{ this.count }}</button>`,
ViewModel: {
count: {
default: 0
}
},
events: {
"button click": function() {
this.viewModel.count++;
}
}
});
Instead do it this way:
import { StacheElement } from "can";
class MyElement extends StacheElement {
static view = `<button on:click="this.increment()">Increment {{ this.count }}</button>`;
static props = {
count: 0
};
increment() {
this.count++;
}
}
customElements.define("my-element", MyElement);
When listening to properties on the element for side-effects you can use listenTo like so:
import { StacheElement } from "can";
class MyElement extends StacheElement {
static view = `<button on:click="this.increment()">Increment {{ this.count }}</button>`;
static props = {
count: 0
};
increment() {
this.count++;
}
connected() {
this.listenTo("count", () => {
console.log("Count is now", this.count);
});
}
}
customElements.define("my-element", MyElement);
If the options above do not work for you, you can replace your events
object with a can-control like:
import { StacheElement, Control } from "can/everything";
class MyElement extends StacheElement {
static view = `<button>Increment {{ this.count }}</button>`;
static props = {
count: 0
};
connected() {
const EventsControl = Control.extend({
"button click": function() {
this.element.count++;
}
});
new EventsControl(this);
}
}
customElements.define("my-element", MyElement);
No content element
can-component supported a <content/>
element as a way to inserting light DOM content from a parent component like so:
import { Compoent } from "can";
Component.extend({
tag: "my-child",
view: `<content />`
});
Component.extend({
tag: "my-parent",
view: `<my-child>Hello from the parent</my-child>`
});
With improvements to can-stache, it's now possible to pass templates through properties. This gives more flexibility.
import { StacheElement } from "can";
class MyChild extends StacheElement {
static view = `{{ this.content() }}`;
}
customElements.define("my-child", MyChild);
class MyParent extends StacheElement {
static view = `
{{< content }}
Hello from the parent
{{/ content }}
<my-child content:from="content" />
`;
}
customElements.define("my-parent", MyParent);
You can also use the <can-template> tag to pass templates that have access to the same scope as the component they are being passed to:
import { StacheElement } from "can";
class MyChild extends StacheElement {
static view = `{{ this.content() }}`;
}
customElements.define("my-child", MyChild);
class MyParent extends StacheElement {
static view = `
{{ let where = "the parent" }}
<my-child>
<can-template name="content">
Hello from {{ where }}
</can-template>
</my-child>
`;
}
customElements.define("my-parent", MyParent);