Aurelia Value Converters — Transforming Data for Display
In this tutorial, you will learn about Aurelia Value Converters. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia value converters transform data between the ViewModel and the View. They format dates, currencies, strings, and complex objects for display. Converters use a toView method for display transformation and a fromView method for input transformation.
What You'll Learn
You will learn how to create value converters, use parameters, chain converters together, and implement two-way conversion for form inputs.
Why It Matters
Converters keep templates clean by moving formatting logic out of the ViewModel. A date formatting converter used in 50 templates can be updated in one place. Converters are pure functions that are easy to test.
Real-World Use
A financial dashboard uses converters for currency formatting, percentage display, file size formatting, date localization, and status labeling. Each converter is a pure function with unit tests.
flowchart LR
A[ViewModel] --> B[toView]
B --> C[View]
C --> D[fromView]
D --> A
B --> E[Parameters]
B --> F[Chained converters]
Basic Converter
// src/resources/value-converters/uppercase.ts
export class UppercaseValueConverter {
toView(value) {
if (!value) return '';
return value.toUpperCase();
}
}
<template>
<p>${message | uppercase}</p>
</template>
Date Format Converter
// src/resources/value-converters/date-format.ts
export class DateFormatValueConverter {
toView(value, format = 'medium') {
if (!value) return '';
const date = new Date(value);
const options = {
short: { month: 'numeric', day: 'numeric', year: '2-digit' },
medium: { month: 'short', day: 'numeric', year: 'numeric' },
long: { month: 'long', day: 'numeric', year: 'numeric' },
full: { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' },
time: { hour: '2-digit', minute: '2-digit' },
datetime: {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}
};
return date.toLocaleDateString('en-US', options[format] || options.medium);
}
}
<template>
<p>Published: ${post.date | dateFormat}</p>
<p>Published: ${post.date | dateFormat:'long'}</p>
<p>Due: ${task.dueDate | dateFormat:'datetime'}</p>
</template>
Currency Converter
// src/resources/value-converters/currency.ts
export class CurrencyValueConverter {
toView(value, symbol = '$', decimals = 2) {
if (value == null || isNaN(value)) return `${symbol}0.00`;
const formatted = Number(value).toFixed(decimals);
return `${symbol}${Number(formatted).toLocaleString('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})}`;
}
fromView(value, symbol = '$') {
if (!value) return 0;
return parseFloat(value.replace(symbol, '').replace(/,/g, ''));
}
}
<template>
<p>Price: ${product.price | currency}</p>
<p>Price: ${product.price | currency:'€':2}</p>
<!-- Two-way binding with fromView -->
<input value.bind="amount | currency" />
</template>
Chaining Converters
<template>
<!-- Apply multiple converters in sequence -->
<p>${message | lowercase | capitalize}</p>
<p>${post.body | truncate:100 | uppercase}</p>
<p>${data.rawDate | dateFormat:'short' | uppercase}</p>
</template>
// lowercase.ts
export class LowercaseValueConverter {
toView(value) { return value ? value.toLowerCase() : ''; }
}
// capitalize.ts
export class CapitalizeValueConverter {
toView(value) {
if (!value) return '';
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
Truncate Converter
// src/resources/value-converters/truncate.ts
export class TruncateValueConverter {
toView(value, maxLength = 100, suffix = '...') {
if (!value) return '';
if (value.length <= maxLength) return value;
return value.substring(0, maxLength).replace(/\s+\S*$/, '') + suffix;
}
// Reverse truncation? Just return the full value
fromView(value) {
return value;
}
}
File Size Converter
// src/resources/value-converters/file-size.ts
export class FileSizeValueConverter {
toView(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
if (!bytes) return '';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
}
<template>
<p>File size: ${file.size | fileSize}</p>
<p>Exact: ${file.size | fileSize:3}</p>
</template>
Status Label Converter
// src/resources/value-converters/status-label.ts
export class StatusLabelValueConverter {
toView(value) {
const labels = {
active: 'Active',
inactive: 'Inactive',
pending: 'Pending Approval',
suspended: 'Suspended',
deleted: 'Deleted'
};
return labels[value] || value;
}
}
// src/resources/value-converters/status-class.ts
export class StatusClassValueConverter {
toView(value) {
const classes = {
active: 'text-green bg-green-light',
pending: 'text-yellow bg-yellow-light',
suspended: 'text-red bg-red-light',
deleted: 'text-gray bg-gray-light'
};
return classes[value] || 'text-gray';
}
}
<template>
<span class="badge ${user.status | statusClass}">
${user.status | statusLabel}
</span>
</template>
Array Converters
// src/resources/value-converters/join.ts
export class JoinValueConverter {
toView(array, separator = ', ') {
if (!array) return '';
return array.join(separator);
}
}
// src/resources/value-converters/sort-by.ts
export class SortByValueConverter {
toView(array, property, direction = 'asc') {
if (!array) return [];
return [...array].sort((a, b) => {
let valA = a[property];
let valB = b[property];
if (valA < valB) return direction === 'asc' ? -1 : 1;
if (valA > valB) return direction === 'asc' ? 1 : -1;
return 0;
});
}
}
<template>
<p>Tags: ${post.tags | join}</p>
<p>Tags: ${post.tags | join:'; '}</p>
<ul>
<li repeat.for="user of users | sortBy:'name'">
${user.name}
</li>
</ul>
<ul>
<li repeat.for="user of users | sortBy:'createdAt':'desc'">
${user.name}
</li>
</ul>
</template>
Sanitize HTML Converter
// src/resources/value-converters/sanitize.ts
export class SanitizeValueConverter {
toView(value) {
if (!value) return '';
// Strip dangerous tags
return value
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/gi, '');
}
}
Common Mistakes
- Not handling null/undefined input. Converters should return a safe default (empty string or zero) for null/undefined values.
- Mutating the input value. Converters should be pure functions. Create and return transformed copies, not mutated originals.
- Forgetting
fromViewfor two-way bindings. If a converter is used with two-way binding, implementfromViewto reverse the transformation. - Over-chaining converters. Long chains indicate that a ViewModel property should have the computed value directly.
- Not registering converters globally. Converters used across multiple views should be globally registered to avoid repeated
requirestatements.
Practice Questions
- What is the purpose of
toViewin a value converter? - When would you implement
fromView? - How do you pass parameters to a converter?
- How do you chain multiple converters?
- Challenge: Create a converter that formats a number as a percentage with configurable decimal places. Create a converter that pluralizes words (e.g., 1 item, 2 items). Create a converter that highlights search terms in text. Chain all three in a template.
FAQ
{{< faq "Can I use converters in repeat bindings?" "Yes. `repeat.for=\"item of items | sortBy:'name'\"` works." >}}Mini Project
Create a converter library with: (1) timeAgo — relative time display (2 minutes ago, 3 hours ago), (2) highlight — highlights search terms in text with <mark> tags, (3) ellipsis — truncates text at word boundary, (4) initials — extracts initials from a name, (5) phone — formats phone numbers. Demonstrate each converter and chain at least two.
What's Next
Now that you understand value converters, learn Aurelia Binding Commands for advanced binding patterns. Then explore Aurelia Conditional Rendering for dynamic templates.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro