DEV Community

Cover image for CanJS 6.0: web components, proxy-based observables, new type system
Kevin Phillips
Kevin Phillips

Posted on

CanJS 6.0: web components, proxy-based observables, new type system

This article is a cross-post from bitovi.com

Hello Web Developers,

Today we are announcing the release of CanJS 6.0. The goal of CanJS is to be the best tool for building data-driven web applications. Building on CanJS 4.0 and CanJS 5.0, CanJS 6.0 is designed to

  • make it easier to get started creating components
  • make observables behave more like normal objects and arrays
  • make it easier to scale your code to larger applications and more complex use-cases

CanJS 6.0 is built around web components - use CanJS to build custom elements that work natively in modern web browsers. CanJS’s StacheElement greatly simplifies the APIs the browser gives you for creating custom elements.

import { StacheElement } from "can";

class MyCounter extends StacheElement {
    static view = `
        Count: <span>{{ this.count }}</span>
        <button on:click="this.increment()">+1</button>
    `;
    static props = {
        count: 0
    };
    increment() {
        this.count++;
    }
}
customElements.define("my-counter", MyCounter);
Enter fullscreen mode Exit fullscreen mode

Interested in trying this out yourself. Use this Codepen.

The foundation of CanJS is key-value observables that allow your application to efficiently react to changes, keep data in sync with your API layer, and re-render exactly the HTML that needs to be updated when something changes.

Like previous versions of CanJS, 6.0 has a rich API for defining how the properties of your components and observables should behave. We’ve taken this a step further in 6.0 by making our observables based on classes with an even easier syntax for specifying type information.

import { ObservableObject } from "can";

class Todo extends ObservableObject {
    static props = {
        name: String,
        completed: false
    };
    toggle() {
        this.completed = !this.completed;
    }
}
Enter fullscreen mode Exit fullscreen mode

Along with simplified observables, the type system in CanJS 6.0 has been completely overhauled. It is now easier than ever to add type information to your observables. Types are strict by default, which means that if a property is set to the wrong type, an error will be thrown so you can fix issues in your application before your users ever see them. CanJS also provides the flexibility to use non-strict types so that you can ensure a value is always converted to the correct type.

As always, types in CanJS can be used without the overhead of setting up a compiler or external type system.

Ok, get your snacks, let’s walk through the new features and how they can simplify your applications.

Web Components

CanJS has been promoting a component architecture since can-component was introduced in CanJS 2.0.0 (in 2013!), which has continued to evolve since then. Today, modern web browsers have native support for what we think of as components through the custom elements APIs. In CanJS 6.0, we are providing support for building native custom elements through StacheElement.

Moving to native web components provides enormous benefits to the development process and makes it easier than ever to build your application out of small, independent components.

JavaScript classes

StacheElement is built on JavaScript classes. While classes are new to lots of JavaScript developers, they are a native feature of the language. This means there are blog posts, videos, and many other resources that developers can use to learn how to use them.

Using classes removes the need for the custom inheritance system that enabled Component.extend({ … }) and makes it easier for developers to get started with CanJS since they no longer need this framework-specific knowledge.

To create a component using StacheElement, just create a class:

class MyThing extends StacheElement {
    static view = `{{ this.greeting }} World`;
}
Enter fullscreen mode Exit fullscreen mode

Observable element properties

A design goal of StacheElement was to make elements work like built-in DOM elements. This enables developers to use them in ways they’re already familiar with and with the tools they already use.

With StacheElement, all of an element’s properties are observable. This means elements can react to property changes just like the elements built in to the browser -- set a property and the view will update if it needs to:

change property

Lifecycle methods and hooks

StacheElement also comes with lifecycle hooks that allow you to ensure your code runs at the right time and lifecycle methods that make your components easy to test.

For example, the following Timer component will increment its time property once every second. This interval is started in the connected hook so that the timer will only run when the component is in the page. The connected hook also returns a teardown function so the interval can be cleared when the component is removed from the page.

import { StacheElement } from "can";

class Timer extends StacheElement {
    static view = `
        {{ this.time }}
    `;
    static props = {
        time: 0
    };
    connected() {
        let timerId = setInterval(() => {
            this.time++;
        }, 1000);
        return () => clearInterval(timerId);
    }
}
customElements.define("my-timer", Timer);
Enter fullscreen mode Exit fullscreen mode

There are three lifecycle methods that can be used to test this component -- initialize, render, and connect:

const timer = new Timer();

// calling `initialize` allows <my-timer>’s properties to be tested
timer.initialize({ time: 5 });
timer.time; // -> 5

// calling `render` allows <my-timer>’s view to be tested
timer.render();
timer.firstElementChild; // -> <p>0</p>

// calling `connect` allows <my-timer>’s `connect` logic to be tested
timer.connect();
// ...some time passes
timer.firstElementChild; // -> <p>42</p>
Enter fullscreen mode Exit fullscreen mode

Connect attributes and properties

Another design goal of StacheElement was to give developers the flexibility to connect an element’s attributes and properties, similar to how many built-in elements “reflect” changes between attributes and properties.

By default, setting an attribute on a component will not set the property, but the fromAttribute binding can be used to set a property whenever an attribute changes:

change attribute

This means that if you want to use your component in static HTML or in HTML generated by your backend web application, you can do that. You can even set properties from JSON or another complex data type:

<my-user
    user-data='{ "first": "Leonardo", "last": "DiCaprio", "age": 44 }'
