DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.6.1
5.33.3 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
      • Weather Report
    • 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

Forms

  • Edit on GitHub

Learn how to create amazing <form>s with CanJS.

Overview

There are three main tasks when working with forms and form elements:

  • setting form element attributes based on view-model data
  • responding to browser events
  • converting data to formats convenient for your business logic

CanJS provides many useful utilities and techniques to help with these tasks. This guide will walk you through:

  • binding to form elements using one-way, two-way, and event binding
  • combining bindings in a “data down, actions up” pattern
  • using shorthand bindings like on:input:value:to
  • storing view-related data in template variables
  • working with common form elements
  • using type converters like type="number"
  • using more complex converters like either-or, boolean-to-inList, equal, and index-to-selected
  • using custom events like enter and radiochange
  • using custom attributes such as values and focused
  • validating forms using virtual properties and validation libraries like validatejs
  • indentifying and navigating bindings conflicts
  • working with related data, promises, and parent-child relationships

We recommend reading the Bindings overview section first before jumping to the topic that interests you—or start at the top to become an expert on it all.

Bindings

When working with form elements, there are two basic kinds of bindings—event bindings and attribute bindings. Event bindings allow you to respond to DOM Events and attribute bindings allow you to bind the value of HTML Attributes to properties in your scope.

The following sections will explain how to use each of these bindings on their own, as well as how to combine them in more advanced binding formats.

Event binding

Event bindings allow you to respond to DOM Events. To set up an event binding, add an attribute to your element like on:DOM_EVENT="CALL_EXPRESSION" where DOM_EVENT is the event you want to respond to and CALL_EXPRESSION is a Call Expression for the function you want to call when the event occurs. Here is a simple example:

This example calls the plusOne function in the scope when a "click" event is triggered on the button. There are many other events that you can listen to. Many of these will be shown in the examples throughout this guide, or you can take a look at https://developer.mozilla.org/en-US/docs/Web/Events for a list.

Attribute binding

Unlike event bindings, which can only be used to call a function when an event occurs, attribute bindings can be used in two directions.

You can use attribute bindings to update the value of an attribute when a property in the scope changes:

In this example, we create a progressValue property in the scope that continually counts up from 0 to 100. We then use value:from="progressValue" to set the value attribute of the <progress> element whenever the progressValue property in the scope changes.

You can also use attribute bindings to update properties in the scope when the value of an attribute changes:

This example uses checked:to="isItChecked" to update the value of the isItChecked property in the scope whenever the checkbox’s checked attribute changes.

Attribute bindings work with any HTML attribute. You will see examples of attributes that are useful to bind to in the Common form elements section or you can take a look at List of Attributes for a comprehensive list.

Data down, actions up

You can combine Event and Attribute bindings in a “data down, actions up” pattern to keep a form element attribute in sync with a property in your scope:

This example uses value:from="name" to set the value attribute of both text fields when name in the scope changes. It also uses on:change="handleAction('set', 'name', scope.element.value)" to listen for "change" events on the text fields and call the handleAction function with the text field’s value.

Data is passed down from the scope to each element using value:from and the action of changing the data is passed up through on:change="handleAction( /* ... */ )", which means that the value attribute of the text field is always in sync with the name property in the scope.

To see a larger example of this pattern, check out the extended example.

Two-way binding

You can achieve the same behavior as the previous example using two-way binding. It is not as explicit as the “data down, actions up” approach, but it greatly reduces the boilerplate in your code. Here is the same example using two-way binding:

In this example, value:bind="name" is set on each text field to achieve two-way binding between the value attribute and the name property in the scope.

Binding attributes on specific events

One other situation where you may want to use separate attribute and event bindings is when you want to listen to events other than "change". Attribute bindings like value:to="name" work by listening for change events in order to know when to update the scope property. For most form elements,"change" events are fired when the element loses focus—not each time the user types into the element.

In order to update a value in the scope each time the user types, you could use the “data down, actions up” approach and listen on: input events, but there is also a shorthand available for updating a value in the scope with the value of an attribute when a specific event occurs. In order to achieve this behavior, you can use on:input:value:to="name". You can combine this with value:from="name" to achieve two-way binding with updates on input events:

Binding to template variables

