Form Inputs
Handling forms and user input is quite a common task in applications. Whether you are building a login form, data entry, or even a chat application, Aurelia allows you to work with forms intuitively.
In Aurelia, the binding system uses
two-way
binding as a default for form elements. Text inputs, text areas and even contenteditable
elements all use a two-way
binding.Many of the concepts discussed here assume knowledge of how Aurelia's template and binding syntax work. We recommend reading the Template syntax & features section before continuing with this section if you are new to Aurelia.
In Aurelia, form elements are reactive, and their changes are directly tied to the underlying view model. Updates flow from the view to the view model, and updates from the view model flow to the view (hence, two-way).
To illustrate how
two-way
binding works in forms, let's break down the workflow:- 1.The user types a value into the input element. The element is for a first name, so they enter John.
- 2.The native form input events are fired, and Aurelia also sees the value has changed.
- 3.The binding system sees the new value and notifies the view model to update the value.
- 4.Any reference to the bound value will be updated without needing any callback functions or additional notification steps (the value changes).
Creating forms in Aurelia requires no special configuration or treatment. Create a form element and add form input controls with bindings. Here is a basic form example for a login form to show you how little code you need.
Firstly, let's create the markup for our login form:
login-component.html
<form submit.trigger="handleLogin()">
<div>
<label for="email">Email:</label>
<input id="email" type="text" value.bind="email">
</div>
<div>
<label for="password">Password:</label>
<input id="password" type="password" value.bind="password">
</div>
<button type="submit">Login</button>
</form>
Before we write the view model code, let's break down what we did here:
- We created a form with two text inputs
- We used
value.bind
to bind the native value attribute of these fields to the corresponding view model properties - We are calling a function
handleLogin
when thesubmit
event on the form is triggered to handle the bindable properties inside
Now, the corresponding view model code:
login-component.ts
export class LoginComponent {
private email = '';
private password = '';
// This function is called when the form is submitted
handleLogin() {
// Call an API/validate the bound values
}
}
There is not a whole lot of code here for what is happening. Whenever the
email
or password
values change, they will be reflected inside of our view model. Inside the handleLogin
method, we would probably validate the data and call an API.Using
submit.trigger
on a form will prevent its default action by applying a event.preventDefault
behind-the-scenes. This means your form will not submit to the action or method attributes on the form, you will need to handle this manually.Binding to text inputs uses a syntax similar to binding to other elements in Aurelia. By default, input elements will use
two-way
binding, which means the value will update in the view when changed inside the view model and updated in the view model when changed in the view.<form>
<label>User value</label><br>
<input type="text" value.bind="userValue" />
</form>
You can even bind to other attributes on form elements such as the
placeholder
attribute.<form>
<label>User value</label><br>
<input type="text" value.bind="userValue" placeholder.bind="myPlaceholder" />
</form>
A textarea element is just like any other form element. It allows you to bind to its value and, by default,
value.bind
will be two-way binding (meaning changes flow from out of the view into the view model and changes in the view-model flow back to the view).<form role="form">
<textarea value.bind="textAreaValue"></textarea>
</form>
Aurelia supports the two-way binding of various data types to checkbox input elements.
Bind a boolean property to an input element's
checked
attribute using checked.bind="myBooleanProperty"
.export class App {
motherboard = false;
cpu = false;
memory = false;
}
<template>
<form>
<h4>Products</h4>
<label><input type="checkbox" checked.bind="motherboard"> Motherboard</label>
<label><input type="checkbox" checked.bind="cpu"> CPU</label>
<label><input type="checkbox" checked.bind="memory"> Memory</label>
motherboard = ${motherboard}<br>
cpu = ${cpu}<br>
memory = ${memory}<br>
</form>
</template>
A set of checkbox elements is a multiple-selection interface. If you have an array that serves as the "selected items" list, you can bind the array to each input's
checked
attribute. The binding system will track the input's checked status, adding the input's value to the array when the input is checked and removing the input's value from the array when the input is unchecked.To define the input's "value", bind the input's
model
attribute: model.bind="product.id"
.export class App {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProductIds = [];
}
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="checkbox" model.bind="product.id" checked.bind="selectedProductIds">
${product.id} - ${product.name}
</label>
<br>
Selected product IDs: ${selectedProductIds}
</form>
</template>
Numbers aren't the only type of value you can store in a "selected items" array. The binding system supports all types, including objects. Here's an example that adds and removes "product" objects from a
selectedProducts
array using the checkbox data-binding.export interface IProduct {
id: number;
name: string;
}
export class App {
products: IProduct[] = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProducts: IProduct[] = [];
}
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="checkbox" model.bind="product" checked.bind="selectedProducts">
${product.id} - ${product.name}
</label>
Selected products:
<ul>
<li repeat.for="product of selectedProducts">${product.id} - ${product.name}</li>
</ul>
</form>
</template>
You may run into situations where the object your input element's model is bound to do not have reference equality to any objects in your checked array. The objects might match by id, but they may not be the same object instance. To support this scenario, you can override Aurelia's default "matcher", which is an equality comparison function that looks like this:
(a, b) => a === b
.You can substitute your chosen function with the right logic to compare your objects.
export class App {
selectedProducts = [
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' }
];
productMatcher = (a, b) => a.id === b.id;
}
<template>
<form>
<h4>Products</h4>
<label>
<input type="checkbox" model.bind="{ id: 0, name: 'Motherboard' }"
matcher.bind="productMatcher"
checked.bind="selectedProducts">
Motherboard
</label>
<label>
<input type="checkbox" model.bind="{ id: 1, name: 'CPU' }"
matcher.bind="productMatcher"
checked.bind="selectedProducts">
CPU
</label>
<label>
<input type="checkbox" model.bind="{ id: 2, name: 'Memory' }"
matcher.bind="productMatcher"
checked.bind="selectedProducts">
Memory
</label>
Selected products:
<ul>
<li repeat.for="product of selectedProducts">${product.id} - ${product.name}</li>
</ul>
</form>
</template>
Finally, here's an example that adds and removes strings from an
selectedProducts
array using the checkbox data-binding. This is example is unique because it does not use model.bind
to assign each checkbox's value. Instead, the input's standard value
attribute is used.Normally we cannot use the standard
value
attribute in conjunction with checked binding because it coerces anything assigned to a string. This example uses an array of strings, so everything works just fine.export class App {
products = ['Motherboard', 'CPU', 'Memory'];
selectedProducts = [];
}
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="checkbox" value.bind="product" checked.bind="selectedProducts">
${product}
</label>
<br>
Selected products: ${selectedProducts}
</form>
</template>
A radio input group is a "single select" interface. Aurelia supports two-way binding any type of property to a group of radio inputs. The examples below illustrate binding number, object, string and boolean properties to sets of radio inputs. In each of the examples, there's a common set of steps:
- 1.Group the radios via the
name
property. Radio buttons with the same value for the name attribute are in the same "radio button group"; only one radio button in a group can be selected at a time. - 2.Define each radio's value using the
model
property. - 3.Two-way bind each radio's
checked
attribute to a "selected item" property on the view model.
Let's start with an example that uses a numeric "selected item" property. Each radio input will be assigned a number value via the model property in this example. Selecting a radio will cause its model value to be assigned to the
selectedProductId
property.export class App {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProductId = null;
}
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="radio" name="group1"
model.bind="product.id" checked.bind="selectedProductId">
${product.id} - ${product.name}
</label>
<br>
Selected product ID: ${selectedProductId}
</form>
</template>
The binding system supports binding all types of radios, including objects. Here's an example that binds a group of radios to a
selectedProduct
object property.export class App {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProduct = null;
}
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="radio" name="group2"
model.bind="product" checked.bind="selectedProduct">
${product.id} - ${product.name}
</label>
Selected product: ${selectedProduct.id} - ${selectedProduct.name}
</form>
</template>
You may run into situations where the object your input element's model is bound to does not have reference equality to any of the objects in your checked attribute bound to. The objects might match by id, but they may not be the same object instance. To support this scenario, you can override Aurelia's default "matcher", which is an equality comparison function that looks like this:
(a, b) => a === b
.You can substitute your chosen function with the right logic to compare your objects.
export class App {
selectedProduct = { id: 1, name: 'CPU' };
productMatcher = (a, b) => a.id === b.id;
}
<template>
<form>
<h4>Products</h4>
<label>
<input type="radio" name="group3"
model.bind="{ id: 0, name: 'Motherboard' }"
matcher.bind="productMatcher"
checked.bind="selectedProduct">
Motherboard
</label>
<label>
<input type="radio" name="group3"
model.bind="{ id: 1, name: 'CPU' }"
matcher.bind="productMatcher"
checked.bind="selectedProduct">
CPU
</label>
<label>
<input type="radio" name="group3"
model.bind="{ id: 2, name: 'Memory' }"
matcher.bind="productMatcher"
checked.bind="selectedProduct">
Memory
</label>