></my-user>

<script type="module">
    class User extends StacheElement {
        static view = `
            <form>
                <input value: bind="user.first">
                <input value: bind="user.last">
                <input value: bind="user.age" type="number">
            </form>
        `;
        static props = {
            user: { type: Person, bind: fromAttribute( "user-data", JSON ) }
        };
    }
    customElements.define("my-user", User);
</script>
Enter fullscreen mode Exit fullscreen mode

Improved Observables

CanJS 6.0 brings the third generation of CanJS key-value observables — can-observable-object. Like can-map and can-define/map/map before it, working with ObservableObject means that you can update your data and the rest of your application will update accordingly.

JavaScript Proxies

ObservableObject was designed to make developing with observables just like developing with normal JavaScript Objects. To make this possible, it is built on a new feature of modern web browsers, the JavaScript Proxy. Using proxies means that properties can be added, changed, and removed in all the ways that are possible with Objects and will always remain observable.

can-observable-array provides the same benefits when working with arrays of data. Using proxies irons out lots of edge cases, such as the ability to make items in an array observable when they are set using array index notation:

const list = new MyDefineList([]);
list[0] = { name: "Mark" }; // list[0] is a plain object

const arr = new MyObservableArray([]);
arr[0] = { name: "Mark" }; // arr[0] is an observable!
Enter fullscreen mode Exit fullscreen mode

JavaScript Classes

ObservableObject and ObservableArray are also built on top of JavaScript classes, so you can create an observable for your application by creating your own class constructor:

class Car extends ObservableObject { }
class Dealership extends ObservableArray { }

const tesla = new Car({ make: "Tesla", model: "Model S" });
const toyota = new Car({ make: "Toyota", model: "Camry" });

const dealership = new DealerShip([ tesla, honda ]);
Enter fullscreen mode Exit fullscreen mode

Simplified property definitions

Like previous CanJS observables, ObservableObject and ObservableArray allow you to specify exactly how the properties of your observables should behave. We’ve made this even easier by simplifying some of the property definitions from can-define.

To learn more about all of the differences in property definitions between can-define and can-observable-object, check out the migration guide.

Type constructors

One of the most common ways developers like to define their properties is by giving them types. With ObservableObject, this is as simple as providing a constructor function (even for built-in constructors):

class Car extends ObservableObject {
    static props = {
        make: String,
        model: String,
        year: Number
    };
}
Enter fullscreen mode Exit fullscreen mode

Asynchronous properties

Another small improvement to property definitions is that asynchronous getters now have their own behavior:

class TodoList extends ObservableObject {
    static props = {
        todosPromise: {
            get() {
                return Todo.getList();
            }
        },
        todos: {
            async(resolve) {
                this.todosPromise.then(resolve);
            }
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Since StacheElement uses these same observable property behaviors under the hood, all of the benefits of ObservableObject and ObservableArray also apply to elements created with CanJS. 🎉

New Type System

As we saw in the previous section, it is very easy to set the type of a property when using CanJS observables. The type system in CanJS 6 has been greatly improved to allow for strict type checking and much greater flexibility. This flexibility means that you can use more rigorous type checking as your application or requirements grow.

CanJS 6 supports strict typing by default. This means if you declare a property is a specific type, an error will be thrown if that property is set to a value of a different type.

class Person extends ObservableObject {
    static props = {
        age: Number
    };
}
var farah = new Person();
farah.age = '4';

// Uncaught Error: "4" (string) is not of type Number.
// Property age is using "type: Number". Use "age: type.convert(Number)"
// to automatically convert values to Numbers when setting the "age" property.
Enter fullscreen mode Exit fullscreen mode

If strict typing is not the best solution for your application, you can also set up a property to always convert its value to a specific type using type.convert:

class Person extends ObservableObject {
    static props = {
        age: type.convert(Number)
    };
}
var person = new Person();
person.age = "4";

person.age; // 4
Enter fullscreen mode Exit fullscreen mode

You can also create “Maybe types” that will allow the value to be null and undefined on top of whatever valid values the type allows. For example type.maybe(Number) will allow the value to be a null, undefined, or a Number and will throw if set to something else.

To see all the ways types can be defined, check out the can-type documentation.

What about the old APIs?

If you have an existing CanJS application, don’t worry! None of the APIs you’re using today are going away. You can continue to use can-component and can-define and update to the new APIs when it makes sense for your application.

Also, if your application needs to support IE11, which doesn’t support Proxies, can-component and can-define will continue to be available for you.

Upgrading

If you do have an existing CanJS application that you are interested in upgrading, check out the migration guide which will explain all of the changes in depth. Make sure to take a look at the Using codemods guide to automate your upgrade process.

What’s Next?

The CanJS core team is going to continue working to make CanJS the best tool to build data-driven web applications. Every bug we fix, change we make, and feature we add is based on talking with the community, community surveys, and lots of user testing. Please come join in the conversation and if you’re interested in being a beta tester, please fill out this survey.

Thank You

  • CanJS developers around the world building some of the most high-profile, high-performance, and amazing pieces of software on the web. Keep building!
  • Contributors big and small to CanJS. Every bug report, feature request, documentation fix, and user test makes CanJS better.
  • Bitovi and its team for helping other companies build quality applications and investing its resources back into open-source development that benefits everyone.

Sincerely and with much love,
CanJS Core Team

Top comments (0)