LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • 0001 to 0023
      • 0088 to 0723
      • 0901 to 0908
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page
  • Data Flow in Forms
  • Creating a Basic Form
  • Binding With Text and Textarea Inputs
  • Text Input
  • Textarea
  • Binding with Checkbox Inputs
  • Booleans
  • Array of Numbers
  • Array of Objects
  • Array of Objects with Matcher
  • Array of Strings
  • Binding with Radio Inputs
  • Binding with Select Elements
  • Single Select (Numbers)
  • Single Select (Objects)
  • Single Select (Objects with Matcher)
  • Single Select (Boolean)
  • Single Select (Strings)
  • Multiple Select (Numbers)
  • Multiple Select (Objects)
  • Multiple Select (Strings)
  • Form Submission
  • File Inputs and Upload Handling
  • Capturing File Data
  • View Model Handling
  • Single File Inputs
  • Validation and Security
  • Form Validation

Was this helpful?

Export as PDF
  1. Templates

Form Inputs

Learn how to build forms in Aurelia, bind data to various input elements, and handle submission and validation.

PreviousBinding behaviorsNextCSS classes and styling

Last updated 2 months ago

Was this helpful?

Handling forms and user input is a common task in most applications. Whether you are building a login form, a data-entry screen, or even a chat interface, Aurelia makes it intuitive to work with forms. By default, Aurelia’s binding system uses two-way binding for form elements (like <input>, <textarea>, and contenteditable elements), which keeps your view and model in sync automatically.

Many of the concepts discussed here assume some familiarity with Aurelia’s binding and template syntax. If you’re new, please read the section first.


Data Flow in Forms

Aurelia’s two-way binding updates your view model properties whenever users enter data into form elements, and likewise updates the form elements if the view model changes:

  1. The user types in the input (e.g., John).

  2. The native input events fire. Aurelia observes the value change.

  3. The binding system updates the corresponding view model property.

  4. Any references to that property automatically reflect its new value.

Because of this automatic synchronization, you generally don’t need to write custom event handlers or watchers to track form inputs.


Creating a Basic Form

Aurelia lets you create forms in pure HTML without any special setup. Here’s a simple login form illustrating how little code is required.

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>

Key Points:

  • We created a form with two inputs: email and password.

  • The value.bind syntax binds these inputs to class properties named email and password.

  • We call a handleLogin() method on submit to process the form data.

And here is the view model (login-component.ts):

login-component.ts
export class LoginComponent {
  private email = "";
  private password = "";

  handleLogin() {
    // Validate credentials or call an API
    console.log(`Email: ${this.email}, Password: ${this.password}`);
  }
}

Whenever the email or password fields change in the UI, their corresponding view model properties are updated. Then, in handleLogin(), you can handle form submission however you wish.

Using submit.trigger on a form prevents the default browser submission. If you want the form to submit normally, return true from your handler or remove submit.trigger entirely.


Binding With Text and Textarea Inputs

Text Input

Binding to text inputs in Aurelia is straightforward:

<form>
  <label>User value:</label><br />
  <input type="text" value.bind="userValue" />
</form>

You can also bind other attributes like placeholder:

<form>
  <label>User value:</label><br />
  <input type="text" value.bind="userValue" placeholder.bind="myPlaceholder" />
</form>

Textarea

Textareas work just like text inputs, with value.bind for two-way binding:

<form>
  <label>Comments:</label><br />
  <textarea value.bind="textAreaValue"></textarea>
</form>

Any changes to textAreaValue in the view model will show up in the <textarea>, and vice versa.


Binding with Checkbox Inputs

Aurelia supports two-way binding for checkboxes in various configurations.

Booleans

Bind a boolean property to the checked attribute:

export class MyApp {
  motherboard = false;
  cpu = false;
  memory = false;
}
<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>

Array of Numbers

When using checkboxes as a multi-select, bind an array to each input’s checked attribute. Provide a model for each checkbox to indicate its value:

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];

  selectedProductIds = [];
}
<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>

Array of Objects

Numbers aren’t the only value type you can store. Here’s how to manage an array of objects:

export interface IProduct {
  id: number;
  name: string;
}

export class MyApp {
  products: IProduct[] = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProducts: IProduct[] = [];
}
<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>

Array of Objects with Matcher

If your objects do not share reference equality (e.g., same data, different instances), define a custom matcher:

export class MyApp {
  selectedProducts = [
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];

  productMatcher = (a, b) => a.id === b.id;
}
<form>
  <h4>Products</h4>
  <label>
    <input
      type="checkbox"
      model.bind="{ id: 0, name: 'Motherboard' }"
      matcher.bind="productMatcher"
      checked.bind="selectedProducts"
    />
    Motherboard
  </label>
  ...