All of the examples shown so far have used variables from the scope for event and attribute bindings. It can be useful to bind to variables without having to create a property on the scope for things that are purely presentational. To create variables local to the template, you can use let variables:

This example creates a template variable focusedOnName that is bound to the focused attribute of the text field and uses it to set a class on the associated <span>. Since this is used entirely for CSS in the template, it makes sense to make focusedOnName a variable local to the template with {{ let }}.

Common form elements

The bindings above make it easy to work with the most common form elements. The following sections describe the most useful attribute and event bindings to use with each of these elements, as well as anything else that is unique to element.

Text field

<input type="text">

Text fields are one of the most common form elements and have many attributes that can be useful to bind to. The most common attribute is value—this is the current value of the text entered into the text field.

Here is an example showing different formats for binding to the value attribute:

Binding to the Enter key

CanJS also allows you to use custom events within event bindings. A custom event that is very useful with text fields is the enter event provided by the can-event-dom-enter package. This event listens for keyup events and filters them to only events where the Enter key is released.

Note: since this is a custom event, you need to opt-in to using it. If you want to use the enter event, make sure you register it like:

import domEvents from "can-dom-events";
import enterEvent from "can-event-dom-enter";

domEvents.addEvent(enterEvent);

Checkbox

<input type="checkbox">

The simplest way to work with checkboxes in CanJS is to bind the checked attribute to a property in the scope:

This allows you to toggle the scope property between true and false.

Binding a checkbox to non-boolean values

You can use a checkbox to represent values other than true and false using the either-or converter.

This converter allows you to pass a scope property and two values—the value that the scope property should be set to if the checkbox is checked and another that should be used if the checkbox is unchecked.

Note: you need to import the can-stache-converters package in order to use the either-or converter.

Notice that the {{ bestTypeOfPet }} property is initially set to "Cats" because that is obviously the correct answer the checkbox is unchecked.

If you would like to choose "Dogs" by default, you can set the checked attribute.

Note: checked is a boolean attribute, so it is the presence of this attribute that sets it to true. This means that any of these will set the bestTypeOfPet property in the scope to "Dogs":

  • <input type="checkbox" checked>
  • <input type="checkbox" checked=true>
  • <input type="checkbox" checked=false>
  • <input type="checkbox" checked="true">
  • <input type="checkbox" checked="false">
  • <input type="checkbox" checked="any other value">

Binding checkboxes to a list

Using a checkbox to toggle a property between two values is very useful, but you often want to use checkboxes to select one or many items from a list. To do this, you can use the boolean-to-inList converter. This will bind the checked attribute to whether or not the item is in a list.

The boolean-to-inList converter will set the checked attribute to true or false based on whether a value is in the list and add or remove an item from the list when the checked attribute changes.

Note: you need to import the can-stache-converters package in order to use the boolean-to-inList converter.

This might be easier to understand with an example:

Radio button

<input type="radio">

When using radio buttons in CanJS, you often want to set the value of a single scope property to a different value for each radio button in a group. The equal converter does exactly this.

Note: you need to import the can-stache-converters package in order to use the equal converter.

This example binds the checked attribute to the equal converter for each radio button, passing the favoriteColor scope property and the color value for each radio button.

Number input

<input type="number">

When working with number inputs, it is important to set the type to type.maybeConvert(Number) to ensure that the value is stored as a number in the scope.

File input

<input type="file">

When using a file input, the value attribute can be used to get a representation of the path to the selected file:

The files property cannot be accessed through an attribute binding (since it is not an attribute); however, you can still use the File API by passing scope.element.files through a "change" event listener:

You could also use on:change="scope.set('selectedFiles', scope.element.files)" as described in the Data down, actions up section if you only need to display the data, but it is more likely that you would want to use the FileList object along with the File JavaScript API.

Date input

<input type="date">

When using a date input, the valueAsDate property can be used to bind a Date property on the component to the input.

Select

<select>

When working with <select> elements, the value attribute of the element will be set to the value of the selected option. This means that using value:bind="..." will allow you to bind the selected value to a property in the scope:

By default, the value will be set to the text of the option if there is no value attribute. Take a look at the difference between the HTML of the previous example and the following:

Binding options by index

