Forms
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 theeither-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 totrue
. This means that any of these will set thebestTypeOfPet
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 theboolean-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 theequal
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 theindex-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 theselected-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 thestring-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 thevalue
attribute, and setsscopeProp
- Event Listener 2 listens for changes to
scopeProp
and sets thevalue
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 ofscopeProp
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 ofscopeProp
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 newmodelId
whenlastSet
changes—this is whenever a new model is chosen from the<select>
- call
resolve
with an empty string whenmakeId
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!