</form>

Array of Strings

If your “selected items” array holds strings, you can rely on the standard value attribute:

export class MyApp {
  products = ["Motherboard", "CPU", "Memory"];
  selectedProducts = [];
}
<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>

Binding with Radio Inputs

Radio groups in Aurelia are similarly straightforward. Only one radio button in a group can be checked at a time.

Numbers

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProductId = null;
}
<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>

Objects

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProduct = null;
}
<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>

Objects with Matcher

If the selected object doesn’t share reference equality, define a custom matcher:

export class MyApp {
  selectedProduct = { id: 1, name: "CPU" };
  productMatcher = (a, b) => a.id === b.id;
}
<form>
  <h4>Products</h4>
  <label>
    <input
      type="radio"
      name="group3"
      model.bind="{ id: 0, name: 'Motherboard' }"
      matcher.bind="productMatcher"
      checked.bind="selectedProduct"
    />
    Motherboard
  </label>
  ...
</form>

Booleans

export class MyApp {
  likesCake = null;
}
<form>
  <h4>Do you like cake?</h4>
  <label>
    <input
      type="radio"
      name="group4"
      model.bind="null"
      checked.bind="likesCake"
    />
    Don't Know
  </label>
  <label>
    <input
      type="radio"
      name="group4"
      model.bind="true"
      checked.bind="likesCake"
    />
    Yes
  </label>
  <label>
    <input
      type="radio"
      name="group4"
      model.bind="false"
      checked.bind="likesCake"
    />
    No
  </label>

  likesCake = ${likesCake}
</form>

Strings

export class MyApp {
  products = ["Motherboard", "CPU", "Memory"];
  selectedProduct = null;
}
<form>
  <h4>Products</h4>
  <label repeat.for="product of products">
    <input
      type="radio"
      name="group5"
      value.bind="product"
      checked.bind="selectedProduct"
    />
    ${product}
  </label>
  <br />
  Selected product: ${selectedProduct}
</form>

Binding with Select Elements

You can use <select> as either a single-select or a multiple-select input:

  1. Use value.bind in single-select mode.

  2. Use value.bind to an array in multiple-select mode.

  3. Provide <option> elements that specify their own model (or value) attributes.

Single Select (Numbers)

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProductId = null;
}
<label>
  Select product:
  <select value.bind="selectedProductId">
    <option model.bind="null">Choose...</option>
    <option repeat.for="product of products" model.bind="product.id">
      ${product.id} - ${product.name}
    </option>
  </select>
</label>
Selected product ID: ${selectedProductId}

Single Select (Objects)

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProduct = null;
}
<label>
  Select product:
  <select value.bind="selectedProduct">
    <option model.bind="null">Choose...</option>
    <option repeat.for="product of products" model.bind="product">
      ${product.id} - ${product.name}
    </option>
  </select>
</label>

Selected product: ${selectedProduct.id} - ${selectedProduct.name}

Single Select (Objects with Matcher)

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  productMatcher = (a, b) => a.id === b.id;
  selectedProduct = { id: 1, name: "CPU" };
}
<label>
  Select product:
  <select value.bind="selectedProduct" matcher.bind="productMatcher">
    <option model.bind="null">Choose...</option>
    <option repeat.for="product of products" model.bind="product">
      ${product.id} - ${product.name}
    </option>
  </select>
</label>

Selected product: ${selectedProduct.id} - ${selectedProduct.name}

Single Select (Boolean)

export class MyApp {
  likesTacos = null;
}
<label>
  Do you like tacos?
  <select value.bind="likesTacos">
    <option model.bind="null">Choose...</option>
    <option model.bind="true">Yes</option>
    <option model.bind="false">No</option>
  </select>
</label>
likesTacos = ${likesTacos}

Single Select (Strings)

export class MyApp {
  products = ["Motherboard", "CPU", "Memory"];
  selectedProduct = "";
}
<label>
  Select product:
  <select value.bind="selectedProduct">
    <option value="">Choose...</option>
    <option repeat.for="product of products" value.bind="product">
      ${product}
    </option>
  </select>
</label>
Selected product: ${selectedProduct}

Multiple Select (Numbers)

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProductIds = [];
}
<label>
  Select products:
  <select multiple value.bind="selectedProductIds">
    <option repeat.for="product of products" model.bind="product.id">
      ${product.id} - ${product.name}
    </option>
  </select>
</label>
Selected product IDs: ${selectedProductIds}

Multiple Select (Objects)