There are times when you need to use numeric indexes for the <option> values. When doing this, you can use the index-to-selected converter to keep “nice” values in your scope property.

Note: you need to import the can-stache-converters package in order to use the index-to-selected converter.

To use this converter, pass it the property in the scope you want to bind and the list that contains the available options. It will convert the index given by the value of the <option> to the selected item in the list.

The HTML for this example is intentionally verbose to make it easier to understand. Normally you would use stache to generate the <option>s directly from the months list like:

<select value:bind="index-to-selected(selectedMonth, months)">
    {{# for(months, index=index month=value) }}
        <option value:from="index">{{ month }}</option>
    {{/ for }}
</select>

The selected-to-index converter is also very useful when you want to bind the <select> element directly to the index instead of converting it to the selected item right away but you still want to be able to use the selected item in another part of your template.

Note: you need to import the can-stache-converters package in order to use the selected-to-index converter.

In the following example, the <select> element is using value:bind="selectedIndex" instead of converting it from an index to the selected item. The text field below it passes that index and the months list to the selected-to-index converter in order to display the corresponding value from the months list. The <select> and <input> are both two-way bound, so changing either the will update the other.

Binding options to non-string values

There are times when you want to two-way bind the value selected and convert the string value into its primitive value (e.g. object, string). You can use the string-to-any converter to do so.

In the following example, the selector is converting the value selected to a primitive and then two-way binding to the variable primitiveValue. When presenting the typeof primitiveValue, you can see that the string stored in the value has been converted.

Note: you need to import the can-stache-converters package in order to use the string-to-any converter.

Select multiple

<select multiple>

When using a <select> element to select multiple values, the value attribute will only give the first value that was selected. In order to get a list of all selected values, CanJS provides the custom values attribute. This makes it very easy to work with <select multiple> elements:

Textarea

<textarea>

You can use any of the techniques for text fields with <textarea>s:

Submit button

<input type="submit">

Submit buttons by default will submit the form to the server when clicked. With CanJS apps, you usually want to handle this with JavaScript instead of using this default behavior. To do this, you just need to set up an event binding and use scope.event so you can call preventDefault to prevent the form from being submitted automatically.

Here is an example of what happens by default. Click the Submit button an notice that the demo reloads:

This happens because the form is being submitted, which performs a GET request for the same URL. Adding the click handler and event.preventDefault prevents this behavior:

This will prevent the form from being submitted when the user explicitly clicks the submit button or presses the Enter key while focused on one of the text fields; however, there are times when a form can be implicitly submitted that this click handler might not catch. In order to handle these cases in all browsers, use an on:submit="..." handler directly on the <form> element:

Advanced topics

Form validation

Validating forms in CanJS has two primary steps—validating your component and displaying errors.

Separating these responsibilities by validating the component directly makes it much easier to unit test your validation rules.

The following section shows how to do form validation manually.

Manual form validation

For forms with a small number of fields, validating input values can be as simple as creating virtual properties that represent the validity of each user input.

When the user input is invalid, errors can be displayed using a CSS class: {{# if(error) }}class="error"{{/ if }}.

Also, in this example the submit button is disabled using the disabled:from="error" binding.

Here is an example showing this kind of manual validation for a phone number:

Binding conflicts

There are times when using two-way binding where the attribute can become out of sync with the scope property it is bound to. To understand this, it is important to understand a little about how bindings work.

When using a binding like value:bind="scopeProp", there are two things that get set up:

  • Event Listener 1 listens for "change" events on the element, reads the value attribute, and sets scopeProp
  • Event Listener 2 listens for changes to scopeProp and sets the value attribute of the element

The problem occurs when setting scopeProp causes the value of scopeProp to be something other than the value attribute of the element.

For example, if scopeProp is defined like this:

set scopeProp(val) {
    return val.toUpperCase();
}

In this scenario, if you type "hello" into the <input> element

  • the input’s value attribute is set to "hello"
  • a "change" event is dispatched on the <input>
  • Event Listener 1 is triggered and calls the scopeProp setter
  • the scopeProp setter sets the value of scopeProp to "HELLO"
  • Event Listener 2 is triggered and sets the value of the <input> to "HELLO"

If you then type "hello" in the <input> element again

  • the value attribute is set to "hello"
  • a "change" event is dispatched on the <input>
  • Event Listener 1 is triggered and calls the scopeProp setter
  • the scopeProp setter sets the value of scopeProp to "HELLO"
  • Event Listener 2 is NOT triggered since scopeProp did not change

At this point the value attribute of the element and scopeProp are no longer the same. However, if you try this in the example below, you’ll notice that the two are always kept in sync:

This is because when using :bind, CanJS will check for this scenario and update the value of the <input> element to be in sync with scopeProp.

If you are using separate bindings like value:from="this.scopeProp" on:change:value:to="this.scopeProp", this does not happen and the values can become out of sync:

With this example it is somewhat complicated to cause the issue; however, there are other scenarios that make it more likely to happen. One of these is when using the value behavior to conditionally resolve with a new value.

The following example sets up a number input that only allows the user to enter odd numbers. It does this by checking the new value whenever lastSet changes and only calling resolve if the number is odd. Try out this example below to see how this works:

Since this example is using value:bind="this.oddNumber", it works correctly. However, if the binding is changed to value:from="this.oddNumber" on:input:value:to="this.oddNumber", the <input> can incorrectly end up with even-numbered values:

Extended examples

Data down, actions up with multiple components

The benefits of the “data down, actions up” pattern become clear when you have multiple components passing actions up to the top-level component. With this setup, there is only one place where state can change within the application, which can make debugging much easier.

The following example has a top-level <pizza-form> component that keeps the lists of ingredients that a user has chosen to top their pizza. It also provides an updateIngredients function for handling the different actions the user can perform. The <pizza-form> passes this function to its children:

<p>
    Selected Ingredients:
    {{# for(meat of this.selectedMeats) }}
        {{ meat }},
    {{/ for }}

    {{# for(veggie of this.selectedVegetables) }}
        {{ veggie }},
    {{/ for }}

    {{# if(this.selectedCheese) }}
        {{ this.selectedCheese }} cheese
    {{/ if }}
</p>

<div>
    <select-one
        listName:raw="cheese"
        update:from="this.updateIngredients"
        default:from="this.selectedCheese"
        options:from="this.availableCheeses">
    </select-one>
</div>

<div>
    <meat-picker
        update:from="this.updateIngredients"
        options:from="this.availableMeats">
    </meat-picker>
</div>

<div>
    <select-many
        listName:raw="vegetables"
        update:from="this.updateIngredients"
        options:from="this.availableVegetables">
    </select-many>
</div>

The child <meat-picker> component uses this function to clear the "meats" list and also passes it to a child of its own:

{{ let showOptions=null }}
<div>
    <label>
        Vegetarian?
            <input
                checked:bind="not( scope.vars.showOptions )"
                on:change="this.update('meats', 'clear')"
                type="checkbox">
    </label>

    {{# if(showOptions) }}
        <select-many
            update:from="update"
            listName:raw="meats"
            options:from="options">
        </select-many>
    {{/ if }}
</div>

This strategy means that all updates throughout the application go through the top-level updateIngredients function. This makes debugging very easy since it is obvious where to put a breakpoint to trace exactly what is causing a change.

Take a look at the example below to see this in action:

Working with related data

The form below has three <select> elements for selecting a make, model and year. Once all three have been selected, a list of vehicles matching the selection is displayed. There are three different APIs being used to load the data for this component:

  • /makes — loads the makes
  • /models — loads the models and years for a selected make
  • /vehicles — loads the vehicles for a selected make, model, and year

There are many useful techniques for working with related data. Before diving in to them, take a look at the example to see how it all works:

Loading initial data

In order to load the initial data, the component makes a request to the /makes API when the page first loads. The component uses a getter to make this API call:

static props = {
    makeId: type.maybeConvert(String),
    makes: {
        get() {
            return ajax({
                type: "GET",
                url: "/makes"
            }).then(resp => resp.data);
        }
    },
    modelId: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("makeId", () => resolve(""));
        }
    },
    get modelsPromise() {
        let makeId = this.makeId;
        if( makeId ) {
            return ajax({
                type: "GET",
                url: "/models",
                data: { makeId }
            }).then(resp => {
                return resp.data;
            });
        }
    },
    models: {
        async(resolve) {
            let promise = this.modelsPromise;
            if(promise) {
                promise.then(resolve);
            }
        }
    },
    get model() {
        let models = this.models,
            modelId = this.modelId;

        if(models && models.length && modelId) {
            let matched = models.filter(model => {
                return modelId == model.id;
            });
            return matched[0];
        }
    },
    get years() {
        let model = this.model;
        return model && model.years;
    },
    year: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("modelId", () => resolve(""));
        }
    },
    vehicles: {
        async(resolve) {
            let year = this.year,
                modelId = this.modelId;

            if(modelId && year) {
                ajax({
                    type: "GET",
                    url: "/vehicles",
                    data: { modelId, year }
                }).then(resp => {
                    resolve(resp.data);
                });
            } else {
                resolve([]);
            }
        }
    }
}

This value is initialized the first time the makes property is used in the view:

<select value:bind="this.makeId"
    {{# if(this.makes.isPending) }}disabled{{/ if }}>
    {{# if(this.makes.isPending) }}
      <option value=''>Loading…</option>
    {{ else }}
      {{^ this.makeId }}
        <option value=''>Select a Make</option>
      {{/ this.makeId }}
      {{# for( make of this.makes.value) }}
        <option value:from="make.id">{{ make.name }}</option>
      {{/ for }}
    {{/ if }}
</select>

{{# if(this.modelsPromise) }}
    {{# if(this.models) }}
        <select value:bind="this.modelId">
            {{^ this.modelId }}
                <option value=''>Select a Model</option>
            {{/ this.modelId }}
            {{# for(model of this.models) }}
                <option value:from="model.id">{{ model.name }}</option>
            {{/ for }}
        </select>
    {{ else }}
        <select disabled><option>Loading Models</option></select>
    {{/ if }}
{{ else }}
    <select disabled><option>Models</option></select>
{{/ if }}

{{# if(this.years) }}
    <select value:bind="this.year">
        {{^ this.year }}
            <option value=''>Select a Year</option>
        {{/ this.year }}
        {{# for(year of this.years ) }}
            <option value:from="year">{{ year }}</option>
        {{/ for }}
    </select>
{{ else }}
    <select disabled><option>Years</option></select>
{{/ if }}

<div>
    {{# for(vehicle of this.vehicles) }}
        <h2>{{ vehicle.name }}</h2>
        <img src:from="vehicle.thumb" width="200px"/>
    {{/ for }}
</div>

Using promises

The makes property is set to the result of calling can-ajax, which returns a promise.

This promise is decorated by can-reflect-promise so that in the view you can easily use metadata related to the promise’s state:

  • state — one of "pending", "resolved", or "rejected"
  • isPending — true if the promise is neither resolved nor rejected, false otherwise.
  • isResolved — true if the promise is resolved, false otherwise.
  • isRejected — true if the promise is rejected, false otherwise.

This also means that in order to get the makes data, we need to use the value of the promise:

<select value:bind="this.makeId"
    {{# if(this.makes.isPending) }}disabled{{/ if }}>
    {{# if(this.makes.isPending) }}
      <option value=''>Loading…</option>
    {{ else }}
      {{^ this.makeId }}
        <option value=''>Select a Make</option>
      {{/ this.makeId }}
      {{# for( make of this.makes.value) }}
        <option value:from="make.id">{{ make.name }}</option>
      {{/ for }}
    {{/ if }}
</select>

{{# if(this.modelsPromise) }}
    {{# if(this.models) }}
        <select value:bind="this.modelId">
            {{^ this.modelId }}
                <option value=''>Select a Model</option>
            {{/ this.modelId }}
            {{# for(model of this.models) }}
                <option value:from="model.id">{{ model.name }}</option>
            {{/ for }}
        </select>
    {{ else }}
        <select disabled><option>Loading Models</option></select>
    {{/ if }}
{{ else }}
    <select disabled><option>Models</option></select>
{{/ if }}

{{# if(this.years) }}
    <select value:bind="this.year">
        {{^ this.year }}
            <option value=''>Select a Year</option>
        {{/ this.year }}
        {{# for(year of this.years ) }}
            <option value:from="year">{{ year }}</option>
        {{/ for }}
    </select>
{{ else }}
    <select disabled><option>Years</option></select>
{{/ if }}

<div>
    {{# for(vehicle of this.vehicles) }}
        <h2>{{ vehicle.name }}</h2>
        <img src:from="vehicle.thumb" width="200px"/>
    {{/ for }}
</div>

In order to avoid having to use .value every time you want to use the data, it can be very useful to split promises into two separate properties—one for the promise and one for the value. Using this “promise splitting” technique, this code could be written like this:

makesPromise: {
    get() {
        return ajax({
            type: "GET",
            url: "/makes"
        }).then(resp => {
            return resp.data;
        });
    }
},
makes: {
    async(resolve) {
        this.makesPromise.then(resolve);
    }
}

This uses a get value for the makesPromise and an asynchronous getter for the makes property.

The asynchronous getter does not return anything, instead it passes the list of makes to resolve. The code above is the same as:

this.makesPromise.then(makes => {
    resolve(makes);
});

Loading new data

In order to load the correct models for a make, a request to the /models API must be made whenever a make is selected. In order to do this, the make <select> element is bound to the makeId property:

<select value:bind="this.makeId"
    {{# if(this.makes.isPending) }}disabled{{/ if }}>
    {{# if(this.makes.isPending) }}
      <option value=''>Loading…</option>
    {{ else }}
      {{^ this.makeId }}
        <option value=''>Select a Make</option>
      {{/ this.makeId }}
      {{# for( make of this.makes.value) }}
        <option value:from="make.id">{{ make.name }}</option>
      {{/ for }}
    {{/ if }}
</select>

{{# if(this.modelsPromise) }}
    {{# if(this.models) }}
        <select value:bind="this.modelId">
            {{^ this.modelId }}
                <option value=''>Select a Model</option>
            {{/ this.modelId }}
            {{# for(model of this.models) }}
                <option value:from="model.id">{{ model.name }}</option>
            {{/ for }}
        </select>
    {{ else }}
        <select disabled><option>Loading Models</option></select>
    {{/ if }}
{{ else }}
    <select disabled><option>Models</option></select>
{{/ if }}

{{# if(this.years) }}
    <select value:bind="this.year">
        {{^ this.year }}
            <option value=''>Select a Year</option>
        {{/ this.year }}
        {{# for(year of this.years ) }}
            <option value:from="year">{{ year }}</option>
        {{/ for }}
    </select>
{{ else }}
    <select disabled><option>Years</option></select>
{{/ if }}

<div>
    {{# for(vehicle of this.vehicles) }}
        <h2>{{ vehicle.name }}</h2>
        <img src:from="vehicle.thumb" width="200px"/>
    {{/ for }}
</div>

The component then uses this property in the modelsPromise getter:

static props = {
    makeId: type.maybeConvert(String),
    makes: {
        get() {
            return ajax({
                type: "GET",
                url: "/makes"
            }).then(resp => resp.data);
        }
    },
    modelId: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("makeId", () => resolve(""));
        }
    },
    get modelsPromise() {
        let makeId = this.makeId;
        if( makeId ) {
            return ajax({
                type: "GET",
                url: "/models",
                data: { makeId }
            }).then(resp => {
                return resp.data;
            });
        }
    },
    models: {
        async(resolve) {
            let promise = this.modelsPromise;
            if(promise) {
                promise.then(resolve);
            }
        }
    },
    get model() {
        let models = this.models,
            modelId = this.modelId;

        if(models && models.length && modelId) {
            let matched = models.filter(model => {
                return modelId == model.id;
            });
            return matched[0];
        }
    },
    get years() {
        let model = this.model;
        return model && model.years;
    },
    year: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("modelId", () => resolve(""));
        }
    },
    vehicles: {
        async(resolve) {
            let year = this.year,
                modelId = this.modelId;

            if(modelId && year) {
                ajax({
                    type: "GET",
                    url: "/vehicles",
                    data: { modelId, year }
                }).then(resp => {
                    resolve(resp.data);
                });
            } else {
                resolve([]);
            }
        }
    }
}

When the view uses the modelsPromise property, it will become bound, which means it

  • will be called once to get its value
  • will cache this initial value
  • will set up listeners for any observables used within the getter

If the modelsPromise is read a second time, the cached value will be returned.

If the makeId property changes, the getter will be called again and a new request will be made to the /models API.

Resetting a selection when its parent list changes

Similar to the makeId, the <select> for models is bound to the modelId property:

<select value:bind="this.makeId"
    {{# if(this.makes.isPending) }}disabled{{/ if }}>
    {{# if(this.makes.isPending) }}
      <option value=''>Loading…</option>
    {{ else }}
      {{^ this.makeId }}
        <option value=''>Select a Make</option>
      {{/ this.makeId }}
      {{# for( make of this.makes.value) }}
        <option value:from="make.id">{{ make.name }}</option>
      {{/ for }}
    {{/ if }}
</select>

{{# if(this.modelsPromise) }}
    {{# if(this.models) }}
        <select value:bind="this.modelId">
            {{^ this.modelId }}
                <option value=''>Select a Model</option>
            {{/ this.modelId }}
            {{# for(model of this.models) }}
                <option value:from="model.id">{{ model.name }}</option>
            {{/ for }}
        </select>
    {{ else }}
        <select disabled><option>Loading Models</option></select>
    {{/ if }}
{{ else }}
    <select disabled><option>Models</option></select>
{{/ if }}

{{# if(this.years) }}
    <select value:bind="this.year">
        {{^ this.year }}
            <option value=''>Select a Year</option>
        {{/ this.year }}
        {{# for(year of this.years ) }}
            <option value:from="year">{{ year }}</option>
        {{/ for }}
    </select>
{{ else }}
    <select disabled><option>Years</option></select>
{{/ if }}

<div>
    {{# for(vehicle of this.vehicles) }}
        <h2>{{ vehicle.name }}</h2>
        <img src:from="vehicle.thumb" width="200px"/>
    {{/ for }}
</div>

This works great for selecting a model from the list given for a particular make; however, if the make changes, the selected modelId will point to a different model in the list for the new make—or it might not exist at all.

In order to handle this parent-child relationship correctly, the modelId property needs to be bound to the value in its own <select> element, but it also needs to be cleared when the value of the parent <select> element changes. The value behavior makes it possible to define properties that are composed from events of other properties on the map.

In order to define modelId, the value behavior will

  • call resolve with the new modelId when lastSet changes—this is whenever a new model is chosen from the <select>
  • call resolve with an empty string when makeId changes to reset the <select> back to the default <option>
static props = {
    makeId: String,
    makes: {
        get() {
            return ajax({
                type: "GET",
                url: "/makes"
            }).then(resp =>
                return resp.data;
            });
        }
    },
    modelId: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("makeId", () => resolve(""));
        }
    },
    get modelsPromise() {
        let makeId = this.makeId;
        if( makeId ) {
            return ajax({
                type: "GET",
                url: "/models",
                data: { makeId }
            }).then(resp => {
                return resp.data;
            });
        }
    },
    models: {
        async(resolve) {
            let promise = this.modelsPromise;
            if(promise) {
                promise.then(resolve);
            }
        }
    },
    get model() {
        let models = this.models,
            modelId = this.modelId;

        if(models && models.length && modelId) {
            let matched = models.filter(model)] => {
                return modelId == model.id;
            });
            return matched[0];
        }
    },
    get years() {
        let model = this.model;
        return model && model.years;
    },
    year: {
        type: String,
        value({ lastSet, listenTo, resolve }) {
            listenTo(lastSet, resolve);

            listenTo("modelId", () => resolve(""));
        }
    },
    vehicles: {
        async(resolve) {
            let year = this.year,
                modelId = this.modelId;

            if(modelId && year) {
                ajax({
                    type: "GET",
                    url: "/vehicles",
                    data: { modelId, year }
                }).then(resp => {
                    resolve(resp.data);
                });
            } else {
                resolve([]);
            }
        }
    }
}

Using this technique allows you to easily define the parent-child relationship between make and model while also keeping all of the code that specifies how modelId works within the modelId DefinitionObject.

Creating and updating data

Interested in adding examples for how to create and update data on the server? Take a look at this issue.

Have a question?

Please ask on our forums or in Slack!

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

On this page

Get help

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