export class MyApp {
  products = [
    { id: 0, name: "Motherboard" },
    { id: 1, name: "CPU" },
    { id: 2, name: "Memory" },
  ];
  selectedProducts = [];
}
<label>
  Select products:
  <select multiple value.bind="selectedProducts">
    <option repeat.for="product of products" model.bind="product">
      ${product.id} - ${product.name}
    </option>
  </select>
</label>

Selected products:
<ul>
  <li repeat.for="product of selectedProducts">
    ${product.id} - ${product.name}
  </li>
</ul>

Multiple Select (Strings)

export class MyApp {
  products = ["Motherboard", "CPU", "Memory"];
  selectedProducts = [];
}
<label>
  Select products:
  <select multiple value.bind="selectedProducts">
    <option repeat.for="product of products" value.bind="product">
      ${product}
    </option>
  </select>
</label>
Selected products: ${selectedProducts}

Form Submission

Typically, a <form> groups related inputs. Aurelia allows you to intercept submission using submit.trigger:

<form submit.trigger="submitMyForm()">...</form>
export class MyApp {
  submitMyForm() {
    // Custom logic, e.g., fetch POST to an API endpoint
    fetch("/register", { method: "POST" /* ... */ });
  }
}

For <form> elements without a method (or method="GET"), Aurelia automatically calls event.preventDefault() to avoid a full page reload. If you prefer the default browser submission, return true from your handler:

export class MyApp {
  submitMyForm() {
    // Possibly do some checks...
    return true; // Allow normal form submission
  }
}

File Inputs and Upload Handling

Working with file uploads in Aurelia typically involves using the standard <input type="file"> element and handling file data in your view model. While Aurelia doesn’t provide special bindings for file inputs, you can easily wire up event handlers or use standard properties to capture and upload files.

Capturing File Data

In most cases, you’ll want to listen for the change event on a file input:

file-upload-component.html
<form>
  <label for="fileUpload">Select files to upload:</label>
  <input
    id="fileUpload"
    type="file"
    multiple
    accept="image/*"
    change.trigger="handleFileSelect($event)"
  />

  <button click.trigger="uploadFiles()" disabled.bind="!selectedFiles.length">
    Upload
  </button>
</form>
  • multiple: Allows selecting more than one file.

  • accept="image/*": Restricts file selection to images (this can be changed to fit your needs).

  • change.trigger="handleFileSelect($event)": Calls a method in your view model to handle the file selection event.

View Model Handling

You can retrieve the selected files from the event object in your view model:

file-upload-component.ts
export class FileUploadComponent {
  public selectedFiles: File[] = [];

  public handleFileSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    if (!input.files?.length) {
      return;
    }
    // Convert the FileList to a real array
    this.selectedFiles = Array.from(input.files);
  }

  public async uploadFiles() {
    if (this.selectedFiles.length === 0) {
      return;
    }

    const formData = new FormData();
    for (const file of this.selectedFiles) {
      // The first argument (key) matches the field name expected by your backend
      formData.append('files', file, file.name);
    }

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });

      if (!response.ok) {
        throw new Error(`Upload failed with status ${response.status}`);
      }

      const result = await response.json();
      console.log('Upload successful:', result);
      // Optionally, reset selected files
      this.selectedFiles = [];
    } catch (error) {
      console.error('Error uploading files:', error);
    }
  }
}

Key Points:

  • Reading File Data: input.files returns a FileList; converting it to an array (Array.from) makes it easier to iterate over.

  • FormData: Using FormData to append files is a convenient way to send them to the server (via Fetch).

  • Error Handling: Always check response.ok to handle server or network errors.

  • Disabling the Button: In the HTML, disabled.bind="!selectedFiles.length" keeps the button disabled until at least one file is selected.

Single File Inputs

If you only need a single file, omit multiple and simplify your logic:

<input type="file" accept="image/*" change.trigger="handleFileSelect($event)" />
public handleFileSelect(event: Event) {
  const input = event.target as HTMLInputElement;
  this.selectedFiles = input.files?.length ? [input.files[0]] : [];
}

Validation and Security

When handling file uploads, consider adding validation and security measures:

  • Server-side Validation: Even if you filter files by type on the client (accept="image/*"), always verify on the server to ensure the files are valid and safe.

  • File Size Limits: Check file sizes either on the client or server (or both) to prevent excessively large uploads.

  • Progress Indicators: For a better user experience, consider using XMLHttpRequest or the Fetch API with progress events (via third-party solutions or polyfills), so you can display an upload progress bar.


Form Validation

Validation is essential for robust, user-friendly forms. Aurelia provides a dedicated Validation plugin that helps you:

  • Validate inputs using built-in or custom rules.

  • Display error messages and warnings.

  • Integrate seamlessly with Aurelia’s binding system.

Template Syntax & Features
Validation