# Introduction

Get acquainted with Aurelia, the documentation, and how to get started.

## What is Aurelia?

**Aurelia is the web framework that feels native to the browser.** Built on web standards with zero overhead from virtual DOMs or magic abstractions, Aurelia delivers exceptional performance while keeping your code clean and maintainable.

**Why developers choose Aurelia:**

* **Efficient bundle sizes** with smart code splitting and async loading
* **Standards-based architecture** that leverages browser capabilities
* **TypeScript-first** with powerful dependency injection built-in
* **No breaking changes** since 2.0 - stable, production-ready platform

If you value web standards, performance, and developer experience over framework hype, Aurelia is built for you.

## Choose Your Path

**👋 New to Aurelia?** Start with our [Complete Getting Started Guide](/getting-started/complete-guide) - build a real app in 15 minutes.

**🔧 Want the concepts first?** Begin with [Essentials](/introduction/essentials) for focused introduction to core concepts.

**🚀 Migrating from another framework?**

* [From React to Aurelia](/coming-from-another-framework/from-react) - Better performance, cleaner code
* [From Vue to Aurelia](/coming-from-another-framework/from-vue) - All the simplicity, better performance
* [From Angular to Aurelia](/coming-from-another-framework/from-angular) - Keep the good, lose the complexity

**⚡ Just want to see it work?** Try our [Quick Install Guide](/getting-started/quick-install-guide) for immediate setup.

{% content-ref url="/pages/yKMT6vhgKGVeVIzMktwn" %}
[Complete Getting Started Guide](/getting-started/complete-guide)
{% endcontent-ref %}

## Why Aurelia Outperforms

### **Performance That Matters**

* **Direct DOM manipulation** - no virtual DOM overhead
* **Batched rendering** for optimal browser performance
* **Efficient memory usage** - applications that don't drain batteries
* **Optimized bundle sizes** - smart code splitting and async loading

### **Standards-First Development**

* **Web Components foundation** - build for the future of the web
* **Modern JavaScript/TypeScript** - no proprietary abstractions
* **Seamless third-party integration** - works with any library
* **Future-proof architecture** - leverages browser capabilities

### **Developer Experience**

* **Powerful dependency injection** built-in, no external libraries needed
* **Two-way data binding** with unidirectional safety
* **Complete ecosystem** - routing, validation, i18n, testing, CLI
* **API stability** - no breaking changes since 2.0, stable production platform

### **Production Ready**

* **Used by enterprise companies** worldwide
* **MIT licensed** open-source with active development
* **Comprehensive testing tools** built-in
* **Strong TypeScript support** from day one

## Built for the Future of Web Development

Aurelia isn't chasing the latest trends - it's built on the fundamental technologies that power the web. By embracing web standards and leveraging browser capabilities, Aurelia applications are inherently future-proof and performant.

**Our focused approach means:**

* **Thoughtful feature development** - every addition serves a clear purpose
* **Direct access to the core team** - your feedback directly shapes the framework
* **Stable, reliable releases** - no breaking changes that disrupt your projects
* **Quality ecosystem** - carefully curated tools and plugins that work seamlessly together

## Direct DOM: Maximum Performance, Minimum Overhead

Aurelia delivers superior performance by working directly with the browser's DOM instead of creating unnecessary abstraction layers.

### **Why Direct DOM Wins**

**Performance Advantages:**

* **Zero virtual DOM overhead** - no diffing algorithms or reconciliation cycles
* **Faster rendering** - direct updates where they're needed
* **Smaller memory footprint** - no duplicate DOM trees in memory
* **Better battery life** - efficient resource usage on mobile devices

**Developer Benefits:**

* **Predictable behavior** - work directly with web platform APIs
* **Easier debugging** - inspect actual DOM elements, not virtual representations
* **Better third-party integration** - no compatibility issues with DOM-based libraries
* **Standards-aligned** - leverage browser capabilities instead of fighting them

Modern browsers are incredibly fast at DOM manipulation. Aurelia's intelligent observation and batching system ensures you get maximum performance without the complexity and overhead of virtual DOM solutions.

## Your Learning Journey

**Start Here:**

1. [**Complete Getting Started Guide**](/getting-started/complete-guide) - Build your first app in 15 minutes
2. [**Essentials**](/introduction/essentials) - Core concepts explained clearly
3. [**Components**](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/components/README.md) - Build reusable UI components

**Build Real Apps:** 4. [**Templates & Binding**](/templates/templates) - Master Aurelia's powerful templating 5. [**Router**](/getting-to-know-aurelia/aurelia-router) - Add navigation to your applications 6. [**Forms & Validation**](/aurelia-packages/validation) - Handle user input effectively 7. [**Testing**](/developer-guides/overview/testing) - Test your applications thoroughly 8. [**Performance**](/developer-guides/performance-optimization-techniques) - Optimize for production 9. [**Advanced Scenarios**](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/advanced-scenarios/README.md) - Complex application patterns

**Need Help?** Join our [Discord community](https://discord.gg/RBtyM6u) or check [GitHub Discussions](https://github.com/aurelia/aurelia/discussions) for support.

## Join the Aurelia Community

Ready to contribute or need support? The Aurelia community welcomes developers of all skill levels.

**Get Support:**

* [**Discord Server**](https://discord.gg/RBtyM6u) - Real-time chat with the community
* [**GitHub Discussions**](https://github.com/aurelia/aurelia/discussions) - Q\&A and feature discussions
* [**Stack Overflow**](https://stackoverflow.com/questions/tagged/aurelia) - Technical questions and answers

**Contribute:**

* [**Contributor Guide**](/community-contribution/contributor-guide) - How to contribute code and documentation
* [**GitHub Issues**](https://github.com/aurelia/aurelia/issues) - Report bugs or request features
* [**Documentation**](/community-contribution/writing-documentation) - Help improve these docs

## Ready to Start Building?

Jump into our [Complete Getting Started Guide](/getting-started/complete-guide) and build your first Aurelia application in 15 minutes. You'll be amazed at how intuitive and powerful web development can be with the right tools.


# The Aurelia Philosophy

*These are our fighting words*

The web development industry has lost its mind. Frameworks reinvent perfectly good web standards. Developers rewrite applications every eighteen months to chase the new hotness. Testing requires PhD-level framework knowledge just to mock a simple service.

We refuse to participate in this insanity.

We believe frameworks should enhance the web, not replace it. We believe your knowledge should compound, not expire. We believe testing should be trivial, not heroic.

Most importantly, we believe you shouldn't have to forget everything you know about web development just to use our framework.

These beliefs have made us unfashionable. Good. We'd rather be right than trendy.

## Why Not Just Use React?

**Because the biggest crowd is not the only path to victory.**

React is the industry default. It commands stadium keynotes, spawns endless packages, and fills job boards with requirements. We salute the React team for making component thinking mainstream and showing the browser can power ambitious interfaces. That legacy is undeniable.

But scale changes the game. React's strength is modular freedom. Every decision, from routing to state management to forms to dependency injection to testing, invites you to draft your own lineup of supporting libraries. That freedom can be exhilarating. It can also demand constant attention as packages shift, maintainers rotate out, and best practices rewrite themselves overnight. Teams do not just learn React; they learn React plus the custom stack they assembled on day one and have to re-evaluate each quarter.

Aurelia stays intentionally smaller so we can ship the essentials as one cohesive strike force. Our router, dependency injection, binding engine, and testing story are forged together by the same team with the same philosophy. You spend your energy shipping features, not benchmarking which combination of libraries will still be supported next quarter. When we add capability, we do it without erasing the knowledge you already earned.

Choosing Aurelia means joining a community that prizes stability over hype, standards over novelty, and craftsmanship over churn. You gain direct access to maintainers who care about your use case, not just the next keynote demo. Our ecosystem is tighter, but it is aligned, predictable, and built to compound value release after release.

Use React if you want the world of mix-and-match options. Choose Aurelia if you want a unified framework that already lives the principles this manifesto defends.

## Web Standards, Enhanced

**We build on the web, not around it.**

The web platform is brilliant. HTML gives us declarative markup that's intuitive to read and write. CSS provides powerful styling capabilities that scale from simple pages to complex applications. JavaScript offers a flexible, evolving language that runs everywhere. These aren't bugs to be fixed. They're features to be leveraged.

Yet somewhere along the way, the industry decided the web platform wasn't sophisticated enough for "real" applications. Everyone needed their own templating language that looked nothing like HTML. Their own styling solution that avoided CSS. Their own module system that ignored JavaScript's evolution. Learn our special syntax. Forget everything you know about web development. Trust us, we know better than the web standards committees and the collective wisdom of millions of developers.

This is breathtaking arrogance disguised as innovation.

Consider what this means in practice: A developer who's spent years mastering HTML, CSS, and JavaScript sits down with a modern framework and discovers that none of that knowledge applies. The templating syntax is completely foreign. The styling approach bypasses CSS entirely. The component model bears no resemblance to anything they've learned about web development.

We're essentially telling experienced web developers that their expertise is worthless. That the web platform they've mastered is inadequate. That they need to start over with our special way of doing things.

Aurelia takes the opposite approach. We enhance web standards instead of replacing them. Your templates are HTML with binding attributes that read like natural language: `value.bind`, `click.trigger`, `repeat.for`. A web developer can look at Aurelia templates and immediately understand what's happening, even if they've never seen the framework before.

Your components are JavaScript classes with predictable lifecycle methods. No magical decorators that fundamentally change how JavaScript works. No special compilation steps that transform your code into something unrecognizable. Just classes with methods that get called at logical points in the component's life.

Your styles are CSS. Not CSS-in-JS. Not a special styling language. Not a build-time transformation that generates CSS for you. Just CSS, working exactly like CSS should work, with all the power and flexibility you expect.

This philosophy extends to how we handle emerging standards. When Web Components were still experimental, we didn't bet the entire framework on them. When they became stable, we didn't ignore them. We built compatibility layers that let you use Aurelia components as Web Components when that makes sense, while keeping our core architecture independent of any single standard's success or failure.

We've watched frameworks rise and fall based on their alignment with web standards. The ones that fought against the platform eventually lost. The ones that embraced it survived and thrived. We're not just building for today's web. We're building for the web's long-term evolution.

When you learn Aurelia, you're not just learning a framework. You're deepening your understanding of the web platform itself. The binding concepts translate to vanilla JavaScript. The component patterns work with or without the framework. The architectural principles apply to any web application.

Your knowledge doesn't expire when the next framework revolution comes along. It compounds, making you a better web developer regardless of what tools you use next. That's the difference between building on the web platform and building around it.

{% hint style="info" %}
Want to see this philosophy in action? Check out our comprehensive guide on [Working with Web Standards](/developer-guides/working-with-web-standards) to learn how Aurelia enhances native web APIs like Fetch, Intersection Observer, Geolocation, and many more.
{% endhint %}

## Convention Over Configuration

**Stop making the same decisions over and over.**

Here's a thought experiment: How many different ways can you structure a web application? How many reasonable approaches are there to naming components, organizing files, or wiring up dependencies?

The answer is: a few good ways, and hundreds of terrible ways.

Yet modern web development has become obsessed with giving you infinite configuration options. Choose your file naming strategy from seventeen possibilities. Configure your binding syntax from forty-three variants. Set up your directory structure using our flexible, powerful, completely overwhelming configuration system. Make four hundred decisions before you can render "Hello World."

This is madness disguised as flexibility.

Consider what this means for a team. Every developer has their own preferences. The React developer wants JSX everywhere. The Vue developer prefers template syntax. The Angular developer expects decorators. The result? Endless bike-shedding discussions about tooling choices instead of productive work on actual features.

Or consider what this means for learning. A new developer doesn't just need to learn the framework. They need to learn your team's specific configuration choices, your particular file organization, your custom naming conventions. Every project becomes a unique snowflake with its own special setup.

Convention over configuration means we make the boring decisions so you don't have to. Create a file called `user-profile.ts` and another called `user-profile.html`, and you automatically get a `<user-profile>` component. Mark a property with `@bindable`, and a method called `propertyChanged` automatically becomes a change callback. Put components in a `components` folder, put services in a `services` folder, and everything just works.

These aren't arbitrary restrictions. They're carefully chosen defaults based on years of experience building real applications. We've seen what works and what doesn't. We've observed the patterns that emerge naturally when teams build maintainable applications. We've codified those patterns into conventions that guide you toward good practices.

But here's the crucial part: conventions should accelerate you, not constrain you. When you genuinely need something different, when your specific use case demands a different approach, every convention has an escape hatch. Need a custom component name? Use `@customElement('custom-name')`. Need different binding behavior? Configure it explicitly. Need a non-standard file organization? Override the defaults.

The key word is "earn." You have to consciously choose to deviate from the convention. You have to be explicit about what you want instead. This creates a natural pressure toward consistency while preserving flexibility for genuine edge cases.

Some developers hate this philosophy. They see conventions as limitations on their creativity. They want to configure every detail of the framework's behavior. They view our defaults as opinions imposed on their artistic vision.

We think they're optimizing for the wrong thing. Yes, you could spend three days configuring your perfect setup. You could create a unique file organization that perfectly reflects your mental model. You could customize every aspect of the framework's behavior to match your preferences.

But why? Your user registration component isn't fundamentally different from everyone else's. Your data binding requirements aren't uniquely artistic. Your architectural needs aren't so special that they require a completely novel approach.

Time spent on configuration is time not spent on features. Energy devoted to framework setup is energy not devoted to solving user problems. Cognitive load consumed by tooling choices is cognitive load unavailable for business logic.

Conventions are liberation from meaningless choices. They're shared understanding across teams and projects. They're productivity multipliers that let you focus on what actually matters: building great applications that solve real problems for real people.

## Intuitive Syntax That Reads Like You Think

**Code should express intent, not implementation details.**

Here's a simple test: Show a piece of code to someone who's never seen your framework before. Can they understand what it's trying to accomplish? Or do they need to memorize a dozen framework-specific concepts before the code makes sense?

Most modern frameworks fail this test spectacularly. Their templates are filled with cryptic symbols, magical directives, and abstract concepts that have no relationship to the underlying intent. A simple list becomes a mystical incantation that only the initiated can decipher.

Consider this common pattern across different frameworks: You want to show a user's name if they're logged in, and a login button if they're not. This is basic conditional rendering. Something any web developer should be able to understand at a glance.

In many frameworks, this simple intent gets buried under layers of framework-specific syntax. Special directives with obscure names. Conditional operators that work differently than JavaScript. Template languages that require you to think in framework abstractions rather than your actual problem domain.

Aurelia takes a different approach. Our syntax reads like natural language: `<div if.bind="user.isLoggedIn">Welcome, ${user.name}!</div>`. An experienced web developer can look at this and immediately understand what's happening, even if they've never seen Aurelia before. The intent is clear. The behavior is predictable. The syntax maps directly to the concept.

This isn't an accident. It's the result of obsessive attention to developer experience. We believe that the cognitive load of understanding your framework should be as close to zero as possible. Your mental energy should be devoted to solving business problems, not translating between human intent and framework abstractions.

The benefits of intuitive syntax extend far beyond personal productivity. When your code reads like natural language, it becomes self-documenting. New team members can understand existing code without extensive framework training. Code reviews focus on business logic rather than framework syntax. Bug reports can be understood by non-technical stakeholders.

But intuitive syntax is about more than just readability. It's about predictability. When syntax clearly expresses intent, the behavior becomes obvious. There are no hidden side effects, no mysterious edge cases, no framework magic that changes behavior based on context you can't see.

Consider error handling. In many frameworks, when something goes wrong, the error messages are filled with framework internals. Stack traces point to generated code. Error descriptions use framework jargon that tells you nothing about what actually went wrong in your application.

Aurelia's errors tell you exactly what went wrong and where. When a binding fails, we tell you which property couldn't be found and in which component. When a lifecycle hook throws an exception, we show you the exact method and component that caused the problem. The error messages use the same vocabulary as your code, not our internal implementation details.

This philosophy extends to debugging. When you inspect an Aurelia application in browser dev tools, you see your components, your properties, your methods. The framework doesn't hide behind layers of generated code or proxy objects. What you wrote is what you see. What you debug is what you ship.

The industry has somehow convinced itself that complexity is inevitable. That developer tools must be complicated because the problems they solve are complicated. That you need deep framework knowledge to be productive.

We reject this premise entirely. Complex problems don't require complex tools. They require powerful tools with simple interfaces. The best frameworks make hard things possible and easy things effortless, all while staying out of your way.

When your framework's syntax fights against your natural thinking patterns, every day becomes a translation exercise. You know what you want to accomplish, but you have to constantly convert your intent into framework-specific abstractions. This cognitive overhead accumulates over time, making you slower, more error-prone, and ultimately less satisfied with your work.

Intuitive syntax isn't just about making code easier to write. It's about making development more humane. Your tools should amplify your capabilities, not drain your mental energy. They should feel like natural extensions of your thinking, not obstacles to overcome.

## Built for Testing

**If you can't test it easily, we designed it wrong.**

Aurelia's dependency injection system isn't just for organization. It's what makes testing actually pleasant. Need to test a component that depends on services? Just pass in mocks. Everything is classes and interfaces, which means everything is mockable.

No elaborate test setup, no framework gymnastics, no "testing utilities" that are more complex than the code you're testing. Our component lifecycle is predictable and hookable. You can test each phase independently, spy on lifecycle methods, and verify that cleanup happens when it should.

This isn't an accident. We've worked on too many projects where testing was painful, so we made it a first-class concern. If testing something in Aurelia feels hard, that's a bug we want to fix.

## Stability Over the Latest Hotness

**We're boring, and proud of it.**

Framework churn is a disease. Every six months there's a new paradigm that's going to "revolutionize" web development. Everyone rewrites their applications to chase the shiny new thing. Six months later, that thing is deprecated in favor of the next revolution.

We refuse to participate in this madness.

There are Aurelia applications running in production today that were built in 2015. Nine years later, they still work. The same patterns, the same APIs, the same mental models. The Aurelia of 2015 is, in many fundamental ways, still the Aurelia of today.

This is not an accident. It's a philosophical choice.

While other frameworks have gone through complete rewrites that invalidated entire ecosystems, we've evolved gradually. We've added capabilities without breaking existing ones. We've improved performance without changing programming models.

This makes us unfashionable. Conferences don't invite us to give talks about the revolutionary new paradigm we've invented. Tech Twitter doesn't buzz about our latest rewrite. We don't get to claim we've "solved" web development with our groundbreaking new approach.

Fine by us. Your applications work. Your knowledge compounds instead of becoming obsolete. Your teams stay productive instead of spending months learning the new hotness. That's worth more than being trendy.

## Architecture for Growing Applications

**Start simple, scale thoughtfully.**

The web development industry has a scaling problem, but it's not the one you think. The real problem isn't technical. It's architectural. Most frameworks force you into a false choice: build quick and dirty prototypes that collapse under their own weight, or start with enterprise-grade complexity that crushes productivity from day one.

This is a failure of imagination, not engineering.

Real applications don't start complex. They start simple and become complex over time as requirements evolve, user needs change, and business logic accumulates. The framework you choose should support this natural progression, not fight against it.

Too many frameworks optimize for one end of the spectrum. The trendy ones make demos look effortless but leave you stranded when you need real architecture. The enterprise ones handle massive complexity beautifully but make simple things unnecessarily complicated. You're forced to choose between fast starts and sustainable growth.

Aurelia refuses this false choice. We designed every system to scale gracefully from simple to sophisticated without forcing you to rewrite your mental model or refactor your foundation.

Consider dependency injection. In a small Aurelia application, you might start with simple classes and direct instantiation. As your application grows and testing becomes important, you naturally introduce interfaces and constructor injection. When complexity demands it, you add scoped instances, factory patterns, and custom resolution strategies. Each step builds on the previous one. Your early decisions remain valid even as your architecture evolves.

The component system works the same way. Start with simple, self-contained components that handle their own data and logic. As requirements grow, introduce shared services, component communication patterns, and hierarchical state management. The simple components don't become wrong or need rewriting. They just become part of a larger, more sophisticated system.

Our conventions support this progression naturally. The file-based naming that works for a dozen components still works for hundreds. The lifecycle methods that handle simple initialization also handle complex orchestration. The binding patterns that manage basic data flow scale to handle intricate component interactions.

This isn't theoretical scaling. We've seen applications grow from weekend prototypes to enterprise systems serving millions of users. The fundamental patterns remain consistent. The code written in the early days doesn't become technical debt that needs to be paid down. It becomes the foundation that everything else builds on.

Compare this to frameworks that require architectural rewrites at different scales. Start with simple state management, then throw it away for a "proper" state solution. Begin with basic routing, then replace it with enterprise routing when you need features. Use simple components initially, then refactor everything when you need composition patterns.

This constant rewriting isn't progress. It's waste. It's time spent fighting your tools instead of building features. It's knowledge that expires instead of compounds. It's technical debt disguised as best practices.

We've watched too many projects hit the scaling wall and collapse under their own success. They start fast with a framework that makes demos look effortless. Six months later, they're drowning in unmanageable code, competing patterns, and architectural inconsistencies. The very framework that gave them early velocity becomes the bottleneck that prevents growth.

We've also seen projects start with enterprise-grade complexity from day one. Multiple abstraction layers, sophisticated patterns, elaborate architectures. Six months later, they're still building infrastructure instead of features. The complexity that was supposed to enable scale becomes the burden that prevents delivery.

Aurelia's approach is different. The patterns that work for small applications are the same patterns that work for large applications, just applied at different scales. Dependency injection scales from simple constructor parameters to complex service hierarchies. Component composition scales from basic parent-child relationships to sophisticated architectural patterns. Convention-based structure scales from individual files to team-based module organization.

This consistency has profound implications for team productivity. New developers don't need to learn different patterns as the application grows. Senior developers don't need to constantly re-architect as requirements evolve. The mental model that serves you well early in the project continues serving you well as the project matures.

Your testing strategies scale the same way. The mocking patterns that work for simple components work for complex service interactions. The lifecycle testing that validates basic behavior also validates sophisticated orchestration. The integration tests that cover simple workflows extend naturally to cover complex business processes.

Your debugging experience remains consistent as complexity grows. The error messages that help you understand simple binding issues also help you understand complex dependency resolution problems. The development tools that illuminate basic component behavior continue working as your component hierarchies become more sophisticated.

The industry has convinced itself that scaling requires fundamental architectural changes. That simple patterns must be abandoned for complex ones. That early code must be rewritten for production use.

We believe that well-designed simple patterns naturally evolve into well-designed complex patterns. That the best foundation for a large application is a small application that grew thoughtfully. That architectural consistency across scales is more valuable than perfect optimization at any individual scale.

When you build with Aurelia, you're not just building an application. You're growing an architecture. The decisions you make early don't constrain your future options. They create a foundation that supports whatever your application becomes.

Start simple. Scale thoughtfully. Never rewrite your foundation. That's how real applications get built.

## Popular Is Overrated

**We don't chase GitHub stars. We chase solutions.**

Let's be honest about where we stand. We're not the popular choice. We don't have billion-dollar tech giants throwing marketing budgets at us. We don't have armies of developer advocates making conference circuit rounds. We don't have influencers creating viral videos about the Aurelia revolution.

React has more GitHub stars than we'll ever see. Vue gets more downloads in a day than we get in a month. Angular dominates job postings and Stack Overflow questions.

And we're completely fine with that.

Popularity is a lagging indicator, not a leading one. It tells you what was trendy yesterday, not what will solve your problems tomorrow. The most popular framework isn't necessarily the best framework for your specific needs. It's just the one with the most mindshare at this particular moment.

We've been around long enough to watch the cycles. Technologies rise and fall based on hype as much as merit. Yesterday's revolutionary breakthrough becomes tomorrow's legacy system. The graveyard of web development is full of frameworks that were once the absolute hottest thing in tech.

While the popular frameworks optimize for conference buzz and Twitter engagement, we optimize for applications that work reliably over years, not months. We worry about whether our abstractions will make sense to your team three years from now, not whether they'll trend on Hacker News next week.

The tech industry loves its popularity contests. Industry analysts write reports about the "big three" and mention us as a footnote, if at all. Bootcamps teach the frameworks that help graduates get hired fastest, not necessarily the ones that will serve them best in the long run.

That's the game, and we understand it. But we're playing a different game entirely.

We're optimizing for developers who value substance over signals. For teams who care more about shipping reliable software than using the latest hotness. For organizations who measure success by user satisfaction, not developer satisfaction surveys.

The core team doesn't maintain Aurelia as a side project or resume builder. We stake our professional reputations on it. We bet our careers on it. We build production applications with it every day. When we make a design mistake, we live with it in our own codebases. When we get something right, we benefit directly. Our incentives are perfectly aligned with yours.

Being the unpopular choice gives us something invaluable: complete intellectual freedom. We don't have to pretend every new JavaScript trend is revolutionary. We don't have to generate artificial excitement to satisfy venture capitalists. We don't have to compromise our engineering principles to chase market share.

We can afford to be boring when boring is more reliable. We can afford to be unfashionable when unfashionable is more sustainable. We can afford to be right instead of popular.

The frameworks that are popular today won't necessarily be popular tomorrow. There will be new paradigms, new solutions, new ways of thinking about web development. The cycle will continue, as it always has.

But the applications built with Aurelia will keep running. The teams using Aurelia will keep shipping. The problems solved with Aurelia will stay solved.

Because quality outlasts popularity. Substance outlasts hype. Reliability outlasts trendiness.

We're not trying to win a popularity contest. We're trying to build the most thoughtful, sustainable, and useful framework we can. For developers and teams who share those values.

That's exactly the position we want to be in.

## We Trust You With Power Tools

**No training wheels, no safety scissors.**

Most frameworks treat you like you can't be trusted with real power. They give you a carefully curated set of approved patterns and lock everything else behind "here be dragons" warnings. They make architectural decisions for you and provide no way to change them. They assume you'll hurt yourself if given too much control.

We think that's insulting to your intelligence.

Aurelia is designed around a radical premise: you know what you're doing. You understand your requirements better than we do. You should have the power to make your own architectural decisions, even if we wouldn't make the same ones.

Don't like our default binding syntax? Configure it. Want custom template behaviors? Build them. Need different binding modes? Create them. The templating and binding systems are designed for extensibility at every level.

This isn't theoretical power. It's real, practical extensibility that teams use in production applications. We've seen developers create custom binding behaviors that handle complex scenarios we never anticipated. We've seen teams build specialized value converters that transform data in domain-specific ways. We've seen companies extend the templating system with custom attributes that encapsulate their business logic.

But here's the beautiful part: you don't need any of this complexity to get started. Aurelia works perfectly out of the box without any configuration. Most teams never need to replace a single component. The power is there when you need it, invisible when you don't.

Compare this to frameworks that make you choose between "simple but limited" and "powerful but complex." We give you simple AND powerful. The default experience just works. The advanced capabilities are there when your requirements demand them.

Need direct DOM access? Take it. The framework won't fight you or wrap everything in virtual abstractions. Need to hook into the component lifecycle at a granular level? Every lifecycle method is available and predictable. Need to customize how dependency injection works? The entire container is configurable.

Want to integrate a third-party library that expects to own part of the DOM? Go ahead. Aurelia won't interfere with your jQuery plugins, your D3 visualizations, or your WebGL canvases. We give you escape hatches everywhere because we know real applications have messy requirements.

Our dependency injection system exemplifies this philosophy. Out of the box, it handles constructor injection with sensible defaults. But if you need custom resolution strategies, scoped instances, factory functions, or completely custom behaviors, the system is flexible enough to handle it all.

The templating system works the same way. The default syntax handles 95% of use cases elegantly. But when you need custom binding behaviors, specialized value converters, or completely novel template constructs, the architecture supports it without forcing you to fight the framework.

Yes, this means you can hurt yourself. You can create circular dependencies that crash at runtime. You can bind to expensive computations that tank performance. You can write components that leak memory all over the place. You can architect systems that are impossible to maintain.

We're not going to stop you. We're also not going to assume you're incompetent enough to do these things accidentally.

The industry trend is toward frameworks that make dangerous things impossible. Every API is locked down. Every extension point is carefully controlled. Every architectural decision is made for you "for your own good."

We prefer frameworks that make powerful things possible. We trust you to understand the trade-offs. We trust you to test your code. We trust you to make responsible decisions about performance and maintainability.

This philosophy extends to our error handling. When something goes wrong, we don't hide it behind friendly abstractions. We show you exactly what failed, where it failed, and why it failed. Our error messages assume you're capable of understanding technical details and fixing the underlying problem.

Other frameworks optimize for protecting developers from themselves. We optimize for empowering developers to solve their problems. Sometimes those problems require sharp tools, direct access, and the freedom to make unconventional choices.

The difference is trust. We trust that you're a professional who can handle professional tools. We trust that you'll read the documentation, understand the implications, and make informed decisions about your architecture.

When you need to do something unusual, something the framework designers never anticipated, you shouldn't have to fight your tools or work around artificial limitations. You should be able to extend, customize, and override as needed.

That's what real power looks like. Not just the ability to configure options, but the ability to fundamentally change how the framework behaves when your requirements demand it.

The training wheels come off. The safety scissors get put away. We hand you the real tools and trust you to build something amazing.

## Complete, Not Cobbled Together

**Everything works together because we designed it that way.**

Modern web development has become an exercise in integration hell. Install seventeen npm packages with incompatible APIs. Configure twenty-three build tools that don't know about each other. Wire together forty-nine loosely related libraries that were never designed to work as a system. Then spend three days debugging version conflicts, peer dependency warnings, and mysterious build failures.

This is insanity disguised as flexibility.

The JavaScript ecosystem's obsession with modularity has created a paradox: in trying to make everything composable, we've made nothing actually compose well. Every package is an island. Every library has its own conventions, its own configuration format, its own way of handling errors. Integrating them requires endless glue code, adapter layers, and configuration files that exist solely to make incompatible things pretend to work together.

Aurelia takes a radically different approach. We're a complete system designed as a complete system from day one.

Our router doesn't just handle navigation. It understands our component lifecycle, integrates with our dependency injection system, and works seamlessly with our templating engine. When you navigate to a route, the router knows how to instantiate components with their dependencies, bind their properties, and manage their lifecycle hooks. No adapters, no glue code, no configuration mapping.

Our templating system doesn't exist in isolation. It's deeply integrated with our binding engine, our component system, and our validation framework. When you bind to a property in a template, the system understands the component's lifecycle, respects the validation rules, and integrates with the change detection system. Everything flows together naturally.

Our testing utilities aren't afterthoughts built by the community. They're designed specifically for Aurelia's architecture. They understand our dependency injection system, so mocking is trivial. They know our component lifecycle, so testing different phases is straightforward. They integrate with our templating system, so testing complex UI interactions doesn't require elaborate setup.

This integration goes deeper than just APIs that work well together. Our error messages are consistent across all systems because they're built by the same team with the same philosophy. Our debugging experience is coherent because all the pieces understand each other. Our performance optimizations work across the entire stack because we control the entire stack.

Compare this to the typical modern web application. You have a routing library that knows nothing about your state management solution. A component framework that's unaware of your validation library. A testing framework that requires custom adapters to work with your template syntax. Each piece is excellent in isolation, but together they create a fragmented experience.

When something goes wrong in a cobbled-together system, good luck figuring out where. Is it the router? The state manager? The component library? The build tool? The integration layer between two of them? You'll spend more time debugging the interactions between your tools than the actual business logic they're supposed to support.

When something goes wrong in Aurelia, the error messages tell you exactly what happened and where. The stack traces point to your code, not framework internals or integration adapters. The debugging experience is consistent because there's one coherent system, not a collection of independent libraries pretending to be friends.

This approach has real costs. We can't always use the "best of breed" solution for every individual piece. Our router might not have every feature of the most advanced standalone routing library. Our state management might not be as sophisticated as the latest state management trend. We have to build and maintain more code instead of delegating to the community.

But the benefits are transformative. You spend your time building features, not integrating tools. You debug your application logic, not framework compatibility issues. You learn one coherent system instead of a dozen different libraries with conflicting philosophies.

Your team onboards faster because there's one consistent way of doing things across the entire application. Your application performs better because all the pieces are optimized to work together. Your codebase is more maintainable because there are fewer integration layers and compatibility shims.

When you need to add a new feature, you're working with the framework, not against a collection of competing libraries. When you need to debug a problem, you're working within one system with consistent patterns, not trying to understand the interaction between multiple independent systems.

The industry has convinced itself that maximum modularity leads to maximum flexibility. That the best system is one assembled from the best individual components. That integration is someone else's problem.

We believe integration is the most important problem. That a complete system designed to work together will always be more reliable than a collection of perfect pieces that weren't designed for each other.

We built Aurelia as a complete, integrated solution because that's what real applications need. Not maximum theoretical flexibility, but maximum practical reliability. Not the coolest individual components, but the most coherent overall experience.

You don't have to become an expert in seventeen different libraries just to build a web application. You don't have to spend weeks researching compatibility matrices and integration guides. You don't have to maintain a tower of adapters and glue code that adds complexity without adding value.

You just build your application. The framework handles the rest.

## No Surprises

**What you see is what you get.**

Framework magic is a disease. Hidden behaviors, implicit conventions that change based on context, APIs that behave differently depending on what phase of the moon it is. Too many frameworks treat mystery as a feature.

Aurelia is boringly predictable.

When you bind to a property, it binds to that property. When you trigger an event, it triggers that event. When you inject a dependency, you get that dependency. The behavior you see is the behavior you get.

Our error messages tell you exactly what went wrong and how to fix it. Our lifecycle hooks run in the order you'd expect. Our binding system does what the syntax suggests it does.

This predictability isn't an accident. It's a core design principle. Complex applications require dependable foundations. When your framework behaves surprisingly, everything built on top of it becomes fragile.

Boring reliability beats clever unpredictability every time.

## Progress Through Evolution, Not Revolution

**We improve by addition, not destruction.**

The tech industry is obsessed with dramatic rewrites. Every few years, someone declares that everything we've learned is wrong and we need to start over from scratch. New paradigms emerge that invalidate entire ecosystems. Frameworks abandon their users in pursuit of architectural purity.

This is progress theater, not actual progress.

Real progress preserves what works while improving what doesn't. It builds on existing knowledge instead of discarding it. It respects the investments people have made in learning your platform, building their applications, and training their teams.

Too many frameworks treat major versions like clean slates. They change fundamental concepts, abandon proven patterns, and force you to relearn everything. The justification is always the same: "We had to break things to make them better."

We believe progress and stability aren't opposites. They're requirements that must be balanced.

Consider how we approached Aurelia 2. We rebuilt the framework's internals for dramatically better performance. We redesigned the binding system for more predictable behavior. We modernized the architecture to support emerging web standards. We fixed years of accumulated design debt.

But we didn't throw away what worked.

If you know how to build an Aurelia 1 application, you know how to build an Aurelia 2 application. The same conventions work. The same mental models apply. The same patterns scale. Your components are still classes with lifecycle methods. Your templates still use the same binding syntax. Your dependency injection still works the same way.

We made the performance improvements invisible to your application code. We enhanced the binding system without changing its behavior from your perspective. We modernized the architecture while preserving the programming model you'd learned.

Yes, some things changed. Some APIs became more consistent. Some edge cases were fixed. Some deprecated features were finally removed. But the core experience of building with Aurelia remained fundamentally the same.

This approach has costs. We couldn't make radical performance improvements that would have required completely different programming models. We couldn't chase architectural trends that would have invalidated existing knowledge. We had to think carefully about every change because we knew people were depending on the current behavior.

But this approach also has benefits. Your Aurelia 1 knowledge transferred directly to Aurelia 2. Your team didn't need months of retraining. Your migration path was evolutionary, not revolutionary. Your investment in the platform continued paying dividends.

When other frameworks release major versions, teams often decide it's easier to rewrite their applications than migrate them. When we released Aurelia 2, teams migrated because it was the obvious next step, not a leap into the unknown.

Progress without preservation is just churn. We choose sustainable progress over revolutionary change because we're building for the long term. Your applications deserve better than constant rewrites. Your teams deserve better than endless relearning. Your users deserve better than instability disguised as innovation.

***

These aren't just marketing principles. They're the beliefs that shaped every architectural decision, every API design, and every line of documentation. When you choose Aurelia, you're choosing more than a framework. You're choosing an approach to web development that values your time, respects your intelligence, and bets on the long-term health of the web platform.

Whether you're building your first application or your hundredth, whether you're working solo or on a team of dozens, whether you're prototyping or preparing for production, Aurelia is designed to meet you where you are and grow with you where you're going.

**This is why we build.**


# Essentials

Aurelia is a modern JavaScript framework that empowers you to build exceptional web applications with clean, maintainable code. If you're familiar with HTML, CSS, and modern JavaScript/TypeScript, this essentials guide will introduce you to Aurelia's core concepts and get you productive quickly.

This section provides a practical overview of Aurelia's fundamental building blocks. Each concept links to comprehensive documentation where you can dive deeper, but these essentials give you everything needed to start building with confidence.

## What Makes Aurelia Different

Aurelia stands out by embracing web standards and keeping things simple:

* **Standards-based**: Built on modern web standards without unnecessary abstractions
* **Convention over configuration**: Intuitive patterns that reduce boilerplate
* **Powerful templating**: Rich binding system with excellent performance
* **Dependency injection**: Clean, testable architecture out of the box

## Core Concepts

### [Components](/introduction/essentials/components)

Components are the building blocks of your Aurelia application. Learn how to create reusable UI elements with clean separation between view and view-model logic.

### [Templates](/introduction/essentials/templates)

Aurelia's templating system provides powerful data binding, event handling, and conditional rendering with a syntax that feels natural and expressive.

### [Dependency Injection](/introduction/essentials/dependency-injection)

Aurelia's dependency injection system promotes clean architecture by managing your application's services and their dependencies automatically.

### [Reactivity](/introduction/essentials/reactivity)

Understand how Aurelia's observation system automatically tracks changes and updates your UI efficiently without virtual DOM overhead.

## Next Steps

Ready to build something? Try our [Quick Start Guide](/getting-started/quick-start-guide) to create your first Aurelia application. For comprehensive coverage of all features, explore the full documentation sections on [Templates](/templates/overview), [Components](/components/components), and [Getting to Know Aurelia](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-to-know-aurelia/introduction/README.md).

The [Tutorials](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/tutorials/README.md) section contains step-by-step guides for building real applications, while [Developer Guides](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/developer-guides/README.md) covers advanced topics and best practices.


# Components

Components are the fundamental building blocks of Aurelia applications. A component consists of a view-model (TypeScript class) and an optional view (HTML template) that work together to create reusable UI elements.

## Basic Component Structure

Every Aurelia component starts with a simple class:

```typescript
export class MyComponent {
  message = 'Hello from Aurelia!';
}
```

And its corresponding HTML template (no `<template>` wrapper is needed in Aurelia 2):

```html
<h1>${message}</h1>
```

The `${message}` syntax creates a binding between your view-model property and the template, automatically updating the UI when the property changes.

## When to Create a Component?

Before creating a component, consider these guidelines:

### Create a component when:

* ✅ You need reusable UI that appears in multiple places
* ✅ The UI has its own behavior and state
* ✅ You want to encapsulate complexity (a component should do one thing well)
* ✅ The UI represents a meaningful concept in your domain (e.g., `<user-card>`, `<product-list>`)

### Use a custom attribute instead when:

* ✅ You're adding behavior to existing elements without changing structure
* ✅ You're creating a decorator or modifier (e.g., `tooltip`, `draggable`)
* ✅ Multiple behaviors can be combined on the same element
* ✅ Examples: `<button tooltip="Save changes">`, `<div draggable sortable>`

### Use a value converter when:

* ✅ You're just formatting data for display
* ✅ The transformation is pure (same input → same output)
* ✅ Examples: `${date | dateFormat}`, `${price | currency}`

## Custom Elements

To create reusable custom elements, use the `@customElement` decorator:

```typescript
import { customElement } from 'aurelia';

@customElement('hello-world')
export class HelloWorld {
  name = 'World';
}
```

```html
<h1>Hello, ${name}!</h1>
```

## Using Components

After creating a component, you need to make it available for use. There are two ways to do this:

### Option 1: Import in Templates (Recommended for Most Cases)

Import the component where you need it using the `<import>` element:

```html
<import from="./hello-world"></import>

<div>
  <hello-world></hello-world>
</div>
```

This is the recommended approach because:

* Components are only loaded where they're used
* Better code organization and maintainability
* Clear dependencies in each template

### Option 2: Global Registration

Register components globally in your `main.ts` to use them anywhere without imports:

```typescript
import Aurelia from 'aurelia';
import { MyApp } from './my-app';
import { HelloWorld } from './hello-world';

Aurelia
  .register(HelloWorld)  // Register globally
  .app(MyApp)
  .start();
```

Now `<hello-world></hello-world>` works in any template without `<import>`.

**When to use global registration:**

* Components used on almost every page (headers, footers, layout components)
* Shared UI components used throughout the app
* Components you want available in all templates by default

**When to use local imports:**

* Feature-specific components
* Most custom components
* Better tree-shaking and bundle optimization

## Bindable Properties

Make component properties configurable from the outside using `@bindable`:

```typescript
import { bindable } from 'aurelia';

export class UserCard {
  @bindable name: string;
  @bindable email: string;
  @bindable avatar: string;
}
```

```html
<div class="user-card">
  <img src.bind="avatar" alt="Avatar">
  <h3>${name}</h3>
  <p>${email}</p>
</div>
```

Use the component by binding values to its properties:

```html
<user-card name.bind="user.name" email.bind="user.email" avatar.bind="user.avatar"></user-card>
```

## Component Lifecycle

Components have lifecycle hooks for initialization and cleanup:

```typescript
export class MyComponent {
  created() {
    // Component instance created
  }

  binding() {
    // Data binding starts
  }

  bound() {
    // Data binding completed
  }

  attaching() {
    // Before DOM attachment
  }

  attached() {
    // Component attached to DOM
  }

  detaching() {
    // Before DOM removal
  }

  unbinding() {
    // Data binding being removed
  }
}
```

## Common Component Patterns

### Pattern: Container/Presenter (Smart/Dumb Components)

**Use case**: Separate data management from presentation logic.

**Container (Smart) Component** - Manages data and business logic:

```typescript
import { resolve } from '@aurelia/kernel';
import { IRouter } from '@aurelia/router';

export class UserListPage {
  private router = resolve(IRouter);
  users: User[] = [];
  isLoading = false;

  async binding() {
    this.isLoading = true;
    try {
      const response = await fetch('/api/users');
      this.users = await response.json();
    } finally {
      this.isLoading = false;
    }
  }

  viewUser(user: User) {
    this.router.load(`/users/${user.id}`);
  }

  deleteUser(user: User) {
    // Handle deletion
  }
}
```

```html
<div class="page">
  <h1>Users</h1>
  <loading-spinner if.bind="isLoading"></loading-spinner>

  <user-list
    users.bind="users"
    on-view.bind="(user) => viewUser(user)"
    on-delete.bind="(user) => deleteUser(user)">
  </user-list>
</div>
```

**Presenter (Dumb) Component** - Pure presentation, no data fetching:

```typescript
import { bindable } from 'aurelia';

export class UserList {
  @bindable users: User[];
  @bindable onView: (user: User) => void;
  @bindable onDelete: (user: User) => void;
}
```

```html
<div class="user-list">
  <div repeat.for="user of users" class="user-card">
    <h3>${user.name}</h3>
    <p>${user.email}</p>
    <button click.trigger="onView(user)">View</button>
    <button click.trigger="onDelete(user)">Delete</button>
  </div>
</div>
```

**Why this works**: Container components handle complexity (data, routing, state), while presenter components are simple, reusable, and easy to test. You can reuse `<user-list>` anywhere without worrying about data fetching.

### Pattern: Composition with Slots

**Use case**: Create flexible container components that accept custom content.

```typescript
export class Card {
  @bindable title: string;
  @bindable actions: boolean = false;
}
```

```html
<div class="card">
  <div class="card-header">
    <h2>${title}</h2>
  </div>

  <div class="card-body">
    <slot></slot> <!-- Main content goes here -->
  </div>

  <div class="card-footer" if.bind="actions">
    <slot name="actions"></slot> <!-- Named slot for actions -->
  </div>
</div>
```

**Usage:**

```html
<card title="User Profile" actions.bind="true">
  <!-- Default slot content -->
  <p>Name: ${user.name}</p>
  <p>Email: ${user.email}</p>

  <!-- Named slot content -->
  <button slot="actions" click.trigger="edit()">Edit</button>
  <button slot="actions" click.trigger="delete()">Delete</button>
</card>
```

**Why this works**: Slots make components flexible without needing dozens of bindable properties. The component controls the structure while consumers control the content.

### Pattern: Form Components with Two-Way Binding

**Use case**: Reusable form inputs that work seamlessly with parent form state.

```typescript
import { bindable, BindingMode } from 'aurelia';

export class FormInput {
  @bindable label: string;
  @bindable({ mode: BindingMode.twoWay }) value: string;
  @bindable type: string = 'text';
  @bindable required: boolean = false;
  @bindable error: string;
}
```

```html
<div class="form-group">
  <label>
    ${label}
    <span if.bind="required" class="required">*</span>
  </label>

  <input
    type.bind="type"
    value.bind="value"
    class="form-control ${error ? 'is-invalid' : ''}">

  <div class="error-message" if.bind="error">
    ${error}
  </div>
</div>
```

**Usage:**

```typescript
export class RegistrationForm {
  email: string = '';
  password: string = '';
  emailError: string;

  validateEmail() {
    this.emailError = this.email.includes('@') ? '' : 'Invalid email';
  }
}
```

```html
<form-input
  label="Email"
  value.bind="email"
  type="email"
  required.bind="true"
  error.bind="emailError"
  blur.trigger="validateEmail()">
</form-input>

<form-input
  label="Password"
  value.bind="password"
  type="password"
  required.bind="true">
</form-input>
```

**Why this works**: Two-way binding with `BindingMode.twoWay` keeps the parent and child in sync automatically. Changes in either place propagate to both.

### Pattern: Stateful UI Components

**Use case**: Components that manage their own internal state.

```typescript
import { bindable } from 'aurelia';

export class Accordion {
  @bindable items: AccordionItem[];
  expandedIndex: number | null = null;

  toggle(index: number) {
    this.expandedIndex = this.expandedIndex === index ? null : index;
  }

  isExpanded(index: number) {
    return this.expandedIndex === index;
  }
}
```

```html
<div class="accordion">
  <div repeat.for="item of items" class="accordion-item">
    <button
      class="accordion-header ${isExpanded($index) ? 'expanded' : ''}"
      click.trigger="toggle($index)">
      ${item.title}
    </button>

    <div class="accordion-content" show.bind="isExpanded($index)">
      ${item.content}
    </div>
  </div>
</div>
```

**Why this works**: The component owns its UI state (which item is expanded) while accepting data as props. This keeps the parent simple - it just provides data, not UI state.

### Pattern: Event-Emitting Components

**Use case**: Child components that notify parents of user actions.

```typescript
import { bindable } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { IEventAggregator } from '@aurelia/kernel';

export class SearchBox {
  @bindable placeholder: string = 'Search...';
  @bindable onSearch: (query: string) => void;

  private ea = resolve(IEventAggregator);
  query: string = '';

  handleSearch() {
    // Option 1: Callback binding
    if (this.onSearch) {
      this.onSearch(this.query);
    }

    // Option 2: Event aggregator (for loosely coupled components)
    this.ea.publish('search:query', this.query);
  }

  handleClear() {
    this.query = '';
    this.handleSearch();
  }
}
```

```html
<div class="search-box">
  <input
    value.bind="query"
    placeholder.bind="placeholder"
    keyup.trigger="handleSearch() & debounce:300">

  <button click.trigger="handleClear()" if.bind="query">
    Clear
  </button>
</div>
```

**Usage:**

```html
<!-- Using callback -->
<search-box on-search.bind="(query) => performSearch(query)"></search-box>

<!-- Or listen via event aggregator -->
```

```typescript
export class ProductCatalog {
  private ea = resolve(IEventAggregator);

  binding() {
    this.ea.subscribe('search:query', query => {
      this.performSearch(query);
    });
  }
}
```

**Why this works**: Components can communicate via callbacks (tight coupling) or events (loose coupling) depending on your needs. Use callbacks for parent-child communication, events for unrelated components.

## Best Practices

### Keep Components Focused

* ✅ Each component should have one clear responsibility
* ✅ If a component is doing too much, split it into smaller components
* ❌ Avoid "god components" that handle everything

### Favor Composition Over Inheritance

* ✅ Use slots and component composition
* ✅ Build complex UIs from simple, reusable pieces
* ❌ Avoid deep inheritance hierarchies

### Make Components Predictable

* ✅ Use bindable properties for inputs
* ✅ Use callbacks or events for outputs
* ✅ Document what bindables are required vs optional
* ❌ Don't manipulate parent state directly

### Test-Friendly Components

* ✅ Presenter components are easy to test (just props)
* ✅ Keep business logic in services, not components
* ✅ Use dependency injection for testability

## What's Next

* Learn more about [component lifecycles](/components/component-lifecycles)
* Explore [bindable properties](/components/bindable-properties) in detail
* Understand [shadow DOM and slots](/components/shadow-dom-and-slots) for advanced composition


# Templates

Aurelia's templating system provides powerful data binding, event handling, and control flow with intuitive syntax. Templates are HTML files enhanced with binding expressions and template controls.

## Data Binding

### Text Interpolation

Display dynamic content using string interpolation:

```html
<h1>${title}</h1>
<p>Welcome, ${user.firstName} ${user.lastName}!</p>
```

### Property Binding

Bind to element properties and attributes:

```html
<!-- Property binding -->
<input value.bind="message">
<img src.bind="imageUrl" alt.bind="imageAlt">

<!-- Attribute binding -->
<div class.bind="cssClass" id.bind="elementId">

<!-- Boolean attributes -->
<button disabled.bind="isLoading">Submit</button>
```

### Two-way Binding

Create two-way data flow with `.bind`:

```html
<input value.bind="searchQuery">
<p>Searching for: ${searchQuery}</p>
```

As you type, both the input and paragraph update automatically.

## Event Handling

Handle user interactions with `.trigger`:

```html
<button click.trigger="save()">Save</button>
<form submit.trigger="handleSubmit($event)">
  <input keyup.trigger="validateInput($event)">
</form>
```

```typescript
export class MyComponent {
  save() {
    console.log('Saving...');
  }

  handleSubmit(event: Event) {
    event.preventDefault();
    // Handle form submission
  }

  validateInput(event: KeyboardEvent) {
    // Validate as user types
  }
}
```

## Conditional Rendering

Show or hide content based on conditions:

```html
<!-- Show/hide elements -->
<div if.bind="isLoggedIn">
  <p>Welcome back!</p>
</div>

<div else>
  <p>Please log in</p>
</div>

<!-- Conditionally show content -->
<p show.bind="hasMessages">You have new messages</p>
<p hide.bind="isLoading">Content loaded</p>
```

## List Rendering

Display dynamic lists with `repeat.for`:

```html
<ul>
  <li repeat.for="item of items">${item.name}</li>
</ul>

<!-- With index -->
<div repeat.for="product of products">
  <h3>${$index + 1}. ${product.title}</h3>
  <p>${product.description}</p>
</div>
```

## Template References

Access DOM elements directly:

```html
<input ref="searchInput" value.bind="query">
<button click.trigger="focusSearch()">Focus Search</button>
```

```typescript
export class MyComponent {
  searchInput: HTMLInputElement;

  focusSearch() {
    this.searchInput.focus();
  }
}
```

## Template Variables

Create local variables within templates:

```html
<div with.bind="user">
  <h2>${firstName} ${lastName}</h2>
  <p>${email}</p>
</div>

<!-- Using let for computed values -->
<div let="fullName.bind="firstName + ' ' + lastName">
  <h2>${fullName}</h2>
</div>
```

## What's Next

* Explore the complete [template syntax overview](/templates/overview)
* Learn about [value converters](/templates/value-converters) for data transformation
* Discover [custom attributes](/templates/custom-attributes) for reusable behaviors


# Dependency Injection

Aurelia's dependency injection (DI) system manages your application's services and their dependencies automatically, promoting clean architecture and testable code.

## Creating Services

Services are regular classes that encapsulate state and call out to other dependencies. Rather than assigning collaborators manually, grab them from the container via `resolve()`:

```typescript
import { resolve } from '@aurelia/kernel';
import { IHttpClient } from '@aurelia/fetch-client';

export class UserService {
  private readonly http = resolve(IHttpClient);
  private readonly cache: User[] = [];

  async getUsers(): Promise<User[]> {
    const response = await this.http.fetch('/api/users');
    const payload = await response.json();
    this.cache.splice(0, this.cache.length, ...payload);
    return this.cache;
  }

  async createUser(userData: CreateUserRequest): Promise<User> {
    const response = await this.http.fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(userData),
      headers: { 'Content-Type': 'application/json' },
    });
    const user = await response.json();
    this.cache.push(user);
    return user;
  }
}
```

## Service Registration

Register services using the interface pattern so components depend on tokens, not classes:

```typescript
import { DI } from '@aurelia/kernel';

export interface IUserService {
  getUsers(): Promise<User[]>;
  createUser(userData: CreateUserRequest): Promise<User>;
}

export const IUserService = DI.createInterface<IUserService>('IUserService', x => x.singleton(UserService));
```

When this module loads the container wires `IUserService` to a singleton `UserService` instance. Swapping implementations (e.g., a mock service in tests) only requires a different registration.

## Using Services in Components

Use the `resolve()` function to inject services into components:

```typescript
import { resolve } from '@aurelia/kernel';
import { IUserService } from './user-service';

export class UserList {
  private users: User[] = [];
  private userService = resolve(IUserService);

  async created() {
    this.users = await this.userService.getUsers();
  }

  async addUser(userData: CreateUserRequest) {
    const newUser = await this.userService.createUser(userData);
    this.users.push(newUser);
  }
}
```

**Why `resolve()`?**

* Clean, modern syntax
* No decorators needed
* Better TypeScript inference
* Easier to test

## Service Dependencies

Services can depend on other services using `resolve()`:

```typescript
import { resolve } from '@aurelia/kernel';
import { IHttpClient } from '@aurelia/fetch-client';
import { ILogger } from '@aurelia/kernel';

export class UserService {
  private http = resolve(IHttpClient);
  private logger = resolve(ILogger);

  async getUsers(): Promise<User[]> {
    try {
      const response = await this.http.fetch('/api/users');
      return await response.json();
    } catch (error) {
      this.logger.error('Failed to fetch users', error);
      throw error;
    }
  }
}
```

You can resolve multiple dependencies - Aurelia handles the wiring automatically.

## Service Lifetimes

Control how services are instantiated:

```typescript
// Singleton (default) - one instance per application
export const IUserService = DI.createInterface<IUserService>('IUserService', x => x.singleton(UserService));
export type IUserService = UserService;

// Transient - new instance every time
export const IEventLogger = DI.createInterface<IEventLogger>('IEventLogger', x => x.transient(EventLogger));
export type IEventLogger = EventLogger;
```

## Configuration Services

Create configuration objects for your services:

```typescript
export interface ApiConfig {
  baseUrl: string;
  timeout: number;
  retries: number;
}

export const IApiConfig = DI.createInterface<ApiConfig>('IApiConfig');

// Register in main.ts
import Aurelia, { Registration } from 'aurelia';
import { IApiConfig } from './services/api-config';

Aurelia.register(
  Registration.instance(IApiConfig, {
    baseUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3,
  })
);
```

```typescript
import { resolve } from '@aurelia/kernel';
import { IHttpClient } from '@aurelia/fetch-client';
import { IApiConfig } from './services/api-config';

export class ApiService {
  private readonly config = resolve(IApiConfig);
  private readonly http = resolve(IHttpClient);

  constructor() {
    this.http.configure((cfg) => {
      cfg.baseUrl = this.config.baseUrl;
      cfg.timeout = this.config.timeout;
    });
  }
}
```

## Resolver toolbox

The DI container ships a set of resolver helpers in `@aurelia/kernel`. Resolvers change *how* a dependency is located at runtime—perfect for optional services, per-scope instances, or discovering every implementation of an interface. Every resolver works both as a decorator (`@all(IMetricSink)`) and inside `resolve(...)`.

| Resolver                                                        | Example                                               | What it does                                                                                                                                        |
| --------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `all(key)`                                                      | `resolve(all(IMetricSink))`                           | Returns **all** registrations for a key (useful for plugin pipelines).                                                                              |
| `last(key)`                                                     | `resolve(last(ISink))`                                | Grabs the most recently registered instance.                                                                                                        |
| `lazy(key)`                                                     | `resolve(lazy(IHttpClient))`                          | Injects a function that resolves the dependency on demand.                                                                                          |
| `optional(key)` / `own(key)`                                    | `resolve(optional(IMaybeService))`                    | Returns `undefined` (or the child container value) when nothing is registered.                                                                      |
| `factory(key)`                                                  | `resolve(factory(MyModelClass))`                      | Gives you a function that constructs the service manually (passing constructor args if needed).                                                     |
| `newInstanceForScope(key)`                                      | `resolve(newInstanceForScope(IValidationController))` | Creates and registers a brand-new instance in the current component scope, making it available to descendants via `resolve(IValidationController)`. |
| `newInstanceOf(Type)`                                           | `resolve(newInstanceOf(Logger))`                      | Constructs a fresh instance of a concrete class or interface implementation without polluting the container.                                        |
| `resource(key)` / `optionalResource(key)` / `allResources(key)` | `resolve(optionalResource(MyElement))`                | Resolves using resource semantics (look in the current component first, then root) which is handy for templating resources.                         |
| `ignore`                                                        | `@ignore private unused?: Foo`                        | Tells the container to skip a constructor parameter completely.                                                                                     |

```typescript
import { all, resolve } from '@aurelia/kernel';

export class MetricsPanel {
  private sinks = resolve(all(IMetricSink));

  attached() {
    for (const sink of this.sinks) {
      sink.flush();
    }
  }
}
```

### Creating custom resolvers

If none of the built-ins fit, use `createResolver` to craft your own semantics. The helper wires up decorator + runtime support automatically:

```typescript
import { createResolver, resolve } from '@aurelia/kernel';

const newest = createResolver((key, handler, requestor) => {
  const instances = requestor.getAll(key);
  return instances[instances.length - 1];
});

export const newestLogger = newest(ILogger);

export class AuditTrail {
  private readonly logger = resolve(newestLogger);
}
```

Because resolvers are plain DI registrations, you can package them inside libraries or register them globally via `Aurelia.register(...)`, keeping consumption ergonomic in templates and services alike.

## Testing with DI

DI makes testing straightforward by allowing easy mocking:

```typescript
// Test setup
const mockUserService = {
  getUsers: () => Promise.resolve([{ id: 1, name: 'Test User' }]),
  createUser: (data) => Promise.resolve({ id: 2, ...data })
};

const container = DI.createContainer();
container.register(Registration.instance(IUserService, mockUserService));

// Test your component with mocked dependencies
const component = container.get(UserList);
```

## What's Next

* Learn more about [dependency injection concepts](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection)
* Explore [service creation patterns](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection/creating-services)
* Understand [DI resolvers](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection/resolvers) for advanced scenarios


# Reactivity

Aurelia's reactivity system automatically tracks changes to your data and updates the UI efficiently. Unlike frameworks that use virtual DOM, Aurelia observes your data directly and surgically updates only what has changed.

## When to Use Which Reactivity Feature?

Aurelia offers several reactivity tools. Here's how to choose:

### Use simple properties (no decorator) when:

* ✅ **You only need UI updates** - Properties bound in templates are automatically observed
* ✅ Most common case - Just declare the property and bind it
* ✅ Example: `todos: Todo[] = []` with `repeat.for="todo of todos"`

### Use getters (computed) when:

* ✅ **Value depends on other properties** and calculation is cheap
* ✅ Automatic dependency tracking - no manual configuration needed
* ✅ Example: `get fullName() { return this.firstName + ' ' + this.lastName; }`

### Use `@computed` decorator when:

* ✅ **Expensive calculations** that should be cached
* ✅ You want to **explicitly control dependencies** (not automatic)
* ✅ Deep observation needed for nested objects
* ✅ Example: Complex filtering, heavy aggregations

### Use `@computed` on methods when:

* **A method call in template** needs dependency tracking
* You want **explicit tracked dependencies** instead of proxy-based tracking
* You want to **disable tracking** for a specific method call
* Observation only activates when the method is called from an observation context (template, computed). Normal calls are unaffected
* Example: complex filtering or formatting methods used directly from templates

### Use `@observable` when:

* ✅ **You need to run code** when a property changes (side effects)
* ✅ You want the `propertyChanged(newValue, oldValue)` callback
* ✅ Examples: Validation, analytics tracking, syncing data

### Use `watch()` when:

* ✅ **Complex expressions** - watching multiple properties or nested values
* ✅ Need more flexibility than `@observable`
* ✅ Examples: `@watch('user.address.city')`, `@watch(vm => vm.total > 100)`

### Use manual observation when:

* ✅ Building **libraries or advanced features**
* ✅ Need **fine-grained control** over subscription lifecycle
* ✅ Performance critical code requiring optimization

## Automatic Change Detection

Aurelia automatically observes properties that are bound in your templates. No decorators or special setup required:

```typescript
export class TodoApp {
  todos: Todo[] = [];
  filter: string = 'all';

  addTodo(text: string) {
    // UI updates automatically when todos changes
    this.todos.push({ id: Date.now(), text, completed: false });
  }

  removeTodo(index: number) {
    // UI updates automatically
    this.todos.splice(index, 1);
  }
}
```

```html
<div>
  <h2>Todos (${todos.length})</h2>
  <input value.bind="filter" placeholder="Filter todos">
  <ul>
    <li repeat.for="todo of todos" if.bind="shouldShow(todo)">
      ${todo.text}
      <button click.trigger="removeTodo($index)">Remove</button>
    </li>
  </ul>
</div>
```

The `${todos.length}`, `value.bind="filter"`, and `repeat.for="todo of todos"` create automatic observation - Aurelia tracks changes to these properties and updates the UI accordingly.

## Computed Properties

Getter properties automatically become reactive when their dependencies change:

```typescript
export class ShoppingCart {
  items: CartItem[] = [];
  
  get total() {
    // This computed property automatically updates when items change
    return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }

  get itemCount() {
    // Also reactive - updates when items array changes
    return this.items.length;
  }

  addItem(product: Product, quantity: number = 1) {
    // UI updates automatically for total, itemCount, and items display
    this.items.push({ ...product, quantity });
  }
}
```

```html
<div class="cart">
  <h3>Cart (${itemCount} items)</h3>
  <div repeat.for="item of items" class="cart-item">
    <span>${item.name}</span>
    <span>$${item.price} x ${item.quantity}</span>
  </div>
  <div class="total">Total: $${total}</div>
</div>
```

### Decorator `computed`

For some reason, when it's more preferrable to specify dependencies of a getter manually, rather than automatically tracked on read, you can use the decorator `@computed` to declare the dependencies, like the following example:

```ts
import { computed } from 'aurelia';

export class ShoppingCart {
  items: CartItem[] = [];

  // we only care when there's a change in the number of items
  // but not when the price or quantity of each item changes
  @computed('items.length')
  get total() {
    // This computed property automatically updates when items change
    return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }

  // other code ...
}
```

You can also specify multiple properties as dependencies, like the following example:

```ts
import { computed } from 'aurelia';

export class ShoppingCart {
  items: CartItem[] = [];
  gst = .1;

  // we only care when there's a change in the number of items
  // but not when the price or quantity of each item changes
  @computed('items.length', 'gst')
  get total() {
    // This computed property automatically updates when items change
    return this.items.reduce((sum, item) => sum + (item.price * this.tax * item.quantity), 0);
  }

  get tax() {
    return 1 + this.gst;
  }

  // other code ...
}
```

Basides the above basic usages, the `computed` decorator also supports a few more options, depending on the needs of an application.

#### Method tracking with `@computed`

When you call a method from a template (not a getter), use `@computed` to control dependency tracking for that call. Observation only activates when the method is called from an observation context (e.g. a template binding or another computed observation). A normal function call will not trigger any observation.

> Note: Using `@computed` on methods is currently experimental. The syntax and behavior may change before the final release.

```ts
import { computed } from 'aurelia';

export class ProductList {
  filter = '';
  products: Product[] = [];
  nested = { prop: '' };
  prop = '';
  prop2 = '';

  // proxy-based tracking (default)
  @computed
  matches(product: Product) {
    return product.name.includes(this.filter);
  }

  // explicit dependency paths
  @computed('filter', 'nested.prop')
  matches2(product: Product) {
    return product.name.includes(this.filter);
  }

  // explicit dependency function
  @computed((instance: ProductList) => instance.prop + instance.prop2)
  matches3(product: Product) {
    return product.name.includes(this.filter);
  }

  // options form with string deps
  @computed({ deps: ['filter', 'nested.prop'] })
  matches4(product: Product) {
    return product.name.includes(this.filter);
  }

  // options form with getter function dep
  @computed({ deps: (instance: ProductList) => instance.prop + instance.prop2 })
  matches5(product: Product) {
    return product.name.includes(this.filter);
  }
}
```

Behavior:

* `deps` omitted (or `undefined`) uses proxy-based tracking.
* `deps: []` explicitly disables tracking for the decorated method/getter.
* `deps` with strings enables explicit string-based dependency tracking.
* `deps` with a getter function enables function-based dependency tracking.
* Strings and functions cannot be mixed in the same `deps` declaration.
* Stacking `@computed` on the same method overrides prior tracking metadata (last applied wins).

#### Flush timing with `flush`

Like how you can specify flush mode of computed getter with `@computed({ flush: 'sync' })`, flush mode of `@computed` can also be done in a similar way, like the following example:

```ts
import { computed } from 'aurelia';

export class ShoppingCart {
  items: CartItem[] = [];
  gst = .1;

  // we only care when there's a change in the number of items, or gst
  // but not when the price or quantity of each item changes
  @computed({
    deps: ['items.length', 'gst'],
    flush: 'sync'
  })
  get total() {
    // This computed property automatically updates when items change
    return this.items.reduce((sum, item) => sum + (item.price * this.tax * item.quantity), 0);
  }

  get tax() {
    return 1 + this.gst;
  }

  // other code ...
}
```

#### Deep observation with `deep`

Sometimes you also want to automatically observe all properties of an object recursively, regardless at what level, `deep` option on the `@computed` decorator can be used to achieve this goal, like the following example:

```ts
import { computed } from 'aurelia';

export class ShoppingCart {
  _cart = {
    items: [],
    gst: 0.1,
  }

  // we care about any changes inside cart items, or gst
  @computed({
    deps: ['_cart'],
    deep: true,
  })
  get total() {
    // This computed property automatically updates when items change
    return this._cart.items.reduce((sum, item) => sum + (item.price * this._cart.gst * item.quantity), 0);
  }

  get tax() {
    return 1 + this.gst;
  }

  // other code ...
}
```

Now whenever `_cart.items[].price` or `_cart.items[].quantity` (or whatever else properties on each element in the `items` array), or `_cart.gst` changes, the `total` is considered dirty.

> \[!WARNING] `deep` observation doesn't observe non-existent properties, which means newly added properties won't trigger any changes notification. Replace the entire object instead.

## Deep Observation

Aurelia can observe nested object changes:

```typescript
export class UserProfile {
  user = {
    name: 'John Doe',
    address: {
      street: '123 Main St',
      city: 'Anytown',
      country: 'USA'
    },
    preferences: {
      theme: 'dark',
      notifications: true
    }
  };

  updateAddress(newAddress: Partial<Address>) {
    // Nested property changes are automatically detected
    Object.assign(this.user.address, newAddress);
  }
}
```

## Array Observation

Arrays are automatically observed for mutations:

```typescript
export class TaskList {
  tasks: Task[] = [];

  addTask(task: Task) {
    this.tasks.push(task); // Automatically triggers UI update
  }

  completeTask(index: number) {
    this.tasks[index].completed = true; // Property change observed
  }

  removeTasks(indices: number[]) {
    // Multiple array changes batched into single UI update
    indices.sort((a, b) => b - a).forEach(index => {
      this.tasks.splice(index, 1);
    });
  }
}
```

## When You Need @observable

The `@observable` decorator is only needed when you want to react to property changes in your view-model code (not just the template):

```typescript
import { observable } from 'aurelia';

export class UserProfile {
  @observable userName: string = '';

  // This method is called whenever userName changes
  userNameChanged(newValue: string, oldValue: string) {
    console.log(`Username changed from ${oldValue} to ${newValue}`);
    this.validateUsername(newValue);
  }

  private validateUsername(name: string) {
    // Perform validation when username changes
  }
}
```

```html
<!-- userName is still automatically observed for template updates -->
<input value.bind="userName">
<p>Hello, ${userName}!</p>
```

## Effect Observation

Create side effects that run when observed data changes:

```typescript
import { watch } from 'aurelia';

export class Analytics {
  currentPage: string = '/';
  user: User | null = null;

  constructor() {
    // Watch properties and react to changes
    watch(() => this.currentPage, (newPage) => {
      this.trackPageView(newPage);
    });

    watch(() => this.user, (newUser, oldUser) => {
      if (oldUser) this.trackUserLogout(oldUser);
      if (newUser) this.trackUserLogin(newUser);
    });
  }

  private trackPageView(page: string) {
    console.log(`Page view: ${page}`);
  }
}
```

## Manual Observation Control

For advanced scenarios, manually control observation:

```typescript
import { resolve } from '@aurelia/kernel';
import { IObserverLocator } from '@aurelia/runtime';

export class AdvancedComponent {
  private observerLocator = resolve(IObserverLocator);
  data = { value: 0 };

  attached() {
    // Manually observe a property
    const observer = this.observerLocator.getObserver(this.data, 'value');
    observer.subscribe((newValue, oldValue) => {
      console.log(`Value changed: ${oldValue} -> ${newValue}`);
    });
  }
}
```

## Performance Considerations

Aurelia's observation is highly optimized:

* **Batched Updates**: Multiple changes are batched into single DOM updates
* **Surgical Updates**: Only changed elements are updated, not entire component trees
* **Smart Detection**: Observes only bound properties, not entire objects
* **No Virtual DOM**: Direct DOM manipulation eliminates virtual DOM overhead

## Common Reactivity Patterns

### Pattern: Form Validation with @observable

**Use case**: Validate input as the user types, show errors immediately.

```typescript
import { observable } from 'aurelia';

export class RegistrationForm {
  @observable email: string = '';
  @observable password: string = '';

  emailError: string = '';
  passwordError: string = '';

  emailChanged(newValue: string) {
    // Run validation whenever email changes
    if (!newValue) {
      this.emailError = 'Email is required';
    } else if (!newValue.includes('@')) {
      this.emailError = 'Please enter a valid email';
    } else {
      this.emailError = '';
    }
  }

  passwordChanged(newValue: string) {
    if (newValue.length < 8) {
      this.passwordError = 'Password must be at least 8 characters';
    } else {
      this.passwordError = '';
    }
  }

  get isValid(): boolean {
    return !this.emailError && !this.passwordError && this.email && this.password;
  }
}
```

```html
<form>
  <input value.bind="email" type="email" placeholder="Email">
  <span class="error" if.bind="emailError">${emailError}</span>

  <input value.bind="password" type="password" placeholder="Password">
  <span class="error" if.bind="passwordError">${passwordError}</span>

  <button disabled.bind="!isValid" click.trigger="submit()">Register</button>
</form>
```

**Why this works**: `@observable` triggers validation automatically as users type. The `isValid` getter recomputes whenever errors change, enabling/disabling the submit button reactively.

### Pattern: Computed Filtering and Sorting

**Use case**: Filter and sort a list based on user input without re-fetching data.

```typescript
import { observable } from 'aurelia';

export class ProductCatalog {
  products: Product[] = [];

  @observable searchQuery: string = '';
  @observable sortBy: 'name' | 'price' = 'name';
  @observable maxPrice: number = 1000;

  get filteredProducts(): Product[] {
    // Automatically recomputes when searchQuery, maxPrice, or products change
    return this.products
      .filter(p =>
        p.name.toLowerCase().includes(this.searchQuery.toLowerCase()) &&
        p.price <= this.maxPrice
      )
      .sort((a, b) => {
        if (this.sortBy === 'price') {
          return a.price - b.price;
        }
        return a.name.localeCompare(b.name);
      });
  }

  get resultCount(): number {
    return this.filteredProducts.length;
  }

  async binding() {
    const response = await fetch('/api/products');
    this.products = await response.json();
  }
}
```

```html
<div class="catalog">
  <input value.bind="searchQuery" placeholder="Search products...">

  <select value.bind="sortBy">
    <option value="name">Sort by Name</option>
    <option value="price">Sort by Price</option>
  </select>

  <input type="range" min="0" max="1000" value.bind="maxPrice">
  <span>Max: $${maxPrice}</span>

  <p>${resultCount} products found</p>

  <div repeat.for="product of filteredProducts" class="product-card">
    <h3>${product.name}</h3>
    <p>$${product.price}</p>
  </div>
</div>
```

**Why this works**: The `filteredProducts` getter automatically recomputes when any dependency changes. No manual refresh needed - the UI stays in sync with filters.

### Pattern: Syncing Data with @watch

**Use case**: Keep related data in sync, like saving to localStorage or syncing with a server.

```typescript
import { watch, observable } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { ILogger } from '@aurelia/kernel';

export class DraftEditor {
  private logger = resolve(ILogger);

  @observable content: string = '';
  @observable title: string = '';

  lastSaved: Date | null = null;
  isSaving: boolean = false;

  constructor() {
    // Load from localStorage on startup
    this.content = localStorage.getItem('draft-content') || '';
    this.title = localStorage.getItem('draft-title') || '';
  }

  // Watch for changes and auto-save
  @watch('content')
  @watch('title')
  async contentChanged() {
    if (this.isSaving) return;

    this.isSaving = true;
    try {
      // Save to localStorage immediately
      localStorage.setItem('draft-content', this.content);
      localStorage.setItem('draft-title', this.title);

      // Debounced server sync (implement as needed)
      await this.syncToServer();

      this.lastSaved = new Date();
      this.logger.debug('Draft saved');
    } finally {
      this.isSaving = false;
    }
  }

  private async syncToServer() {
    // Sync to server with debouncing
  }
}
```

```html
<div class="editor">
  <input value.bind="title" placeholder="Title">
  <textarea value.bind="content" placeholder="Start writing..."></textarea>

  <div class="status">
    <span if.bind="isSaving">Saving...</span>
    <span if.bind="lastSaved && !isSaving">
      Saved at ${lastSaved.toLocaleTimeString()}
    </span>
  </div>
</div>
```

**Why this works**: `@watch` observes both `content` and `title`, automatically saving changes. The pattern prevents data loss and provides user feedback.

### Pattern: Dependent Computations

**Use case**: Chain computed properties where one depends on another.

```typescript
export class OrderSummary {
  items: OrderItem[] = [];

  @observable discountCode: string = '';
  @observable taxRate: number = 0.08;

  get subtotal(): number {
    return this.items.reduce((sum, item) =>
      sum + (item.price * item.quantity), 0
    );
  }

  get discount(): number {
    // Depends on subtotal and discountCode
    if (this.discountCode === 'SAVE10') {
      return this.subtotal * 0.1;
    }
    if (this.discountCode === 'SAVE20') {
      return this.subtotal * 0.2;
    }
    return 0;
  }

  get afterDiscount(): number {
    // Depends on subtotal and discount
    return this.subtotal - this.discount;
  }

  get tax(): number {
    // Depends on afterDiscount and taxRate
    return this.afterDiscount * this.taxRate;
  }

  get total(): number {
    // Final total depends on afterDiscount and tax
    return this.afterDiscount + this.tax;
  }
}
```

```html
<div class="order-summary">
  <p>Subtotal: $${subtotal.toFixed(2)}</p>

  <input value.bind="discountCode" placeholder="Discount code">
  <p if.bind="discount > 0" class="discount">
    Discount: -$${discount.toFixed(2)}
  </p>

  <p>Tax (${(taxRate * 100).toFixed(0)}%): $${tax.toFixed(2)}</p>

  <p class="total">Total: $${total.toFixed(2)}</p>
</div>
```

**Why this works**: Computed properties automatically form a dependency chain. When `subtotal` changes, `discount` updates, which updates `afterDiscount`, then `tax`, and finally `total`. All cascade automatically.

### Pattern: Optimized List Updates with @computed

**Use case**: Expensive computations on large lists that should only recalculate when necessary.

```typescript
import { computed } from 'aurelia';

export class DataAnalytics {
  dataPoints: DataPoint[] = []; // Large array

  @observable dateRange: DateRange;
  @observable selectedMetric: string = 'sales';

  // Only recalculate when dependencies actually change
  @computed('dataPoints.length', 'dateRange', 'selectedMetric')
  get filteredData(): DataPoint[] {
    console.log('Filtering data (expensive)');

    return this.dataPoints.filter(point =>
      point.date >= this.dateRange.start &&
      point.date <= this.dateRange.end &&
      point.metric === this.selectedMetric
    );
  }

  @computed('filteredData.length')
  get statistics(): Statistics {
    console.log('Computing statistics (expensive)');

    const values = this.filteredData.map(d => d.value);
    return {
      mean: this.mean(values),
      median: this.median(values),
      stdDev: this.standardDeviation(values)
    };
  }

  // Heavy computation methods
  private mean(values: number[]): number { /* ... */ }
  private median(values: number[]): number { /* ... */ }
  private standardDeviation(values: number[]): number { /* ... */ }
}
```

**Why this works**: `@computed` with explicit dependencies prevents unnecessary recalculations. Changing individual data point properties won't trigger recalculation - only changes to array length, date range, or metric do.

## Best Practices

### Choose the Right Tool

* ✅ **Start simple** - Use plain properties and getters first
* ✅ Add `@observable` only when you need side effects
* ✅ Use `@computed` for expensive operations, not simple getters
* ❌ Don't over-engineer - most scenarios don't need `@watch` or manual observation

### Keep Computations Pure

* ✅ Computed getters should have no side effects
* ✅ Same inputs should always produce same outputs
* ❌ Don't modify state inside getters
* ❌ Don't make API calls in computed properties

### Optimize Performance

* ✅ Use `@computed` with explicit dependencies for expensive calculations
* ✅ Debounce rapid changes (user input, scroll events)
* ✅ Batch related updates together
* ❌ Don't create unnecessary watchers

### Handle Async Operations

* ✅ Use `@watch` or `propertyChanged` callbacks for async side effects
* ✅ Track loading states during async operations
* ✅ Handle errors gracefully
* ❌ Don't make computed properties async

## What's Next

* Learn about [observing property changes](/getting-to-know-aurelia/state-and-observation/observation/observing-property-changes-with-observable) in detail
* Explore [effect observation](/getting-to-know-aurelia/state-and-observation/observation/effect-observation) for advanced reactive patterns
* Understand [watching data](/getting-to-know-aurelia/state-and-observation/watching-data) strategies


# Complete Getting Started Guide

Complete getting started guide for Aurelia 2 - from installation to building your first interactive application in 15 minutes.

**Build a real Aurelia application in 15 minutes.** This hands-on guide shows you why developers choose Aurelia for its performance, simplicity, and standards-based approach. No prior Aurelia experience required.

## What You'll Discover

Build a polished task management app while experiencing Aurelia's key advantages:

* **🚀 Instant two-way data binding** - no boilerplate code required
* **⚡ Blazing fast rendering** - direct DOM updates, no virtual DOM overhead
* **🎯 Intuitive component model** - clean, testable architecture
* **🛠️ Modern TypeScript development** - with built-in dependency injection

**The result?** A production-quality app with clean, maintainable code in just 15 minutes.

## Prerequisites

You'll need:

* **Node.js 18+ (recommended latest LTS)** ([Download here](https://nodejs.org/))
* **A code editor** ([VS Code](https://code.visualstudio.com/) recommended)
* **Basic knowledge** of HTML, CSS, and JavaScript

## Quick Try (No Installation)

Want to see Aurelia in action before creating a project? Use [Run Aurelia in the browser without a build step](/getting-started/browser-no-build). That page is the maintained no-install path for plain HTML files, CodePen demos, plugin imports, and reduced bug reproductions.

## Create Your First Project

### Step 1: Initialize Project

Create a new project using the makes command:

```bash
npx makes aurelia
```

When prompted:

* **Project name**: `my-task-app`
* Choose **TypeScript** or **JavaScript** template (TypeScript recommended)
* **Install dependencies**: `Yes`

```bash
cd my-task-app
npm run dev
```

Your app opens at `http://localhost:9000` showing "Hello World!"

### Step 2: Project Structure

Your new project contains:

```
my-task-app/
├── src/
│   ├── main.ts          # Application entry point
│   ├── my-app.html      # Root component template
│   ├── my-app.ts        # Root component logic
│   └── my-app.css       # Component styles
├── index.html           # Main HTML file
├── vite.config.js       # Vite configuration
└── package.json         # Dependencies and scripts
```

**Key files to understand:**

* **`main.ts`**: Starts your Aurelia application
* **`my-app.ts`**: Your root component's logic (TypeScript)
* **`my-app.html`**: Your root component's template (HTML)

## Understanding Aurelia Components

Aurelia apps are built with **components**. Each component has two parts:

### View-Model (Logic)

**`src/my-app.ts`**:

```typescript
export class MyApp {
  message = 'Hello World!';

  // Methods and properties go here
}
```

### View (Template)

**`src/my-app.html`**:

```html
<h1>${message}</h1>
<!-- HTML template goes here -->
```

The `${}` syntax binds data from your view-model to the template. When `message` changes, the `<h1>` automatically updates!

## Build Your Task App

Let's transform the hello world app into a task manager. We'll build it step by step.

### Step 3: Update the Template

Replace contents of **`src/my-app.html`**:

```html
<div class="app">
  <h1>My Task Manager</h1>

  <!-- Add new task form -->
  <div class="add-task">
    <input
      value.bind="newTaskText"
      placeholder="Enter a new task..."
      keydown.trigger="addTaskOnEnter($event)">
    <button click.trigger="addTask()">Add Task</button>
  </div>

  <!-- Task counter -->
  <p class="task-count">
    ${tasks.length} task${tasks.length === 1 ? '' : 's'} total
  </p>

  <!-- Task list -->
  <ul class="task-list">
    <li repeat.for="task of tasks" class="task-item">
      <label class="task-label">
        <input
          type="checkbox"
          checked.bind="task.completed"
          change.trigger="updateTaskCount()">
        <span class="${task.completed ? 'completed' : ''}">${task.text}</span>
      </label>
      <button click.trigger="removeTask(task)" class="remove-btn">×</button>
    </li>
  </ul>

  <!-- Empty state -->
  <p if.bind="tasks.length === 0" class="empty-state">
    No tasks yet. Add one above!
  </p>

  <!-- Completed tasks counter -->
  <p if.bind="completedTaskCount > 0" class="completed-count">
    ✅ ${completedTaskCount} completed
  </p>
</div>
```

### Step 4: Update the Logic

Replace contents of **`src/my-app.ts`**:

```typescript
export class MyApp {
  newTaskText = '';
  tasks: Task[] = [
    { id: 1, text: 'Learn Aurelia basics', completed: false },
    { id: 2, text: 'Build a task app', completed: false },
    { id: 3, text: 'Celebrate! 🎉', completed: false }
  ];
  private nextId = 4;

  get completedTaskCount(): number {
    return this.tasks.filter(task => task.completed).length;
  }

  addTask(): void {
    if (this.newTaskText.trim()) {
      this.tasks.push({
        id: this.nextId++,
        text: this.newTaskText.trim(),
        completed: false
      });
      this.newTaskText = '';
    }
  }

  addTaskOnEnter(event: KeyboardEvent): void {
    if (event.key === 'Enter') {
      this.addTask();
    }
  }

  removeTask(taskToRemove: Task): void {
    this.tasks = this.tasks.filter(task => task !== taskToRemove);
  }

  updateTaskCount(): void {
    // This method triggers reactivity update for computed properties
    // In most cases, Aurelia handles this automatically
  }
}

interface Task {
  id: number;
  text: string;
  completed: boolean;
}
```

### Step 5: Add Styles

Replace contents of **`src/my-app.css`**:

```css
/* Reset and base styles */
* {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.6;
  margin: 0;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
}

.app {
  max-width: 600px;
  margin: 0 auto;
  background: white;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

h1 {
  color: #333;
  text-align: center;
  margin-bottom: 2rem;
  font-size: 2.5rem;
  font-weight: 300;
}

/* Add task form */
.add-task {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.add-task input {
  flex: 1;
  padding: 0.75rem;
  border: 2px solid #e1e5e9;
  border-radius: 6px;
  font-size: 1rem;
  transition: border-color 0.2s;
}

.add-task input:focus {
  outline: none;
  border-color: #667eea;
}

.add-task button {
  padding: 0.75rem 1.5rem;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
  transition: background 0.2s;
}

.add-task button:hover {
  background: #5a6fd8;
}

/* Task counters */
.task-count, .completed-count {
  color: #666;
  font-size: 0.9rem;
  margin: 1rem 0;
}

.completed-count {
  color: #22c55e;
  font-weight: 500;
}

/* Task list */
.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.task-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem;
  border: 1px solid #e1e5e9;
  border-radius: 6px;
  margin-bottom: 0.5rem;
  transition: all 0.2s;
}

.task-item:hover {
  border-color: #667eea;
  background: #f8fafc;
}

.task-label {
  display: flex;
  align-items: center;
  cursor: pointer;
  flex: 1;
}

.task-label input[type="checkbox"] {
  margin-right: 0.75rem;
  transform: scale(1.2);
}

.task-label span.completed {
  text-decoration: line-through;
  color: #9ca3af;
}

.remove-btn {
  background: #ef4444;
  color: white;
  border: none;
  border-radius: 4px;
  width: 2rem;
  height: 2rem;
  cursor: pointer;
  font-size: 1.2rem;
  transition: background 0.2s;
}

.remove-btn:hover {
  background: #dc2626;
}

/* Empty state */
.empty-state {
  text-align: center;
  color: #9ca3af;
  font-style: italic;
  padding: 2rem;
}
```

### Step 6: See It Work!

Save your files and check your browser. You now have a fully functional task manager! Try:

* **Adding tasks** by typing and clicking "Add Task" or pressing Enter
* **Completing tasks** by checking the checkboxes
* **Removing tasks** by clicking the × button
* **Watching the counters** update automatically

## Key Concepts You Just Learned

### 1. **Data Binding**

```html
<input value.bind="newTaskText">
<span>${task.text}</span>
```

Aurelia automatically keeps your HTML in sync with your TypeScript properties.

### 2. **Event Handling**

```html
<button click.trigger="addTask()">Add Task</button>
<input keydown.trigger="addTaskOnEnter($event)">
```

Connect user interactions to your methods seamlessly.

### 3. **Conditional Rendering**

```html
<p if.bind="tasks.length === 0">No tasks yet!</p>
```

Show or hide elements based on conditions.

### 4. **List Rendering**

```html
<li repeat.for="task of tasks">
  ${task.text}
</li>
```

Display dynamic lists that update automatically.

### 5. **Computed Properties**

```typescript
get completedTaskCount(): number {
  return this.tasks.filter(task => task.completed).length;
}
```

Derived values that update automatically when dependencies change.

## Next Steps

Congratulations! You've built a real Aurelia application. Here's what to explore next:

### Immediate Next Steps

* [**Components Guide**](/components/components) - Create reusable components
* [**Templates Deep Dive**](/templates/overview) - Master Aurelia's templating
* [**Dependency Injection**](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection) - Manage services and data

### Building Real Apps

* [**Router**](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) - Add navigation between pages
* [**Forms**](/templates/forms/forms-1) - Handle complex user input
* [**HTTP Client**](/aurelia-packages/overview) - Connect to APIs

### Development Workflow

* [**Build Tools**](/developer-guides/bundlers) - Optimize your development setup
* [**Testing**](/developer-guides/overview) - Test your applications
* [**Debugging**](/developer-guides/debugging-and-troubleshooting) - Debug effectively

## Common Questions

### **"Should I use TypeScript or JavaScript?"**

TypeScript is recommended for better development experience, error catching, and IntelliSense. But JavaScript works perfectly fine too.

### **"How does this compare to React/Vue/Angular?"**

Aurelia focuses on standards-based development with minimal learning curve. If you know HTML, CSS, and JavaScript, you already know most of Aurelia.

### **"Can I use this in production?"**

Absolutely! Aurelia 2 is production-ready and used by companies worldwide. The framework is stable, performant, and well-tested.

### **"What if I get stuck?"**

* [**Documentation**](/) - Comprehensive guides and API docs
* [**GitHub Discussions**](https://github.com/aurelia/aurelia/discussions) - Community Q\&A
* [**Discord**](https://discord.gg/RBtyM6u) - Real-time chat with the community

## You're Ready!

You now understand Aurelia's core concepts and have built a working application. The framework's strength lies in its simplicity - what you just learned covers 80% of what you'll use in real applications.

Ready to build something amazing? Dive into the guides above or start building your next project with Aurelia!


# Quick Install Guide

Get Aurelia running in under 5 minutes with this quick installation guide.

Get Aurelia up and running in 5 minutes or less.

## Prerequisites

* A code editor of your choice
* [Node.js](https://nodejs.org/en/) (latest version recommended) for the scaffolded app path

## Option 1: Try Aurelia in the browser

Want to try Aurelia immediately without installing anything? Use [Run Aurelia in the browser without a build step](/getting-started/browser-no-build). That page is the canonical no-build setup and covers module scripts, CDN imports, version pinning, CodePen usage, and package registration.

For a more complete no-build application, see the [realworld-vanilla example](https://github.com/aurelia/aurelia/tree/master/examples/realworld-vanilla), which demonstrates a full application with routing.

## Option 2: Create Your App

Aurelia uses the [Makes](https://www.npmjs.com/package/makes) scaffolding tool. No global installs required.

```bash
npx makes aurelia
```

When prompted:

* **Project name**: Enter your project name
* **Setup**: Choose TypeScript (recommended) or ESNext
* **Install dependencies**: Select "Yes"

{% hint style="info" %}
**Why TypeScript?** Get intellisense, type safety, and better tooling support out of the box.
{% endhint %}

### Run Your App

Navigate to your project and start the development server:

```bash
cd your-project-name
npm start
```

Your browser will automatically open to `http://localhost:8080` showing your new Aurelia app.

## Verify Everything Works

You should see "Hello World!" displayed in your browser. The development server watches for changes and auto-reloads.

## What's Next?

* **New to Aurelia?** Try our [Hello World Tutorial](/getting-started/quick-start-guide) for a hands-on introduction
* **Ready for more?** Explore our [developer guides](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/developer-guides/README.md) and [tutorials](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/tutorials/README.md)
* **Need help?** Check out [troubleshooting](/developer-guides/debugging-and-troubleshooting)

## Recommended Reading

Want deeper context after the quick start? These guides build on the concepts introduced here:

* **Component basics** – Understand how views and view-models pair up plus when to use imports vs. global registration in [Component essentials](/introduction/essentials/components).
* **Project structure & conventions** – See how the scaffolded files fit together in the [Complete guide's project structure section](/getting-started/complete-guide#step-2-project-structure).
* **Template syntax & binding** – Explore binding commands, loops, and conditionals in the [template syntax overview](/templates/overview).
* **Dependency injection** – Learn how Aurelia's DI container works and when to call `resolve()` in the [DI overview](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection/overview).
* **Routing** – When you need multiple pages, follow the [router getting started guide](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) to add navigation.

## Core Concepts (Optional Reading)

Aurelia is built on familiar web technologies with a few key concepts:

* **Components**: Made of view-models (`.ts`/`.js`) and views (`.html`)
* **Conventions**: File names and structure follow predictable patterns
* **Dependency Injection**: Built-in system for managing services and dependencies
* **Enhanced HTML**: Templates use familiar HTML with powerful binding syntax

These concepts become clearer as you build with Aurelia. Start with the tutorial above to see them in action!


# Browser without a build step

Run Aurelia 2 directly in the browser with a module script and CDN imports.

You can run Aurelia from a plain HTML page by importing the framework as a browser ES module. This works well for CodePen demos, reduced bug reproductions, documentation examples, and small pages where a Vite or Webpack setup would be more ceremony than value.

For application development, the scaffolded Vite setup is still the recommended path. A build tool gives you TypeScript, file-based templates, CSS imports, optimized output, and local package management.

## What to include

A no-build Aurelia page needs these pieces:

* A host element, such as `<app-root></app-root>`.
* A `<script type="module">` block. Static `import` statements only work in module scripts.
* An ESM CDN URL for `aurelia`.
* `CustomElement.define(...)` for inline component definitions.
* `new Aurelia().app({ component, host }).start()` to attach Aurelia to the host element.

The smallest useful page looks like this:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Aurelia without a build step</title>
    <link rel="modulepreload" href="https://esm.sh/aurelia@2.0.0-rc.1" crossorigin>
  </head>
  <body>
    <app-root></app-root>

    <script type="module">
      import { Aurelia, CustomElement } from 'https://esm.sh/aurelia@2.0.0-rc.1';

      const AppRoot = CustomElement.define({
        name: 'app-root',
        template: `
          <h1>\${message}</h1>
          <input value.bind="message">
          <p>You typed: \${message}</p>
        `,
      }, class {
        message = 'Hello from Aurelia';
      });

      await new Aurelia()
        .app({
          component: AppRoot,
          host: document.querySelector('app-root'),
        })
        .start();
    </script>
  </body>
</html>
```

In CodePen, put the host element and the `<script type="module">` block in the HTML panel. If you put the JavaScript in a normal script panel, the browser will reject static `import` statements unless that panel is explicitly configured to emit a module script.

## Pin package versions

Use `@latest` only for quick local experiments. For bug reports, examples in issues, CodePen links, and documentation, pin every Aurelia package to the same version:

```javascript
import { Aurelia, CustomElement } from 'https://esm.sh/aurelia@2.0.0-rc.1';
import { I18nConfiguration } from 'https://esm.sh/@aurelia/i18n@2.0.0-rc.1';
```

When you update one Aurelia package URL, update the others in the same page. Mixing versions can produce duplicate containers, missing registrations, or subtle binding/runtime mismatches.

Avoid bundling each Aurelia package URL independently in a multi-package browser demo. For example, do not add `?bundle` to both `aurelia` and `@aurelia/i18n` unless you have confirmed the CDN keeps a single shared copy of Aurelia's internals. Separate bundles can hide duplicated framework modules.

The examples on this page use `esm.sh`. The same pattern also works with other CDNs that serve browser-ready ES modules. For example, jsDelivr exposes Aurelia's ESM build at `https://cdn.jsdelivr.net/npm/aurelia@2.0.0-rc.1/+esm`.

## Add official Aurelia packages

Import each package from the CDN and register its configuration before calling `.app(...).start()`. This i18n example shows the pattern:

```html
<app-root></app-root>

<script type="module">
  import { Aurelia, CustomElement } from 'https://esm.sh/aurelia@2.0.0-rc.1';
  import { I18nConfiguration } from 'https://esm.sh/@aurelia/i18n@2.0.0-rc.1';

  const AppRoot = CustomElement.define({
    name: 'app-root',
    template: `
      <h1 t="heading"></h1>
      <p t="[text]intro"></p>
    `,
  }, class {});

  await new Aurelia()
    .register(
      I18nConfiguration.customize((options) => {
        options.initOptions = {
          lng: 'en',
          resources: {
            en: {
              translation: {
                heading: 'Hello from i18n',
                intro: 'Text translations update textContent.',
              },
            },
          },
        };
      }),
    )
    .app({
      component: AppRoot,
      host: document.querySelector('app-root'),
    })
    .start();
</script>
```

The `t` attribute writes translated text by default. Use `t="[html]key"` only when the translated value is trusted HTML that you intentionally want inserted as markup.

## Enhance existing HTML

Use `enhance(...)` when the page already contains the markup you want Aurelia to bind. This is useful for progressive enhancement, server-rendered HTML, or a small demo that does not need a custom root element definition.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Aurelia without a build step</title>
  </head>
  <body>
    <p>${message}</p>

    <script type="module">
      import { Aurelia } from 'https://esm.sh/aurelia@2.0.0-rc.1';

      await new Aurelia()
        .enhance({
          component: { message: 'Hello from enhanced HTML' },
          host: document.body,
        })
        .start();
    </script>
  </body>
</html>
```

Use `.app(...)` when Aurelia owns a component host such as `<app-root>`. Use `.enhance(...)` when Aurelia should bind markup that is already in the document.

## Browser-only constraints

No-build examples run as plain browser JavaScript. Keep these constraints in mind:

* Browsers do not run TypeScript directly. Use JavaScript in the page, or use a build tool for TypeScript examples.
* Decorator syntax needs transpilation. Prefer `CustomElement.define(...)` for no-build examples.
* Local `.html`, `.css`, and `.json` imports are bundler features in most Aurelia apps. Inline small templates in the component definition, or fetch external data over HTTP.
* If your example fetches local files, serve it from `http://localhost` instead of opening it with `file://`. Browser security rules block many `file://` fetches.
* Keep CDN package versions aligned across `aurelia`, `@aurelia/*` packages, and any plugin packages used by those integrations.

When an example grows beyond a single page, move it to the normal project setup from the [Quick Install Guide](/getting-started/quick-install-guide).


# Aurelia for New Developers

New to Javascript, Node.js and front-end development in general? Don't worry, we got you.

Welcome to the magical world of Javascript development. This guide is for any newcomer to front-end development who isn't that experienced with modern tooling or Javascript frameworks.

## Getting started

For the purposes of this tutorial and as a general rule for any modern framework like Aurelia, you will be using a terminal of some sort. On Windows, this can be the Command Prompt or Powershell. On macOS, it'll be Terminal (or any other Terminal alternative), the same thing with Linux.

To work with Aurelia, you will need to install Node.js. If you are new to Node.js, it is used by almost every tool in the front-end ecosystem now, from Webpack to other niche bundlers and tools. It underpins the front-end ecosystem.

The easiest way to install Node.js is from the official website [here](https://nodejs.org/en/). Download the installer for your operating system and then follow the prompts.

## Download a code editor

To write code, you need an editor that will help you. The most popular choice for Javascript development is [Visual Studio Code](https://code.visualstudio.com/). It is a completely free and open-source code editor made by Microsoft, which has great support for Aurelia applications and Node.js.

## Create a new Aurelia project

We will be following the instructions in the [Quick install guide](/getting-started/quick-install-guide) to bootstrap a new Aurelia application. After installing Node.js, that's it. You don't need to install anything else to create a new Aurelia application, here's how we do it.

Open up a Terminal/Command Prompt window and run the following:

```bash
npx makes aurelia
```

You are going to be presented with a few options when you run this command. Don't worry, we'll go through each screen step by step.

### Step 1. Name your project

You will be asked to enter a name for your project, this can be anything you want. If you can't think of a name just enter `my-app` and then hit enter.

### Step 2. Choose your options

In step 2 you will be presented with three options.

* Option one: "Default ESNext Aurelia 2 App" this is a basic Aurelia 2 Javascript application using Babel for transpiling and Webpack for the bundler.
* Option two: "Default Typescript Aurelia 2 App" this is a basic Aurelia 2 TypeScript application with Webpack for the bundler.
* Option three: "Custom Aurelia 2 App" no defaults, you choose everything.

In this guide, we are going to go with the most straightforward option, option #1.

### Step 3. Install the dependencies

You are going to be asked if you want to install the Npm dependencies and the answer is yes. For this guide we are using Npm, so select option #2.

Depending on your internet connection speed, this can take a while.

### Step 4. Run the sample app

After the installation is finished you should see a little block of text with the heading, "Get Started" follow the instructions. Firstly, `cd my-app` to go into the directory where we installed our app. Then run `npm start` to run our example app.

Your web browser should open automatically and point to <http://localhost:9000>

Any changes you make to the files in the `src` directory of your app will cause the dev server to refresh the page with your new changes. Edit `my-app.html` and save it to see the browser update. Cool!

## Building your app

In the last section we created a new application and ran the development server, but in the "real world" you will build and deploy your site for production.

Run the Npm build command by running the following in your Terminal or Command Prompt window:

```
npm run build
```

This will build your application for production and create a new folder called `dist`.

## Keep Learning

Once the CLI basics feel comfortable, continue with these focused guides:

* **Build something interactive** – Follow the [Hello World tutorial](/getting-started/quick-start-guide) for a guided, hands-on walkthrough.
* **See the bigger picture** – Work through the [Complete getting started guide](/getting-started/complete-guide) to assemble a full-featured sample app.
* **Understand components** – Dive into the [component essentials](/introduction/essentials/components) to learn how Aurelia pairs view-models with HTML.
* **Master templates & binding** – Explore the [templates overview](/introduction/essentials/templates) plus the [template syntax deep dive](/templates/overview).
* **Share data across components** – Learn how and when to use the DI container in the [dependency injection overview](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection).
* **Troubleshoot faster** – Bookmark [debugging and troubleshooting](/developer-guides/debugging-and-troubleshooting) for common fixes and CLI tips.


# Hello World Tutorial

Learn the basics of Aurelia by building an interactive Hello, World! application from scratch

Build your first Aurelia app in 10 minutes! This complete guide takes you from zero to a working interactive application with live data binding.

## What You'll Build

An interactive hello world app where typing in a text field instantly updates the greeting. No page refreshes, no complex setup - just pure Aurelia magic.

{% embed url="<https://stackblitz.com/edit/aurelia-hello-world?embed=1&file=my-app.html>" %}

## Prerequisites

* Recent version of [Node.js](https://nodejs.org/en/) installed
* Basic knowledge of JavaScript, HTML, and CSS

## Step 1: Create Your Project

Open your terminal and create a new Aurelia project:

```bash
npx makes aurelia
```

When prompted:

* Project name: `hello-world`
* Setup: Choose TypeScript or ESNext (JavaScript)
* Install dependencies: `yes`

Navigate to your project and start the development server:

```bash
cd hello-world
npm start
```

A browser window opens showing "Hello World". Congratulations! You just ran your first Aurelia app.

## Step 2: Understand the Basics

Aurelia apps are built with **components** that have two parts:

* **View-model** (`.ts`/`.js`): Your logic and data
* **View** (`.html`): Your template

Open `src/my-app.ts` to see your first view-model:

```typescript
export class MyApp {
  message = 'Hello World!';
}
```

And `src/my-app.html` for the view:

```html
<div class="message">${message}</div>
```

The `${message}` syntax is **string interpolation** - it displays the value from your view-model.

## Step 3: Create an Interactive Component

Let's build something more interesting. Create a new component for personalized greetings.

Create `src/hello-name.ts`:

```typescript
export class HelloName {
  name = 'World';
}
```

Create `src/hello-name.html`:

```html
<div>
  <h2>Hello, ${name}!</h2>
  <p>
    <label>Enter your name:</label>
    <input type="text" value.bind="name">
  </p>
</div>
```

The magic is in `value.bind="name"` - this creates **two-way binding** between the input and your view-model property. Change one, and the other updates automatically.

## Step 4: Use Your Component

Update `src/my-app.html` to use your new component:

```html
<import from="./hello-name"></import>

<div class="app">
  <h1>My Aurelia App</h1>
  <hello-name></hello-name>
</div>
```

**Important:** The `<import>` element is required to use your component. It tells Aurelia to load the `hello-name` component from the specified path. Without it, the `<hello-name>` tag won't work and nothing will render (with no error message).

**Alternative:** You can also [register components globally](/introduction/essentials/components#option-2-global-registration) in `main.ts` if you want to use them everywhere without imports.

## Step 5: Test Your App

Save your files and watch the browser automatically refresh. You'll see:

* A heading that says "Hello, World!"
* A text input with "World" pre-filled
* As you type in the input, the heading updates instantly

That's it! You've built a reactive Aurelia application with:

* Custom components
* Data binding
* Real-time updates

## Key Concepts You Learned

1. **Components**: Building blocks made of view-models and views
2. **String Interpolation**: `${property}` displays data in templates
3. **Two-way Binding**: `value.bind` keeps input and data synchronized
4. **Component Registration**: `<import>` and custom element tags
5. **Conventions**: File names become component names

## Next Steps

Ready to dive deeper? Explore:

* [Template Syntax](/templates/overview) - loops, conditionals, and more bindings
* [Component Lifecycle](/components/components) - hooks for advanced behavior
* [Dependency Injection](/introduction/essentials/dependency-injection) - sharing services between components
* [Router](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) - multi-page applications

The fundamentals you learned here apply to every Aurelia app you'll build. Start experimenting and see what you can create!


# Intermediate Tutorial

Take your Aurelia skills to the next level by building a feature-rich todo application. This tutorial covers component composition, filtering, local storage, and real-world patterns.

## What You'll Learn

* Creating multiple components and composing them
* Component communication with bindable properties
* Advanced list rendering and filtering
* Form handling with validation
* Local storage persistence
* Computed properties and reactive updates
* Template patterns for real apps

## Prerequisites

* Completed the [Hello World Tutorial](/getting-started/quick-start-guide)
* Basic understanding of [Templates](/templates/overview)
* Familiarity with TypeScript

## The App We're Building

A todo application with:

* ✅ Add, complete, and delete tasks
* 🏷️ Categorize tasks (Work, Personal, Shopping)
* 🔍 Filter by category and completion status
* 💾 Auto-save to local storage
* 📊 Task statistics

## Step 1: Project Setup

```bash
npx makes aurelia
# Name: todo-app
# Select TypeScript
cd todo-app
npm run dev
```

## Step 2: Data Models

Create `src/models.ts`:

```typescript
export interface Todo {
  id: string;
  title: string;
  description: string;
  category: Category;
  completed: boolean;
  createdAt: Date;
}

export type Category = 'work' | 'personal' | 'shopping';

export const CATEGORIES: Category[] = ['work', 'personal', 'shopping'];

export const CATEGORY_LABELS: Record<Category, string> = {
  work: 'Work',
  personal: 'Personal',
  shopping: 'Shopping'
};

export const CATEGORY_COLORS: Record<Category, string> = {
  work: '#3b82f6',
  personal: '#10b981',
  shopping: '#f59e0b'
};
```

## Step 3: Storage Service

Create `src/storage-service.ts`:

```typescript
import { DI } from 'aurelia';

export const IStorageService = DI.createInterface<IStorageService>(
  'IStorageService',
  x => x.singleton(StorageService)
);

export interface IStorageService extends StorageService {}

export class StorageService {
  private readonly STORAGE_KEY = 'aurelia-todos';

  saveTodos(todos: any[]): void {
    try {
      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(todos));
    } catch (error) {
      console.error('Failed to save todos:', error);
    }
  }

  loadTodos(): any[] {
    try {
      const data = localStorage.getItem(this.STORAGE_KEY);
      return data ? JSON.parse(data) : [];
    } catch (error) {
      console.error('Failed to load todos:', error);
      return [];
    }
  }

  clearTodos(): void {
    localStorage.removeItem(this.STORAGE_KEY);
  }
}
```

The service is automatically registered as a singleton via `DI.createInterface`.

## Step 4: Main App Component

Update `src/my-app.ts`:

```typescript
import { resolve } from 'aurelia';
import { IStorageService } from './storage-service';
import { Todo, Category, CATEGORIES } from './models';

export class MyApp {
  private readonly storage = resolve(IStorageService);

  todos: Todo[] = [];
  filterCategory: Category | 'all' = 'all';
  filterCompleted: 'all' | 'active' | 'completed' = 'all';

  constructor() {
    this.loadTodos();
  }

  // Computed property for filtered todos
  get filteredTodos(): Todo[] {
    let filtered = this.todos;

    // Filter by category
    if (this.filterCategory !== 'all') {
      filtered = filtered.filter(todo => todo.category === this.filterCategory);
    }

    // Filter by completion status
    if (this.filterCompleted === 'active') {
      filtered = filtered.filter(todo => !todo.completed);
    } else if (this.filterCompleted === 'completed') {
      filtered = filtered.filter(todo => todo.completed);
    }

    return filtered;
  }

  // Statistics computed properties
  get totalTodos(): number {
    return this.todos.length;
  }

  get activeTodos(): number {
    return this.todos.filter(todo => !todo.completed).length;
  }

  get completedTodos(): number {
    return this.todos.filter(todo => todo.completed).length;
  }

  get categories(): (Category | 'all')[] {
    return ['all', ...CATEGORIES];
  }

  // Todo operations
  addTodo(todo: Omit<Todo, 'id' | 'createdAt'>): void {
    const newTodo: Todo = {
      ...todo,
      id: crypto.randomUUID(),
      createdAt: new Date()
    };

    this.todos.push(newTodo);
    this.saveTodos();
  }

  toggleTodo(todo: Todo): void {
    todo.completed = !todo.completed;
    this.saveTodos();
  }

  deleteTodo(todo: Todo): void {
    const index = this.todos.indexOf(todo);
    if (index > -1) {
      this.todos.splice(index, 1);
      this.saveTodos();
    }
  }

  clearCompleted(): void {
    this.todos = this.todos.filter(todo => !todo.completed);
    this.saveTodos();
  }

  // Persistence
  private saveTodos(): void {
    this.storage.saveTodos(this.todos);
  }

  private loadTodos(): void {
    const loaded = this.storage.loadTodos();
    this.todos = loaded.map(todo => ({
      ...todo,
      createdAt: new Date(todo.createdAt)
    }));
  }
}
```

## Step 5: Create Todo Form Component

Create `src/todo-form.ts`:

```typescript
import { bindable } from 'aurelia';
import { Category, CATEGORIES, CATEGORY_LABELS } from './models';

export class TodoForm {
  @bindable onSubmit?: (data: any) => void;

  formData = {
    title: '',
    description: '',
    category: 'work' as Category
  };

  categories = CATEGORIES;
  categoryLabels = CATEGORY_LABELS;

  get isValid(): boolean {
    return this.formData.title.trim().length > 0;
  }

  handleSubmit(): void {
    if (!this.isValid) return;

    this.onSubmit?.({
      title: this.formData.title.trim(),
      description: this.formData.description.trim(),
      category: this.formData.category,
      completed: false
    });

    this.resetForm();
  }

  resetForm(): void {
    this.formData = {
      title: '',
      description: '',
      category: 'work'
    };
  }
}
```

Create `src/todo-form.html`:

```html
<div class="todo-form">
  <h2>Add New Todo</h2>
  <form submit.trigger="handleSubmit()">
    <div class="form-group">
      <label for="title">Title *</label>
      <input
        id="title"
        type="text"
        value.bind="formData.title"
        placeholder="Enter todo title"
        required />
    </div>

    <div class="form-group">
      <label for="description">Description</label>
      <textarea
        id="description"
        value.bind="formData.description"
        placeholder="Optional description"
        rows="3"></textarea>
    </div>

    <div class="form-group">
      <label for="category">Category</label>
      <select id="category" value.bind="formData.category">
        <option repeat.for="cat of categories" value.bind="cat">
          ${categoryLabels[cat]}
        </option>
      </select>
    </div>

    <button type="submit" disabled.bind="!isValid">
      Add Todo
    </button>
  </form>
</div>
```

## Step 6: Create Todo Item Component

Create `src/todo-item.ts`:

```typescript
import { bindable } from 'aurelia';
import { Todo, CATEGORY_LABELS, CATEGORY_COLORS } from './models';

export class TodoItem {
  @bindable todo!: Todo;
  @bindable onToggle?: (todo: Todo) => void;
  @bindable onDelete?: (todo: Todo) => void;

  categoryLabels = CATEGORY_LABELS;
  categoryColors = CATEGORY_COLORS;

  get categoryColor(): string {
    return this.categoryColors[this.todo.category];
  }

  handleToggle(): void {
    this.onToggle?.(this.todo);
  }

  handleDelete(): void {
    if (confirm(`Delete "${this.todo.title}"?`)) {
      this.onDelete?.(this.todo);
    }
  }

  get formattedDate(): string {
    return this.todo.createdAt.toLocaleDateString();
  }
}
```

Create `src/todo-item.html`:

```html
<div class="todo-item ${todo.completed ? 'completed' : ''}">
  <div class="todo-content">
    <label class="todo-checkbox">
      <input
        type="checkbox"
        checked.bind="todo.completed"
        change.trigger="handleToggle()" />
      <span class="checkmark"></span>
    </label>

    <div class="todo-details">
      <h3 class="todo-title">${todo.title}</h3>
      <p if.bind="todo.description" class="todo-description">
        ${todo.description}
      </p>
      <div class="todo-meta">
        <span class="todo-category" style="background-color: ${categoryColor}">
          ${categoryLabels[todo.category]}
        </span>
        <span class="todo-date">${formattedDate}</span>
      </div>
    </div>
  </div>

  <button
    class="delete-btn"
    click.trigger="handleDelete()"
    title="Delete todo">
    ×
  </button>
</div>
```

## Step 7: Main App Template

Update `src/my-app.html`:

```html
<import from="./todo-form"></import>
<import from="./todo-item"></import>

<div class="app">
  <header class="app-header">
    <h1>📝 Aurelia Todo App</h1>
    <div class="stats">
      <span class="stat">Total: ${totalTodos}</span>
      <span class="stat">Active: ${activeTodos}</span>
      <span class="stat">Completed: ${completedTodos}</span>
    </div>
  </header>

  <main class="app-main">
    <div class="sidebar">
      <todo-form on-submit.bind="(data) => addTodo(data)"></todo-form>
    </div>

    <div class="content">
      <!-- Filters -->
      <div class="filters">
        <div class="filter-group">
          <label>Category:</label>
          <select value.bind="filterCategory">
            <option value="all">All Categories</option>
            <option repeat.for="cat of categories" value.bind="cat">
              ${cat === 'all' ? 'All' : cat}
            </option>
          </select>
        </div>

        <div class="filter-group">
          <label>Status:</label>
          <select value.bind="filterCompleted">
            <option value="all">All</option>
            <option value="active">Active</option>
            <option value="completed">Completed</option>
          </select>
        </div>

        <button
          if.bind="completedTodos > 0"
          click.trigger="clearCompleted()"
          class="clear-btn">
          Clear Completed
        </button>
      </div>

      <!-- Todo List -->
      <div class="todo-list">
        <div if.bind="filteredTodos.length === 0" class="empty-state">
          <p>No todos found!</p>
          <small if.bind="filterCategory !== 'all' || filterCompleted !== 'all'">
            Try changing your filters
          </small>
        </div>

        <todo-item
          repeat.for="todo of filteredTodos; key: id"
          todo.bind="todo"
          on-toggle.bind="(todo) => toggleTodo(todo)"
          on-delete.bind="(todo) => deleteTodo(todo)">
        </todo-item>
      </div>
    </div>
  </main>
</div>
```

## Step 8: Styling

Update `src/my-app.css`:

```css
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #f5f5f5;
  color: #333;
}

.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.app-header {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  margin-bottom: 2rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.app-header h1 {
  margin-bottom: 1rem;
}

.stats {
  display: flex;
  gap: 2rem;
}

.stat {
  font-size: 0.9rem;
  color: #666;
}

.app-main {
  display: grid;
  grid-template-columns: 350px 1fr;
  gap: 2rem;
}

/* Todo Form */
.todo-form {
  background: white;
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.todo-form h2 {
  font-size: 1.2rem;
  margin-bottom: 1rem;
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

.form-group input,
.form-group textarea,
.form-group select {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

button {
  padding: 0.75rem 1.5rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

button:hover:not(:disabled) {
  background: #2563eb;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Filters */
.filters {
  background: white;
  padding: 1.5rem;
  border-radius: 8px;
  margin-bottom: 1rem;
  display: flex;
  gap: 1rem;
  align-items: flex-end;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.filter-group {
  flex: 1;
}

.filter-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.9rem;
  font-weight: 500;
}

.filter-group select {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.clear-btn {
  background: #ef4444;
}

.clear-btn:hover {
  background: #dc2626;
}

/* Todo List */
.todo-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.empty-state {
  background: white;
  padding: 3rem;
  border-radius: 8px;
  text-align: center;
  color: #999;
}

/* Todo Item */
.todo-item {
  background: white;
  padding: 1rem;
  border-radius: 8px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s;
}

.todo-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.todo-item.completed {
  opacity: 0.6;
}

.todo-content {
  display: flex;
  gap: 1rem;
  flex: 1;
}

.todo-checkbox {
  cursor: pointer;
  position: relative;
}

.todo-checkbox input {
  cursor: pointer;
}

.todo-details {
  flex: 1;
}

.todo-title {
  font-size: 1.1rem;
  margin-bottom: 0.25rem;
}

.todo-item.completed .todo-title {
  text-decoration: line-through;
}

.todo-description {
  color: #666;
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
}

.todo-meta {
  display: flex;
  gap: 1rem;
  align-items: center;
}

.todo-category {
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  color: white;
  font-weight: 500;
}

.todo-date {
  font-size: 0.8rem;
  color: #999;
}

.delete-btn {
  background: transparent;
  color: #ef4444;
  border: 1px solid #ef4444;
  width: 32px;
  height: 32px;
  padding: 0;
  font-size: 1.5rem;
  line-height: 1;
}

.delete-btn:hover {
  background: #ef4444;
  color: white;
}

@media (max-width: 768px) {
  .app-main {
    grid-template-columns: 1fr;
  }

  .filters {
    flex-direction: column;
    align-items: stretch;
  }
}
```

## What You've Learned

* **Component Composition** - Created reusable TodoForm and TodoItem components
* **Component Communication** - Used `@bindable` and callback functions for parent-child communication
* **Dependency Injection** - Created and injected StorageService
* **Computed Properties** - Implemented filtered lists and statistics
* **List Rendering** - Used `repeat.for` with keys for efficient updates
* **Conditional Rendering** - Showed/hid elements based on state
* **Form Handling** - Built forms with validation and submission
* **Local Storage** - Persisted data across sessions
* **Template Patterns** - Applied real-world templating techniques

## Next Steps

Enhance your app with:

* **Drag-and-drop** reordering
* **Edit mode** for todos
* **Due dates** and reminders
* **Search** functionality
* **Dark mode** toggle
* **Export/import** todos

## Related Documentation

* [Templates Overview](/templates/templates)
* [Component Basics](/components/components)
* [Extended Tutorial](/getting-started/extended-tutorial)
* [Dependency Injection](/introduction/essentials/dependency-injection)
* [Form Handling](/templates/forms/forms)
* [List Rendering](/templates/repeats-and-list-rendering)


# Extended Tutorial

Multi-step Project Pulse tutorial that introduces routing, component communication, and real-world app patterns.

Build a small but realistic app while learning the Aurelia router, component communication, and real-world navigation patterns.

{% embed url="<https://codesandbox.io/p/devbox/bold-cache-cyn542?embed=1>" %}

## What you will build

* A Dashboard and a Projects area
* Nested routes for Projects (Overview + Activity)
* A project detail view with route parameters
* Filterable lists with query params you can share
* Router events and active navigation styling
* Multi-locale internationalization (i18n)
* Form validation with localized error messages
* Modal dialogs for confirmations and editing
* Centralized state management

## Prerequisites

* Completed the [Hello World Tutorial](/getting-started/quick-start-guide)
* Basic TypeScript and HTML

## Steps

### Core (Steps 1–4)

1. [Step 1: Project setup + app shell](/getting-started/extended-tutorial/step-1-project-setup)
2. [Step 2: Routing + nested layouts](/getting-started/extended-tutorial/step-2-routing-and-layout)
3. [Step 3: Overview page + filters + events](/getting-started/extended-tutorial/step-3-projects-overview)
4. [Step 4: Detail route + guards](/getting-started/extended-tutorial/step-4-project-detail-and-guards)

### Advanced routing (Steps 5–6)

5. [Step 5: Router events + polish](/getting-started/extended-tutorial/step-5-router-events-and-polish)
6. [Step 6: Route data + auth roles](/getting-started/extended-tutorial/step-6-route-data-and-roles)

### Aurelia packages (Steps 7–10)

7. [Step 7: Internationalization (i18n)](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-started/extended-tutorial/step-7-internationalization.md)
8. [Step 8: Form validation with i18n](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-started/extended-tutorial/step-8-form-validation.md)
9. [Step 9: Dialogs](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-started/extended-tutorial/step-9-dialogs.md)
10. [Step 10: State management](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-started/extended-tutorial/step-10-state-management.md)

If you are new to Aurelia, start with steps 1–4 to learn routing fundamentals. Steps 5–6 cover advanced routing patterns. Steps 7–10 introduce additional Aurelia packages for building production-ready applications.


# Step 1: Project setup + app shell

Set up Project Pulse, enable the router, and build the shared app shell.

In this step you will create the project, enable the router, and build a shared layout component.

## 1. Create the project

```bash
npx makes aurelia
# Name: project-pulse
# Select TypeScript
cd project-pulse
npm run dev
```

## 2. Enable the router and active link styling

Update `src/main.ts` to register the router and configure an active class for navigation links:

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { MyApp } from './my-app';

Aurelia
  .register(RouterConfiguration.customize({ activeClass: 'is-active' }))
  .app(MyApp)
  .start();
```

Any `<a load="...">` link will now get the `is-active` class when the route is active.

## 3. Create the app shell

Create the folders `src/components` and `src/pages` if you do not have them yet.

Create `src/components/app-shell.ts`:

```typescript
export class AppShell {}
```

Create `src/components/app-shell.html`:

```html
<div class="shell">
  <header class="shell__header">
    <div class="shell__title">
      <au-slot name="title">Project Pulse</au-slot>
    </div>
    <div class="shell__actions">
      <au-slot name="actions"></au-slot>
    </div>
  </header>

  <main class="shell__body">
    <au-slot></au-slot>
  </main>
</div>
```

Next step: [Step 2: Routing + nested layouts](/getting-started/extended-tutorial/step-2-routing-and-layout)


# Step 2: Routing + nested layouts

Add primary routes, a Projects layout, and nested child routes.

In this step you will create the root pages and a Projects layout that hosts child routes.

## 1. Create the dashboard

Create `src/pages/dashboard-page.ts`:

```typescript
export class DashboardPage {}
```

Create `src/pages/dashboard-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">Dashboard</h1>
  <a au-slot="actions" load="../projects">View Projects</a>

  <p>Welcome to Project Pulse. Use the Projects page to manage tasks.</p>
</app-shell>
```

**Note:** Links inside routed components are resolved relative to the current route. Use `../` when you want to navigate to a sibling at the parent level.

## 2. Create placeholder child pages

We will build these pages in later steps, but we need the files now so the routes can resolve.

Create `src/pages/projects-overview-page.ts`:

```typescript
export class ProjectsOverviewPage {}
```

Create `src/pages/projects-overview-page.html`:

```html
<p>Projects overview goes here.</p>
```

Create `src/pages/projects-activity-page.ts`:

```typescript
export class ProjectsActivityPage {}
```

Create `src/pages/projects-activity-page.html`:

```html
<p>Activity summary goes here.</p>
```

## 3. Create the Projects layout with child routes

Create `src/pages/projects-page.ts`:

```typescript
import { route } from '@aurelia/router';
import { ProjectsActivityPage } from './projects-activity-page';
import { ProjectsOverviewPage } from './projects-overview-page';

@route({
  routes: [
    { path: ['', 'overview'], component: ProjectsOverviewPage, title: 'Overview' },
    { path: 'activity', component: ProjectsActivityPage, title: 'Activity' }
  ]
})
export class ProjectsPage {}
```

Create `src/pages/projects-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">Projects</h1>
  <a au-slot="actions" load="../dashboard">Back to Dashboard</a>

  <nav class="projects-subnav">
    <a load="">Overview</a>
    <a load="activity">Activity</a>
  </nav>

  <au-viewport></au-viewport>
</app-shell>
```

The nested `<au-viewport>` is where the Overview and Activity pages render.

## 4. Wire up the root routes

Update `src/my-app.ts`:

```typescript
import { route } from '@aurelia/router';
import { DashboardPage } from './pages/dashboard-page';
import { ProjectsPage } from './pages/projects-page';

@route({
  routes: [
    { path: ['', 'dashboard'], component: DashboardPage, title: 'Dashboard' },
    { path: 'projects', component: ProjectsPage, title: 'Projects' }
  ]
})
export class MyApp {}
```

Update `src/my-app.html`:

```html
<nav class="main-nav">
  <a load="dashboard">Dashboard</a>
  <a load="projects">Projects</a>
</nav>

<au-viewport></au-viewport>
```

Notice the `is-active` class when you navigate between Dashboard and Projects.

Next step: [Step 3: Overview page + filters + events](/getting-started/extended-tutorial/step-3-projects-overview)


# Step 3: Overview page + filters + events

Add real data, reusable components, filters with query params, and event-driven updates.

In this step you will add real data, reusable components, query param syncing, and deep child-to-parent communication with the Event Aggregator.

## 1. Define shared models and data

Create `src/models.ts`:

```typescript
export type Task = {
  id: string;
  title: string;
  done: boolean;
};

export type Project = {
  id: string;
  name: string;
  tasks: Task[];
};
```

Create `src/project-data.ts`:

```typescript
import { Project } from './models';

export const PROJECTS: Project[] = [
  {
    id: 'alpha',
    name: 'Onboarding',
    tasks: [
      { id: 'alpha-1', title: 'Create welcome pack', done: false },
      { id: 'alpha-2', title: 'Schedule kickoff', done: false }
    ]
  },
  {
    id: 'beta',
    name: 'Release prep',
    tasks: [
      { id: 'beta-1', title: 'Finalize changelog', done: true },
      { id: 'beta-2', title: 'QA smoke test', done: false }
    ]
  }
];
```

## 2. Build reusable components

Create `src/components/project-card.ts`:

```typescript
import { bindable } from 'aurelia';
import { Project } from '../models';

export class ProjectCard {
  @bindable project!: Project;
  @bindable onRemove?: (project: Project) => void;

  remove(): void {
    this.onRemove?.(this.project);
  }
}
```

Create `src/components/project-card.html`:

```html
<import from="./task-list"></import>

<section class="project-card">
  <header class="project-card__header">
    <h3>${project.name}</h3>
    <span class="project-card__count">
      ${project.tasks.length} tasks
    </span>
  </header>

  <task-list tasks.bind="project.tasks" project-id.bind="project.id"></task-list>

  <footer class="project-card__footer">
    <a load="route: ../project-detail; params.bind: { id: project.id }">
      Open project
    </a>
    <button class="project-card__remove" click.trigger="remove()">
      Remove
    </button>
  </footer>
</section>
```

The `route:` instruction uses a route **id**. Because this link lives inside the Overview child route, we prefix with `../` so the router resolves the id from the parent (Projects) route context.

Create `src/components/task-list.ts`:

```typescript
import { bindable } from 'aurelia';
import { Task } from '../models';

export class TaskList {
  @bindable tasks: Task[] = [];
  @bindable projectId = '';
}
```

Create `src/components/task-list.html`:

```html
<import from="./task-item"></import>

<ul class="task-list">
  <li repeat.for="task of tasks">
    <task-item task.bind="task" project-id.bind="projectId"></task-item>
  </li>
</ul>
```

Create `src/components/task-item.ts`:

```typescript
import { IEventAggregator, resolve } from '@aurelia/kernel';
import { bindable } from 'aurelia';
import { Task } from '../models';

export class TaskItem {
  @bindable task!: Task;
  @bindable projectId = '';

  private readonly ea = resolve(IEventAggregator);

  notifyToggle(): void {
    this.ea.publish('task:toggled', {
      projectId: this.projectId,
      taskId: this.task.id,
      done: this.task.done
    });
  }
}
```

Create `src/components/task-item.html`:

```html
<label class="task-item">
  <input type="checkbox" checked.bind="task.done" change.trigger="notifyToggle()" />
  <span class.bind="task.done ? 'task-item__done' : ''">
    ${task.title}
  </span>
</label>
```

## 3. Replace the Overview page with real behavior

Update `src/pages/projects-overview-page.ts`:

```typescript
import { IEventAggregator, IDisposable, resolve } from '@aurelia/kernel';
import { IRouter, IRouteViewModel, Params, RouteNode } from '@aurelia/router';
import { observable } from 'aurelia';
import { Project } from '../models';
import { PROJECTS } from '../project-data';

export class ProjectsOverviewPage implements IRouteViewModel {
  @observable searchQuery = '';

  projects: Project[] = structuredClone(PROJECTS);
  filteredProjects: Project[] = this.projects;
  recentActivity: string[] = [];
  newProjectName = '';

  private readonly ea = resolve(IEventAggregator);
  private readonly router = resolve(IRouter);
  private subscription?: IDisposable;

  loading(_params: Params, next: RouteNode): void {
    const query = next.queryParams.get('q');
    this.searchQuery = query ?? '';
    this.applyFilter();
  }

  bound(): void {
    this.subscription = this.ea.subscribe('task:toggled', ({ projectId, taskId, done }) => {
      const project = this.projects.find(item => item.id === projectId);
      const task = project?.tasks.find(item => item.id === taskId);

      if (!project || !task) return;

      const status = done ? 'completed' : 'reopened';
      this.recentActivity.unshift(`${project.name}: ${task.title} ${status}`);
      this.recentActivity = this.recentActivity.slice(0, 5);
    });
  }

  unbinding(): void {
    this.subscription?.dispose();
  }

  searchQueryChanged(): void {
    this.applyFilter();
  }

  clearSearch(): void {
    this.searchQuery = '';
    this.applyFilter();
    this.syncQueryToUrl();
  }

  syncQueryToUrl(): void {
    void this.router.load('overview', {
      context: this,
      queryParams: this.searchQuery ? { q: this.searchQuery } : {}
    });
  }

  handleNewProjectKeydown(event: KeyboardEvent): void {
    if (event.key === 'Enter') {
      this.addProject();
    }
  }

  addProject(): void {
    const name = this.newProjectName.trim();
    if (!name) return;

    const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
    this.projects = [
      ...this.projects,
      { id, name, tasks: [] }
    ];
    this.newProjectName = '';
    this.applyFilter();
  }

  removeProject(project: Project): void {
    this.projects = this.projects.filter(item => item !== project);
    this.applyFilter();
  }

  private applyFilter(): void {
    const term = this.searchQuery.trim().toLowerCase();
    this.filteredProjects = term
      ? this.projects.filter(project => project.name.toLowerCase().includes(term))
      : this.projects;
  }
}
```

Update `src/pages/projects-overview-page.html`:

```html
<import from="../components/project-card"></import>

<section class="toolbar">
  <input
    value.bind="searchQuery"
    placeholder="Search projects" />
  <button if.bind="searchQuery" click.trigger="clearSearch()">
    Clear
  </button>
  <button if.bind="searchQuery" click.trigger="syncQueryToUrl()">
    Share Filter
  </button>
</section>

<section class="project-create">
  <input
    value.bind="newProjectName"
    placeholder="New project name"
    keydown.trigger="handleNewProjectKeydown($event)" />
  <button disabled.bind="!newProjectName.trim()" click.trigger="addProject()">
    Add project
  </button>
</section>

<div class="project-grid">
  <project-card
    repeat.for="project of filteredProjects"
    project.bind="project"
    on-remove.bind="(project) => removeProject(project)">
  </project-card>
</div>

<aside class="activity" if.bind="recentActivity.length">
  <h2>Recent activity</h2>
  <ul>
    <li repeat.for="entry of recentActivity">${entry}</li>
  </ul>
</aside>
```

The `loading` hook reads the query params from `next.queryParams`. The Share Filter button uses `IRouter.load()` to update the URL.

## 4. Replace the Activity page with real data

Update `src/pages/projects-activity-page.ts`:

```typescript
import { PROJECTS } from '../project-data';

export class ProjectsActivityPage {
  totalProjects = PROJECTS.length;

  get totalTasks(): number {
    return PROJECTS.reduce((total, project) => total + project.tasks.length, 0);
  }

  get completedTasks(): number {
    return PROJECTS.reduce(
      (total, project) => total + project.tasks.filter(task => task.done).length,
      0
    );
  }
}
```

Update `src/pages/projects-activity-page.html`:

```html
<section class="activity-summary">
  <h2>Activity Summary</h2>
  <p>Total projects: ${totalProjects}</p>
  <p>Tasks completed: ${completedTasks} / ${totalTasks}</p>
</section>
```

Next step: [Step 4: Detail route + guards](/getting-started/extended-tutorial/step-4-project-detail-and-guards)


# Step 4: Detail route + guards

Add a detail route with parameters and protect it with router guards.

In this step you will add a parameterized detail route and guard it with `canLoad` and `canUnload`.

## 1. Add the detail route

Update `src/pages/projects-page.ts` to add the detail child route:

```typescript
import { route } from '@aurelia/router';
import { ProjectDetailPage } from './project-detail-page';
import { ProjectsActivityPage } from './projects-activity-page';
import { ProjectsOverviewPage } from './projects-overview-page';

@route({
  routes: [
    { path: ['', 'overview'], component: ProjectsOverviewPage, title: 'Overview' },
    { path: 'activity', component: ProjectsActivityPage, title: 'Activity' },
    { id: 'project-detail', path: 'detail/:id', component: ProjectDetailPage, title: 'Project Detail' }
  ]
})
export class ProjectsPage {}
```

We keep `detail` in the URL to keep the child routes explicit and readable. The `id: 'project-detail'` value is what the Overview page uses in its `route:` instruction, with a `../` prefix to resolve against the parent context.

## 2. Build the detail page

Create `src/pages/project-detail-page.ts`:

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';
import { Project } from '../models';
import { PROJECTS } from '../project-data';

export class ProjectDetailPage implements IRouteViewModel {
  projectId = '';
  project: Project | null = null;
  noteDraft = '';

  canLoad(params: Params): boolean | string {
    const candidate = PROJECTS.find(item => item.id === params.id);
    return candidate ? true : 'projects';
  }

  loading(params: Params): void {
    this.projectId = params.id ?? '';
    this.project = PROJECTS.find(item => item.id === this.projectId) ?? null;
  }

  canUnload(): boolean {
    if (!this.noteDraft.trim()) return true;
    return confirm('You have an unsaved note. Leave this page?');
  }

  saveNote(): void {
    this.noteDraft = '';
  }
}
```

Create `src/pages/project-detail-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">${project?.name ?? 'Project'}</h1>
  <a au-slot="actions" load="../overview">Back to Projects</a>

  <section if.bind="project">
    <p class="project-meta">Project id: ${projectId}</p>

    <label class="note">
      <span>Note</span>
      <textarea value.bind="noteDraft" placeholder="Add a quick note"></textarea>
    </label>
    <button if.bind="noteDraft" click.trigger="saveNote()">Save Note</button>

    <ul class="task-list">
      <li repeat.for="task of project.tasks">
        <span class.bind="task.done ? 'task-item__done' : ''">${task.title}</span>
      </li>
    </ul>
  </section>

  <p if.bind="!project">Project not found.</p>
</app-shell>
```

**Guard recap:**

* `canLoad` prevents invalid IDs and redirects to `projects`.
* `canUnload` prompts when there is unsaved user input.

Next step: [Step 5: Router events + polish](/getting-started/extended-tutorial/step-5-router-events-and-polish)


# Step 5: Router events + polish

Add router event listeners and polish the navigation experience.

In this step you will listen to router events and surface navigation state in the UI.

## 1. Listen to router events in the Projects layout

Update `src/pages/projects-page.ts`:

```typescript
import { IDisposable, resolve } from '@aurelia/kernel';
import { IRouterEvents, NavigationEndEvent, route } from '@aurelia/router';
import { ProjectsActivityPage } from './projects-activity-page';
import { ProjectsOverviewPage } from './projects-overview-page';

@route({
  routes: [
    { path: ['', 'overview'], component: ProjectsOverviewPage, title: 'Overview' },
    { path: 'activity', component: ProjectsActivityPage, title: 'Activity' }
  ]
})
export class ProjectsPage {
  lastNavigation = '';

  private readonly events = resolve(IRouterEvents);
  private subscription?: IDisposable;

  bound(): void {
    this.subscription = this.events.subscribe(
      'au:router:navigation-end',
      (event: NavigationEndEvent) => {
        this.lastNavigation = event.instructions.toPath();
      }
    );
  }

  unbinding(): void {
    this.subscription?.dispose();
  }
}
```

Update `src/pages/projects-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">Projects</h1>
  <a au-slot="actions" load="../dashboard">Back to Dashboard</a>

  <nav class="projects-subnav">
    <a load="./">Overview</a>
    <a load="activity">Activity</a>
  </nav>

  <p class="nav-meta" if.bind="lastNavigation">
    Last navigation: ${lastNavigation}
  </p>

  <au-viewport></au-viewport>
</app-shell>
```

## 2. Recap the routing features used

You now have:

* Root routes and child routes
* Active navigation styling via `activeClass`
* Query params synced from the UI
* Programmatic navigation via `IRouter.load`
* Router events via `IRouterEvents`
* Guards (`canLoad` and `canUnload`) for real-world UX

If you want, expand this app by adding child routes under `projects/detail/:id`, or persist notes to storage.

Next step: [Step 6: Route data + auth roles](/getting-started/extended-tutorial/step-6-route-data-and-roles)


# Step 6: Route data + auth roles

Use route data for roles and enforce access with a router hook.

Optional step: This adds route data for role requirements and enforces it with a router hook. You will also add a small auth status widget to toggle the admin role in the UI. If you want to keep the tutorial beginner-friendly, you can stop after Step 5.

## 1. Create a tiny auth service

Create `src/services/auth-service.ts`:

```typescript
import { DI } from '@aurelia/kernel';

export type User = {
  name: string;
  roles: string[];
};

export const IAuthService = DI.createInterface<IAuthService>(
  'IAuthService',
  x => x.singleton(AuthService)
);

export interface IAuthService extends AuthService {}

export class AuthService {
  private user: User | null = { name: 'Taylor', roles: ['member'] };

  getCurrentUser(): User | null {
    return this.user;
  }

  hasRole(role: string): boolean {
    return !!this.user?.roles.includes(role);
  }

  toggleRole(role: string): void {
    if (!this.user) return;

    if (this.user.roles.includes(role)) {
      this.user = { ...this.user, roles: this.user.roles.filter(item => item !== role) };
      return;
    }

    this.user = { ...this.user, roles: [...this.user.roles, role] };
  }
}
```

## 2. Create a role-based router hook

Create `src/role-hook.ts`:

```typescript
import { resolve } from '@aurelia/kernel';
import { lifecycleHooks } from '@aurelia/runtime-html';
import { IRouteViewModel, Params, RouteNode } from '@aurelia/router';
import { IAuthService } from './services/auth-service';

@lifecycleHooks()
export class RoleHook {
  private readonly auth = resolve(IAuthService);

  canLoad(_viewModel: IRouteViewModel, _params: Params, next: RouteNode): boolean | string {
    const requiredRoles = next.data?.roles as string[] | undefined;
    const fallbackRoute = (next.data?.fallbackRoute as string | undefined) ?? 'forbidden';

    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

    const hasRequiredRole = requiredRoles.some(role => this.auth.hasRole(role));
    return hasRequiredRole ? true : fallbackRoute;
  }
}
```

Register the hook in `src/main.ts`:

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { MyApp } from './my-app';
import { RoleHook } from './role-hook';

Aurelia
  .register(
    RouterConfiguration.customize({ activeClass: 'is-active' }),
    RoleHook
  )
  .app(MyApp)
  .start();
```

## 3. Add protected routes with route data

Create `src/pages/admin-page.ts`:

```typescript
export class AdminPage {}
```

Create `src/pages/admin-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">Admin</h1>
  <a au-slot="actions" load="../dashboard">Back to Dashboard</a>

  <p>Only admins can access this page.</p>
</app-shell>
```

Create `src/pages/forbidden-page.ts`:

```typescript
export class ForbiddenPage {}
```

Create `src/pages/forbidden-page.html`:

```html
<import from="../components/app-shell"></import>

<app-shell>
  <h1 au-slot="title">Access denied</h1>
  <a au-slot="actions" load="../dashboard">Back to Dashboard</a>

  <p>You do not have permission to view that page.</p>
</app-shell>
```

Update `src/my-app.ts` to add route data:

```typescript
import { route } from '@aurelia/router';
import { AdminPage } from './pages/admin-page';
import { DashboardPage } from './pages/dashboard-page';
import { ForbiddenPage } from './pages/forbidden-page';
import { ProjectsPage } from './pages/projects-page';

@route({
  routes: [
    { path: ['', 'dashboard'], component: DashboardPage, title: 'Dashboard' },
    { path: 'projects', component: ProjectsPage, title: 'Projects' },
    {
      path: 'admin',
      component: AdminPage,
      title: 'Admin',
      data: {
        roles: ['admin'],
        fallbackRoute: 'forbidden'
      }
    },
    { path: 'forbidden', component: ForbiddenPage, title: 'Access denied' }
  ]
})
export class MyApp {}
```

## 4. Add an auth status widget

Create `src/components/auth-status.ts`:

```typescript
import { resolve } from '@aurelia/kernel';
import { IAuthService, User } from '../services/auth-service';

export class AuthStatus {
  private readonly auth = resolve(IAuthService);

  user: User | null = this.auth.getCurrentUser();

  toggleAdmin(): void {
    this.auth.toggleRole('admin');
    this.user = this.auth.getCurrentUser();
  }
}
```

Create `src/components/auth-status.html`:

```html
<section class="auth-status">
  <p if.bind="user">
    Signed in as ${user.name}. Roles: ${user.roles.join(', ')}
  </p>
  <p if.bind="!user">Signed out.</p>

  <button click.trigger="toggleAdmin()">Toggle Admin Role</button>
</section>
```

Update `src/pages/dashboard-page.html` to show it:

```html
<import from="../components/app-shell"></import>
<import from="../components/auth-status"></import>

<app-shell>
  <h1 au-slot="title">Dashboard</h1>
  <a au-slot="actions" load="../projects">View Projects</a>

  <p>Welcome to Project Pulse. Use the Projects page to manage tasks.</p>

  <auth-status></auth-status>
</app-shell>
```

Finally, add the Admin link in `src/my-app.html`:

```html
<nav class="main-nav">
  <a load="dashboard">Dashboard</a>
  <a load="projects">Projects</a>
  <a load="admin">Admin</a>
</nav>

<au-viewport></au-viewport>
```

## Recap

* Route `data` carries the `roles` requirement.
* The `RoleHook` reads `next.data` and redirects to `forbidden` if roles are missing.
* `activeClass` keeps the main nav styled for the active route.

Back to: [Extended Tutorial overview](/getting-started/extended-tutorial)


# From React to Aurelia

React developers: Discover why Aurelia's standards-based approach delivers better performance and cleaner code without the complexity.

**Coming from React?** You'll love Aurelia's approach to component development. Get the productivity of React with better performance, cleaner templates, and no virtual DOM overhead.

## Why React Developers Choose Aurelia

### **🚀 Performance That Actually Matters**

```typescript
// React: Virtual DOM reconciliation overhead
function UserList({ users, onUserClick }) {
  return (
    <div>
      {users.filter(u => u.isActive).map(user => (
        <UserCard key={user.id} user={user} onClick={onUserClick} />
      ))}
    </div>
  );
}

// Aurelia: Direct DOM updates, no virtual overhead
export class UserList {
  @bindable users: User[];
  @bindable onUserClick: (user: User) => void;
}
```

```html
<!-- Aurelia template: Clean HTML, faster rendering -->
<div>
  <user-card repeat.for="user of users.filter(u => u.isActive)" 
             user.bind="user" 
             on-click.bind="() => onUserClick(user)">
  </user-card>
</div>
```

**Result:** 30-50% faster rendering with smaller bundle sizes.

### **✨ Cleaner Component Code**

```typescript
// React: Hooks complexity and re-render management
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  const debouncedSearch = useCallback(
    debounce(async (searchTerm) => {
      setLoading(true);
      try {
        const data = await searchAPI(searchTerm);
        setResults(data);
      } finally {
        setLoading(false);
      }
    }, 300),
    []
  );

  useEffect(() => {
    if (query.length > 2) {
      debouncedSearch(query);
    } else {
      setResults([]);
    }
  }, [query, debouncedSearch]);

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <div>Loading...</div>}
      {results.map(result => <Result key={result.id} data={result} />)}
    </div>
  );
}

// Aurelia: Simple, intuitive code
export class SearchComponent {
  query = '';
  results: SearchResult[] = [];
  loading = false;

  @watch('query')
  async queryChanged(newQuery: string) {
    if (newQuery.length > 2) {
      this.loading = true;
      try {
        this.results = await searchAPI(newQuery);
      } finally {
        this.loading = false;
      }
    } else {
      this.results = [];
    }
  }
}
```

```html
<div>
  <input value.bind="query & debounce:300" placeholder="Search...">
  <div if.bind="loading">Loading...</div>
  <result repeat.for="result of results" data.bind="result"></result>
</div>
```

**No hooks complexity. No re-render cycles. Just clean, maintainable code.**

### **🎯 TypeScript-First Development**

```typescript
// React: Complex prop typing
interface UserCardProps {
  user: User;
  onEdit?: (user: User) => void;
  onDelete?: (user: User) => void;
  className?: string;
  children?: React.ReactNode;
}

const UserCard: React.FC<UserCardProps> = ({ 
  user, onEdit, onDelete, className, children 
}) => {
  // Component logic
};

// Aurelia: Built-in TypeScript support
export class UserCard {
  @bindable user: User;
  @bindable onEdit?: (user: User) => void;
  @bindable onDelete?: (user: User) => void;
  
  // Automatic type checking, no prop interfaces needed
}
```

### **🔥 Better Developer Experience**

| Feature             | React                                   | Aurelia                               |
| ------------------- | --------------------------------------- | ------------------------------------- |
| **Component State** | useState, useReducer                    | Simple class properties               |
| **Side Effects**    | useEffect with dependencies             | @watch decorator or lifecycle hooks   |
| **Performance**     | memo, useMemo, useCallback              | Automatic optimization                |
| **Styling**         | CSS-in-JS or external files             | Automatic CSS loading + Shadow DOM    |
| **Forms**           | Controlled components + validation libs | Two-way binding + built-in validation |

## Your React Knowledge Transfers

### **JSX → Aurelia Templates**

```jsx
// React JSX
<div className={`card ${isActive ? 'active' : ''}`}>
  <h2>{user.name}</h2>
  <button onClick={() => handleEdit(user)}>Edit</button>
  {showDetails && (
    <div>
      <p>{user.bio}</p>
      {user.posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )}
</div>

// Aurelia HTML (cleaner, more intuitive)
<div class="card" active.class="isActive">
  <h2>${user.name}</h2>
  <button click.trigger="handleEdit(user)">Edit</button>
  <div if.bind="showDetails">
    <p>${user.bio}</p>
    <post-card repeat.for="post of user.posts" post.bind="post"></post-card>
  </div>
</div>
```

### **Props → Bindable Properties**

```tsx
// React
interface Props {
  data: any[];
  onItemClick: (item: any) => void;
  loading?: boolean;
}

const MyComponent: React.FC<Props> = ({ data, onItemClick, loading = false }) => {
  // Component logic
};

// Aurelia
export class MyComponent {
  @bindable data: any[];
  @bindable onItemClick: (item: any) => void;
  @bindable loading = false;
  
  // That's it - cleaner and more intuitive
}
```

### **State Management**

```typescript
// React: Context + useReducer or external library
const UserContext = createContext();

function UserProvider({ children }) {
  const [state, dispatch] = useReducer(userReducer, initialState);
  return (
    <UserContext.Provider value={{ state, dispatch }}>
      {children}
    </UserContext.Provider>
  );
}

// Aurelia: Built-in dependency injection
@singleton()
export class UserService {
  private users: User[] = [];
  
  addUser(user: User) {
    this.users.push(user);
  }
  
  getUsers() {
    return this.users;
  }
}

// Use anywhere with simple injection
export class UserList {
  private userService = resolve(UserService);
  
  get users() {
    return this.userService.getUsers();
  }
}
```

## Quick Migration Path

### **1. Start with Familiar Concepts (5 minutes)**

```bash
npx makes aurelia my-aurelia-app
cd my-aurelia-app
npm run dev
```

### **2. Convert a React Component (10 minutes)**

Create your first Aurelia component using familiar React patterns:

```typescript
// React component you're used to
const TodoList = ({ todos, onToggle, onDelete }) => (
  <ul>
    {todos.map(todo => (
      <li key={todo.id}>
        <input 
          type="checkbox" 
          checked={todo.completed}
          onChange={() => onToggle(todo.id)}
        />
        <span>{todo.text}</span>
        <button onClick={() => onDelete(todo.id)}>Delete</button>
      </li>
    ))}
  </ul>
);

// Equivalent Aurelia component
export class TodoList {
  @bindable todos: Todo[];
  @bindable onToggle: (id: number) => void;
  @bindable onDelete: (id: number) => void;
}
```

```html
<!-- todo-list.html -->
<ul>
  <li repeat.for="todo of todos">
    <input type="checkbox" 
           checked.bind="todo.completed"
           change.trigger="onToggle(todo.id)">
    <span>${todo.text}</span>
    <button click.trigger="onDelete(todo.id)">Delete</button>
  </li>
</ul>
```

### **3. Experience the Differences (5 minutes)**

* **No build step complexity** - just works with any bundler
* **No prop drilling** - dependency injection handles state
* **No re-render debugging** - direct DOM updates
* **No hooks confusion** - simple class properties and methods

## What You'll Gain

### **📈 Performance Benefits**

* **Faster initial load** - no virtual DOM library overhead
* **Faster updates** - direct DOM manipulation
* **Smaller bundles** - efficient code splitting
* **Better mobile performance** - less JavaScript execution

### **🧹 Cleaner Codebase**

* **Less boilerplate** - no prop interfaces, no memo wrapping
* **Intuitive templates** - HTML that looks like HTML
* **Simpler state management** - class properties instead of hooks
* **Better separation of concerns** - HTML, CSS, and TypeScript in separate files

### **🚀 Better Developer Experience**

* **Stronger TypeScript integration** - built from the ground up for TypeScript
* **No re-render optimization needed** - automatically efficient
* **Powerful CLI tools** - scaffolding and build tools that just work
* **Excellent debugging** - inspect actual DOM, not virtual representations

## Ready to Make the Switch?

```bash
# Try Aurelia now
npx makes aurelia my-first-aurelia-app
cd my-first-aurelia-app
npm run dev
```

**Next Steps:**

1. [**Complete Getting Started Guide**](/getting-started/complete-guide) - Build your first app in 15 minutes
2. [**Component Guide**](/components/components) - Master Aurelia's component model
3. [**Templates Deep Dive**](/templates/overview) - Learn the templating system
4. [**Migration Examples**](/migrating-to-aurelia-2/migrating-to-aurelia-2) - See more migration patterns

**Questions?** Join our [Discord community](https://discord.gg/RBtyM6u) where developers discuss their experiences with different frameworks.

**Ready to experience the difference?** [Start building with Aurelia now](/getting-started/complete-guide).


# From Vue to Aurelia

Vue developers: Love Vue's simplicity? Aurelia takes it further with better performance, stronger TypeScript support, and zero magic.

**Vue developer?** You already appreciate simple, intuitive frameworks. Aurelia takes that philosophy even further with better performance, stronger TypeScript support, and standards-based architecture.

## Why Vue Developers Love Aurelia

### **🎯 Vue's Simplicity + Better Performance**

```vue
<!-- Vue: Reactivity with Proxy overhead -->
<template>
  <div>
    <input v-model="searchQuery" placeholder="Search users...">
    <div v-if="loading">Loading...</div>
    <user-card 
      v-for="user in filteredUsers" 
      :key="user.id"
      :user="user"
      @edit="handleEdit"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

const searchQuery = ref('')
const loading = ref(false)
const users = ref<User[]>([])

const filteredUsers = computed(() => 
  users.value.filter(user => 
    user.name.toLowerCase().includes(searchQuery.value.toLowerCase())
  )
)

watch(searchQuery, async (newQuery) => {
  if (newQuery.length > 2) {
    loading.value = true
    // Search logic
    loading.value = false
  }
})
</script>
```

```typescript
// Aurelia: Same simplicity, better performance
export class UserSearch {
  searchQuery = '';
  loading = false;
  users: User[] = [];

  // Computed properties work automatically - no wrapper needed
  get filteredUsers() {
    return this.users.filter(user => 
      user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
    );
  }

  // Watching is clean and intuitive
  @watch('searchQuery')
  async onSearchChange(newQuery: string) {
    if (newQuery.length > 2) {
      this.loading = true;
      // Search logic
      this.loading = false;
    }
  }
}
```

```html
<!-- Aurelia template: Clean HTML, no special directives -->
<div>
  <input value.bind="searchQuery & debounce:300" placeholder="Search users...">
  <div if.bind="loading">Loading...</div>
  <user-card repeat.for="user of filteredUsers" 
             user.bind="user"
             edit.bind="handleEdit">
  </user-card>
</div>
```

**Result:** Same clean code style, but with direct DOM updates and no proxy overhead.

### **✨ Better TypeScript Integration**

```vue
<!-- Vue: TypeScript support is good but requires setup -->
<script setup lang="ts">
interface Props {
  user: User
  editable?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  editable: false
})

const emit = defineEmits<{
  edit: [user: User]
  delete: [id: number]
}>()
</script>

// Aurelia: TypeScript-first, no setup needed
export class UserCard {
  @bindable user: User;
  @bindable editable = false;
  
  // Events are just methods - no emit setup
  edit() {
    // Pass as callback via .bind, e.g. on-edit.bind="() => edit()"
  }
  
  delete() {
    // Type-safe event handling
  }
}
```

### **🚀 Standards-Based Architecture**

```vue
<!-- Vue: Custom template syntax -->
<template>
  <div :class="{ active: isActive, loading: isLoading }">
    <slot name="header">
      <h2>{{ title }}</h2>
    </slot>
    <div v-show="expanded">
      <slot>Default content</slot>
    </div>
  </div>
</template>

<!-- Aurelia: Closer to web standards -->
<div class="card" active.class="isActive" loading.class="isLoading">
  <au-slot name="header">
    <h2>${title}</h2>
  </au-slot>
  <div show.bind="expanded">
    <au-slot>Default content</au-slot>
  </div>
</div>
```

**Aurelia's approach:** Build on web standards instead of creating new syntax.

## Your Vue Knowledge Transfers Perfectly

### **Template Syntax Comparison**

| Vue                             | Aurelia                      | Benefit                            |
| ------------------------------- | ---------------------------- | ---------------------------------- |
| `v-model="value"`               | `value.bind="value"`         | Two-way binding works the same     |
| `v-if="condition"`              | `if.bind="condition"`        | Same conditional logic             |
| `v-for="item in items"`         | `repeat.for="item of items"` | Same iteration, better performance |
| `@click="handler"`              | `click.trigger="handler"`    | Same event handling                |
| `:class="{ active: isActive }"` | `active.class="isActive"`    | Cleaner conditional classes        |

### **Component Structure**

```vue
<!-- Vue Single File Component -->
<template>
  <div class="my-component">
    <h1>{{ message }}</h1>
    <button @click="updateMessage">Update</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const message = ref('Hello Vue!')

const updateMessage = () => {
  message.value = 'Updated!'
}
</script>

<style scoped>
.my-component {
  padding: 20px;
  border: 1px solid #ccc;
}
</style>
```

```typescript
// Aurelia: Similar structure, separate files (or inline)
export class MyComponent {
  message = 'Hello Aurelia!';
  
  updateMessage() {
    this.message = 'Updated!';
  }
}
```

```html
<!-- my-component.html -->
<div class="my-component">
  <h1>${message}</h1>
  <button click.trigger="updateMessage()">Update</button>
</div>
```

```css
/* my-component.css - automatically loaded! */
.my-component {
  padding: 20px;
  border: 1px solid #ccc;
}
```

### **Reactivity Comparison**

```vue
<!-- Vue: Composition API -->
<script setup>
const count = ref(0)
const doubled = computed(() => count.value * 2)

watch(count, (newValue) => {
  console.log(`Count changed to ${newValue}`)
})
</script>

// Aurelia: Plain JavaScript/TypeScript
export class Counter {
  count = 0;
  
  // Computed properties are just getters
  get doubled() {
    return this.count * 2;
  }
  
  // Watching is explicit and clear
  @watch('count')
  countChanged(newValue: number) {
    console.log(`Count changed to ${newValue}`);
  }
}
```

### **Component Communication**

```vue
<!-- Vue: Props and Emits -->
<script setup>
interface Props {
  items: Item[]
}

const props = defineProps<Props>()
const emit = defineEmits<{
  itemSelected: [item: Item]
}>()

const selectItem = (item: Item) => {
  emit('itemSelected', item)
}
</script>

// Aurelia: Bindable properties and callable methods
export class ItemList {
  @bindable items: Item[];
  
  // Just call this method from parent template
  selectItem(item: Item) {
    // Parent can bind to this with select-item.bind="(item) => handleSelection(item)"
  }
}
```

## Migration Path: Vue → Aurelia

### **1. Quick Start (5 minutes)**

```bash
npx makes aurelia my-aurelia-app
cd my-aurelia-app
npm run dev
```

### **2. Convert Your First Vue Component (10 minutes)**

Let's convert a typical Vue component:

```vue
<!-- Vue Todo Component -->
<template>
  <div class="todo-app">
    <input 
      v-model="newTodo" 
      @keyup.enter="addTodo"
      placeholder="Add a todo..."
    >
    <ul>
      <li 
        v-for="todo in todos" 
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <input 
          type="checkbox" 
          v-model="todo.completed"
        >
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">Remove</button>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Todo {
  id: number
  text: string
  completed: boolean
}

const newTodo = ref('')
const todos = ref<Todo[]>([])
let nextId = 1

const addTodo = () => {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: nextId++,
      text: newTodo.value.trim(),
      completed: false
    })
    newTodo.value = ''
  }
}

const removeTodo = (id: number) => {
  todos.value = todos.value.filter(todo => todo.id !== id)
}
</script>
```

```typescript
// Aurelia equivalent - cleaner and more intuitive
export class TodoApp {
  newTodo = '';
  todos: Todo[] = [];
  private nextId = 1;

  addTodo() {
    if (this.newTodo.trim()) {
      this.todos.push({
        id: this.nextId++,
        text: this.newTodo.trim(),
        completed: false
      });
      this.newTodo = '';
    }
  }

  removeTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }
  
  onEnterKey(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      this.addTodo();
    }
  }
}

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
```

```html
<!-- todo-app.html -->
<div class="todo-app">
  <input 
    value.bind="newTodo" 
    keydown.trigger="onEnterKey($event)"
    placeholder="Add a todo..."
  >
  <ul>
    <li repeat.for="todo of todos" completed.class="todo.completed">
      <input type="checkbox" checked.bind="todo.completed">
      <span>${todo.text}</span>
      <button click.trigger="removeTodo(todo.id)">Remove</button>
    </li>
  </ul>
</div>
```

### **3. Experience the Differences**

* **No ref() wrappers** - plain JavaScript properties
* **No .value everywhere** - direct property access
* **Better TypeScript** - no generic complications
* **Automatic CSS loading** - matching CSS files load automatically

## What You'll Gain Moving from Vue

### **📈 Performance Improvements**

* **Direct DOM updates** instead of virtual DOM reconciliation
* **Smaller runtime** - no proxy reactivity overhead
* **Better tree shaking** - more efficient bundling
* **Faster startup** - less framework initialization code

### **🧹 Cleaner Development Experience**

* **No composition API complexity** - just class properties and methods
* **Better TypeScript support** - built for TypeScript from day one
* **Simpler testing** - no special test utilities needed
* **Standards-based** - closer to web platform APIs

### **🚀 Enhanced Capabilities**

* **Built-in dependency injection** - no need for provide/inject complexity
* **Powerful templating** - lambda expressions and better binding
* **Shadow DOM support** - true component encapsulation
* **Better routing** - type-safe, more powerful navigation

## Vue vs Aurelia: Feature Comparison

| Feature                     | Vue 3       | Aurelia 2         | Winner  |
| --------------------------- | ----------- | ----------------- | ------- |
| **Learning Curve**          | Easy        | Easy              | Tie     |
| **TypeScript Support**      | Good        | Excellent         | Aurelia |
| **Performance**             | Good        | Better            | Aurelia |
| **Bundle Size**             | Small       | Smaller           | Aurelia |
| **Template Syntax**         | Custom      | Standards-based   | Aurelia |
| **State Management**        | Pinia/Vuex  | Built-in DI       | Aurelia |
| **Component Communication** | Props/Emits | Bindable/Callable | Tie     |
| **Ecosystem Size**          | Large       | Focused           | Vue     |

## Ready to Experience the Upgrade?

```bash
# Start your Aurelia journey
npx makes aurelia my-vue-to-aurelia-app
cd my-vue-to-aurelia-app
npm run dev
```

**Next Steps:**

1. [**Complete Getting Started Guide**](/getting-started/complete-guide) - Build a real app in 15 minutes
2. [**Component Guide**](/components/components) - Master component patterns
3. [**Templates Deep Dive**](/templates/overview) - Advanced templating
4. [**Dependency Injection**](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection) - Powerful DI system

**Questions?** Join our [Discord community](https://discord.gg/RBtyM6u) where developers discuss framework experiences and best practices.

**Ready to take the next step?** [Start building with Aurelia](/getting-started/complete-guide) and experience web development the way it should be.


# From Angular to Aurelia

Angular developers: Keep the best parts (DI, TypeScript, CLI) while eliminating the complexity and improving performance.

**Angular developer?** You'll feel right at home with Aurelia. Keep everything you love—dependency injection, TypeScript, powerful CLI—while eliminating boilerplate, improving performance, and simplifying your development experience.

## Why Angular Developers Choose Aurelia

### **🎯 All the Power, None of the Complexity**

```typescript
// Angular: Heavy ceremony and boilerplate
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-user-search',
  template: `
    <div>
      <input 
        [value]="searchQuery" 
        (input)="onSearchInput($event)"
        placeholder="Search users..."
      >
      <div *ngIf="loading">Loading...</div>
      <app-user-card 
        *ngFor="let user of filteredUsers; trackBy: trackByUserId"
        [user]="user"
        (userEdit)="onUserEdit($event)"
      ></app-user-card>
    </div>
  `
})
export class UserSearchComponent implements OnInit, OnDestroy {
  @Input() users: User[] = [];
  @Output() userEdit = new EventEmitter<User>();
  
  searchQuery = '';
  filteredUsers: User[] = [];
  loading = false;
  
  private destroy$ = new Subject<void>();
  private searchSubject = new Subject<string>();

  ngOnInit() {
    this.searchSubject.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      takeUntil(this.destroy$)
    ).subscribe(query => this.performSearch(query));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  onSearchInput(event: Event) {
    const target = event.target as HTMLInputElement;
    this.searchQuery = target.value;
    this.searchSubject.next(this.searchQuery);
  }

  trackByUserId(index: number, user: User): number {
    return user.id;
  }

  private async performSearch(query: string) {
    if (query.length > 2) {
      this.loading = true;
      // Search logic
      this.loading = false;
    } else {
      this.filteredUsers = [];
    }
  }

  onUserEdit(user: User) {
    this.userEdit.emit(user);
  }
}

// Aurelia: Clean, intuitive code
export class UserSearch {
  @bindable users: User[];
  @bindable userEdit: (user: User) => void;
  
  searchQuery = '';
  loading = false;

  // Computed properties work automatically
  get filteredUsers() {
    if (this.searchQuery.length < 3) return [];
    return this.users.filter(user => 
      user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
    );
  }

  // Simple debounced search
  @watch('searchQuery')
  async searchChanged(newQuery: string) {
    if (newQuery.length > 2) {
      this.loading = true;
      // Search logic  
      this.loading = false;
    }
  }
}
```

```html
<!-- Aurelia template: Clean HTML -->
<div>
  <input value.bind="searchQuery & debounce:300" placeholder="Search users...">
  <div if.bind="loading">Loading...</div>
  <user-card repeat.for="user of filteredUsers" 
             user.bind="user"
             user-edit.bind="() => userEdit(user)">
  </user-card>
</div>
```

**Result:** 70% less code with the same functionality and better performance.

### **🚀 Dependency Injection Without the Complexity**

```typescript
// Angular: Complex DI with decorators and modules
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(
    private http: HttpClient,
    @Inject('API_URL') private apiUrl: string
  ) {}
}

@NgModule({
  providers: [
    { provide: 'API_URL', useValue: 'https://api.example.com' },
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})
export class AppModule {}

// Aurelia: Simple, powerful DI
export const IUserService = DI.createInterface<IUserService>(
  'IUserService',
  x => x.singleton(UserService)
);

export class UserService {
  private http = resolve(IHttpClient);
  private config = resolve(IApiConfig);
  
  // That's it - no modules, no complex setup
}

// Use anywhere
export class UserList {
  private userService = resolve(IUserService);
  
  // Clean, type-safe injection
}
```

### **✨ Better TypeScript Integration**

```typescript
// Angular: Lots of ceremony for type safety
interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-detail',
  template: `
    <div *ngIf="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <button (click)="editUser()">Edit</button>
    </div>
  `
})
export class UserDetailComponent {
  @Input() user: User | null = null;
  @Output() edit = new EventEmitter<User>();

  editUser() {
    if (this.user) {
      this.edit.emit(this.user);
    }
  }
}

// Aurelia: TypeScript-first design
export class UserDetail {
  @bindable user: User | null = null;
  @bindable edit: (user: User) => void;

  editUser() {
    if (this.user) {
      this.edit(this.user);
    }
  }
}
```

```html
<!-- Aurelia template with automatic type checking -->
<div if.bind="user">
  <h2>${user.name}</h2>
  <p>${user.email}</p>
  <button click.trigger="editUser()">Edit</button>
</div>
```

## Your Angular Knowledge Transfers

### **Template Syntax Translation**

| Angular                      | Aurelia                      | Benefit                            |
| ---------------------------- | ---------------------------- | ---------------------------------- |
| `[property]="value"`         | `property.bind="value"`      | Same one-way binding               |
| `[(ngModel)]="value"`        | `value.bind="value"`         | Simpler two-way binding            |
| `(click)="handler()"`        | `click.trigger="handler()"`  | Same event handling                |
| `*ngIf="condition"`          | `if.bind="condition"`        | Cleaner conditional syntax         |
| `*ngFor="let item of items"` | `repeat.for="item of items"` | Same iteration, better performance |
| `[class.active]="isActive"`  | `active.class="isActive"`    | More intuitive class binding       |

### **Component Architecture**

```typescript
// Angular Component
@Component({
  selector: 'app-todo-list',
  template: `
    <div class="todo-app">
      <input 
        [(ngModel)]="newTodo" 
        (keyup.enter)="addTodo()"
        placeholder="Add todo..."
      >
      <ul>
        <li *ngFor="let todo of todos; trackBy: trackByTodoId"
            [class.completed]="todo.completed">
          <input 
            type="checkbox" 
            [(ngModel)]="todo.completed"
          >
          <span>{{ todo.text }}</span>
          <button (click)="deleteTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  `,
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
  @Input() todos: Todo[] = [];
  @Output() todoAdded = new EventEmitter<Todo>();
  @Output() todoDeleted = new EventEmitter<number>();
  
  newTodo = '';
  private nextId = 1;

  addTodo() {
    if (this.newTodo.trim()) {
      const todo: Todo = {
        id: this.nextId++,
        text: this.newTodo.trim(),
        completed: false
      };
      this.todoAdded.emit(todo);
      this.newTodo = '';
    }
  }

  deleteTodo(id: number) {
    this.todoDeleted.emit(id);
  }

  trackByTodoId(index: number, todo: Todo): number {
    return todo.id;
  }
}

// Aurelia Component - much cleaner
export class TodoList {
  @bindable todos: Todo[] = [];
  @bindable todoAdded: (todo: Todo) => void;
  @bindable todoDeleted: (id: number) => void;
  
  newTodo = '';
  private nextId = 1;

  addTodo() {
    if (this.newTodo.trim()) {
      const todo: Todo = {
        id: this.nextId++,
        text: this.newTodo.trim(),
        completed: false
      };
      this.todoAdded(todo);
      this.newTodo = '';
    }
  }

  deleteTodo(id: number) {
    this.todoDeleted(id);
  }
  
  onEnterKey(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      this.addTodo();
    }
  }
}
```

```html
<!-- todo-list.html - clean, readable -->
<div class="todo-app">
  <input 
    value.bind="newTodo" 
    keydown.trigger="onEnterKey($event)"
    placeholder="Add todo..."
  >
  <ul>
    <li repeat.for="todo of todos" completed.class="todo.completed">
      <input type="checkbox" checked.bind="todo.completed">
      <span>${todo.text}</span>
      <button click.trigger="deleteTodo(todo.id)">Delete</button>
    </li>
  </ul>
</div>
```

### **Services and DI Comparison**

```typescript
// Angular Service
@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(
    private http: HttpClient,
    @Inject('API_CONFIG') private config: ApiConfig
  ) {}

  async getUsers(): Promise<User[]> {
    return this.http.get<User[]>(`${this.config.baseUrl}/users`).toPromise();
  }
}

// Aurelia Service - cleaner and more flexible
export const IDataService = DI.createInterface<IDataService>(
  'IDataService', 
  x => x.singleton(DataService)
);

export class DataService {
  private http = resolve(IHttpClient);
  private config = resolve(IApiConfig);

  async getUsers(): Promise<User[]> {
    return this.http.get(`${this.config.baseUrl}/users`);
  }
}
```

## Migration Benefits for Angular Developers

### **📈 Performance Gains**

* **No Zone.js overhead** - direct DOM updates instead of change detection
* **Smaller bundle sizes** - less framework code, better tree shaking
* **Faster startup** - no complex bootstrap process
* **Better runtime performance** - efficient batched updates

### **🧹 Development Experience Improvements**

* **Less boilerplate** - no modules, less ceremony
* **Simpler testing** - no TestBed setup complexity
* **Better debugging** - inspect actual DOM, not framework abstractions
* **Cleaner templates** - HTML that looks like HTML

### **🚀 Modern Development Features**

* **Built-in hot reload** - better development experience
* **Automatic CSS loading** - no need to import stylesheets
* **Shadow DOM support** - true component encapsulation
* **Standards-based** - closer to web platform APIs

## Quick Migration Path

### **1. Set Up Your Aurelia Environment (5 minutes)**

```bash
npx makes aurelia my-aurelia-app
cd my-aurelia-app
npm run dev
```

### **2. Convert Your First Angular Component (15 minutes)**

Take any Angular component and follow this pattern:

```typescript
// Angular
@Component({
  selector: 'app-user-profile',
  template: `
    <div class="profile" [class.editing]="isEditing">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <button *ngIf="!isEditing" (click)="startEdit()">Edit</button>
      <div *ngIf="isEditing">
        <input [(ngModel)]="editName" placeholder="Name">
        <input [(ngModel)]="editEmail" placeholder="Email">
        <button (click)="saveChanges()">Save</button>
        <button (click)="cancelEdit()">Cancel</button>
      </div>
    </div>
  `
})
export class UserProfileComponent {
  @Input() user: User;
  @Output() userUpdated = new EventEmitter<User>();
  
  isEditing = false;
  editName = '';
  editEmail = '';

  startEdit() {
    this.isEditing = true;
    this.editName = this.user.name;
    this.editEmail = this.user.email;
  }

  saveChanges() {
    const updatedUser = { ...this.user, name: this.editName, email: this.editEmail };
    this.userUpdated.emit(updatedUser);
    this.isEditing = false;
  }

  cancelEdit() {
    this.isEditing = false;
  }
}

// Aurelia - same functionality, cleaner code
export class UserProfile {
  @bindable user: User;
  @bindable userUpdated: (user: User) => void;
  
  isEditing = false;
  editName = '';
  editEmail = '';

  startEdit() {
    this.isEditing = true;
    this.editName = this.user.name;
    this.editEmail = this.user.email;
  }

  saveChanges() {
    const updatedUser = { ...this.user, name: this.editName, email: this.editEmail };
    this.userUpdated(updatedUser);
    this.isEditing = false;
  }

  cancelEdit() {
    this.isEditing = false;
  }
}
```

```html
<!-- user-profile.html -->
<div class="profile" editing.class="isEditing">
  <h2>${user.name}</h2>
  <p>${user.email}</p>
  <button if.bind="!isEditing" click.trigger="startEdit()">Edit</button>
  <div if.bind="isEditing">
    <input value.bind="editName" placeholder="Name">
    <input value.bind="editEmail" placeholder="Email">
    <button click.trigger="saveChanges()">Save</button>
    <button click.trigger="cancelEdit()">Cancel</button>
  </div>
</div>
```

### **3. Experience the Improvements**

* **No change detection cycles** - updates happen directly
* **No modules to configure** - components work immediately
* **Better TypeScript support** - everything is typed by default
* **Cleaner templates** - HTML without framework-specific syntax

## Angular vs Aurelia: Feature Comparison

| Feature                  | Angular              | Aurelia             | Winner  |
| ------------------------ | -------------------- | ------------------- | ------- |
| **TypeScript Support**   | Excellent            | Excellent           | Tie     |
| **Dependency Injection** | Powerful but complex | Powerful and simple | Aurelia |
| **Performance**          | Good with OnPush     | Better by default   | Aurelia |
| **Learning Curve**       | Steep                | Gentle              | Aurelia |
| **Bundle Size**          | Large                | Smaller             | Aurelia |
| **CLI Tools**            | Excellent            | Excellent           | Tie     |
| **Enterprise Features**  | Comprehensive        | Comprehensive       | Tie     |
| **Ecosystem**            | Huge                 | Focused             | Angular |
| **Standards Compliance** | Good                 | Excellent           | Aurelia |

## What Angular Concepts Work in Aurelia

✅ **Dependency Injection** - Even more powerful and simpler\
✅ **TypeScript** - First-class support, better integration\
✅ **Component Architecture** - Same concepts, cleaner implementation\
✅ **Services** - Same patterns, less boilerplate\
✅ **Routing** - More powerful, type-safe navigation\
✅ **Testing** - Simpler setup, same testing patterns\
✅ **CLI Tools** - Full-featured CLI for scaffolding and building

## Ready for a Better Angular Experience?

```bash
# Start your Aurelia journey
npx makes aurelia my-angular-to-aurelia-app
cd my-angular-to-aurelia-app
npm run dev
```

**Next Steps:**

1. [**Complete Getting Started Guide**](/getting-started/complete-guide) - Build a real app in 15 minutes
2. [**Dependency Injection Guide**](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection) - Master Aurelia's DI system
3. [**Router Guide**](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) - Type-safe navigation
4. [**Testing Guide**](/developer-guides/overview) - Test your applications

**Questions?** Join our [Discord community](https://discord.gg/RBtyM6u) where developers discuss enterprise framework experiences and architectural decisions.

**Ready to experience Angular without the complexity?** [Start building with Aurelia](/getting-started/complete-guide) today.


# Templates Overview & Quick Reference

Welcome to the Aurelia templating documentation! This guide covers everything you need to build dynamic, interactive UIs with Aurelia's powerful templating system.

## Quick Start

New to Aurelia templates? Start here:

1. [**Cheat Sheet**](/templates/cheat_sheet) - Quick syntax reference for all template features
2. [**Template Syntax Overview**](/templates/overview) - Core concepts and features
3. [**Real-World Recipes**](/templates/real-world-recipes/recipes) - Complete working examples

## How Do I...?

Find what you need quickly with this task-based guide:

### Display Data

* **Show dynamic text?** → [Text Interpolation](/templates/overview/text-interpolation) - Use `${property}`
* **Bind to element properties?** → [Attribute Binding](/templates/overview/attribute-binding) - Use `.bind`, `.to-view`, `.two-way`
* **Format data for display?** → [Value Converters](/templates/value-converters) - Use `${value | converter}`
* **Show/hide elements?** → [Conditional Rendering](/templates/conditional-rendering) - Use `if.bind` or `show.bind`

### Work with Lists

* **Loop through an array?** → [List Rendering](/templates/repeats-and-list-rendering) - Use `repeat.for="item of items"`
* **Get the current index?** → [List Rendering: Contextual Properties](/templates/repeats-and-list-rendering#contextual-properties) - Use `$index`, `$first`, `$last`
* **Handle empty lists?** → [Conditional Rendering](/templates/conditional-rendering) - Combine `if.bind="items.length === 0"` with `else`
* **Preserve row state across refreshed records?** → [List Rendering: Stable Item Identity](/templates/repeats-and-list-rendering#stable-item-identity-with-keys) - Use `key.bind` or `key:`
* **Optimize very large lists?** → [List Rendering: Performance](/templates/repeats-and-list-rendering#performance-best-practices) - Consider virtual scrolling or pagination

### Handle User Input

* **Capture button clicks?** → [Event Binding](/templates/overview/event-binding) - Use `click.trigger="method()"`
* **Handle keyboard input?** → [Event Binding: Keyboard Events](/templates/overview/event-binding#keyboard-key-mappings-and-custom-modifiers) - Use `keydown.trigger:enter="submit()"`
* **Create two-way form bindings?** → [Forms](/templates/forms/forms) - Use `value.bind="property"`
* **Prevent default behavior?** → [Event Binding: Modifiers](/templates/overview/event-binding#event-modifiers-enhancing-event-handling) - Use `click.trigger:prevent="method()"`
* **Debounce rapid input?** → [Event Binding: Binding Behaviors](/templates/overview/event-binding#throttling-and-debouncing-event-handlers) - Use `input.trigger="search() & debounce:300"`

### Conditional Logic

* **Show content based on a condition?** → [Conditional Rendering: if.bind](/templates/conditional-rendering#using-ifbind)
* **Toggle visibility frequently?** → [Conditional Rendering: show.bind](/templates/conditional-rendering#using-showbind)
* **Handle multiple conditions?** → [Conditional Rendering: switch.bind](/templates/conditional-rendering#using-switchbind)
* **Show if/else branches?** → [Conditional Rendering: else](/templates/conditional-rendering#ifelse-structures) - Use `else` after `if.bind`

### Styling

* **Apply dynamic CSS classes?** → [Class & Style Binding](/templates/class-and-style-bindings)
* **Bind inline styles?** → [Class & Style Binding](/templates/class-and-style-bindings)
* **Toggle classes conditionally?** → [Class & Style Binding](/templates/class-and-style-bindings)

### Components

* **Use a component in my template?** → [Component Basics: Importing](/components/components#importing-components) - Use `<import from="./my-component"></import>`
* **Make a component available globally?** → [Component Basics: Global Registration](/components/components#global-registration)
* **Pass data to a component?** → [Bindable Properties](/components/bindable-properties) - Use `@bindable` and bind in parent
* **Get a reference to an element?** → [Template References](/templates/overview/template-references) - Use `ref="elementName"`
* **Slot content into a component?** → [Slotted Content](/components/shadow-dom-and-slots)

### Forms

* **Build a form?** → [Forms: Basic Inputs](/templates/forms/forms#basic-input-binding)
* **Validate form input?** → [Validation Plugin](/aurelia-packages/validation)
* **Handle form submission?** → [Forms: Submission](/templates/forms/submission)
* **Work with checkboxes?** → [Forms: Collections](/templates/forms/collections#boolean-checkboxes)
* **Handle file uploads?** → [Forms: File Uploads](/templates/forms/file-uploads)

### Advanced

* **Create reusable template behaviors?** → [Custom Attributes](/templates/custom-attributes)
* **Transform data in templates?** → [Value Converters](/templates/value-converters)
* **Control binding behavior?** → [Binding Behaviors](/templates/binding-behaviors)
* **Work with promises?** → [Template Promises](/templates/overview/template-promises) - Use `promise.bind`
* **Create local template variables?** → [Template Variables](/templates/overview/template-variables) - Use `<let>`
* **Change binding context for a section?** → [`with.bind`](/templates/with)
* **Bind element focus state?** → [`focus`](/templates/focus) - Use `focus.bind`
* **Spread config objects / forward captured attrs?** → [Spread operators](/templates/spread-binding) - Use `...$bindables` / `...$attrs`
* **Render markup elsewhere in the DOM?** → [Portalling elements](/getting-to-know-aurelia/composition-patterns/portalling-elements) - Use `portal`
* **Compose components dynamically?** → [Dynamic composition](/getting-to-know-aurelia/composition-patterns/dynamic-composition) - Use `<au-compose>`
* **Work with SVG?** → [SVG](/templates/svg)
* **Use lambda expressions?** → [Lambda Expressions](/templates/lambda-expressions)

## Documentation Structure

### Core Concepts

* [**Template Syntax Overview**](/templates/overview) - Start here for the big picture
* [**Cheat Sheet**](/templates/cheat_sheet) - Quick reference for all syntax

### Template Syntax

* [**Text Interpolation**](/templates/overview/text-interpolation) - Display data with `${}`
* [**Attribute Binding**](/templates/overview/attribute-binding) - Bind to element properties and attributes
* [**Event Binding**](/templates/overview/event-binding) - Handle user interactions
* [**Template References**](/templates/overview/template-references) - Access DOM elements with `ref`
* [**Template Variables**](/templates/overview/template-variables) - Create local variables with `<let>`
* [**`with.bind`**](/templates/with) - Re-scope a section to an object
* [**Template Promises**](/templates/overview/template-promises) - Handle async data with `promise.bind`
* [**Spread operators**](/templates/spread-binding) - Bindables spreading and attribute transferring
* [**Globals**](/templates/overview/globals) - Built-in global functions and values

### Display Logic

* [**Conditional Rendering**](/templates/conditional-rendering) - Show/hide content with `if`, `show`, `switch`
* [**List Rendering**](/templates/repeats-and-list-rendering) - Loop over data with `repeat.for`
* [**Class & Style Bindings**](/templates/class-and-style-bindings) - Dynamic CSS

### Data Transformation

* [**Value Converters**](/templates/value-converters) - Format and transform data (like pipes)
* [**Binding Behaviors**](/templates/binding-behaviors) - Control binding flow (debounce, throttle, etc.)

### Forms & Input

* [**Forms Overview**](/templates/forms/forms) - Working with form inputs
* [**Form Collections**](/templates/forms/collections) - Checkboxes, radios, multi-select
* [**Form Submission**](/templates/forms/submission) - Submit forms and handle user feedback
* [**File Uploads**](/templates/forms/file-uploads) - Handle file inputs and uploads
* [**Validation Plugin**](/aurelia-packages/validation) - Validate user input

### Extensibility

* [**Custom Attributes**](/templates/custom-attributes) - Create reusable template behaviors
* [**Advanced Custom Attributes**](/templates/advanced-custom-attributes) - Complex attribute patterns

### Other Features

* [**Lambda Expressions**](/templates/lambda-expressions) - Arrow functions in templates
* [**Local Templates**](/templates/local-templates) - Inline component definitions
* [**SVG**](/templates/svg) - Working with SVG elements

### Real-World Examples

* [**Recipes**](/templates/real-world-recipes/recipes) - Complete, production-ready examples
  * [Product Catalog](/templates/real-world-recipes/product-catalog) - Search, filter, sort
  * [Shopping Cart](/templates/real-world-recipes/shopping-cart) - Add/remove items, calculate totals
  * More coming soon—follow the contribution guide in the recipes index to share yours!

## Learning Path

Not sure where to start? Follow this path:

### Beginner

1. Read [**Template Syntax Overview**](/templates/overview)
2. Learn [**Text Interpolation**](/templates/overview/text-interpolation) and [**Attribute Binding**](/templates/overview/attribute-binding)
3. Try [**Event Binding**](/templates/overview/event-binding) for interactivity
4. Practice with [**Product Catalog Recipe**](/templates/real-world-recipes/product-catalog)

### Intermediate

5. Master [**Conditional Rendering**](/templates/conditional-rendering) (`if`, `show`, `switch`)
6. Learn [**List Rendering**](/templates/repeats-and-list-rendering) (`repeat.for`)
7. Explore [**Value Converters**](/templates/value-converters) for data formatting
8. Build forms with [**Forms Guide**](/templates/forms/forms)
9. Try [**Shopping Cart Recipe**](/templates/real-world-recipes/shopping-cart)

### Advanced

10. Create [**Custom Attributes**](/templates/custom-attributes)
11. Use [**Binding Behaviors**](/templates/binding-behaviors) for fine control
12. Work with [**Template Promises**](/templates/overview/template-promises)
13. Explore [**Advanced Custom Attributes**](/templates/advanced-custom-attributes)
14. Adapt real-world patterns from the [**Template Recipes collection**](/templates/real-world-recipes/recipes)

## Performance Tips

* Use `.to-view` binding for display-only data
* Add `key` to `repeat.for` for dynamic lists
* Use `show.bind` for frequent visibility toggles
* Use `if.bind` for infrequent changes
* Debounce rapid input events
* Keep expressions simple - move logic to view model

## Common Pitfalls

* **Components not appearing?** → Don't forget `<import from="./component"></import>` (or register globally)
* **Array changes not detected?** → Use array methods like `push()`, `splice()`, not direct index assignment
* **Form input not updating?** → Use `.bind` or `.two-way`, not `.to-view`
* **Performance issues with large lists?** → Add `key.bind` or `key:` to `repeat.for`
* **Bindings not working?** → Check for typos in property names and binding commands

## Need Help?

* Check the [**Cheat Sheet**](/templates/cheat_sheet) for quick syntax reference
* Browse [**Recipes**](/templates/real-world-recipes/recipes) for complete examples
* Review [**Template Syntax Overview**](/templates/overview) for core concepts
* Search the "How Do I...?" section above
* Visit [Aurelia Discourse](https://discourse.aurelia.io/) for community support
* Check [GitHub Issues](https://github.com/aurelia/aurelia/issues) for known issues

## Related Documentation

* [**Components**](/components/components) - Build reusable UI components
* [**Essentials**](/introduction/essentials) - Core Aurelia concepts
* [**Router**](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) - Navigation and routing
* [**Dependency Injection**](/introduction/essentials/dependency-injection) - Share services between components


# Cheat Sheet

Quick reference guide for Aurelia 2 templating syntax and common patterns.

## Data Binding

### Binding Modes

| Syntax       | Direction                                           | Use Case                          |
| ------------ | --------------------------------------------------- | --------------------------------- |
| `.bind`      | Auto (two-way for form elements, to-view otherwise) | Default choice for most scenarios |
| `.to-view`   | View Model → View                                   | Display-only data (performance)   |
| `.two-way`   | View Model ↔ View                                   | Form inputs requiring sync        |
| `.from-view` | View → View Model                                   | Capture user input only           |
| `.one-time`  | View Model → View (once)                            | Static data that never changes    |

### Common Bindings

```html
<!-- Text & Attributes -->
<div title.bind="tooltip">${message}</div>
<img src.bind="imageUrl" alt.bind="altText">

<!-- Form Inputs -->
<input value.bind="name">
<input value.two-way="email">
<textarea value.bind="comments"></textarea>

<!-- Boolean Attributes -->
<button disabled.bind="isLoading">Submit</button>
<input required.bind="isRequired">

<!-- Checkboxes -->
<input type="checkbox" checked.bind="isActive">
<input type="checkbox" model.bind="item.id" checked.bind="selectedIds">

<!-- Radio Buttons -->
<input type="radio" model.bind="option1" checked.bind="selectedOption">
<input type="radio" model.bind="option2" checked.bind="selectedOption">

<!-- Select -->
<select value.bind="selectedValue">
  <option repeat.for="opt of options" value.bind="opt.id">${opt.name}</option>
</select>
```

## String Interpolation

```html
<!-- Simple -->
<p>${firstName} ${lastName}</p>

<!-- Expressions -->
<p>${count * 2}</p>
<p>${isActive ? 'Active' : 'Inactive'}</p>

<!-- Optional Chaining -->
<p>${user?.profile?.name ?? 'Guest'}</p>
```

## Event Binding

### Basic Events

```html
<!-- Click Events -->
<button click.trigger="save()">Save</button>
<button click.capture="handleCapture()">Capture Phase</button>

<!-- Form Events -->
<form submit.trigger="handleSubmit($event)">
<input input.trigger="onInput($event)">
<input change.trigger="onChange()">

<!-- Keyboard Events -->
<input keydown.trigger="onKeyDown($event)">
<input keyup.trigger="onKeyUp($event)">

<!-- Mouse Events -->
<div mouseover.trigger="onHover()">
<div mouseout.trigger="onLeave()">
```

### Event Modifiers

```html
<!-- Keyboard Modifiers -->
<input keydown.trigger:ctrl="onCtrlKey()">
<input keydown.trigger:enter="onEnter()">
<input keydown.trigger:ctrl+enter="onCtrlEnter()">

<!-- Mouse Button Modifiers -->
<button click.trigger:left="onLeftClick()">
<button click.trigger:middle="onMiddleClick()">
<button click.trigger:right="onRightClick()">

<!-- Event Control -->
<a click.trigger:prevent="navigate()">Link</a>
<div click.trigger:stop="handleClick()">Stop Propagation</div>
<div click.trigger="handleSelfClick() & self">Only Direct Clicks</div>
```

### Available Key Modifiers

| Category      | Modifiers                                         |
| ------------- | ------------------------------------------------- |
| Meta keys     | `ctrl`, `alt`, `shift`, `meta`                    |
| Special keys  | `enter`, `escape`, `space`, `tab`                 |
| Arrow keys    | `arrowup`, `arrowdown`, `arrowleft`, `arrowright` |
| Letters       | `a`-`z` (case-insensitive)                        |
| Numbers       | `0`-`9`                                           |
| Event control | `prevent`, `stop`                                 |
| Mouse buttons | `left`, `middle`, `right`                         |

Combine with `+`: `keydown.trigger:ctrl+shift+enter="handler()"`

### Binding Behaviors

```html
<!-- Throttle (max once per interval) -->
<input input.trigger="search($event.target.value) & throttle:300">

<!-- Debounce (wait until user stops) -->
<input input.trigger="search($event.target.value) & debounce:500">

<!-- Update trigger - control when binding updates -->
<input value.bind="name & updateTrigger:'blur'">
<input value.bind="name & updateTrigger:'blur':'paste'">

<!-- Signal - manual update triggering -->
<span>${expensiveComputation | format & signal:'refresh'}</span>
<!-- Then in code: signaler.dispatchSignal('refresh') -->

<!-- Self - only handle events from the element itself -->
<div click.trigger="onClick() & self">
  <button>Click me</button> <!-- Won't trigger onClick -->
</div>
```

## Conditional Rendering

### if.bind vs show\.bind vs hide.bind

| Feature          | `if.bind`                     | `show.bind` / `hide.bind`   |
| ---------------- | ----------------------------- | --------------------------- |
| DOM Manipulation | Adds/removes from DOM         | Shows/hides (display: none) |
| Performance      | Better for infrequent changes | Better for frequent toggles |
| Resources        | Cleans up events/bindings     | Keeps everything in memory  |
| Use When         | Content rarely changes        | Content toggles frequently  |

```html
<!-- if.bind - Removes from DOM -->
<div if.bind="isLoggedIn">Welcome back!</div>
<div else>Please log in</div>

<!-- show.bind - CSS display control -->
<div show.bind="isVisible">Toggled content</div>

<!-- hide.bind - Inverse of show.bind -->
<div hide.bind="isHidden">Hidden when true</div>

<!-- if with caching control -->
<expensive-component if="value.bind: showComponent; cache: false"></expensive-component>
```

{% hint style="info" %}
`hide.bind` is an alias for `show.bind` with inverted logic. `hide.bind="true"` is equivalent to `show.bind="false"`.
{% endhint %}

### switch.bind

```html
<!-- Basic Switch -->
<template switch.bind="status">
  <span case="pending">Waiting...</span>
  <span case="approved">Approved!</span>
  <span case="rejected">Rejected</span>
  <span default-case>Unknown</span>
</template>

<!-- Multiple Cases -->
<template switch.bind="role">
  <admin-panel case.bind="['admin', 'superadmin']"></admin-panel>
  <user-panel case="user"></user-panel>
  <guest-panel default-case></guest-panel>
</template>

<!-- Fall-through -->
<template switch.bind="level">
  <span case="high" fall-through.bind="true">High priority</span>
  <span case="medium">Medium priority</span>
  <span default-case>Low priority</span>
</template>
```

## List Rendering

### Basic Syntax

```html
<!-- Simple Array -->
<ul>
  <li repeat.for="item of items">${item.name}</li>
</ul>

<!-- With Keys (recommended for dynamic lists) -->
<div repeat.for="user of users; key: id">
  ${user.name}
</div>

<!-- With Index -->
<div repeat.for="item of items">
  ${$index + 1}. ${item.name}
</div>

<!-- Number Range -->
<div repeat.for="i of 5">Item ${i}</div>
```

### Contextual Properties

| Property    | Type    | Description                             |
| ----------- | ------- | --------------------------------------- |
| `$index`    | number  | Zero-based index (0, 1, 2...)           |
| `$first`    | boolean | True for first item                     |
| `$last`     | boolean | True for last item                      |
| `$middle`   | boolean | True for middle items                   |
| `$even`     | boolean | True for even indices                   |
| `$odd`      | boolean | True for odd indices                    |
| `$length`   | number  | Total number of items                   |
| `$parent`   | object  | Parent binding context                  |
| `$previous` | any     | Previous item (when contextual enabled) |

```html
<!-- Using Contextual Properties -->
<div repeat.for="item of items">
  <span class="${$even ? 'even' : 'odd'}">
    ${$index + 1} of ${$length}: ${item.name}
  </span>
  <span if.bind="$first">👑 First!</span>
  <span if.bind="$last">🏁 Last!</span>
</div>

<!-- Nested Repeats with $parent -->
<div repeat.for="category of categories">
  <h2>${category.name}</h2>
  <div repeat.for="item of category.items">
    ${item.name} in ${$parent.category.name}
  </div>
</div>
```

### Advanced Collection Types

```html
<!-- Sets -->
<div repeat.for="tag of selectedTags">
  ${tag}
</div>

<!-- Maps -->
<div repeat.for="[key, value] of configMap">
  ${key}: ${value}
</div>

<!-- Destructuring -->
<div repeat.for="{ id, name, email } of users">
  ${name} (${email})
</div>

<!-- Shallow aliases are supported -->
<div repeat.for="{ id: userId, name } of users">
  ${userId}: ${name}
</div>
```

Object destructuring creates reactive one-way locals. Use `user of users` for nested patterns, defaults, rest properties, or other complex access.

## Value Converters

### Syntax

```html
<!-- Basic -->
<p>${price | currency}</p>

<!-- With Parameters -->
<p>${date | dateFormat:'MM/DD/YYYY'}</p>
<p>${text | truncate:50:true}</p>

<!-- Chaining -->
<p>${input | sanitize | capitalize | truncate:100}</p>

<!-- In Bindings -->
<input value.bind="searchTerm | normalize">
```

### Common Built-in Patterns

```typescript
// Create a value converter
import { valueConverter } from 'aurelia';

@valueConverter('currency')
export class CurrencyValueConverter {
  toView(value: number, currencyCode = 'USD'): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currencyCode
    }).format(value);
  }
}
```

## Template References

```html
<!-- Element Reference -->
<input ref="searchInput" value.bind="query">
<button click.trigger="searchInput.focus()">Focus Input</button>

<!-- Component (view-model) Reference -->
<my-component component.ref="myComponent"></my-component>
<button click.trigger="myComponent.refresh()">Refresh</button>

<!-- Controller Reference (for advanced use) -->
<my-component controller.ref="myComponentController"></my-component>

<!-- Custom Attribute Reference -->
<div my-tooltip.ref="tooltipInstance" my-tooltip="Hello"></div>
```

| Ref Type         | Returns                   | Use Case                  |
| ---------------- | ------------------------- | ------------------------- |
| `ref`            | HTMLElement               | DOM manipulation          |
| `component.ref`  | View model instance       | Call component methods    |
| `controller.ref` | Controller instance       | Advanced lifecycle access |
| `my-attr.ref`    | Custom attribute instance | Access attribute methods  |

## Template Variables

```html
<!-- let - Local Variables (kebab-case converts to camelCase) -->
<let full-name.bind="firstName + ' ' + lastName"></let>
<h1>Hello, ${fullName}</h1>

<!-- let with to-binding-context - Assigns to view model -->
<let to-binding-context computed-value.bind="items.length * 2"></let>
<!-- Now this.computedValue is available in the view model -->

<!-- with - Scope Binding -->
<div with.bind="user">
  <p>${firstName} ${lastName}</p>
  <p>${email}</p>
</div>

<!-- Multiple Variables -->
<let greeting.bind="'Hello'"></let>
<let name.bind="user.name"></let>
<p>${greeting}, ${name}!</p>
```

{% hint style="info" %}
By default, `<let>` creates template-local variables. Add `to-binding-context` to assign values directly to the view model instead.
{% endhint %}

## Class & Style Binding

```html
<!-- Class Binding -->
<div class.bind="isActive ? 'active' : 'inactive'"></div>
<div class.bind="cssClasses"></div>

<!-- Single class toggle -->
<div active.class="isActive"></div>

<!-- Style Binding -->
<div style.bind="{ color: textColor, 'font-size': fontSize + 'px' }"></div>
<div style.background-color.bind="bgColor"></div>
<div style.width.bind="width + 'px'"></div>
```

## Attribute Binding

```html
<!-- Force attribute (not property) binding with & attr -->
<img src.bind="imageUrl & attr">

<!-- Useful for ARIA and data attributes -->
<button aria-label.bind="label & attr" aria-pressed.bind="isPressed & attr">
<div data-id.bind="item.id & attr">

<!-- Without & attr, bindings target DOM properties by default -->
```

## Promises in Templates

```html
<div promise.bind="fetchData()">
  <span pending>Loading...</span>
  <span then="data">
    Loaded: ${data.title}
  </span>
  <span catch="error">
    Error: ${error.message}
  </span>
</div>
```

## Spread Binding

```html
<!-- Spread all bindable properties from an object -->
<user-card ...$bindables="user"></user-card>

<!-- Equivalent explicit syntax -->
<user-card $bindables.spread="user"></user-card>

<!-- Shorthand (expression in attribute name) -->
<user-card ...user></user-card>
```

```typescript
// If user = { name: 'Jane', email: 'jane@example.com', avatarUrl: '...' }
// And UserCard has @bindable name, email, avatarUrl
// Then spread passes all matching properties automatically
```

{% hint style="info" %}
Spread binding is always one-way (to-view). Only properties that exist in the object at evaluation time create bindings.
{% endhint %}

## Custom Attributes

```html
<!-- Using Custom Attributes -->
<div my-attribute="value"></div>
<div my-attribute.bind="dynamicValue"></div>

<!-- With Multiple Parameters -->
<div tooltip="text.bind: tooltipText; position: top; delay: 300"></div>
```

## Component Import & Usage

```html
<!-- Import -->
<import from="./my-component"></import>
<import from="./utils/helpers" as="helpers"></import>

<!-- Usage -->
<my-component title.bind="pageTitle" on-save.bind="handleSave"></my-component>

<!-- Inline Component -->
<template as-custom-element="inline-component">
  <bindable name="title"></bindable>
  <h1>${title}</h1>
</template>

<inline-component title="Hello"></inline-component>
```

## Dynamic Composition

```html
<!-- Compose with component reference -->
<au-compose component.bind="MyComponent"></au-compose>

<!-- Compose with view model and view -->
<au-compose view-model.bind="dynamicViewModel" view.bind="dynamicView"></au-compose>

<!-- Compose with model data -->
<au-compose component.bind="CardComponent" model.bind="{ title: 'Hello', content: cardContent }"></au-compose>

<!-- Compose with string path -->
<au-compose view-model="./components/dynamic-panel"></au-compose>
```

## Focus Management

```html
<!-- Two-way focus binding -->
<input focus.bind="isInputFocused">

<!-- Focus on condition -->
<input focus.bind="shouldFocus">

<!-- Programmatic focus via ref -->
<input ref="nameInput">
<button click.trigger="nameInput.focus()">Focus Input</button>
```

## Quick Decision Trees

### When to use if vs show?

```
Need to toggle visibility?
├─ Toggles frequently (e.g., dropdown, tab content)
│  └─ Use show.bind (faster, preserves state)
└─ Toggles infrequently (e.g., admin panel, authenticated content)
   └─ Use if.bind (saves memory, cleans up resources)
```

### Which binding mode?

```
Binding to form input?
├─ YES → Use .bind (auto two-way)
└─ NO  → Displaying data only?
         ├─ YES → Use .to-view (better performance)
         └─ NO  → Need to capture user changes?
                  ├─ YES → Use .two-way
                  └─ NO  → Static data?
                           └─ Use .one-time
```

### When to use keys in repeat.for?

```
Using repeat.for with dynamic list?
├─ List items can be added/removed/reordered?
│  └─ YES → Always use keys (key.bind or key:)
└─ List is static or append-only?
   └─ Keys optional (but recommended)
```

## Common Patterns

### Loading States

```html
<!-- Using switch for multiple states -->
<template switch.bind="state">
  <div case="loading">Loading...</div>
  <div case="error">Error: ${error.message}</div>
  <div case="empty">No items found</div>
  <div default-case>
    <div repeat.for="item of items; key: id">${item.name}</div>
  </div>
</template>

<!-- Or using nested if/else -->
<div if.bind="isLoading">Loading...</div>
<template else>
  <div if.bind="error">Error: ${error.message}</div>
  <template else>
    <div if.bind="items.length === 0">No items found</div>
    <div else>
      <div repeat.for="item of items; key: id">${item.name}</div>
    </div>
  </template>
</template>
```

### Form Validation Display

```html
<input value.bind="email" class="${errors.email ? 'invalid' : ''}">
<span if.bind="errors.email" class="error">${errors.email}</span>
```

### Computed Display Values

```html
<let total.bind="items.reduce((sum, item) => sum + item.price, 0)"></let>
<p>Total: ${total | currency}</p>
```

### Dynamic CSS Classes

```html
<div class="card ${isActive ? 'active' : ''} ${isHighlighted ? 'highlight' : ''}">
  Content
</div>
```

## Component Lifecycle (Quick Reference)

| Hook          | When Called                                      | Common Use                             |
| ------------- | ------------------------------------------------ | -------------------------------------- |
| `constructor` | Instance created                                 | Inject dependencies with `resolve()`   |
| `created`     | After constructor                                | Access `$controller`                   |
| `binding`     | Before bindings evaluated                        | Initialize from bindables              |
| `bound`       | Bindings connected                               | Setup dependent on bound values        |
| `attaching`   | DOM inserted; descendants may still be attaching | Async setup work                       |
| `attached`    | Owned subtree attached                           | DOM measurement, third-party libraries |
| `detaching`   | Before DOM removal                               | Cleanup, save state                    |
| `unbinding`   | Before bindings disconnect                       | Cleanup subscriptions                  |

```typescript
import { resolve } from '@aurelia/kernel';

export class MyComponent {
  // Properties injected via DI
  private api = resolve(IApiService);

  // Lifecycle methods
  binding() { /* Called first */ }
  attached() { /* DOM is ready */ }
  detaching() { /* Cleanup */ }
}
```

## Performance Tips

1. **Use appropriate binding modes**: `.to-view` for display-only data
2. **Add keys to repeat.for**: Enables efficient DOM reuse
3. **Use show\.bind for frequent toggles**: Avoids DOM manipulation overhead
4. **Use if.bind for infrequent changes**: Saves memory and resources
5. **Debounce/throttle rapid events**: Prevents excessive handler calls
6. **Keep expressions simple**: Move complex logic to view model
7. **Use value converters**: Separate formatting from view model logic

## Common Gotchas

| Issue                | Problem                      | Solution                                                 |
| -------------------- | ---------------------------- | -------------------------------------------------------- |
| Binding not updating | Object/array mutation        | Use `splice()`, reassign, or use observable              |
| Event not firing     | Wrong event name             | Check: `click.trigger` not `onclick.trigger`             |
| Style not applied    | CSS property name            | Use kebab-case: `background-color` not `backgroundColor` |
| Async data undefined | Template renders before data | Use `promise.bind` or `if.bind="data"`                   |
| `$parent` undefined  | Not in a repeat              | Only available inside `repeat.for`                       |

## Accessibility Reminders

```html
<!-- Labels for Form Inputs -->
<label for="email">Email:</label>
<input id="email" value.bind="email">

<!-- ARIA Attributes -->
<button
  aria-label.bind="buttonLabel & attr"
  aria-busy.bind="isLoading & attr"
  disabled.bind="isLoading">
  ${isLoading ? 'Loading...' : 'Submit'}
</button>

<!-- Role Attributes -->
<div role="alert" if.bind="errorMessage">
  ${errorMessage}
</div>
```

## Related Documentation

* [Template Syntax Overview](/templates/overview)
* [Attribute Binding](/templates/overview/attribute-binding)
* [Event Binding](/templates/overview/event-binding)
* [Conditional Rendering](/templates/conditional-rendering)
* [List Rendering](/templates/repeats-and-list-rendering)
* [Value Converters](/templates/value-converters)
* [Binding Behaviors](/templates/binding-behaviors)
* [Form Inputs](/templates/forms/forms)
* [Class & Style Binding](/templates/class-and-style-bindings)
* [Spread Binding](/templates/spread-binding)
* [Local Templates](/templates/local-templates)
* [Lambda Expressions](/templates/lambda-expressions)


# Visual Diagrams

Visual diagrams to help understand Aurelia's templating system. Every diagram below is rendered with GitBook-friendly Mermaid so it stays legible in dark and light modes.

## Data Binding Flow

### One-Way Binding (View Model → View)

{% @mermaid/diagram content="flowchart LR
VM\["View Model<br/>message = 'Hi'"]
binding\[".to-view binding"]
view\["View<br/><p>${message}</p> → <p>Hi</p>"]
VM --> binding --> view" %}

Use one-way bindings for read-only flows or whenever the DOM only needs to reflect state.

### Two-Way Binding (View Model ↔ View)

{% @mermaid/diagram content="flowchart LR
VM\["View Model<br/>name = 'Bob'"]
binding\[".bind (.two-way)"]
view\["<input value.bind='name'>"]
VM <-->|"property updates"| binding
binding <-->|"user typing"| view" %}

Two-way bindings keep inputs and view-model properties in sync. Typing "Alice" updates `name`, which in turn refreshes every binding that depends on it.

### From-View Binding (View → View Model)

{% @mermaid/diagram content="flowchart LR
view\["<input value.from-view='query'>"]
binding\[".from-view binding"]
VM\["View Model<br/>query = ''"]
view --> binding --> VM" %}

`.from-view` captures user input without pushing view-model changes back into the DOM—handy for debounced searches or analytics where the DOM already mirrors the value elsewhere.

## Binding Mode Decision Tree

{% @mermaid/diagram content="flowchart TD
start(\[Need to bind a value?])
start --> input{Is it a form control?}
input -->|Yes| readInput{Need to read user input?}
readInput -->|Yes| useTwoWay\["Use .bind<br/>(default .two-way)"]
readInput -->|No| useOneWayInput\["Use .to-view"]
input -->|No| attr{Is it a regular attribute?}
attr -->|Value changes often| dynamicAttr\["Use .bind or .to-view"]
attr -->|Value never changes| staticAttr\["Use .one-time"]" %}

## Conditional Rendering: if vs show

### if.bind – Adds/Removes from the DOM

{% @mermaid/diagram content="stateDiagram-v2
\[\*] --> Hidden
Hidden --> Visible: isVisible becomes true
Visible --> Hidden: isVisible becomes false
Visible: Element exists in DOM<br/>Events attached<br/>Memory reclaimed when removed" %}

`if.bind` creates and disposes the DOM subtree. It frees memory and automatically detaches listeners any time the condition flips back to `false`.

### show\.bind – CSS Display Toggle

{% @mermaid/diagram content="stateDiagram-v2
\[\*] --> Hidden
Hidden --> Visible: isVisible becomes true
Visible --> Hidden: isVisible becomes false
Hidden: Element stays in DOM<br/>style.display = 'none'<br/>Events stay attached" %}

`show.bind` toggles `display: none` without touching the DOM tree. It is ideal for frequently toggled sections that should keep their internal state alive.

### Decision Matrix

| Capability       | `if.bind`            | `show.bind`                |
| ---------------- | -------------------- | -------------------------- |
| DOM manipulation | Create/destroy nodes | Toggle CSS display         |
| Memory           | Released when hidden | Always allocated           |
| Toggle speed     | Slightly slower      | Instant                    |
| Event cleanup    | Automatic            | Handled manually if needed |
| Component init   | Runs every attach    | Runs once                  |
| Best for         | Rare toggles         | Frequent toggles           |

## List Rendering with repeat.for

### Basic Flow

{% @mermaid/diagram content="flowchart LR
items\["View Model<br/>items = \[{ id: 1, name: 'A' }, ...]"]
directive\["repeat.for='item of items'"]
template\["<li>${item.name}</li>"]
items --> directive --> template" %}

### With Keys for Efficient Updates

By default, the repeat controller tracks scopes by the actual item reference. When you insert `X` in between existing objects (`[A, B, C] → [A, X, B, C]`), Aurelia reuses the same scopes for `A`, `B`, and `C` because their references are unchanged; only `X` produces a new view. The `_scopeMap` maintained inside `packages/runtime-html/src/resources/template-controllers/repeat.ts` (see `_createScopes` and `_applyIndexMap`) stores either the raw item reference or your explicit key, which is why Aurelia can diff without re-rendering.

Provide a `key` only when you recreate objects between refreshes (for example, mapping API data into new literals) or when the list contains primitives. In those cases a property such as `id` gives Aurelia a stable identity to match.

{% @mermaid/diagram content="flowchart TB
subgraph "Reference identity (default)"
oldA\[A reference]
oldB\[B reference]
oldC\[C reference]
oldA --> reuseA\[same object reused]
oldB --> reuseB\[same object reused]
oldC --> reuseC\[same object reused]
end
subgraph "Keyed identity"
keyId\["key.bind: id"] --> match\[Compare ids when objects are recreated]
end" %}

### Contextual Properties

| Property  | Description                  | Example Values (list of 3)        |
| --------- | ---------------------------- | --------------------------------- |
| `$index`  | Zero-based index             | `0`, `1`, `2`                     |
| `$first`  | True only for the first item | `true`, `false`, `false`          |
| `$last`   | True only for the last item  | `false`, `false`, `true`          |
| `$even`   | True when `$index % 2 === 0` | `true`, `false`, `true`           |
| `$odd`    | True when `$index % 2 === 1` | `false`, `true`, `false`          |
| `$length` | Total length of the iterable | `3`                               |
| `item`    | Current iteration value      | `'Apple'`, `'Banana'`, `'Cherry'` |

## Event Binding: Trigger vs Capture

### Bubbling Phase (.trigger)

{% @mermaid/diagram content="flowchart TD
btn\["<button> (event origin)"]
div\["\<div .trigger>"]
body\["<body>"]
doc\[document]
win\[window]
btn --> div --> body --> doc --> win
classDef bubble fill:#f0f4ff,stroke:#4466dd,stroke-width:2px;
class btn,div,body,doc,win bubble;" %}

`.trigger` listens during the bubble phase as the event travels from the target back toward the window.

### Capturing Phase (.capture)

{% @mermaid/diagram content="flowchart TD
win\[window]
doc\[document]
body\["<body>"]
div\["\<div .capture>"]
btn\["<button> (event destination)"]
win --> doc --> body --> div --> btn
classDef capture fill:#fff7e6,stroke:#f0a500,stroke-width:2px;
class win,doc,body,div,btn capture;" %}

`.capture` intercepts the event on its way down the DOM tree before child handlers run.

### Event Flow Complete Picture

{% @mermaid/diagram content="sequenceDiagram
participant Window
participant Document
participant Body
participant Div
participant Button
Window->>Document: Capture
Document->>Body: Capture
Body->>Div: Capture (.capture)
Div->>Button: Target
Button-->>Div: Bubble (.trigger)
Div-->>Body: Bubble
Body-->>Document: Bubble
Document-->>Window: Bubble" %}

## Value Converters Pipeline

{% @mermaid/diagram content="graph LR
vm\["View Model value: 299.99"] --> currency
currency\["currency:'USD'"] --> truncate
truncate\["truncate:10"] --> output\[Rendered text]
output --> final\["$299.99..."]" %}

### Converter Flow Detail

{% @mermaid/diagram content="flowchart LR
vm\["price = 299.99"] --> currency
currency\["CurrencyConverter.toView(299.99, 'USD')<br/>→ '$299.99 USD'"] --> truncate
truncate\["TruncateConverter.toView('$299.99 USD', 10)<br/>→ '$299.99...' "] --> view\["DOM"]" %}

## Component Communication

### Parent → Child (Bindable Properties)

{% @mermaid/diagram content="flowchart LR
parent\["Parent component<br/>user = { name: 'Alice', email: '<alice@ex.com>' }"] --> bind\["user.bind='user'"] --> child\["<user-card> view<br/>@bindable user"]" %}

```html
<user-card user.bind="user"></user-card>
```

### Child → Parent (Callback Binding)

Use `.bind` to pass a callback reference to the child now that the deprecated `.call` binding command is gone.

{% @mermaid/diagram content="flowchart LR
parent\["Parent<br/>handleDelete(user)"] --> callback\["on-delete.bind='handleDelete'"] --> child\["Child<br/>@bindable onDelete"] --> action\["deleteUser() → this.onDelete?.(this.user)"]" %}

```html
<!-- Parent template -->
<user-card user.bind="user" on-delete.bind="handleDelete"></user-card>

// Child view-model
import { bindable } from '@aurelia/runtime-html';

export class UserCard {
  @bindable() public onDelete: (user: User) => void;

  deleteUser(): void {
    this.onDelete?.(this.user);
  }
}
```

## Form Checkbox Collections

{% @mermaid/diagram content="flowchart TD
products\["products = \[{ id:1, name:'Mouse' }, { id:2, name:'Keyboard' }]"] --> repeat\["<label repeat.for='p of products'>"]
repeat --> checkbox\["\<input type=checkbox<br/>model.bind='p.id'<br/>checked.bind='selectedIds'>"]
checkbox --> logic{Checked?}
logic -->|Yes| add\[Add model value to selectedIds]
logic -->|No| remove\[Remove model value from selectedIds]" %}

When the user checks "Keyboard", Aurelia pushes `2` into `selectedIds`. Unchecking "Mouse" removes `1`, keeping the array aligned with the checked boxes.

## Template Lifecycle

{% @mermaid/diagram content="flowchart TD
created\[Component created] --> compiled\[Template compiled]
compiled --> binding\[Binding lifecycle]
binding --> bindingsConnected\[Bindings connected]
bindingsConnected --> bound\[Bound lifecycle]
bound --> attached\[Attached lifecycle]
attached --> active\[Component active]
active --> detaching\[Detaching lifecycle]
detaching --> unbinding\[Unbinding lifecycle]
unbinding --> removed\[Component removed]" %}

## Performance: Binding Modes Comparison

| Binding Mode       | Setup Cost              | Updates                   | Memory Footprint               | Typical Use                    |
| ------------------ | ----------------------- | ------------------------- | ------------------------------ | ------------------------------ |
| `.one-time`        | Set value once          | Never updates             | No observers hooked up         | Static text that never changes |
| `.to-view`         | Set value + observer    | Whenever property changes | One source observer            | Displaying reactive state      |
| `.bind` (.two-way) | Bidirectional observers | View ↔ ViewModel          | Source observer + DOM listener | Form controls that read/write  |

Internals note: the `PropertyBinding.bind` implementation wires observers based on the binding mode flags. `.one-time` evaluates the expression once without connecting, `.to-view` connects the source side so it can re-run when dependencies change, and `.bind`/`.two-way` also subscribes to the target observer (for example, an input element) so user input flows back to the view model. This mirrors the logic in `packages/runtime-html/src/binding/property-binding.ts` where `toView`, `fromView`, and `oneTime` determine which observers are created.

## Computed Properties Reactivity

```ts
items = [
  { price: 10, qty: 2 },
  { price: 20, qty: 1 }
];

get total() {
  return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
```

{% @mermaid/diagram content="flowchart LR
items\[items array] --> getter\[Get total]
getter --> view\["<p>Total: ${total}</p>"]
priceChange\["items\[0].price = 15"] --> dirtyCheck\["Aurelia detects dependency change"] --> getter" %}

Aurelia re-runs getters whenever any accessed dependency (the array itself or a member property) mutates, then propagates the new value into the DOM.

## Related Documentation

* [Template Syntax Overview](/templates/overview)
* [Cheat Sheet](/templates/cheat_sheet)
* [Conditional Rendering](/templates/conditional-rendering)
* [List Rendering](/templates/repeats-and-list-rendering)
* [Event Binding](/templates/overview/event-binding)
* [Value Converters](/templates/value-converters)


# Template Syntax

Aurelia 2's templating system is the cornerstone of crafting rich, interactive user interfaces for your web applications. It transcends the limitations of static HTML, empowering you to create truly dynamic views that respond intelligently to both application data and user interactions. At its core, Aurelia templating establishes a fluid and intuitive connection between your HTML templates and your application logic written in JavaScript or TypeScript, resulting in a responsive and data-driven UI development experience.

Forget static HTML pages. Aurelia 2 templates are living, breathing views that actively engage with your application's underlying code. They react in real-time to data modifications and user actions, ensuring your UI is always in sync and providing a seamless user experience. This deep integration not only streamlines your development workflow but also significantly reduces boilerplate, allowing you to build sophisticated UIs with greater clarity and efficiency.

From the moment you initiate an Aurelia 2 project, you'll find yourself working with templates that are both comfortably familiar in their HTML structure and remarkably powerful in their extended capabilities. Whether you're structuring the layout for a complex component or simply displaying data within your HTML, Aurelia 2's templating syntax is meticulously designed to be both highly expressive and exceptionally developer-friendly, making UI development a truly enjoyable and productive process.

If you need even finer control, dive into the focused guides linked throughout this section—for example, [binding mode behaviors](/templates/binding-behaviors#binding-mode-behaviors) for forcing one-time/one-way flow on demand, or the new [event modifier catalog](/templates/overview/event-binding#event-modifiers) for tailoring DOM events.

## Key Features of Aurelia Templating

Aurelia's templating engine is packed with features designed to enhance your UI development workflow and capabilities:

* **Effortless Two-Way Data Binding:** Experience truly seamless synchronization between your application's data model and the rendered view. Aurelia's robust two-way data binding automatically keeps your model and UI in perfect harmony, eliminating manual DOM manipulation and ensuring data consistency with minimal effort.
* **Extendable HTML with Custom Elements and Attributes:** Break free from standard HTML limitations by creating your own reusable components and HTML attributes. Encapsulate complex UI logic and behavior into custom elements and attributes, promoting modularity, code reuse, and a more maintainable codebase. This allows you to tailor HTML to the specific needs of your application.
* **Adaptive Dynamic Composition for Flexible UIs:** Build truly dynamic and adaptable user interfaces with Aurelia's dynamic composition. Render different components and templates on-the-fly based on your application's state, user interactions, or any dynamic condition. This enables you to create flexible layouts and UI structures that respond intelligently to changing requirements.
* **Expressive and Intuitive Templating Syntax:** Harness the power of Aurelia's rich templating syntax to handle common UI patterns with ease. From iterating over lists of data and conditionally rendering UI elements to effortlessly managing user events, Aurelia's syntax is designed to be both powerful and remarkably intuitive, reducing complexity and boosting productivity.
* **Simplified Data Integration with Expressions and Interpolation:** Seamlessly integrate your application data into your templates using Aurelia's straightforward expression syntax. Effortlessly bind data to HTML elements and manipulate attributes directly within your templates using interpolation, making data display and interaction a breeze.

Aurelia 2's templating system is more than just a way to write HTML; it's a comprehensive toolkit for building modern, dynamic web applications with efficiency and elegance. By embracing its features, you'll unlock a more productive and enjoyable UI development experience.

## Learn the Syntax by Topic

Jump straight into the focused articles that break down each template capability:

* **Text & expression binding** – Start with the [text interpolation guide](/templates/overview/text-interpolation) to master `${ }` expressions and formatting tips.
* **Attribute & property binding** – Control DOM attributes, classes, and styles with the [attribute binding reference](/templates/overview/attribute-binding).
* **Event handling** – Wire up DOM interactions plus modifiers using the [event binding guide](/templates/overview/event-binding#event-modifiers).
* **Template references** – Capture DOM elements, child components, or controllers via the [template reference walkthrough](/templates/overview/template-references).
* **Template variables** – Share computed values inside markup with the [template variables guide](/templates/overview/template-variables).
* **Async UI flows** – Render placeholders or await data using the [template promises article](/templates/overview/template-promises).
* **Advanced scenarios** – Combine bindings with conditionals, loops, and partials in the [recipes collection](/templates/real-world-recipes/recipes) and [forms guide](/templates/forms/forms).


# Attribute binding

Attribute binding in Aurelia is a powerful feature that allows you to dynamically bind data from your view model to any native HTML attribute within your templates. This enables real-time updates to element attributes such as classes, styles, `src`, `alt`, and other standard HTML attributes, enhancing the interactivity and responsiveness of your applications.

## Basic Binding Syntax

The fundamental syntax for binding to attributes in Aurelia is simple and intuitive:

```html
<div attribute-name.bind="value"></div>
```

* `attribute-name.bind="value"`: The binding declaration.
  * `attribute-name`: The target HTML attribute you want to bind to.
  * `.bind`: The default binding command (uses Aurelia's default binding mode for that target).
  * `value`: The expression or property from the view model to bind.

You can bind to virtually any attribute listed in the [HTML Attributes Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes).

### Example: Binding the `title` Attribute

```html
<!-- my-app.html -->
<div title.bind="tooltipText">Hover over me!</div>
```

```typescript
// my-app.ts
export class MyApp {
  tooltipText = 'This is a tooltip';
}
```

*Result:* The `div` will have a `title` attribute with the value "This is a tooltip". Hovering over the div will display the tooltip.

{% hint style="info" %}
When using an empty expression in a binding, such as `attribute-name.bind` or `attribute-name.bind=""`, Aurelia automatically infers the expression based on the camelCase version of the target attribute. For example, `attribute-name.bind=""` is equivalent to `attribute-name.bind="attributeName"`. This behavior applies to other binding commands as well:

* `.one-time`
* `.to-view`
* `.from-view`
* `.two-way`
* `.attr`
  {% endhint %}

## Binding Techniques and Syntax

Aurelia provides multiple methods for attribute binding, each tailored for specific use cases and offering different levels of data flow control.

### 1. Interpolation Binding

Interpolation allows embedding dynamic values directly within strings. This is useful for concatenating strings with dynamic data.

**Example: Binding the `id` Attribute Using Interpolation**

```html
<!-- my-app.html -->
<div>
  <h1 id="${headingId}">Dynamic Heading</h1>
</div>
```

```typescript
// my-app.ts
export class MyApp {
  headingId = 'main-heading';
}
```

*Result:* The `h1` element will have an `id` attribute set to "main-heading".

### 2. Keyword Binding

Aurelia supports several binding keywords that define the direction and frequency of data flow between the view model and the view:

* **`.one-time`**: Updates the view from the view model only once. Subsequent changes in the view model do not affect the view.
* **`.to-view`**: Continuously updates the view from the view model.
* **`.from-view`**: Updates the view model based on changes in the view.
* **`.two-way`**: Establishes a two-way data flow, keeping both the view and view model in sync.
* **`.bind`**: Automatically determines the appropriate binding mode. Defaults to `.two-way` for form elements (e.g., `input`, `textarea`) and `.to-view` for most other elements.

#### Examples of Keyword Binding

```html
<!-- my-app.html -->
<!-- Two-way binding: changes in input update 'firstName' and vice versa -->
<input type="text" value.two-way="firstName" placeholder="First Name">

<!-- To-view binding: changes in 'lastName' update the input, but not vice versa -->
<input type="text" value.to-view="lastName" placeholder="Last Name">

<!-- One-time binding: input value is set once from 'middleName' -->
<input type="text" value.one-time="middleName" placeholder="Middle Name">

<!-- Binding a link's href attribute using to-view -->
<a href.to-view="profile.blogUrl">Blog</a>

<!-- Binding a link's href attribute using one-time -->
<a href.one-time="profile.twitterUrl">Twitter</a>

<!-- Binding a link's href attribute using bind (auto mode) -->
<a href.bind="profile.linkedInUrl">LinkedIn</a>
```

```typescript
// my-app.ts
export class MyApp {
  firstName = 'John';
  lastName = 'Doe';
  middleName = 'A.';
  profile = {
    blogUrl: 'https://johnsblog.com',
    twitterUrl: 'https://twitter.com/johndoe',
    linkedInUrl: 'https://linkedin.com/in/johndoe'
  };
}
```

*Result:* The input fields and links will reflect the bound properties with varying degrees of reactivity based on the binding keyword used.

### 3. Vue-style shorthand for `.bind`

If you are used to Vue or other template syntaxes, Aurelia ships with an attribute pattern that treats a leading colon as an alias for `.bind`. This allows you to write more compact markup without giving up any functionality.

```html
<!-- These two lines are equivalent -->
<input value.bind="firstName">
<input :value="firstName">

<!-- Works with any attribute -->
<img :src="profile.avatarUrl" :alt="profile.fullName">
```

The shorthand always creates a property binding (the same as `attribute.bind`). If you need a different binding mode, fall back to the explicit syntax (`value.two-way`, `value.one-time`, etc.). Event shorthands that start with `@` are handled separately in the [event binding guide](/templates/overview/event-binding#shorthand-syntax-for-events).

### 3. Binding to Images

Binding image attributes such as `src` and `alt` ensures that images update dynamically based on the view model data.

**Example: Dynamic Image Binding**

```html
<!-- my-app.html -->
<img src.bind="imageSrc" alt.bind="imageAlt" />
```

```typescript
// my-app.ts
export class MyApp {
  imageSrc = 'https://example.com/image.jpg';
  imageAlt = 'Example Image';
}
```

*Result:* The `img` element will display the image from the specified `src` and use the provided `alt` text.

### 4. Disabling Elements

Dynamically enabling or disabling form elements enhances user interaction and form validation.

**Example: Binding the `disabled` Attribute**

```html
<!-- my-app.html -->
<button disabled.bind="isButtonDisabled">Submit</button>
<input type="text" disabled.bind="isInputDisabled" placeholder="Enter text" />
```

```typescript
// my-app.ts
export class MyApp {
  isButtonDisabled = true;
  isInputDisabled = false;

  toggleButton() {
    this.isButtonDisabled = !this.isButtonDisabled;
  }

  toggleInput() {
    this.isInputDisabled = !this.isInputDisabled;
  }
}
```

*Result:* The `Submit` button starts as disabled, and the input field is enabled. Calling `toggleButton()` or `toggleInput()` will toggle their disabled states.

### 5. Binding `innerHTML` and `textContent`

Choose between `innerHTML` for rendering HTML content and `textContent` for rendering plain text to control how content is displayed within elements.

**Example: Rendering HTML vs. Text**

```html
<!-- my-app.html -->
<div innerhtml.bind="htmlContent"></div>
<div textcontent.bind="plainText"></div>
```

```typescript
// my-app.ts
export class MyApp {
  htmlContent = '<strong>This is bold text.</strong>';
  plainText = '<strong>This is not bold text.</strong>';
}
```

*Result:*

* The first `div` will render the bold text as HTML.
* The second `div` will display the HTML tags as plain text.

## Advanced Binding Techniques

Explore more sophisticated binding scenarios to handle complex data interactions and ensure seamless attribute management.

### 0. Treating existing markup as a custom element with `as-element`

When outside systems dictate the tag name you must render (for example, a CMS that only allows `<div>` and `<section>`), you can still hydrate one of your custom elements by adding `as-element="component-name"` to any real DOM node. The compiler will instantiate `component-name` for that element while leaving the original tag in place.

```html
<!-- Render a section, but run it through the <page-card> custom element -->
<section as-element="page-card" header.bind="title">
  <p>Projected slot content still works as usual.</p>
</section>
```

* Use `as-element` when you want the behavior of a custom element but must keep the original tag name for semantic or styling reasons.
* Unlike `<template as-custom-element="...">`, this does not create a local element definition; it simply aliases an existing element instance.
* The attribute can appear anywhere inside your markup except on the root `<template>` surrogate (putting it there triggers AUR0702).

This makes `as-element` a handy compatibility feature when integrating Aurelia components into environments with strict HTML requirements.

### 1. How Attribute Binding Works

Aurelia employs a mapping function to translate view model properties to corresponding HTML attributes. This typically involves converting `kebab-case` attribute names to `camelCase` property names. However, not all properties directly map to attributes, especially custom or non-standard attributes.

**Example: Automatic Mapping**

```html
<!-- my-app.html -->
<input value.bind="userName" />
```

```typescript
// my-app.ts
export class MyApp {
  userName = 'JaneDoe';
}
```

*Result:* The `input` element's `value` attribute is bound to the `userName` property. Changes in `userName` update the `input` value and vice versa.

#### Property vs. Attribute Targeting

By default, Aurelia bindings target DOM **properties** rather than HTML **attributes**. This distinction is important because:

* **Properties** are JavaScript object properties on DOM elements (e.g., `element.value`)
* **Attributes** are the HTML markup attributes (e.g., `<input value="...">`)

For most standard HTML attributes, this works seamlessly because browsers synchronize property and attribute values. However, for custom attributes or when you specifically need attribute targeting, use the `.attr` binding command or the `attr` binding behavior.

### 2. Using the `.attr` Binding Command

When automatic mapping fails or when dealing with non-standard attributes, use the `.attr` binding command to ensure proper attribute binding.

**Example: Binding a Custom Attribute**

```html
<!-- my-app.html -->
<input my-custom-attr.attr="customValue" />
```

```typescript
// my-app.ts
export class MyApp {
  customValue = 'Custom Attribute Value';
}
```

*Result:* The `input` element will have a `my-custom-attr` attribute set to "Custom Attribute Value".

### 3. The `attr` Binding Behavior

The `attr` binding behavior is a powerful feature that forces any property binding to target the HTML attribute instead of the DOM property. This is especially useful for:

* Custom attributes that don't have corresponding DOM properties
* Data attributes (`data-*`)
* ARIA attributes
* SVG attributes
* Cases where you need to ensure attribute-specific behavior

**Example: Using the `attr` Binding Behavior**

```html
<!-- my-app.html -->
<input pattern.bind="patternProp & attr" />
<div data-tooltip.bind="tooltipText & attr"></div>
<svg>
  <circle cx.bind="centerX & attr" cy.bind="centerY & attr" r="50" />
</svg>
```

```typescript
// my-app.ts
export class MyApp {
  patternProp = '[A-Za-z]{3,}';
  tooltipText = 'This is a custom tooltip';
  centerX = 100;
  centerY = 100;
}
```

*Result:* All bindings will target their respective HTML attributes directly, ensuring proper DOM attribute manipulation.

{% hint style="warning" %}
The `attr` binding behavior can only be used with property bindings (`.bind`, `.one-way`, `.two-way`, `.to-view`, `.from-view`). It cannot be used with event bindings (`.trigger`, `.capture`) or reference bindings (`.ref`).
{% endhint %}

### 4. Attribute Mapping and Custom Elements

Aurelia includes a built-in attribute mapper that handles common HTML attribute-to-property mappings automatically. For example:

* `maxlength` → `maxLength`
* `readonly` → `readOnly`
* `tabindex` → `tabIndex`
* `contenteditable` → `contentEditable`

You can extend this mapping for custom elements or third-party components:

```typescript
// main.ts
import { Aurelia, AppTask, IAttrMapper } from '@aurelia/runtime-html';

Aurelia
  .register(
    AppTask.creating(IAttrMapper, (attrMapper) => {
      attrMapper.useMapping({
        'CUSTOM-INPUT': {
          'max-length': 'maxLength',
          'min-length': 'minLength'
        }
      });

      attrMapper.useGlobalMapping({
        'custom-attr': 'customAttribute'
      });

      attrMapper.useTwoWay(
        (element, attr) => element.tagName === 'CUSTOM-INPUT' && attr === 'value'
      );
    })
  )
  .app(MyApp)
  .start();
```

### 5. Special Attribute Handling

Aurelia provides specialized handling for certain attributes:

#### Class Attributes

```html
<!-- Single class binding -->
<div class.bind="isActive && 'active'"></div>

<!-- Multiple class binding -->
<div class.bind="getClasses()"></div>
```

#### Style Attributes

```html
<!-- Style property binding -->
<div style.background-color.bind="bgColor"></div>
<div style.font-size.bind="fontSize + 'px'"></div>

<!-- Full style object binding -->
<div style.bind="styleObject"></div>
```

```typescript
export class MyApp {
  bgColor = 'red';
  fontSize = 16;
  styleObject = {
    color: 'blue',
    'font-weight': 'bold',
    margin: '10px'
  };

  getClasses() {
    return this.isActive ? 'active highlight' : 'inactive';
  }
}
```

## Practical Use Cases

To better illustrate attribute bindings, here are several practical scenarios showcasing different binding techniques.

### 1. Dynamic Class Binding

**Example: Toggling CSS Classes**

```html
<!-- my-app.html -->
<div class.bind="isActive ? 'active' : 'inactive'">Status</div>
```

```typescript
// my-app.ts
export class MyApp {
  isActive = true;

  toggleStatus() {
    this.isActive = !this.isActive;
  }
}
```

*Result:* The `div` will have the class `active` when `isActive` is `true` and `inactive` when `false`. Calling `toggleStatus()` toggles the class.

### 2. Styling Elements Dynamically

**Example: Binding Inline Styles**

```html
<!-- my-app.html -->
<div style.backgroundColor.bind="bgColor">Colored Box</div>
```

```typescript
// my-app.ts
export class MyApp {
  bgColor = 'lightblue';

  changeColor(newColor: string) {
    this.bgColor = newColor;
  }
}
```

*Result:* The `div`'s background color reflects the current value of `bgColor`. Invoking `changeColor('coral')` will update the background to coral.

### 3. Conditional Attribute Rendering

**Example: Conditionally Setting the `required` Attribute**

```html
<!-- my-app.html -->
<input type="email" required.bind="isEmailRequired" placeholder="Enter your email" />
```

```typescript
// my-app.ts
export class MyApp {
  isEmailRequired = true;

  toggleEmailRequirement() {
    this.isEmailRequired = !this.isEmailRequired;
  }
}
```

*Result:* The `input` field will be required based on the `isEmailRequired` property. Toggling this property will add or remove the `required` attribute.

## Notes on Syntax

While attribute binding in Aurelia is versatile and robust, there are certain syntactical nuances and limitations to be aware of to prevent unexpected behavior.

1. **Expression Syntax Restrictions**
   * **No Chaining with `;` or `,`**: Expressions within `${}` cannot be chained using semicolons `;` or commas `,`. Each interpolation expression should represent a single, complete expression.
   * **Restricted Primitives and Operators**: Certain JavaScript primitives and operators cannot be used within interpolation expressions. These include:
     * `Boolean`
     * `String`
     * `instanceof`
     * `typeof`
     * Bitwise operators (except for the pipe `|` used with value converters)
   * **Usage of Pipe `|`**: The pipe character `|` is reserved exclusively for Aurelia's value converters within bindings and cannot be used as a bitwise operator.
2. **Attribute Targeting Syntax**

   The presence of both `.bind` and `.attr` syntaxes can be confusing. Here's why both exist:

   * **Property vs. Attribute Binding**: `.bind` targets the DOM property, which is suitable for standard attributes that have corresponding DOM properties. However, for custom or non-standard attributes that do not have direct property mappings, `.attr` is necessary to bind directly to the attribute itself.
   * **Example: Binding `id` Using Property and Attribute**

     ```html
     <!-- Property Binding -->
     <input id.bind="inputId" />

     <!-- Attribute Binding -->
     <input id.attr="inputId" />
     ```

     ```typescript
     // my-app.ts
     export class MyApp {
       inputId = 'user-input';
     }
     ```

     *Result:*

     * Using `.bind`, Aurelia binds to the `id` property of the `input` element.
     * Using `.attr`, Aurelia binds directly to the `id` attribute in the DOM.
3. **Choosing Between Interpolation and Keyword Binding**

   Both interpolation and keyword binding can achieve similar outcomes. The choice between them often comes down to preference and specific use case requirements.

   * **Performance and Features**: There is no significant performance difference between the two. Both are equally efficient and offer similar capabilities.
   * **Readability and Maintainability**: Interpolation can be more readable for simple string concatenations, while keyword bindings offer more explicit control for complex bindings.

{% hint style="info" %}
For complex transformations or formatting, consider using Aurelia's value converters instead of embedding extensive logic within interpolation expressions. This practice enhances code maintainability and separation of concerns.
{% endhint %}

### Example: Using Value Converters for Formatting

**Binding with a Value Converter**

```html
<!-- my-app.html -->
<span class="price">${totalPrice | currency}</span>
```

```typescript
// my-app.ts
export class MyApp {
  totalPrice = 199.99;
}
```

```typescript
// currency-value-converter.ts
export class CurrencyValueConverter {
  toView(value: number) {
    return `$${value.toFixed(2)}`;
  }
}
```

*Result:* Displays the `totalPrice` formatted as currency, e.g., "$199.99".

## Troubleshooting and Common Issues

### 1. Binding Behavior Errors

**Error: AUR9994 - Invalid Binding Type for 'attr' Binding Behavior**

This error occurs when the `attr` binding behavior is used with non-property bindings:

```html
<!-- ❌ Incorrect: Using attr with event binding -->
<button click.trigger="save() & attr">Save</button>

<!-- ✅ Correct: Remove attr from event binding -->
<button click.trigger="save()">Save</button>

<!-- ✅ Correct: Use attr with property binding -->
<input value.bind="query & attr">
```

### 2. Null and Undefined Values

When bound properties are `null` or `undefined`, attributes are removed from the DOM:

```typescript
export class MyApp {
  tooltipText: string | null = null; // Will remove title attribute
  isDisabled: boolean | undefined = undefined; // Will remove disabled attribute
}
```

```html
<!-- These attributes will be removed when values are null/undefined -->
<div title.bind="tooltipText">Content</div>
<button disabled.bind="isDisabled">Click me</button>
```

### 3. Custom Attribute Conflicts

When using `.attr` binding command, Aurelia bypasses custom attribute detection:

```html
<!-- This will create a DOM attribute, NOT invoke a custom attribute -->
<div my-custom.attr="value"></div>

<!-- This will invoke the custom attribute -->
<div my-custom.bind="value"></div>
```

### 4. Boolean Attribute Handling

HTML boolean attributes (like `disabled`, `checked`, `readonly`) have special handling:

```html
<!-- These all result in disabled="disabled" or no attribute -->
<button disabled.bind="true">Always Disabled</button>
<button disabled.bind="false">Never Disabled</button>
<button disabled.bind="isDisabled">Conditionally Disabled</button>
```

### 5. SVG Attributes

SVG attributes may require the `attr` binding behavior for proper functionality:

```html
<svg>
  <!-- For SVG-specific attributes, use attr binding behavior -->
  <circle cx.bind="x & attr" cy.bind="y & attr" r.bind="radius & attr" />
  <text text-anchor.bind="anchor & attr">Label</text>
</svg>
```

## Performance Considerations

### 1. Binding Mode Selection

Choose the appropriate binding mode for optimal performance:

* Use `.one-time` for static values that never change
* Use `.to-view` for display-only data
* Use `.two-way` only when bidirectional synchronization is needed
* Use `.from-view` for write-only scenarios

### 2. Expression Complexity

Keep binding expressions simple for better performance:

```html
<!-- ❌ Complex expression in template -->
<div class.bind="items.filter(i => i.active).length > 0 ? 'has-active' : 'no-active'"></div>

<!-- ✅ Move logic to view model -->
<div class.bind="hasActiveItems ? 'has-active' : 'no-active'"></div>
```

```typescript
export class MyApp {
  get hasActiveItems() {
    return this.items.some(i => i.active);
  }
}
```

### 3. Computed Properties

Use computed properties with proper dependencies for efficient updates:

```typescript
import { computed } from '@aurelia/runtime';

export class MyApp {
  items = [];

  @computed({ deps: ['items'] })
  get itemCount() {
    return this.items.length;
  }
}
```

You can also use the shorthand syntax for simple dependencies:

```typescript
@computed('items')
get itemCount() {
  return this.items.length;
}
```

## Best Practices

### 1. Consistent Naming

Use consistent naming conventions for attributes and properties:

```html
<!-- Use kebab-case for attributes -->
<my-component data-id.bind="itemId" custom-prop.bind="value"></my-component>
```

### 2. Type Safety

Leverage TypeScript for better development experience:

```typescript
interface User {
  id: number;
  name: string;
  avatar?: string;
}

export class UserProfile {
  user: User = { id: 1, name: 'John' };

  get avatarUrl(): string {
    return this.user.avatar ?? '/default-avatar.png';
  }
}
```

### 3. Error Boundaries

Handle potential errors in binding expressions:

```html
<!-- Use optional chaining and fallbacks -->
<img src.bind="user?.avatar ?? defaultAvatar" alt.bind="user?.name ?? 'Unknown User'">
```

### 4. Accessibility

Ensure proper accessibility attributes:

```html
<!-- Include ARIA attributes for screen readers -->
<button
  disabled.bind="isLoading"
  aria-busy.bind="isLoading & attr"
  aria-label.bind="buttonLabel & attr">
  ${isLoading ? 'Loading...' : 'Submit'}
</button>
```

## Summary

Attribute binding in Aurelia offers a flexible and powerful means to synchronize data between your view model and the DOM. By understanding and utilizing the various binding commands and techniques, you can create dynamic, responsive, and maintainable user interfaces. Always consider the specific needs of your project when choosing between different binding strategies, and leverage Aurelia's features to their fullest to enhance your application's interactivity and user experience.

Key takeaways:

* Use the appropriate binding mode for your use case
* Understand the difference between property and attribute targeting
* Leverage the `attr` binding behavior for custom attributes
* Handle null/undefined values gracefully
* Consider performance implications of complex expressions
* Follow accessibility best practices
* Use TypeScript for better development experience


# Event binding

Event binding in Aurelia 2 offers a streamlined approach to managing DOM events directly within your templates. By declaratively attaching event listeners in your view templates, you can effortlessly respond to user interactions like clicks, keystrokes, form submissions, and more. This guide explores the intricacies of event binding in Aurelia 2, providing detailed explanations and practical examples to deepen your understanding and effective utilization of this feature.

## Understanding Event Binding

Aurelia 2 simplifies the connection between DOM events and your view model methods. It employs a clear and concise syntax, enabling you to specify the event type and the corresponding method to be invoked in your view model when that event occurs.

### Event Binding Syntax

The general syntax for event binding in Aurelia 2 follows this pattern:

```html
<element event.command="methodName(argument1, argument2, ...)">
```

* `<element>`: The HTML element to which you are attaching the event listener.
* `event`: The name of the DOM event you wish to listen for (e.g., `click`, `input`, `mouseover`).
* `.command`: The binding command that instructs Aurelia how to handle the event. Common commands are `.trigger` and `.capture`.
* `methodName`: The name of the method in your view model that will be executed when the event is dispatched.
* `argument1`, `argument2`, ...: Optional arguments that you can pass to the `methodName`.

### Event Binding Commands: `.trigger` and `.capture`

Aurelia 2 primarily offers two commands for event binding, each controlling the event listening phase:

1. **`.trigger`**: This command attaches an event listener that reacts to events during the **bubbling phase**. This is the most frequently used and generally recommended command for event binding as it aligns with typical event handling patterns in web applications. Events are first captured by the deepest element and then propagate upwards through the DOM tree.
2. **`.capture`**: This command listens for events during the **capturing phase**. Capturing is the less common phase where events propagate downwards from the window to the target element. `.capture` is typically used in specific scenarios, such as when you need to intercept an event before it reaches child elements, potentially preventing default behaviors or further propagation.

{% hint style="info" %}
The `.delegate` command from Aurelia 1 has been removed in Aurelia 2. If you need to migrate from Aurelia 1 code that uses `.delegate`, you can use the `@aurelia/compat-v1` package, or simply replace `.delegate` with `.trigger` in most cases, as `.trigger` in Aurelia 2 efficiently handles event bubbling for dynamic content.
{% endhint %}

#### Example: Click Event Binding using `.trigger`

To bind a click event on a button to a method named `handleClick` in your view model, you would use:

```html
<button click.trigger="handleClick()">Click Me</button>
```

When a user clicks the "Click Me" button, Aurelia will execute the `handleClick` method defined in your associated view model.

### Shorthand syntax for events (`@event`)

To make it easier for teams migrating from Vue or other frameworks, Aurelia also understands the `@event="handler"` shorthand. The compiler converts it to the equivalent `event.trigger` binding, including modifiers after a colon.

```html
<!-- These two lines are identical -->
<button click.trigger="save()">Save</button>
<button @click="save()">Save</button>

<!-- Modifiers work the same way -->
<button @click:ctrl+enter="send()">Send (Ctrl + Enter)</button>
```

Use whichever style you prefer—the generated instructions are the same. If you need capturing semantics, use the explicit `event.capture` syntax because the shorthand only targets the bubbling (`.trigger`) command.

### Passing Event Data to Handlers

Often, you need access to the event object or want to pass additional data to your event handler method. Aurelia provides a straightforward way to do this.

To pass the DOM event object itself to your handler, use the `$event` special variable:

```html
<button click.trigger="handleClick($event)">Click Me</button>
```

In your view model, the `handleClick` method would accept the event object as a parameter:

```typescript
export class MyViewModel {
  handleClick(event: MouseEvent) {
    console.log('Button clicked!', event);
    // Access event properties like event.target, event.clientX, etc.
  }
}
```

You can also pass custom arguments along with the event:

```html
<button click.trigger="removeItem(item.id, $event)">Remove Item</button>
```

```typescript
export class MyViewModel {
  removeItem(itemId: number, event: MouseEvent) {
    console.log(`Removing item with ID: ${itemId}`, event);
    // Logic to remove the item
  }
}
```

## Common DOM Events

Aurelia 2 supports binding to all standard DOM events. Here are some frequently used events in web development:

### `click`

The `click` event is triggered when a pointing device button (typically a mouse button) is both pressed and released while the pointer is inside the element. It is commonly used for buttons, links, and interactive elements.

```html
<button click.trigger="submitForm()">Submit</button>
<a href="#" click.trigger="openModal()">Learn More</a>
```

### `input`

The `input` event fires when the value of an `<input>`, `<textarea>`, or `<select>` element has been changed. It's useful for real-time validation or dynamic updates based on user input.

```html
<input type="text" input.trigger="updateSearchQuery($event.target.value)" placeholder="Search..." />
```

### `change`

The `change` event is fired when the value of an element has been changed *and* the element loses focus. This is often used for `<input>`, `<select>`, and `<textarea>` elements when you want to react after the user has finished making changes.

```html
<select change.trigger="selectTheme($event.target.value)">
  <option value="light">Light Theme</option>
  <option value="dark">Dark Theme</option>
</select>
```

### `mouseover` and `mouseout`

The `mouseover` event occurs when the mouse pointer is moved onto an element, and `mouseout` occurs when it is moved off of an element. These are useful for hover effects and interactive UI elements.

```html
<div mouseover.trigger="highlight()" mouseout.trigger="unhighlight()">Hover Me</div>
```

### `keydown`, `keyup`, and `keypress`

These keyboard events are triggered when a key is pressed down, released, or pressed and released, respectively. `keydown` and `keyup` are generally preferred for capturing special keys like arrows, `Ctrl`, `Shift`, etc., while `keypress` is more suited for character input.

```html
<input type="text" keydown.trigger="handleKeyDown($event)" />
```

## Controlling Event Propagation

In DOM event handling, events can "bubble" up the DOM tree (from the target element up to the document) or "capture" down (from the document to the target element). Sometimes you need to control this propagation. Within your event handler methods, you can use methods of the event object to manage propagation:

* `event.stopPropagation()`: Prevents the event from further bubbling up the DOM tree to parent elements.
* `event.preventDefault()`: Prevents the default action associated with the event (if it's cancelable), without stopping event propagation. For example, `preventDefault` on a click event of a link (`<a>`) would stop the browser from navigating to the link's `href`.

## Advanced Event Binding Techniques

Aurelia 2 provides capabilities beyond basic event binding, allowing for performance optimization and handling specific scenarios.

### Throttling and Debouncing Event Handlers

For events that fire rapidly and repeatedly, such as `mousemove`, `scroll`, or `input`, calling an event handler function on every event can be performance-intensive. Aurelia's binding behaviors offer `throttle` and `debounce` to limit the rate at which your handler is invoked.

**Throttling**: Ensures a function is called at most once in a specified time interval.

```html
<div mousemove.trigger="trackMouse($event) & throttle:50">Move mouse here</div>
```

In this example, `trackMouse` will be executed at most every 50 milliseconds, even if `mousemove` events are firing more frequently.

**Debouncing**: Delays the execution of a function until after a certain amount of time has passed since the *last* time the event was triggered. Useful for autocomplete or search features to avoid making API calls on every keystroke.

```html
<input type="text" input.trigger="searchQuery($event.target.value) & debounce:300" placeholder="Search" />
```

Here, `searchQuery` will be called 300ms after the user *stops* typing, reducing the number of search requests.

#### Performance Considerations

When using throttling and debouncing, consider these performance best practices:

* **Choose appropriate delays**: Too short delays may not provide performance benefits, while too long delays can make the UI feel unresponsive.
* **Monitor handler complexity**: Ensure that even throttled/debounced handlers are optimized for performance.
* **Use signals for immediate updates**: When you need to force immediate execution of a throttled/debounced handler (e.g., on form submission), use signals:

```html
<input input.trigger="search($event.target.value) & debounce:300:'immediate'">
<button click.trigger="signaler.dispatchSignal('immediate')">Search Now</button>
```

### Custom Events

Aurelia 2 fully supports custom events, which are essential when working with custom elements or integrating third-party libraries that dispatch their own events.

```html
<my-custom-element data-loaded.trigger="handleDataLoaded($event)"></my-custom-element>
```

In this scenario, `data-loaded` is a custom event emitted by `<my-custom-element>`. `handleDataLoaded` in the parent view model will be invoked when this custom event is dispatched.

## Event Binding Examples and Use Cases

To solidify your understanding, let's explore practical examples showcasing different event binding scenarios in Aurelia 2.

### Self-Delegating Events with `.self`

The `self` binding behavior ensures that an event handler is only triggered if the event originated directly from the element to which the listener is attached, and not from any of its child elements (due to event bubbling).

```html
<div click.trigger="divClicked() & self">
  <p>Clicking here will trigger divClicked</p>
  <button click.trigger="buttonClicked()">Clicking button will NOT trigger divClicked</button>
</div>
```

In this setup, `divClicked()` will only be executed if the click originates directly on the `<div>` element. Clicks on the `<button>` (a child element) will trigger `buttonClicked()` but will *not* bubble up to trigger `divClicked()` due to the `& self` behavior.

### Checkbox `change` Event and Two-Way Binding

Combine event binding with two-way binding for interactive form elements like checkboxes.

```html
<input type="checkbox" checked.bind="isAgreed" change.trigger="agreementChanged()" id="agreementCheckbox">
<label for="agreementCheckbox">I agree to the terms</label>
```

```typescript
export class MyViewModel {
  isAgreed = false;

  agreementChanged() {
    console.log('Agreement status changed:', this.isAgreed);
    // Perform actions based on checkbox state
  }
}
```

Here, `checked.bind="isAgreed"` keeps the `isAgreed` property in sync with the checkbox state (two-way binding). `change.trigger="agreementChanged()"` additionally allows you to execute custom logic when the checkbox state changes.

### Handling Keyboard Events for Specific Keys

React to specific key presses within input fields.

```html
<input type="text" keydown.trigger="handleKeyDown($event)" placeholder="Type here">
```

```typescript
export class MyViewModel {
  handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      console.log('Enter key pressed!');
      // Perform action on Enter key press (e.g., submit form)
      event.preventDefault(); // Prevent default form submission if inside a form
    } else if (event.key === 'Escape') {
      console.log('Escape key pressed!');
      // Handle Escape key press (e.g., clear input)
    }
    // ... handle other keys as needed
  }
}
```

This example shows how to check `event.key` to handle specific keys like "Enter" and "Escape".

### Event Delegation for Dynamic Lists

Event delegation is a powerful technique for efficiently handling events on dynamically generated lists. Attach a single event listener to the parent `<ul>` or `<div>` instead of individual listeners to each list item using `.trigger`.

```html
<ul click.trigger="listItemClicked($event)">
  <li repeat.for="item of items" data-item-id="${item.id}">${item.name}</li>
</ul>
```

```typescript
export class MyViewModel {
  items = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];

  listItemClicked(event: Event) {
    const target = event.target as HTMLElement;
    if (target.tagName === 'LI') {
      const itemId = target.dataset.itemId;
      console.log(`List item clicked, ID: ${itemId}`);
      // Logic to handle click on list item with ID itemId
    }
  }
}
```

The `listItemClicked` handler attached to the `<ul>` will be triggered for clicks on any `<li>` within it due to event bubbling. We check `event.target` to ensure the click originated from an `<li>` and extract the `data-item-id`. This approach provides efficient event handling for dynamic lists without requiring individual listeners on each item.

### Custom Event Communication Between Components

Parent components can listen for and react to custom events dispatched by child custom elements.

**Custom Element (Child):**

```typescript
import { bindable, customElement, resolve } from 'aurelia';
import { INode } from '@aurelia/runtime-html';

@customElement({ name: 'my-button', template: `<button click.trigger="handleClick()">\${label}</button>` })
export class MyButton {
  private element = resolve(INode) as HTMLElement;
  @bindable label = 'Click Me';

  handleClick() {
    this.element.dispatchEvent(new CustomEvent('button-clicked', {
      bubbles: true, // Allow event to bubble up
      detail: { message: 'Button with label "' + this.label + '" was clicked' }
    }));
  }
}
```

**Parent Component (Parent):**

```html
<my-button label="Action Button" button-clicked.trigger="handleButtonClick($event)"></my-button>
```

```typescript
export class ParentViewModel {
  handleButtonClick(event: CustomEvent) {
    console.log('Custom event "button-clicked" received:', event.detail.message);
    // Handle the custom event
  }
}
```

When the button in `<my-button>` is clicked, it dispatches a custom event `button-clicked`. The parent component listens for this event using `button-clicked.trigger` and executes `handleButtonClick`, receiving event details in `$event.detail`.

### Autocomplete with Debounced Input

Implement autocomplete functionality with debouncing to reduce API calls during typing.

```html
<input type="text" input.trigger="autocomplete($event.target.value) & debounce:500" placeholder="Start typing..." />
<ul if.bind="suggestions.length">
  <li repeat.for="suggestion of suggestions">${suggestion}</li>
</ul>
```

```typescript
export class MyViewModel {
  searchQuery = '';
  suggestions = [];

  autocomplete(query: string) {
    this.searchQuery = query;
    if (query.length > 2) {
      // Simulate API call for suggestions (replace with actual API call)
      setTimeout(() => {
        this.suggestions = [`${query} suggestion 1`, `${query} suggestion 2`, `${query} suggestion 3`];
      }, 300);
    } else {
      this.suggestions = [];
    }
  }
}
```

The `autocomplete` method will be called 500ms after the last `input` event. This delay allows users to finish typing before triggering the (simulated) autocomplete API call, improving performance.

### Event Modifiers: Enhancing Event Handling

Event modifiers provide a declarative way to apply conditions or actions to event bindings directly in your templates. Event modifiers are appended to the event name after a colon:

```html
<element event.trigger[:modifier]="methodName()">
```

Aurelia provides built-in modifiers for common event handling scenarios, and you can extend them with custom mappings.

| Modifier                                                    | Works with            | Description                                                                                  |
| ----------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------- |
| `prevent`                                                   | Any event             | Calls `event.preventDefault()` before running your handler.                                  |
| `stop`                                                      | Any event             | Calls `event.stopPropagation()` before running your handler.                                 |
| `ctrl`, `alt`, `shift`, `meta`                              | Keyboard/Mouse events | Ensures the corresponding meta key is pressed. Multiple keys can be chained (`:ctrl+enter`). |
| Named keys (`enter`, `escape`, `tab`, `a`, `ArrowUp`, etc.) | Keyboard events       | Only invokes your handler when the pressed key matches.                                      |
| `left`, `middle`, `right`                                   | Mouse events          | Filters mouse buttons.                                                                       |

```html
<!-- Submit only on Ctrl + Enter, prevent default form submission -->
<textarea @keydown:ctrl+enter.prevent="submitDraft()"></textarea>

<!-- Ignore bubbling clicks; only fire when the element itself is clicked -->
<button click.trigger="destroy()" @click:left.stop.prevent></button>

<!-- When using dot syntax, the command still comes first -->
<div scroll.trigger="syncScroll($event)" @scroll.prevent></div>
```

Modifiers are additive: `@click:ctrl+enter.prevent` checks modifier keys first and only then calls your handler (after canceling the DOM default). If a modifier check fails (for example, the required key is not pressed) the handler simply does not run.

#### Mouse and Keyboard Event Modifiers

Aurelia has built-in support for modifiers related to mouse buttons and keyboard keys.

**Example: `ctrl` Key Modifier**

Execute `onCtrlClick()` only when the button is clicked *and* the `Ctrl` key is pressed.

```html
<button click.trigger:ctrl="onCtrlClick()">Ctrl + Click</button>
```

**Example: `ctrl+enter` Key Combination**

Execute `send()` only when the `Enter` key is pressed *while* the `Ctrl` key is also held down. Modifiers can be combined using `+`.

```html
<textarea keydown.trigger:ctrl+enter="send()"></textarea>
```

#### `prevent` and `stop` Modifiers

Declaratively call `event.preventDefault()` and `event.stopPropagation()` using modifiers.

**Example: `prevent` and `stop` Modifiers**

Call `validate()` when the button is clicked, and also prevent the default button behavior and stop event propagation.

```html
<button click.trigger:stop:prevent="validate()">Validate</button>
```

#### Mouse Button Modifiers: `left`, `middle`, `right`

Handle clicks based on specific mouse buttons.

**Example: `middle` Mouse Button Modifier**

Execute `newTab()` only when the button is clicked with the middle mouse button.

```html
<button click.trigger:middle="newTab()">Open in New Tab (Middle Click)</button>
```

#### Keyboard Key Mappings and Custom Modifiers

You can use character codes as modifiers for keyboard events. For example, `75` is the char code for uppercase 'K'.

**Example: `Ctrl + K` using Char Code Modifier**

Execute `openSearchDialog()` when `Ctrl + K` is pressed in the textarea.

```html
<textarea keydown.trigger:ctrl+75="openSearchDialog()"></textarea>
```

While using char codes works, it can be less readable. You can create custom key mappings to use more descriptive modifier names. For example, map `upper_k` to the key code for 'K'.

**Custom Key Mapping Setup (in your main application file, e.g., `main.ts`):**

```typescript
import Aurelia, { AppTask, IKeyMapping } from 'aurelia';

Aurelia.register(
  AppTask.creating(IKeyMapping, mapping => {
    mapping.keys.upper_k = 'K'; // Map 'upper_k' to 'K'
  })
);
```

Now you can use `:upper_k` as a modifier:

```html
<textarea keydown.trigger:ctrl+upper_k="openSearchDialog()"></textarea>
```

This makes your template more readable as `:ctrl+upper_k` is more self-explanatory than `:ctrl+75`.

{% hint style="info" %}
Aurelia provides default key mappings for lowercase letters 'a' through 'z' (both as key codes and letter names). For uppercase letters, only key code mappings are provided by default (e.g., `:65` for 'A'). You can extend these mappings as shown above to create more semantic modifier names.
{% endhint %}

#### Extending modifier handling

The runtime registers `EventModifier`, `IModifiedEventHandlerCreator`, and a set of default creators (mouse, keyboard, generic) inside `EventModifierRegistration`. If you need custom semantics—gestures, wheel direction checks, or application-specific shortcuts—add your own creator and register it with the container:

```typescript
import { EventModifierRegistration, IModifiedEventHandlerCreator } from '@aurelia/runtime-html';
import { Registration } from '@aurelia/kernel';

class WheelModifier implements IModifiedEventHandlerCreator {
  public readonly type = 'wheel';
  public getHandler(modifier: string) {
    const parts = modifier.split('.');
    return (event: WheelEvent) => {
      if (parts.includes('vertical') && Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
        return false; // Ignore horizontal scrolls
      }
      if (parts.includes('invert')) {
        event.deltaY *= -1;
      }
      return true;
    };
  }
}

Aurelia.register(
  EventModifierRegistration,
  Registration.singleton(IModifiedEventHandlerCreator, WheelModifier)
);
```

After registration you can bind to `@wheel:vertical.invert="onScroll($event)"`. Returning `false` from the handler vetoes the event (your view-model method will not be called), while returning `true` allows the binding to proceed.

## Common Pitfalls and Troubleshooting

### Event Handler Issues

1. **Event not firing**: Verify that the event name is correct and the element supports that event type.
2. **Handler not found**: Ensure the method exists in your view model and is properly spelled.
3. **Context issues**: Remember that event handlers execute in the context of the view model, so `this` refers to the view model instance.

### Performance Issues

1. **Frequent event handlers**: Use throttling or debouncing for events that fire rapidly (e.g., `mousemove`, `scroll`, `input`).
2. **Complex handlers**: Keep event handlers lightweight. Move heavy processing to separate methods called asynchronously.
3. **Memory leaks**: Aurelia automatically manages event listener cleanup, but be cautious with manual event listeners in your handlers.

### Binding Behavior Conflicts

1. **Multiple rate limiters**: You cannot apply both `throttle` and `debounce` to the same binding (Error AUR9996).
2. **Duplicate behaviors**: Avoid applying the same binding behavior multiple times (Error AUR0102).
3. **Behavior order**: When chaining behaviors, order matters: `event.trigger="handler() & behavior1 & behavior2"`.

### Event Modifier Issues

1. **Incorrect syntax**: Modifiers must be placed after the command: `click.trigger:ctrl` not `click:ctrl.trigger`.
2. **Unsupported modifiers**: Verify that the modifier is supported for the event type.
3. **Custom modifiers**: Ensure custom key mappings are registered before use.

### Debugging Tips

1. **Console logging**: Add console.log statements to verify event firing:

   ```typescript
   handleClick(event: MouseEvent) {
     console.log('Click handler called', event);
     // Your logic here
   }
   ```
2. **Browser dev tools**: Use the browser's event listener inspection to verify bindings are attached.
3. **Event object inspection**: Log the entire event object to understand available properties:

   ```typescript
   handleEvent(event: Event) {
     console.log('Event details:', {
       type: event.type,
       target: event.target,
       currentTarget: event.currentTarget
     });
   }
   ```

## Conclusion

Event binding in Aurelia 2 is a powerful and intuitive mechanism for creating interactive web applications. By mastering the syntax, commands, event modifiers, and advanced techniques like throttling and custom events, you can effectively handle user interactions and build dynamic, responsive user interfaces. Leverage the `.trigger` command for typical scenarios and `.capture` when you need to intercept events during the capturing phase. With these tools and patterns, you can craft a seamless and engaging user experience in your Aurelia 2 applications.


# Text interpolation

Text interpolation allows you to display dynamic values in your views. By wrapping an expression with `${}`, you can render variables, object properties, function results, and more within your HTML. This is conceptually similar to [JavaScript template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).

## Template expressions

Expressions inside `${}` can perform operations such as arithmetic, function calls, or ternaries:

{% code title="Addition example" %}

```html
<p>Quick maths: ${2 + 2}</p>
<!-- Outputs "Quick maths: 4" -->
```

{% endcode %}

### Calling functions

You can call functions defined on your view model. For example:

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  adder(val1: number, val2: number): number {
    return parseInt(val1) + parseInt(val2);
  }
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<p>Behold mathematics, 6 + 1 = ${adder(6, 1)}</p>
<!-- Outputs "Behold mathematics, 6 + 1 = 7" -->
```

{% endcode %}

### Using ternaries

You can also use ternary operations:

{% code title="my-app.html" %}

```html
<p>${isTrue ? 'True' : 'False'}</p>
```

{% endcode %}

This will display either "True" or "False" depending on the boolean value of `isTrue`.

### Complex expressions

You can use more sophisticated expressions for dynamic content:

{% code title="Array operations" %}

```typescript
export class MyApp {
  items = [
    { name: 'Apple', price: 1.50, category: 'fruit' },
    { name: 'Banana', price: 0.80, category: 'fruit' },
    { name: 'Carrot', price: 0.90, category: 'vegetable' }
  ];
}
```

{% endcode %}

{% code title="Object property access" %}

```typescript
export class MyApp {
  user = {
    profile: {
      personal: { firstName: 'John', lastName: 'Doe' },
      settings: { theme: 'dark', notifications: true }
    }
  };
}
```

{% endcode %}

{% code title="Conditional and logical operations" %}

```html
<p>Status: ${isLoggedIn && user ? 'Authenticated' : 'Guest'}</p>
<p>Display: ${showDetails || showSummary ? 'Visible' : 'Hidden'}</p>
<p>Count: ${count || 0}</p>
<p>Message: ${message?.trim() || 'No message'}</p>
```

{% endcode %}

## Optional Syntax

Aurelia supports the following optional chaining and nullish coalescing operators in templates:

* `??`
* `?.`
* `?.()`
* `?.[]`

{% hint style="warning" %}
Note that `??=` is not supported.
{% endhint %}

You can use these operators to safely handle null or undefined values:

{% code title="Optional chaining and nullish coalescing" %}

```html
<p>User Name: ${user?.name ?? 'Anonymous'}</p>
```

{% endcode %}

This helps avoid lengthy if-statements or ternary checks in your view model when dealing with potentially undefined data.

## HTMLElement Interpolation

Aurelia supports passing HTMLElement objects directly to template interpolations. This allows you to dynamically create and insert DOM elements into your templates at runtime.

### Creating elements with `document.createElement()`

You can create DOM elements in your view model and bind them directly:

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  content = document.createElement('button');

  constructor() {
    this.content.textContent = 'Click me!';
    this.content.addEventListener('click', () => {
      alert('Button clicked!');
    });
  }
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<div>${content}</div>
```

{% endcode %}

The button element will be directly inserted into the div, maintaining all its properties and event listeners.

### Parsing HTML strings

You can also parse HTML strings and render the resulting elements:

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  content = (() => {
    const tpl = document.createElement('template');
    tpl.innerHTML = '<button>Parsed Button</button>';
    return tpl.content.firstElementChild as HTMLElement;
  })();
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<div>${content}</div>
```

{% endcode %}

{% hint style="warning" %}
Be cautious about the source of your HTML strings to avoid XSS vulnerabilities. Only do this with trusted content, or sanitize it first.
{% endhint %}

### Security Considerations

When interpolating HTMLElements, be mindful of security implications:

{% code title="Safe practices" %}

```typescript
export class MyApp {
  // ✅ Safe: Creating known elements
  createSafeButton() {
    const button = document.createElement('button');
    button.textContent = 'Safe Button'; // textContent escapes content
    button.className = 'safe-class';
    return button;
  }

  // ❌ Dangerous: Using innerHTML with user input
  createUnsafeElement(userInput: string) {
    const div = document.createElement('div');
    div.innerHTML = userInput; // Can execute scripts!
    return div;
  }

  // ✅ Better: Sanitize user input or use textContent
  createSafeElement(userInput: string) {
    const div = document.createElement('div');
    div.textContent = userInput; // Escapes all HTML
    return div;
  }
}
```

{% endcode %}

{% hint style="danger" %}
**Never use innerHTML with user-provided content** without proper sanitization. This can lead to XSS vulnerabilities.
{% endhint %}

### Dynamic element creation

This feature is particularly useful for dynamic content scenarios:

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  elements: HTMLElement[] = [];

  addElement() {
    const newElement = document.createElement('span');
    newElement.textContent = `Element ${this.elements.length + 1}`;
    newElement.style.color = 'blue';
    this.elements.push(newElement);
  }
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<button click.trigger="addElement()">Add Element</button>
<div repeat.for="element of elements">${element}</div>
```

{% endcode %}

## Notes on syntax

While template interpolation is powerful, there are a few limitations to keep in mind:

1. Aurelia parses **expressions**, not statements (so things like `if`, `for`, `return`, and `function` declarations are not supported inside `${...}`).
2. Some JavaScript tokens are repurposed: `|` is reserved for value converters and `&` is reserved for binding behaviors (so bitwise `|` / `&` are not available).
3. The comma operator (`,`) is not supported.

{% hint style="info" %}
For complex transformations or formatting, consider using Aurelia's value converters instead of cramming too much logic into an interpolation.
{% endhint %}

## Performance Best Practices

### Avoid Complex Expressions

Keep interpolation expressions simple for better performance. Complex computations should be moved to getters or methods:

{% code title="❌ Not recommended" %}

```html
<p>${items.filter(i => i.active).map(i => i.name.toUpperCase()).join(', ')}</p>
```

{% endcode %}

{% code title="✅ Better approach" %}

```typescript
export class MyApp {
  get activeItemNames() {
    return this.items
      .filter(i => i.active)
      .map(i => i.name.toUpperCase())
      .join(', ');
  }
}
```

{% endcode %}

### Array Observation Performance

Aurelia automatically observes arrays used in interpolation. For large arrays that change frequently, consider using computed getters:

{% code title="Large array optimization" %}

```typescript
export class MyApp {
  private _cachedResult: string = '';
  private _lastArrayLength: number = 0;

  get expensiveArrayComputation() {
    if (this.largeArray.length !== this._lastArrayLength) {
      this._cachedResult = this.largeArray
        .filter(/* complex filter */)
        .reduce(/* expensive operation */, '');
      this._lastArrayLength = this.largeArray.length;
    }
    return this._cachedResult;
  }
}
```

{% endcode %}

### Memory Considerations

When using HTMLElement interpolation, ensure proper cleanup to avoid memory leaks:

{% code title="Proper cleanup example" %}

```typescript
export class MyApp {
  elements: HTMLElement[] = [];

  detaching() {
    // Clean up event listeners and references
    this.elements.forEach(el => {
      el.removeEventListener('click', this.handleClick);
    });
    this.elements = [];
  }
}
```

{% endcode %}

## Error Handling and Edge Cases

### Handling Null and Undefined Values

Interpolation gracefully handles `null` and `undefined` values by rendering empty strings:

{% code title="Null/undefined handling" %}

```typescript
export class MyApp {
  name: string | null = null;
  data: any = undefined;
}
```

{% endcode %}

### Error-Prone Expressions

Some expressions can throw runtime errors. Use defensive patterns:

{% code title="❌ Can throw errors" %}

```html
<p>${user.profile.name}</p>           <!-- Error if user or profile is null -->
<p>${items[selectedIndex].title}</p>  <!-- Error if index out of bounds -->
<p>${calculateTotal()}</p>            <!-- Error if method throws -->
```

{% endcode %}

{% code title="✅ Defensive patterns" %}

```html
<p>${user?.profile?.name ?? 'Anonymous'}</p>
<p>${items[selectedIndex]?.title ?? 'No item selected'}</p>
<p>${safeCalculateTotal()}</p>
```

{% endcode %}

### Type Coercion Behavior

Interpolation converts values to strings following JavaScript coercion rules:

{% code title="Type conversion examples" %}

```typescript
export class MyApp {
  number = 42;
  boolean = true;
  array = [1, 2, 3];
  object = { name: 'test' };
}
```

{% endcode %}

### HTMLElement Edge Cases

When interpolating HTMLElements, be aware of these behaviors:

{% code title="HTMLElement considerations" %}

```typescript
export class MyApp {
  nullElement: HTMLElement | null = null;
  detachedElement = document.createElement('div');

  constructor() {
    this.detachedElement.textContent = 'Detached';
    // Element not in DOM yet
  }
}
```

{% endcode %}

## Advanced Example: Dynamic Content with Observer Updates

Here's an example showing how interpolation works with Aurelia's observer system to automatically update the view when data changes:

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  items = [];

  constructor() {
    this.items.push({ name: 'Item 1' }, { name: 'Item 2' });
  }

  addItem() {
    this.items.push({ name: `Item ${this.items.length + 1}` });
  }
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<ul>
  <li repeat.for="item of items">${item.name}</li>
</ul>
<button click.trigger="addItem()">Add Item</button>
```

{% endcode %}

The interpolation is automatically updated by Aurelia's array observer whenever items are added to the collection.


# Template promises

Aurelia 2 significantly simplifies the handling of Promises directly within your templates. Unlike previous versions where promise resolution typically occurred in the view model, Aurelia 2 empowers you to manage asynchronous operations directly in the view.

This is accomplished through the `promise.bind` template controller. It intelligently manages the different states of a Promise: `pending`, `resolved` (`then`), and `rejected` (`catch`). This approach reduces boilerplate code and makes asynchronous data handling in templates more declarative and intuitive.

{% hint style="info" %}
You may also see `promise.resolve="..."` in older examples. It’s an alias for `promise.bind="..."`.
{% endhint %}

## Basic Usage

The `promise.bind` attribute allows you to bind a Promise to a template, rendering different content based on the Promise's current state.

```html
<div promise.bind="myPromise">
  <template pending>Loading data...</template>
  <template then="data">Data loaded: ${data}</template>
  <template catch="error">Error: ${error.message}</template>
</div>
```

In this example:

* **`promise.bind="myPromise"`**: Binds the `div` to the Promise named `myPromise` in your view model.
* **`<template pending>`**: Content rendered while `myPromise` is in the *pending* state (still resolving).
* **`<template then="data">`**: Content rendered when `myPromise` *resolves* successfully. The resolved value is available as `data` within this template.
* **`<template catch="error">`**: Content rendered if `myPromise` *rejects*. The rejection reason (typically an Error object) is available as `error`.

### Simple Example with Different Promise States

Let's illustrate with a view model that manages different promise scenarios:

{% tabs %}
{% tab title="my-app.html" %}

```html
<div>
  <h3>Promise Example 1</h3>
  <div promise.bind="promise1">
    <template pending>Promise 1: Loading...</template>
    <template then="data">Promise 1: Resolved with: ${data}</template>
    <template catch="err">Promise 1: Rejected with error: ${err.message}</template>
  </div>
</div>

<div>
  <h3>Promise Example 2 (No data in 'then' state)</h3>
  <div promise.bind="promise2">
    <template pending>Promise 2: Waiting...</template>
    <template then>Promise 2: Successfully Resolved!</template>
    <template catch>Promise 2: An error occurred!</template>
  </div>
</div>
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  promise1: Promise<string>;
  promise2: Promise<void>;

  constructor() {
    this.promise1 = this.createDelayedPromise('Promise 1 Data', 2000, true); // Resolves after 2 seconds
    this.promise2 = this.createDelayedPromise(undefined, 3000, false); // Rejects after 3 seconds
  }

  createDelayedPromise(data: any, delay: number, shouldResolve: boolean): Promise<any> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (shouldResolve) {
          resolve(data);
        } else {
          reject(new Error('Promise rejected after delay'));
        }
      }, delay);
    });
  }
}
```

{% endtab %}
{% endtabs %}

In this example, `promise1` is set to resolve after 2 seconds, and `promise2` is set to reject after 3 seconds. The template dynamically updates to reflect each promise's state. Notice in `promise2`'s `then` template, we don't specify a variable, indicating we only care about the resolved state, not the resolved value itself.

## Testing and Lifecycle Timing

`Aurelia.start()` completes independently of Promises bound in templates, so a source may still be pending when startup finishes. Aurelia renders the `pending` branch during startup. After the source settles, Aurelia schedules the switch to the `then` or `catch` branch.

In tests, settle the source and then await Aurelia's task queue before asserting the selected branch:

```typescript
import { tasksSettled } from '@aurelia/runtime';

request.resolve(result);
await tasksSettled();
```

Promise branches participate in the lifecycle of their owning view. When that view is removed, Aurelia waits for branch activation or removal already underway before finishing teardown.

## Promise Binding with Functions and Parameters

You can directly bind a function call to `promise.bind`. Aurelia is smart enough to re-invoke the function only when its parameters change, treating function calls in templates as pure operations.

The following example fetches a random advice slip from an API each time a button is clicked:

{% tabs %}
{% tab title="my-app.html" %}

```html
<let adviceIndex.bind="0"></let>

<div promise.bind="fetchAdvice(adviceIndex)">
  <span pending>Fetching advice...</span>
  <span then="adviceData">
    Advice ID: ${adviceData.slip.id}<br>
    "${adviceData.slip.advice}"
    <button click.trigger="adviceIndex = adviceIndex + 1">Get New Advice</button>
  </span>
  <span catch="fetchError">
    Failed to get advice. Error: ${fetchError}
    <button click.trigger="adviceIndex = adviceIndex + 1">Try Again</button>
  </span>
</div>
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  adviceIndex = 0; // Initialize adviceIndex

  fetchAdvice(index: number): Promise<any> {
    // 'index' parameter ensures function re-execution on parameter change
    console.log(`Fetching advice, attempt: ${index}`);
    return fetch("https://api.adviceslip.com/advice", {
      cache: 'no-store' // Prevents caching for example clarity
    })
    .then(response => response.ok
      ? response.json()
      : Promise.reject(new Error(`HTTP error! status: ${response.status}`))
    )
    .catch(error => {
      console.error("Fetch error:", error);
      throw error; // Re-throw to be caught by the promise template
    });
  }
}
```

{% endtab %}
{% endtabs %}

**Key Points:**

* **`adviceIndex`**: This variable, initialized with `let adviceIndex.bind="0"`, acts as a parameter to `fetchAdvice`. Incrementing `adviceIndex` via the button click triggers Aurelia to re-evaluate `fetchAdvice(adviceIndex)`.
* **Function Re-execution**: Aurelia re-executes `fetchAdvice` only when `adviceIndex` changes, ensuring efficient handling of function-based promises.
* **Error Handling**: The `.catch` template gracefully handles fetch errors, providing user-friendly feedback and a "Try Again" button.

{% embed url="<https://stackblitz.com/edit/au2-promise-binding-using-functions-improved?ctl=1&embed=1&file=src/my-app.ts>" %}

## Isolated Promise Binding Scope

The `promise.bind` template controller creates its own isolated scope. This is crucial to prevent naming conflicts and unintended modification of the parent view model or scope.

```html
<div promise.bind="userPromise">
  <template then="userData">
    <user-profile user-data.bind="userData"></user-profile>
    <p>User ID within promise scope: ${userData.id}</p>
    <!-- Accessing parent scope (if needed, though generally discouraged) -->
    <!-- <p>Some parent property: ${$parent.someProperty}</p> -->
  </template>
  <template catch="userError">
    <error-display error-message.bind="userError.message"></error-display>
  </template>
</div>
```

In this example:

* **`userData` and `userError`**: These variables are scoped *only* within the `promise.bind` context. They do not pollute the parent view model scope.
* **Component Communication**: To pass data to child components (like `<user-profile>`), use property binding (e.g., `user-data.bind="userData"`).
* **Parent Scope Access (Discouraged)**: While you *can* access the parent scope using `$parent`, it's generally better to manage data flow through explicit bindings and avoid relying on parent scope access for maintainability.

## Nested Promise Bindings

Aurelia 2 supports nesting `promise.bind` controllers to handle scenarios where one asynchronous operation depends on the result of another.

```html
<div promise.bind="initialFetchPromise">
  <template pending>Fetching initial data...</template>
  <template then="initialResponse" promise.bind="initialResponse.json()">
    <template then="jsonData">
      Data received and deserialized: ${jsonData.name}
    </template>
    <template catch="jsonError">
      Error deserializing JSON: ${jsonError.message}
    </template>
  </template>
  <template catch="fetchError">
    Error fetching initial data: ${fetchError.message}
  </template>
</div>
```

**Flow of Execution:**

1. **`initialFetchPromise`**: The outer `promise.bind` starts with `initialFetchPromise`.
2. **Pending State**: While `initialFetchPromise` is pending, "Fetching initial data..." is displayed.
3. **First `then` (Response)**: When `initialFetchPromise` resolves, the resolved value (`initialResponse`) becomes available in the `then` template.
4. **Nested `promise.bind` (JSON Deserialization)**: Inside the first `then` template, a *nested* `promise.bind` is used: `promise.bind="initialResponse.json()"`. This starts a *new* promise based on deserializing the `initialResponse`.
5. **Nested `then` (JSON Data)**: When `initialResponse.json()` resolves, the parsed JSON data (`jsonData`) is available in *this* `then` template. "Data received and deserialized: ${jsonData.name}" is displayed.
6. **Nested `catch` (JSON Error)**: If `initialResponse.json()` fails (e.g., invalid JSON), the nested `catch` template handles the error.
7. **Outer `catch` (Fetch Error)**: If `initialFetchPromise` initially rejects, the outer `catch` template handles the initial fetch error.

## Promise Bindings in `repeat.for` Loops

When using `promise.bind` within a `repeat.for` loop, it's crucial to manage scope correctly, especially if you need to access data from each promise iteration. Using `let` bindings within the `<template promise.bind="...">` is highly recommended to create proper scoping for each iteration.

```html
<let promiseItems.bind="[[42, true], ['error-string', false], ['success-string', true]]"></let>
<ul>
  <template repeat.for="item of promiseItems">
    <li>
      <template promise.bind="createPromise(item[0], item[1])">
        <let itemData.bind="null"></let> <let itemError.bind="null"></let>
        <span pending>Processing item...</span>
        <span then="itemData">Item processed successfully: ${itemData}</span>
        <span catch="itemError">Item processing failed: ${itemError.message}</span>
      </template>
    </li>
  </template>
</ul>
```

```typescript
export class MyApp {
  promiseItems: any[][]; // Defined in HTML using <let>

  createPromise(value: any, shouldResolve: boolean): Promise<any> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (shouldResolve) {
          resolve(value);
        } else {
          reject(new Error(`Promise rejected for value: ${value}`));
        }
      }, 1000); // Simulate async processing
    });
  }
}
```

**Importance of `<let>` Bindings:**

* **Scoped Context**: The lines `<let itemData.bind="null"></let>` and `<let itemError.bind="null"></let>` inside the `promise.bind` template are *essential*. They create `itemData` and `itemError` properties in the *overriding context* of each `promise.bind` iteration.
* **Preventing Overwriting**: Without these `let` bindings, `itemData` and `itemError` would be created in the *binding context*, which is shared across all iterations of the `repeat.for` loop. This would lead to data from later iterations overwriting data from earlier ones, resulting in incorrect or unpredictable behavior.
* **Correct Output**: With `let` bindings, each iteration of the `repeat.for` loop gets its own isolated scope for `itemData` and `itemError`, ensuring correct rendering for each promise in the list.


# Template references

Template references in Aurelia 2 offer a powerful and declarative mechanism to establish direct links between elements in your HTML templates and properties in your JavaScript or TypeScript view models. Using the `ref` attribute, you can easily obtain references to specific DOM elements, custom element instances, custom attribute instances, or even Aurelia controllers, enabling efficient DOM manipulation and streamlined interaction with template elements.

## Declaring Template References

### Basic Usage: Referencing DOM Elements

To create a template reference to a standard HTML element, simply add the `ref` attribute to the element within your template. The value assigned to `ref` will be the name of the property in your view model that will hold the reference.

```html
<input type="text" ref="firstNameInput" placeholder="First name">
```

In this basic example, `firstNameInput` is declared as a template reference. Aurelia will automatically populate a property in your view model with the same name, making the `<input>` element directly accessible.

### Accessing References in Templates

Template references become immediately available for use within the template itself. You can directly access properties and methods of the referenced element using the reference name.

For example, to dynamically display the current value of the `firstNameInput` field:

```html
<p>You are typing: "${firstNameInput.value}"</p>
```

As the user types in the input field, the `<p>` element will update in real-time, displaying the current value accessed through `firstNameInput.value`.

### Accessing References in View Models

To access a template reference in your view model, you need to declare a property in your view model class that matches the reference name you used in the template. For TypeScript projects, it's strongly recommended to explicitly type this property for enhanced type safety and code maintainability.

```typescript
import { customElement } from 'aurelia';

@customElement({ name: 'my-app', template: `<input type="text" ref="firstNameInput" placeholder="First name">` })
export class MyApp {
  firstNameInput: HTMLInputElement; // Explicitly typed template reference

  bound() {
    // 'firstNameInput' is now available after the view is bound
    console.log('Input element reference:', this.firstNameInput);
  }

  focusInput() {
    if (this.firstNameInput) {
      this.firstNameInput.focus(); // Programmatically focus the input element
    }
  }
}
```

**Important Notes:**

* **Property Naming**: The property name in your view model *must* exactly match the value of the `ref` attribute in your template (`firstNameInput` in the example above).
* **Type Safety**: In TypeScript, always declare the type of your template reference property (e.g., `HTMLInputElement`, `HTMLDivElement`, `MyCustomElement`). This improves code readability and helps catch type-related errors early.
* **Lifecycle Timing**: Template references are *not* available during the view model's constructor. They become available after the view is bound to the view model, typically in lifecycle hooks like `bound()` or later.

## Advanced Usage: Referencing Components and Controllers

Aurelia's `ref` attribute extends beyond simple DOM elements. It provides powerful options to reference component instances and controllers of custom elements and attributes.

### 1. `component.ref`: Referencing Custom Element Instances (View Models)

To obtain a reference to the *view model instance* of a custom element, use `component.ref="expression"`. This was previously known as `view-model.ref` in Aurelia v1.

```html
<my-custom-element component.ref="customElementViewModel"></my-custom-element>
```

In your view model:

```typescript
import { customElement } from 'aurelia';
import { MyCustomElement } from './my-custom-element'; // Assuming MyCustomElement is defined elsewhere

@customElement({ name: 'app', template: `<my-custom-element component.ref="customElementViewModel"></my-custom-element>` })
export class App {
  customElementViewModel: MyCustomElement; // Typed reference to the custom element's view model

  interactWithCustomElement() {
    if (this.customElementViewModel) {
      this.customElementViewModel.someMethodOnViewModel(); // Call a method on the custom element's view model
    }
  }
}
```

`component.ref` is invaluable when you need to directly interact with the logic and data encapsulated within a custom element's view model from a parent component.

### 2. `custom-attribute.ref`: Referencing Custom Attribute Instances (View Models)

Similarly, to reference the *view model instance* of a custom attribute applied to an element, use `custom-attribute.ref="expression"`.

```html
<div my-custom-attribute custom-attribute.ref="customAttributeViewModel"></div>
```

In your view model:

```typescript
import { customElement } from 'aurelia';
import { MyCustomAttribute } from './my-custom-attribute'; // Assuming MyCustomAttribute is defined elsewhere

@customElement({ name: 'app', template: `<div my-custom-attribute custom-attribute.ref="customAttributeViewModel"></div>` })
export class App {
  customAttributeViewModel: MyCustomAttribute; // Typed reference to the custom attribute's view model

  useCustomAttribute() {
    if (this.customAttributeViewModel) {
      this.customAttributeViewModel.doSomethingWithAttribute(); // Call a method on the custom attribute's view model
    }
  }
}
```

`custom-attribute.ref` is useful when you need to interact with the behavior or state managed by a custom attribute from the surrounding view model.

### 3. `controller.ref`: Referencing Aurelia Controller Instances (Advanced)

For more advanced scenarios, `controller.ref="expression"` allows you to access the *Aurelia Controller instance* of a custom element. The Controller provides access to Aurelia's internal workings and lifecycle management for the element. This is less commonly needed but can be powerful for framework-level integrations or very specific use cases.

```html
<my-custom-element controller.ref="customElementController"></my-custom-element>
```

In your view model:

```typescript
import { customElement, Controller } from 'aurelia';

@customElement({ name: 'app', template: `<my-custom-element controller.ref="customElementController"></my-custom-element>` })
export class App {
  customElementController: Controller; // Typed reference to the custom element's Controller

  accessControllerDetails() {
    if (this.customElementController) {
      console.log('Custom Element Controller:', this.customElementController);
      // You can access lifecycle state, bindings, etc. through the controller
    }
  }
}
```

`controller.ref` provides access to the Aurelia Controller, which is an advanced API and typically used for framework extension or very specific control over component lifecycle and binding. For most application development, `component.ref` or direct DOM element references are sufficient.

## Practical Applications and Benefits

Template references significantly enhance Aurelia development by providing a clean, framework-integrated way to interact with elements and components. They offer several key advantages:

* **Direct DOM Manipulation**: Template references provide a structured and type-safe way to obtain direct references to DOM elements, which is essential for tasks like:
  * Focusing input fields programmatically (`elementRef.focus()`).
  * Imperative DOM manipulation when integrating with third-party libraries that require direct element access (e.g., initializing jQuery plugins, interacting with canvas elements, etc.).
  * Fine-grained control over element properties and attributes.
* **Component Interaction**: `component.ref` and `custom-attribute.ref` enable seamless communication and interaction between parent components and their children (custom elements and attributes). This allows for:
  * Calling methods on child component view models.
  * Accessing data and state within child components.
  * Building more complex and encapsulated component structures.
* **Simplified DOM Access**: Template references eliminate the need for manual DOM queries using `document.querySelector` or similar methods within your view models. This leads to:
  * Cleaner and more readable view model code.
  * Reduced risk of brittle selectors that break if the template structure changes.
  * Improved maintainability and refactoring capabilities.
* **Integration with Third-Party Libraries**: Many JavaScript libraries require direct DOM element references for initialization or interaction. Template references provide the ideal mechanism to obtain these references within an Aurelia application without resorting to less maintainable DOM query approaches.

{% hint style="info" %}
By using template references, you move away from string-based DOM queries and embrace a more declarative and type-safe approach to DOM and component interaction within your Aurelia applications. This leads to more robust, maintainable, and efficient code, especially when dealing with complex UI interactions or integrations with external libraries.
{% endhint %}


# Template variables

Aurelia 2 allows you to manage variables directly within your view templates: the `<let>` custom element. This element allows you to declare and initialize variables inline in your HTML, making your templates more dynamic and readable. `<let>` is incredibly versatile, supporting a range of value assignments, from simple strings and interpolation to complex expressions and bindings to your view model. This capability significantly enhances template flexibility and reduces the need for excessive view model code for simple template-specific logic.

## Declaring Template Variables with `<let>`

The `<let>` element provides a straightforward syntax for declaring variables directly within your templates. The basic structure is as follows:

```html
<let variable-name="variable value"></let>
```

* `<let>`: The custom element tag that signals the declaration of a template variable.
* `variable-name`: The name you choose for your template variable. In templates, you will reference this variable name in its camelCase form (e.g., `variableName`).
* `"variable value"`: The initial value assigned to the variable. This can be a string literal, an interpolation expression, a binding expression, or any valid JavaScript expression that Aurelia can evaluate within the template context.

### Basic String Assignment

You can assign simple string literals to `<let>` variables:

```html
<let greeting-message="Hello, Aurelia!"></let>
```

To display the value of this variable in your template, use interpolation with the camelCase version of the variable name:

```html
<p>${greetingMessage}</p>
```

This will render:

```html
<p>Hello, Aurelia!</p>
```

### Binding Expressions for Dynamic Values

`<let>` variables are not limited to static strings. You can use binding expressions to assign dynamic values that are calculated or updated based on your view model or other template logic.

**Example: Simple Mathematical Expression**

```html
<let calculation-result.bind="10 + 5 * 2"></let>
```

Now, you can display the result of this calculation:

```html
<p>The result is: ${calculationResult}</p>
```

This will output:

```html
<p>The result is: 20</p>
```

**Example: Binding to View Model Properties**

You can bind a `<let>` variable to properties defined in your view model, making template variables reactive to changes in your data:

{% tabs %}
{% tab title="my-app.html" %}

```html
<let user-name.bind="userName"></let>

<h1>Welcome, ${userName}!</h1>
<p>Your username variable (from &lt;let&gt;) is: ${userName}</p>
<p>Your username property (from view model) is: ${userName}</p>
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  userName = 'John Doe';
}
```

{% endtab %}
{% endtabs %}

In this example, both `${userName}` interpolations will display "John Doe". If you update the `userName` property in your view model, both interpolations will dynamically reflect the change.

**Example: Using Template Expressions**

`<let>` variables can also be assigned values derived from template expressions, including function calls, ternary operators, and more:

{% tabs %}
{% tab title="my-app.html" %}

```html
<let is-evening.bind="currentHour >= 18"></let>
<let time-of-day-message.bind="isEvening ? 'Good evening' : 'Good day'"></let>

<p>${timeOfDayMessage}, user!</p>
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  currentHour = new Date().getHours();
}
```

{% endtab %}
{% endtabs %}

Here, `isEvening` will be a boolean value based on the current hour, and `timeOfDayMessage` will be dynamically set to either "Good evening" or "Good day" based on the value of `isEvening`.

### Scoping of Template Variables

`<let>` variables are scoped to the template in which they are declared. This means a variable declared with `<let>` is only accessible within the template block where it's defined. This scoping helps prevent naming conflicts and keeps your templates organized and predictable.

**Example: Scoped Variables in `repeat.for`**

When using `<let>` within a `repeat.for` loop, each iteration of the loop will have its own instance of the `<let>` variable, ensuring that variables are correctly associated with each repeated item.

```html
<ul>
  <template repeat.for="item of items">
    <li>
      <let item-index.bind="$index"></let>
      Item ${itemIndex + 1}: ${item.name}
    </li>
  </template>
</ul>
```

In this example, `itemIndex` is scoped to each `<li>` element within the `repeat.for` loop, correctly displaying the index for each item in the list.

### Practical Use Cases for `<let>`

`<let>` is incredibly useful in various template scenarios. Here are a few common use cases:

#### 1. Simplifying Complex Expressions

When you have complex expressions that are used multiple times within a template, you can use `<let>` to assign the result of the expression to a variable, improving readability and maintainability.

**Before using `<let>`:**

```html
<p>Total price (excluding tax): $${quantity * price}</p>
<p>Tax amount (10%): $${(quantity * price) * 0.10}</p>
<p>Final price (including tax): $${(quantity * price) * 1.10}</p>
```

**After using `<let>`:**

```html
<let subtotal.bind="quantity * price"></let>
<p>Total price (excluding tax): $${subtotal}</p>
<p>Tax amount (10%): $${subtotal * 0.10}</p>
<p>Final price (including tax): $${subtotal * 1.10}</p>
```

Using `<let subtotal.bind="quantity * price">` makes the template cleaner and easier to understand, especially if the calculation is more complex.

#### 2. Conditional Rendering Logic

You can use `<let>` in conjunction with conditional attributes like `if.bind` or `else` to manage template variables based on conditions.

```html
<let show-details.bind="isDetailsVisible"></let>

<button click.trigger="isDetailsVisible = !isDetailsVisible">
  ${showDetails ? 'Hide Details' : 'Show Details'}
</button>

<div if.bind="showDetails">
  <!-- Details content here -->
  <p>Detailed information is displayed.</p>
</div>
```

Here, `showDetails` is used to control both the button text and the visibility of the details section, simplifying the conditional logic within the template.

#### 3. Data Transformation within Templates

You can perform simple data transformations directly within your templates using `<let>`, although for more complex transformations, value converters are generally recommended.

**Example: Formatting a Date**

```html
<let formatted-date.bind="new Date().toLocaleDateString()"></let>
<p>Today's date: ${formattedDate}</p>
```

This example formats the current date using `toLocaleDateString()` and stores it in `formattedDate` for display.

#### 4. Creating Reusable Template Snippets

While not its primary purpose, `<let>` can indirectly contribute to creating reusable template snippets by encapsulating logic and variables within a specific section of your template. Combined with custom elements or template parts, `<let>` helps in modularizing your view templates.

### Considerations when Using `<let>`

* **Keep it Simple**: While `<let>` is powerful, it's best used for template-specific variables and simple logic. For complex data manipulation or business logic, keep that in your view model.
* **Readability**: Use descriptive variable names for `<let>` to maintain template readability.
* **Scoping**: Be mindful of the scope of `<let>` variables. They are limited to the template in which they are declared.
* **Alternatives**: For complex data transformations or reusable formatting logic, consider using Aurelia's value converters, which are designed for these purposes and promote better separation of concerns.


# Globals

Learn how Aurelia 2 handles global variables in templates, the built-in list of accessible globals, and when to use them effectively.

By design, Aurelia templates limit direct access to global variables like `window` or `document` for security and maintainability reasons. However, Aurelia recognizes that some JavaScript globals are frequently needed—like `Math`, `JSON`, or `Array`—and therefore provides a **predefined list** of global objects that can be safely accessed in template expressions.

***

## Why Limit Global Access?

* **Security:** Restricting direct access to browser globals reduces the risk of accidental or malicious operations on sensitive objects.
* **Maintainability:** Encourages developers to keep logic in their view models, improving code clarity.
* **Performance:** Minimizes the amount of unnecessary logic in templates, preventing overuse of global operations in tight rendering loops.

Despite these constraints, Aurelia acknowledges the utility of common global constructors and functions. Below is the canonical list accessible within Aurelia 2 templates **without** additional configuration:

* Infinity
* NaN
* isFinite
* isNaN
* parseFloat
* parseInt
* decodeURI
* decodeURIComponent
* encodeURI
* encodeURIComponent
* Array
* BigInt
* Boolean
* Date
* Map
* Number
* Object
* RegExp
* Set
* String
* JSON
* Math
* Intl

***

## Example Usages of Built-In Globals

Below are illustrative examples showing how to use these built-in globals in Aurelia templates. The syntax is identical to standard JavaScript, but you simply call them within Aurelia’s binding expressions.

### 1. Working with `JSON`

Serialize an object for debugging or quick display:

```html
<pre>${JSON.stringify(user, null, 2)}</pre>
```

### 2. Mathematical Operations with Math

Perform simple or complex calculations:

```html
<p>The square root of 16 is: ${Math.sqrt(16)}</p>
```

### 3. Conditional Rendering with isNaN

Use global numeric checks to conditionally display elements:

```html
<input type="text" value.bind="value" />
<p if.bind="isNaN(value)">This is not a valid number!</p>
```

### 4. Regular Expressions with RegExp

Construct inline regular expressions for quick validation:

```html
<input value.bind="email" placeholder="Enter email" />
<p if.bind="new RegExp('^\\S+@\\S+\\.\\S+$').test(email)">
  Valid Email Address
</p>
```

### 5. Dynamic Property Access with Object

Use Object methods for reflection or retrieval:

```html
<p>Property Value: ${Object.getOwnPropertyDescriptor(user, selectedProp)?.value}</p>
```

### 6. Set Operations with Set

De-duplicate arrays or combine sets inline:

```html
<p>Unique Values: ${[...new Set(numbersArray)]}</p>
```

### 7. Encoding & Decoding URLs

Leverage encodeURI / decodeURI for safe link construction:

```html
<a href.bind="encodeURI(externalLink)">Visit External Site</a>
<p>Original URL: ${decodeURI(externalLink)}</p>
```

### 8. Number Formatting with Intl.NumberFormat

Localize numbers, currency, or dates easily:

```html
<p>Price: ${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price)}</p>
```

### 9. Complex Array Manipulations

Filter, map, and transform arrays:

```html
<p>Active Items: ${Array.from(dataSet).filter(i => i.active).map(i => i.name).join(', ')}</p>
```

***

## Best Practices and Considerations

1. Use Sparingly
   * Keep business logic in your view models, not in templates. Inline calls to complex global functions (e.g., JSON.stringify on large data) can degrade performance and reduce readability.
2. Security
   * Even though Aurelia limits global access, treat any data you process via global functions (e.g., decodeURI) with caution to prevent potential XSS attacks or other vulnerabilities.
3. Performance
   * Template expressions run on each re-render. If you repeatedly perform expensive operations (like JSON.stringify on large objects), consider handling them in the view model and binding to a computed property instead.
4. Reactivity
   * Accessing global objects doesn’t magically become reactive. If you want to update the UI when data changes, store and manipulate it in the view model, ensuring Aurelia’s change detection can pick it up.
5. Clarity and Testing
   * Test heavy logic in a view model or service, not in templates. This approach keeps your code testable with unit tests and fosters a separation of concerns.

By sticking to these guidelines, you can leverage Aurelia’s built-in global access without sacrificing maintainability or performance.


# with.bind (scope binding)

Change the binding context for a section of a template using Aurelia's built-in with template controller.

`with` is a **template controller** that creates a new binding scope for its content. It’s useful when you want to “zoom in” on an object so you don’t have to repeat a prefix like `user.` everywhere.

## Basic usage

```html
<template with.bind="user">
  <h2>${firstName} ${lastName}</h2>
  <p>${email}</p>
</template>
```

The inner template’s binding context becomes `user`, so `firstName` resolves as `user.firstName`, etc.

You can also put `with.bind` on a real element:

```html
<section with.bind="user">
  <h2>${firstName} ${lastName}</h2>
</section>
```

## “Shape” a small context object

Sometimes you don’t want to pass the whole object through — just a few values (or renamed values):

```html
<template with.bind="{ profile: user.profile, canEdit: permissions.admin }">
  <user-profile profile.bind="profile"></user-profile>
  <button disabled.bind="!canEdit">Edit</button>
</template>
```

## Updates and lifecycle

* `with` does **not** add/remove DOM like `if` or `repeat`.
* When the `with` value changes, Aurelia **re-binds** the existing view to the new scope (it does not recreate the view).

## Gotchas

* `with` expects an **object**. If your value can be `null`/`undefined`, guard it:

  ```html
  <template with.bind="user || {}">
    ${firstName}
  </template>
  ```
* Prefer `<let>` when you only need a couple of local aliases and don’t need to re-scope a whole block.


# Custom attributes

Learn how to build and enhance Aurelia 2 custom attributes, including advanced configuration, binding strategies, and accessing the host element.

Custom attributes in Aurelia empower you to extend and decorate standard HTML elements by embedding custom behavior and presentation logic. They allow you to wrap or integrate existing HTML plugins and libraries, or simply enhance your UI components with additional dynamic functionality. This guide provides a comprehensive overview—from basic usage to advanced techniques—to help you leverage custom attributes effectively in your Aurelia 2 projects.

***

## Table of Contents

1. [Introduction](#introduction)
2. [Creating a Basic Custom Attribute](#creating-a-basic-custom-attribute)
3. [Custom Attribute Definition Approaches](#custom-attribute-definition-approaches)
   * [Convention-Based Approach](#convention-based-approach)
   * [Decorator-Based Approach](#decorator-based-approach)
   * [Static Definition Approach](#static-definition-approach)
4. [Explicit Custom Attributes](#explicit-custom-attributes)
   * [Explicit Attribute Naming](#explicit-attribute-naming)
   * [Attribute Aliases](#attribute-aliases)
5. [Single Value Binding](#single-value-binding)
6. [Bindable Properties and Change Detection](#bindable-properties-and-change-detection)
   * [Binding Modes](#binding-modes)
   * [Default Property](#default-property)
   * [Bindable Interceptors](#bindable-interceptors)
   * [Custom Change Callbacks](#custom-change-callbacks)
7. [Options Binding for Multiple Properties](#options-binding-for-multiple-properties)
8. [Advanced Bindable Configuration](#advanced-bindable-configuration)
9. [Lifecycle Hooks](#lifecycle-hooks)
10. [Aggregated Change Callbacks](#aggregated-change-callbacks)
11. [Accessing the Host Element](#accessing-the-host-element)
12. [Finding Related Custom Attributes](#finding-related-custom-attributes)
13. [Template Controller Custom Attributes](#template-controller-custom-attributes)
14. [Advanced Configuration Options](#advanced-configuration-options)
15. [Definition Metadata Reference](#definition-metadata-reference)
16. [Watch Integration](#watch-integration)
17. [Integrating Third-Party Libraries](#integrating-third-party-libraries)
18. [Best Practices](#best-practices)

***

## Introduction

Custom attributes are one of the core building blocks in Aurelia 2. Similar to components, they encapsulate behavior and style, but are applied as attributes to existing DOM elements. This makes them especially useful for:

* Decorating elements with additional styling or behavior.
* Wrapping third-party libraries that expect to control their own DOM structure.
* Creating reusable logic that enhances multiple elements across your application.
* Creating template controllers that control the rendering of content.

***

## Creating a Basic Custom Attribute

At its simplest, a custom attribute is defined as a class that enhances an element. Consider this minimal example:

```typescript
export class CustomPropertyCustomAttribute {
  // Custom logic can be added here
}
```

When you apply a similar pattern using CustomElement instead, you are defining a component. Custom attributes are a more primitive (yet powerful) way to extend behavior without wrapping the entire element in a component.

### Example: Red Square Attribute

This custom attribute adds a fixed size and a red background to any element it is applied to:

```typescript
import { INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class RedSquareCustomAttribute {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    // Set fixed dimensions and a red background on initialization
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

**Usage in HTML:**

```html
<import from="./red-square"></import>

<div red-square></div>
```

The `<import>` tag ensures that Aurelia's dependency injection is aware of your custom attribute. When applied, the `<div>` will render with the specified styles.

***

## Custom Attribute Definition Approaches

Aurelia 2 provides multiple approaches for defining custom attributes. For most user scenarios, you'll use either the convention-based or decorator-based approach:

### Convention-Based Approach

Classes ending with `CustomAttribute` are automatically recognized as custom attributes:

```typescript
import { INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class RedSquareCustomAttribute {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

The attribute name is derived from the class name (`red-square` in this case).

### Decorator-Based Approach (Recommended)

Use the `@customAttribute` decorator for explicit control and better IDE support:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({ name: 'red-square' })
export class RedSquare {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

### Static Definition Approach (Framework Internal)

For completeness, the framework also supports defining attributes using a static `$au` property. This approach is primarily used by the framework itself to avoid conventions and decorators, but is available if needed:

```typescript
import { INode, type CustomAttributeStaticAuDefinition } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class RedSquare {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'red-square'
  };

  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

{% hint style="info" %}
**When to use each approach:**

* **Convention-based**: Quick prototyping, simple attributes where the class name matches desired attribute name
* **Decorator-based**: Production code, when you need explicit control over naming, aliases, or other configuration
* **Static definition**: Advanced scenarios, framework extensions, or when you need to avoid decorators for tooling reasons
  {% endhint %}

***

## Explicit Custom Attributes

To gain finer control over your attribute's name and configuration, Aurelia provides the `@customAttribute` decorator. This lets you explicitly define the attribute name and even set up aliases.

### Explicit Attribute Naming

By default, the class name might be used to infer the attribute name. However, you can explicitly set a custom name:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({ name: 'red-square' })
export class RedSquare {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

### Attribute Aliases

You can define one or more aliases for your custom attribute. This allows consumers of your attribute flexibility in naming:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({ name: 'red-square', aliases: ['redify', 'redbox'] })
export class RedSquare {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

Now the attribute can be used interchangeably using any of the registered names:

```html
<div red-square></div>
<div redify></div>
<div redbox></div>
```

***

## Single Value Binding

For simple cases, you might want to pass a single value to your custom attribute without explicitly declaring a bindable property. Aurelia will automatically populate the `value` property if a value is provided.

```typescript
import { INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class HighlightCustomAttribute {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;
  public value: string;

  constructor() {
    // Apply default highlighting style
    this.element.style.backgroundColor = 'yellow';
    this.element.style.padding = '2px 4px';
    this.element.style.borderRadius = '3px';
  }

  binding() {
    // Override default color if a specific color is provided
    if (this.value) {
      this.element.style.backgroundColor = this.value;
    }
  }
}
```

**Usage:**

```html
<import from="./highlight"></import>

<!-- Uses default yellow highlighting -->
<span highlight>Important text</span>

<!-- Uses custom color -->
<span highlight="lightblue">Custom highlighted text</span>
```

To further handle changes in the value over time, you can define the property as bindable:

```typescript
import { bindable, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class HighlightCustomAttribute {
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  @bindable() public value: string;

  constructor() {
    // Apply default highlighting style
    this.element.style.backgroundColor = 'yellow';
    this.element.style.padding = '2px 4px';
    this.element.style.borderRadius = '3px';
    this.element.style.transition = 'background-color 0.3s ease';
  }

  bound() {
    if (this.value) {
      this.element.style.backgroundColor = this.value;
    }
  }

  valueChanged(newValue: string, oldValue: string) {
    this.element.style.backgroundColor = newValue || 'yellow';
  }
}
```

**Usage with dynamic binding:**

```html
<import from="./highlight"></import>

<!-- Color changes reactively based on view model property -->
<span highlight.bind="selectedColor">Dynamic highlighting</span>
```

***

## Bindable Properties and Change Detection

Custom attributes often need to be configurable. Using the @bindable decorator, you can allow users to pass in parameters that change the behavior or style dynamically.

### Binding Modes

Bindable properties support different binding modes that determine how data flows:

```typescript
import { bindable, INode, BindingMode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class InputWrapperCustomAttribute {
  @bindable({ mode: BindingMode.twoWay }) public value: string = '';
  @bindable({ mode: BindingMode.toView }) public placeholder: string = '';
  @bindable({ mode: BindingMode.fromView }) public isValid: boolean = true;
  @bindable({ mode: BindingMode.oneTime }) public label: string = '';

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  // ... implementation
}
```

Available binding modes:

* `BindingMode.toView` (default): Data flows from view model to view
* `BindingMode.fromView`: Data flows from view to view model
* `BindingMode.twoWay`: Data flows both ways
* `BindingMode.oneTime`: Data is set once and never updated

### Default Property

You can specify which property receives the value when the attribute is used with shorthand syntax (without explicitly naming a property). Use the `defaultProperty` option on the `@customAttribute` decorator:

```typescript
import { bindable, customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({ name: 'color-square', defaultProperty: 'color' })
export class ColorSquareCustomAttribute {
  @bindable() public color: string = 'red';
  @bindable() public size: string = '100px';

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.applyStyles();
  }

  bound() {
    this.applyStyles();
  }

  colorChanged(newColor: string) {
    this.element.style.backgroundColor = newColor;
  }

  sizeChanged(newSize: string) {
    this.element.style.width = this.element.style.height = newSize;
  }

  private applyStyles() {
    this.element.style.width = this.element.style.height = this.size;
    this.element.style.backgroundColor = this.color;
  }
}
```

With a default property defined, you can bind directly:

```html
<import from="./color-square"></import>

<!-- Using a literal value -->
<div color-square="blue"></div>

<!-- Or binding the value dynamically -->
<div color-square.bind="myColour"></div>
```

### Bindable Interceptors and Type Coercion

You can intercept and transform values being set on bindable properties using the `set` option, or leverage Aurelia's built-in type coercion system. Coercion is **opt-in**: enable it at startup with `config.coercingOptions.enableCoercion = true`, and be sure each bindable exposes a runtime `type` (for example `type: Number`).

```typescript
import { bindable, INode, coercer } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class ValidatedInputCustomAttribute {
  // Custom value transformation
  @bindable({
    set: (value: string) => value?.trim().toLowerCase()
  }) public email: string = '';

  // Range clamping
  @bindable({
    set: (value: number) => Math.max(0, Math.min(100, value))
  }) public progress: number = 0;

  // Built-in type coercion (automatic number conversion)
  @bindable({ type: Number }) public count: number = 0;

  // Explicit coercion with nullable handling
  @bindable({ 
    type: Number, 
    nullable: false  // Won't coerce null/undefined to 0
  }) public price: number;

  // Custom coercer function
  @bindable({
    set: Boolean // Converts any value to boolean
  }) public isActive: boolean;

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;
}
```

**Built-in Type Coercers:**

* `Number`: Converts strings to numbers (`"123"` → `123`)
* `String`: Converts values to strings (`123` → `"123"`)
* `Boolean`: Converts values to booleans (`"true"` → `true`, `""` → `false`)
* `BigInt`: Converts to BigInt values
* Custom functions: Any function that accepts a value and returns a transformed value

**Advanced Coercion Example:**

```typescript
@customAttribute('typed-inputs')
export class TypedInputsCustomAttribute {
  // Date parsing coercion
  @bindable({
    set: (value: string | Date) => {
      if (typeof value === 'string') {
        const date = new Date(value);
        return isNaN(date.getTime()) ? null : date;
      }
      return value;
    }
  }) public startDate: Date | null;

  // Array coercion from comma-separated strings
  @bindable({
    set: (value: string | string[]) => {
      return typeof value === 'string' 
        ? value.split(',').map(s => s.trim())
        : value;
    }
  }) public tags: string[] = [];
}
```

### Custom Change Callbacks

You can specify custom callback names for change handlers:

```typescript
import { bindable } from '@aurelia/runtime-html';

export class DataVisualizationCustomAttribute {
  @bindable({ callback: 'onDataUpdate' }) public dataset: any[] = [];
  @bindable({ callback: 'onConfigChange' }) public config: any = {};

  onDataUpdate(newData: any[], oldData: any[]) {
    // Handle data changes
    this.redrawChart();
  }

  onConfigChange(newConfig: any, oldConfig: any) {
    // Handle configuration changes
    this.updateChartSettings();
  }
}
```

***

## Options Binding for Multiple Properties

When you have more than one bindable property, you can use options binding syntax to bind multiple properties at once. This powerful syntax supports complex expressions, binding behaviors, and value converters:

### Basic Options Binding

```html
<import from="./color-square"></import>

<!-- Basic property binding -->
<div color-square="color.bind: myColor; size.bind: mySize;"></div>

<!-- Mix of binding modes -->
<div advanced-input="
  value.two-way: inputValue; 
  placeholder.to-view: placeholderText; 
  maxLength.one-time: 50;
"></div>
```

### Advanced Options Binding Features

```html
<!-- Value converters and binding behaviors -->
<div chart-widget="
  data.bind: chartData | sortBy:'date' & debounce:500;
  config.bind: chartConfig;
  theme.bind: currentTheme;
"></div>

<!-- Complex expressions -->
<div validator="
  rules.bind: validationRules;
  isEnabled.bind: userRole === 'admin' || isOwner;
  onError.bind: errors => handleValidationErrors(errors);
"></div>

<!-- Object literals and arrays -->
<div data-table="
  columns.bind: [
    { field: 'name', title: 'Name' },
    { field: 'email', title: 'Email' }
  ];
  options.bind: { 
    pageSize: 10, 
    sortable: true,
    filterable: currentUser.isAdmin
  };
"></div>
```

### Escaping Special Characters

Use backslashes to escape colons in URLs or other values:

```html
<!-- Escape colons in URLs -->
<div url-handler="baseUrl: http\://example.com\:8080/api;"></div>

<!-- Alternative: use binding for complex values -->
<div url-handler="baseUrl.bind: apiBaseUrl;"></div>
```

### Disabling Multi-Binding Parsing

For attributes that need to handle complex strings without parsing:

```typescript
@customAttribute({
  name: 'sql-query',
  noMultiBindings: true  // Treats entire value as single string
})
export class SqlQueryCustomAttribute {
  public value: string; // Receives: "SELECT * FROM users WHERE role: 'admin'"
}
```

```html
<!-- This won't be parsed as bindings due to noMultiBindings: true -->
<div sql-query="SELECT * FROM users WHERE role: 'admin'"></div>
```

***

## Advanced Bindable Configuration

You can also define bindables in the static definition or decorator:

```typescript
import { customAttribute, INode, BindingMode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'advanced-input',
  bindables: {
    value: { mode: BindingMode.twoWay, primary: true },
    placeholder: { mode: BindingMode.toView },
    validation: { callback: 'validateInput' }
  }
})
export class AdvancedInputCustomAttribute {
  public value: string;
  public placeholder: string;
  public validation: any;

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  validateInput(newValidation: any, oldValidation: any) {
    // Handle validation changes
  }
}
```

Or using the static `$au` approach:

```typescript
import { INode, BindingMode, type CustomAttributeStaticAuDefinition } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class AdvancedInput {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'advanced-input',
    bindables: {
      value: { mode: BindingMode.twoWay, primary: true },
      placeholder: { mode: BindingMode.toView },
      validation: { callback: 'validateInput' }
    }
  };

  public value: string;
  public placeholder: string;
  public validation: any;

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  validateInput(newValidation: any, oldValidation: any) {
    // Handle validation changes
  }
}
```

***

## Lifecycle Hooks

Custom attributes support a comprehensive set of lifecycle hooks that allow you to run code at different stages of their existence:

* `created(controller)`: Called after the attribute instance is created
* `binding(initiator, parent)`: Called when data binding begins
* `bound(initiator, parent)`: Called after data binding is complete
* `attaching(initiator, parent)`: Called after the associated element enters the DOM
* `attached(initiator)`: Called after the attribute's own attaching work settles
* `detaching(initiator, parent)`: Called before the element is detached from the DOM
* `unbinding(initiator, parent)`: Called when data binding is being removed

### Example: Using Lifecycle Hooks

```typescript
import { bindable, INode, customAttribute, ICustomAttributeController, IHydratedController } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({ name: 'lifecycle-demo' })
export class LifecycleDemoCustomAttribute {
  @bindable() public value: string = '';

  private readonly element: HTMLElement = resolve(INode) as HTMLElement;

  created(controller: ICustomAttributeController) {
    // Called when the attribute instance is created
    console.log('Custom attribute created');
  }

  binding(initiator: IHydratedController, parent: IHydratedController) {
    // Called when binding begins - good for setup
    console.log('Starting to bind');
    this.applyInitialValue();
  }

  bound(initiator: IHydratedController, parent: IHydratedController) {
    // Called after binding is complete
    console.log('Binding complete');
  }

  attaching(initiator: IHydratedController, parent: IHydratedController) {
    // Called before DOM attachment
    console.log('About to attach to DOM');
  }

  attached(initiator: IHydratedController) {
    // Called after DOM attachment - good for DOM manipulation
    this.initializeThirdPartyLibrary();
  }

  valueChanged(newValue: string, oldValue: string) {
    // Called whenever the value changes
    this.updateDisplay();
  }

  detaching(initiator: IHydratedController, parent: IHydratedController) {
    // Called before DOM detachment - good for cleanup
    this.cleanupEventListeners();
  }

  unbinding(initiator: IHydratedController, parent: IHydratedController) {
    // Called when unbinding - good for final cleanup
    console.log('About to unbind');
    this.finalCleanup();
  }

  private applyInitialValue() {
    this.element.textContent = this.value;
  }

  private updateDisplay() {
    this.element.textContent = this.value;
  }

  private initializeThirdPartyLibrary() {
    // Initialize any third-party libraries that need DOM access
  }

  private cleanupEventListeners() {
    // Remove event listeners to prevent memory leaks
  }

  private finalCleanup() {
    // Final cleanup before the attribute is destroyed
  }
}
```

***

## Aggregated Change Callbacks

Custom attributes provide powerful batching capabilities for handling multiple property changes efficiently:

```typescript
import { bindable, customAttribute } from '@aurelia/runtime-html';

@customAttribute('batch-processor')
export class BatchProcessorCustomAttribute {
  @bindable() public prop1: string;
  @bindable() public prop2: number;
  @bindable() public prop3: boolean;

  // Called when any bindable property changes (batched until next microtask)
  // This is the most efficient way to handle multiple property changes
  propertiesChanged(changes: Record<string, { newValue: unknown; oldValue: unknown }>) {
    console.log('Properties changed:', changes);
    // Example output: { prop1: { newValue: 'new', oldValue: 'old' } }

    // Process all changes at once for better performance
    this.processBatchedChanges(changes);
  }

  // Called for every property change (immediate, not batched)
  // Note: Both propertiesChanged AND individual callbacks will fire
  propertyChanged(key: PropertyKey, newValue: unknown, oldValue: unknown) {
    console.log(`Property ${String(key)} changed from ${oldValue} to ${newValue}`);
  }

  // Individual property callbacks still work alongside aggregated callbacks
  prop1Changed(newValue: string, oldValue: string) {
    console.log('Prop1 individual callback');
  }

  private processBatchedChanges(changes: Record<string, any>) {
    // Efficiently handle multiple property changes
    // Example: Update a chart that depends on multiple data properties
    if ('prop1' in changes || 'prop2' in changes) {
      this.updateVisualization();
    }
  }
}
```

***

## Accessing the Host Element

A key aspect of custom attributes is that they work directly on DOM elements. To manipulate these elements (e.g., updating styles or initializing plugins), you need to access the host element. Aurelia provides a safe way to do this using dependency injection with `INode`.

```typescript
import { INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class RedSquareCustomAttribute {
  // Resolve the host element safely, even in Node.js environments
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    // Now you can modify the host element directly
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}
```

**Note:** While you can also use `resolve(Element)` or `resolve(HTMLElement)`, using `INode` is safer in environments where global DOM constructors might not be available (such as Node.js).

***

## Finding Related Custom Attributes

In complex UIs, you might have multiple custom attributes working together (for example, a dropdown with associated toggle buttons). Aurelia offers the `CustomAttribute.closest` function to traverse the DOM and locate a related custom attribute. This function can search by attribute name or by constructor.

### Example: Searching by Attribute Name

```html
<div foo="1">
  <center>
    <div foo="3">
      <div bar="2"></div>
    </div>
  </center>
</div>
```

```typescript
import { CustomAttribute, INode, customAttribute } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute('bar')
export class Bar {
  private readonly host: HTMLElement = resolve(INode) as HTMLElement;

  binding() {
    // Find the closest ancestor that has the 'foo' custom attribute
    const closestFoo = CustomAttribute.closest(this.host, 'foo');
    if (closestFoo) {
      console.log('Found foo attribute:', closestFoo.viewModel);
      // Access the attribute's value
      console.log('Foo value:', closestFoo.viewModel.value); 
    }
  }
}
```

### Example: Searching by Constructor (Type-Safe)

If you want to search based on the attribute's constructor (for stronger typing), you can do so:

```typescript
import { CustomAttribute, INode, customAttribute } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
import { Foo } from './foo';

@customAttribute('bar')
export class Bar {
  private readonly host: HTMLElement = resolve(INode) as HTMLElement;

  binding() {
    // Find the closest ancestor that is an instance of the Foo custom attribute
    const parentFoo = CustomAttribute.closest(this.host, Foo);
    if (parentFoo) {
      // parentFoo.viewModel is now strongly typed as Foo
      parentFoo.viewModel.someMethod();
      parentFoo.viewModel.someProperty = 'new value';
    }
  }
}
```

### Practical Use Case: Coordinated Form Validation

```typescript
@customAttribute('form-section')
export class FormSectionCustomAttribute {
  @bindable() public sectionName: string;
  @bindable() public isValid: boolean = true;

  validateSection(): boolean {
    // Section-specific validation logic
    return this.isValid;
  }
}

@customAttribute('form-field')
export class FormFieldCustomAttribute {
  @bindable() public fieldName: string;
  @bindable() public required: boolean = false;

  private readonly host = resolve(INode) as HTMLElement;

  validate(): boolean {
    // Find the parent form section
    const section = CustomAttribute.closest(this.host, FormSectionCustomAttribute);
    
    if (section) {
      console.log(`Validating field ${this.fieldName} in section ${section.viewModel.sectionName}`);
      
      // Coordinate with parent section validation
      const isValid = this.performFieldValidation();
      section.viewModel.isValid = section.viewModel.isValid && isValid;
      
      return isValid;
    }
    
    return this.performFieldValidation();
  }

  private performFieldValidation(): boolean {
    // Field-specific validation logic
    return true;
  }
}
```

**Usage:**

```html
<form>
  <div form-section="section-name: personal; is-valid.two-way: personalSectionValid">
    <input form-field="field-name: firstName; required: true" />
    <input form-field="field-name: lastName; required: true" />
  </div>
  
  <div form-section="section-name: contact; is-valid.two-way: contactSectionValid">
    <input form-field="field-name: email; required: true" />
  </div>
</form>
```

### Important Notes

* **DOM Traversal**: `closest()` walks up the DOM tree, checking each ancestor element
* **Multiple Matches**: Returns the **first** (closest) matching attribute found
* **Error Handling**: Throws an error if searching by constructor for a class without an attribute definition
* **Performance**: Efficient DOM traversal, but cache results if called frequently
* **Type Safety**: Constructor-based searches provide better TypeScript support

***

## Template Controller Custom Attributes

Custom attributes can also function as template controllers, which control the rendering of content. Template controllers are similar to built-in directives like `if.bind` and `repeat.for`.

### Creating a Template Controller

```typescript
import { templateController, IViewFactory, ISyntheticView, IRenderLocation, bindable, ICustomAttributeController } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@templateController('permission')
export class PermissionTemplateController {
  @bindable() public userRole: string;
  @bindable() public requiredRole: string;

  public readonly $controller!: ICustomAttributeController<this>;

  private view: ISyntheticView;
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);

  bound() {
    this.updateView();
  }

  userRoleChanged() {
    if (this.$controller.isActive) {
      this.updateView();
    }
  }

  requiredRoleChanged() {
    if (this.$controller.isActive) {
      this.updateView();
    }
  }

  private updateView() {
    const hasPermission = this.userRole === this.requiredRole;

    if (hasPermission) {
      if (!this.view) {
        this.view = this.factory.create().setLocation(this.location);
      }
      if (!this.view.isActive) {
        this.view.activate(this.view, this.$controller, this.$controller.scope);
      }
    } else {
      if (this.view?.isActive) {
        this.view.deactivate(this.view, this.$controller);
      }
    }
  }

  unbinding() {
    if (this.view?.isActive) {
      this.view.deactivate(this.view, this.$controller);
    }
  }
}
```

Usage:

```html
<div permission="user-role.bind: currentUser.role; required-role: admin">
  <h2>Admin Panel</h2>
  <p>Only admins can see this content</p>
</div>
```

You can also use the static definition approach:

```typescript
import { IViewFactory, ISyntheticView, IRenderLocation, type CustomAttributeStaticAuDefinition } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class PermissionTemplateController {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'permission',
    isTemplateController: true,
    bindables: ['userRole', 'requiredRole']
  };

  // ... implementation same as above
}
```

***

## Advanced Configuration Options

Custom attributes support several advanced configuration options:

### No Multi-Bindings

By default, custom attributes support multiple bindings (`attr="prop1: value1; prop2: value2"`). You can disable this:

```typescript
import { customAttribute } from '@aurelia/runtime-html';

@customAttribute({
  name: 'simple-url',
  noMultiBindings: true
})
export class SimpleUrlCustomAttribute {
  public value: string; // Will receive the entire attribute value as a string
}
```

```html
<!-- With noMultiBindings: true, this won't be parsed as bindings -->
<a simple-url="https://example.com:8080/path">Link</a>
```

### Dependencies

You can specify dependencies that should be registered when the attribute is used:

```typescript
import { customAttribute } from '@aurelia/runtime-html';
import { SomeService } from './some-service';

@customAttribute({
  name: 'dependent-attr',
  dependencies: [SomeService]
})
export class DependentAttributeCustomAttribute {
  // SomeService will be registered when this attribute is used
}
```

### Container Strategy (Template Controllers Only)

For template controller custom attributes, you can specify the container strategy to control service isolation:

```typescript
import { templateController, IViewFactory, bindable } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@templateController({
  name: 'isolated-scope',
  containerStrategy: 'new' // Creates a new container for child views
})
export class IsolatedScopeTemplateController {
  @bindable() public isolatedServices: boolean = true;
  
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);
  
  bound() {
    // Views created by this template controller will have their own container
    // allowing for isolated service instances
    const view = this.factory.create().setLocation(this.location);
    // Services registered in child views won't interfere with parent
  }
}

@templateController({
  name: 'shared-scope',
  containerStrategy: 'reuse' // Reuses parent container (default)
})
export class SharedScopeTemplateController {
  // Child views share the same container as the parent
  // More efficient but services are shared
}
```

**Container Strategy Options:**

* **`'reuse'` (default)**: Child views share the parent's container
  * More memory efficient
  * Services are singleton across parent and child views
  * Faster view creation
* **`'new'`**: Creates a new container for child views
  * Provides service isolation
  * Each child view gets its own service instances
  * Useful for plugin systems or complex nested scenarios

**When to Use Container Isolation:**

```typescript
// Good candidate for 'new' container strategy
@templateController('plugin-host')
export class PluginHostTemplateController {
  @bindable() public pluginConfig: PluginConfiguration;
  
  // Each plugin needs isolated services to prevent conflicts
  // Plugin A's HttpClient shouldn't interfere with Plugin B's
}

// Good candidate for 'reuse' strategy (default)
@templateController('simple-conditional')  
export class SimpleConditionalTemplateController {
  @bindable() public condition: boolean;

  // Simple conditional rendering doesn't need service isolation
  // Sharing parent container is more efficient
}
```

## Definition Metadata Reference

Decorators and conventions eventually funnel into a `PartialCustomAttributeDefinition`, defined in `@aurelia/runtime-html`. Knowing every field on that definition unlocks advanced behaviors without sprinkling ad-hoc logic through your class. The table below summarizes the metadata you can provide (either via decorators or by calling `CustomAttribute.define()`/`CustomAttributeDefinition.create()` yourself):

| Property               | Type                                                 | Description                                                                                                                         |
| ---------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `name`                 | `string`                                             | The canonical attribute name. When omitted, Aurelia infers it from the class name.                                                  |
| `aliases`              | `string[]`                                           | Additional attribute names that should map to the same implementation. Use when you need both `awesome-slider` and `awesomeSlider`. |
| `bindables`            | `Record<string, PartialBindableDefinition>` or array | Declares bindable inputs. You can mix shorthand strings with full objects `{ name, mode, callback, attribute }`.                    |
| `defaultProperty`      | `string`                                             | The name of the property that receives the value when using shorthand syntax (e.g., `my-attr="value"`). Defaults to `'value'`.      |
| `isTemplateController` | `boolean`                                            | Marks the attribute as a template controller so Aurelia replaces the host element with the controller's view.                       |
| `noMultiBindings`      | `boolean`                                            | Treats the attribute value as a single literal string instead of `prop: value` pairs. Useful for URLs and DSL-like syntaxes.        |
| `watches`              | `IWatchDefinition[]`                                 | Registers `@watch` entries without decorators—great for framework-level attributes or generated code.                               |
| `dependencies`         | `Key[]`                                              | Additional registrations to install when the attribute definition is added to a container (for example, helper services).           |
| `containerStrategy`    | `'reuse' \| 'new'`                                   | Controls whether template controllers reuse the parent container or spin up an isolated child container.                            |

### Inspecting definitions at runtime

`CustomAttributeDefinition.getDefinition(MyAttribute)` returns the normalized definition object—including inferred defaults and decorator metadata. That makes it simple to write tooling or plugin code that reacts to attribute settings:

```typescript
import { CustomAttributeDefinition } from '@aurelia/runtime-html';

const def = CustomAttributeDefinition.getDefinition(PluginHostCustomAttribute);
if (def.containerStrategy === 'new') {
  console.debug('PluginHost will isolate services per instance.');
}
```

### Creating attributes without decorators

When you need to generate attributes dynamically (for example, inside a plugin) call `CustomAttribute.define` or `CustomAttributeDefinition.create` with a `PartialCustomAttributeDefinition`:

```typescript
import { BindingMode, CustomAttribute } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

const TrackingAttribute = CustomAttribute.define({
  name: 'tracking',
  bindables: {
    category: { mode: BindingMode.oneTime },
    data: { mode: BindingMode.twoWay }
  },
  watches: [
    { expression: 'data.total', callback: 'logChange' }
  ],
  dependencies: [AnalyticsService]
}, class {
  private readonly analytics = resolve(AnalyticsService);

  public logChange(newValue: unknown) {
    this.analytics.track(newValue);
  }
});

Aurelia.register(TrackingAttribute);
```

Because all of these options live on the definition, you keep your constructor and lifecycle hooks focused on runtime behavior while the metadata decides how the attribute integrates with the templating pipeline.

***

## Watch Integration

Custom attributes can integrate with Aurelia's `@watch` decorator for advanced property observation:

```typescript
import { bindable, customAttribute, watch } from '@aurelia/runtime-html';

@customAttribute('data-processor')
export class DataProcessorCustomAttribute {
  @bindable() public data: any[];
  @bindable() public config: any;

  @watch('data', { immediate: true })
  @watch('config')
  onDataOrConfigChange(newValue: any, oldValue: any, propertyName: string) {
    console.log(`${propertyName} changed from`, oldValue, 'to', newValue);
    this.reprocessData();
  }

  private reprocessData() {
    // Process data based on current data and config
  }
}
```

***

## Integrating Third-Party Libraries

Often, you'll want to incorporate functionality from third-party libraries—such as sliders, date pickers, or custom UI components—into your Aurelia applications. Custom attributes provide an excellent way to encapsulate the integration logic, ensuring that the third-party library initializes, updates, and cleans up properly within Aurelia's lifecycle.

### When to Use Custom Attributes for Integration

* **DOM Manipulation:** Many libraries require direct access to the DOM element for initialization.
* **Lifecycle Management:** You can leverage Aurelia's lifecycle hooks (`attached()` and `detaching()`) to manage resource allocation and cleanup.
* **Dynamic Updates:** With bindable properties, you can pass configuration options to the library and update it reactively when those options change.

### Example: Integrating a Hypothetical Slider Library

Consider a third-party slider library called `AwesomeSlider` that initializes a slider on a given DOM element. Below is an example of how to wrap it in a custom attribute.

```typescript
import { customAttribute, bindable, INode } from '@aurelia/runtime-html';
import { resolve, ILogger } from '@aurelia/kernel';
// Import the third-party slider library (this is a hypothetical example)
import AwesomeSlider from 'awesome-slider';

@customAttribute('awesome-slider')
export class AwesomeSliderCustomAttribute {
  // Allow dynamic options to be bound from the view
  @bindable() public options: any = {};

  // The instance of the third-party slider
  private sliderInstance: any;

  // Safely resolve the host element
  private readonly element: HTMLElement = resolve(INode) as HTMLElement;
  private readonly logger = resolve(ILogger);

  attached() {
    // Initialize the slider when the element is attached to the DOM.
    // This ensures that the DOM is ready for manipulation.
    try {
      this.sliderInstance = new AwesomeSlider(this.element, this.options);
    } catch (error) {
      this.logger.error('Failed to initialize AwesomeSlider:', error);
    }
  }

  optionsChanged(newOptions: any, oldOptions: any) {
    // Update the slider if its configuration changes at runtime.
    // This callback is triggered when the bound `options` property changes.
    if (this.sliderInstance && typeof this.sliderInstance.updateOptions === 'function') {
      this.sliderInstance.updateOptions(newOptions);
    }
  }

  detaching() {
    // Clean up the slider instance when the element is removed from the DOM.
    // This prevents memory leaks and removes event listeners.
    if (this.sliderInstance && typeof this.sliderInstance.destroy === 'function') {
      this.sliderInstance.destroy();
      this.sliderInstance = null;
    }
  }
}
```

In place of our hypothetical `AwesomeSlider` library, you can use any third-party library that requires DOM manipulation such as jQuery plugins, D3.js, or even custom UI components.

***

## Best Practices

### Separation of Concerns

Keep your custom attribute logic focused on enhancing the host element, and avoid heavy business logic. Custom attributes should be presentational or behavioral enhancements, not data processing units.

```typescript
// ✅ Good - focused on DOM enhancement
@customAttribute('tooltip')
export class TooltipCustomAttribute {
  @bindable() public text: string;
  // Implementation focused on showing/hiding tooltip
}

// ❌ Bad - mixing business logic
@customAttribute('tooltip')
export class TooltipCustomAttribute {
  @bindable() public userId: string;
  
  async fetchUserData() {
    // Don't do data fetching in custom attributes
    return await this.api.getUser(this.userId);
  }
}
```

### Performance

* **Minimize DOM manipulations**: Cache style properties and batch updates when possible
* **Use `propertiesChanged`**: For multiple property changes, batch updates to reduce DOM thrashing
* **Lifecycle hook timing**: Use appropriate hooks for initialization
  * `constructor()`: Basic setup, non-DOM operations
  * `attached()`: DOM-dependent initialization, third-party library setup
  * `detaching()`: Cleanup before DOM removal

```typescript
@customAttribute('performance-optimized')
export class PerformanceOptimizedCustomAttribute {
  @bindable() public width: string;
  @bindable() public height: string;
  @bindable() public color: string;

  // ✅ Batch multiple property changes
  propertiesChanged(changes: Record<string, any>) {
    const element = this.element;
    if ('width' in changes) element.style.width = changes.width.newValue;
    if ('height' in changes) element.style.height = changes.height.newValue;
    if ('color' in changes) element.style.backgroundColor = changes.color.newValue;
  }
}
```

### Memory Management

* **Clean up event listeners**: Always remove event listeners to prevent memory leaks
* **Dispose third-party instances**: Call proper cleanup methods for external libraries
* **Weak references**: Use WeakMap/WeakSet for object references when appropriate

```typescript
@customAttribute('event-handler')
export class EventHandlerCustomAttribute {
  private eventListener: EventListener;
  private thirdPartyInstance: any;

  attached() {
    this.eventListener = this.handleClick.bind(this);
    this.element.addEventListener('click', this.eventListener);
    
    this.thirdPartyInstance = new SomeLibrary(this.element);
  }

  detaching() {
    // ✅ Always clean up
    this.element.removeEventListener('click', this.eventListener);
    this.thirdPartyInstance?.destroy();
    this.thirdPartyInstance = null;
  }
}
```

### Error Handling

* **Graceful degradation**: Handle initialization failures gracefully
* **Validation**: Validate bindable property values
* **Logging**: Use Aurelia's logging system for debugging

```typescript
@customAttribute('robust-attribute')
export class RobustCustomAttribute {
  @bindable() public config: any;
  private readonly logger = resolve(ILogger);

  attached() {
    try {
      this.initializeFeature();
    } catch (error) {
      this.logger.error('Failed to initialize feature:', error);
      // Fallback behavior
      this.element.classList.add('feature-unavailable');
    }
  }

  configChanged(newConfig: any) {
    if (!this.isValidConfig(newConfig)) {
      this.logger.warn('Invalid configuration provided');
      return;
    }
    this.updateConfiguration(newConfig);
  }
}
```

### Testing

Write comprehensive unit tests covering lifecycle hooks, property changes, and edge cases:

```typescript
// Example test structure
describe('MyCustomAttribute', () => {
  it('should initialize correctly', () => { /* ... */ });
  it('should handle property changes', () => { /* ... */ });
  it('should clean up on detach', () => { /* ... */ });
  it('should handle invalid input gracefully', () => { /* ... */ });
});
```

### Documentation and Maintainability

* **Document public APIs**: Clearly document bindable properties and their expected types
* **Use meaningful names**: Choose descriptive names for attributes and properties
* **Provide usage examples**: Include HTML usage examples in comments
* **Type everything**: Use strong TypeScript typing for better IDE support

### Type Safety Best Practices

```typescript
// ✅ Strong typing with interfaces
interface ChartConfiguration {
  readonly type: 'line' | 'bar' | 'pie';
  readonly data: ChartData;
  readonly options?: ChartOptions;
}

@customAttribute('chart')
export class ChartCustomAttribute {
  @bindable() public config: ChartConfiguration;
  
  // ✅ Typed change handlers
  configChanged(newConfig: ChartConfiguration, oldConfig: ChartConfiguration) {
    // TypeScript will catch type errors
    if (newConfig.type !== oldConfig?.type) {
      this.recreateChart(newConfig);
    }
  }
}
```

```typescript
interface SliderOptions {
  min: number;
  max: number;
  step: number;
}

@customAttribute('typed-slider')
export class TypedSliderCustomAttribute {
  @bindable() public options: SliderOptions = { min: 0, max: 100, step: 1 };
  @bindable() public value: number = 0;

  optionsChanged(newOptions: SliderOptions, oldOptions: SliderOptions) {
    // Type-safe change handling
  }
}
```

## Advanced Features Summary

### Computed Bindables with Getters

Custom attributes support getter-based bindables for computed properties:

```typescript
@customAttribute('computed-display')
export class ComputedDisplayCustomAttribute {
  @bindable() public firstName: string = '';
  @bindable() public lastName: string = '';

  // Computed bindable using getter
  @bindable()
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`.trim();
  }

  // Optional setter for two-way binding
  set fullName(value: string) {
    const parts = value.split(' ');
    this.firstName = parts[0] || '';
    this.lastName = parts.slice(1).join(' ') || '';
  }

  fullNameChanged(newName: string) {
    // Responds to computed property changes
    this.updateDisplay(newName);
  }
}
```

### Bindable Inheritance

Bindable properties properly inherit from parent classes:

```typescript
@customAttribute('base-widget')
export class BaseWidgetCustomAttribute {
  @bindable() public theme: string = 'default';
  @bindable() public size: 'small' | 'medium' | 'large' = 'medium';
}

@customAttribute('advanced-widget')
export class AdvancedWidgetCustomAttribute extends BaseWidgetCustomAttribute {
  @bindable() public animation: boolean = true;
  @bindable() public tooltip: string = '';
  
  // Inherits theme and size bindables from parent class
  // Can override parent behavior if needed
  themeChanged(newTheme: string, oldTheme: string) {
    super.themeChanged?.(newTheme, oldTheme);
    this.applyAdvancedThemeFeatures(newTheme);
  }
}
```

### Error Handling and Lifecycle Management

```typescript
@customAttribute('robust-widget')
export class RobustWidgetCustomAttribute {
  private disposables: Array<() => void> = [];
  private readonly logger = resolve(ILogger);

  created(controller: ICustomAttributeController) {
    this.logger.debug('Widget attribute created', { controller });
  }

  attached() {
    try {
      this.initializeWidget();
    } catch (error) {
      this.logger.error('Widget initialization failed', error);
      this.fallbackToDefaultBehavior();
    }
  }

  detaching() {
    // Clean up all disposables
    this.disposables.forEach(dispose => {
      try {
        dispose();
      } catch (error) {
        this.logger.warn('Cleanup error', error);
      }
    });
    this.disposables.length = 0;
  }

  private addDisposable(dispose: () => void) {
    this.disposables.push(dispose);
  }
}
```


# focus custom attribute

Bind an element's focus state with Aurelia's built-in focus custom attribute.

`focus` is a built-in custom attribute that lets you bind an element’s focus state to a view-model property.

By default it’s **two-way**:

* setting the property focuses/blurs the element
* focusing/blurring the element updates the property

## Basic usage (two-way)

```html
<input focus.bind="isFocused">

<button click.trigger="isFocused = true">Focus the input</button>
<button click.trigger="isFocused = false">Blur the input</button>
```

```ts
export class MyPage {
  public isFocused = false;
}
```

## One-way focus (recommended for “open a thing, focus an input”)

Two-way focus can be surprising if you don’t want a `blur` to change your state. In that case, make it one-way:

```html
<input focus.to-view="shouldFocusSearch">
```

Now the input won’t write back to `shouldFocusSearch` when it blurs.

## Common pattern: focus when showing a panel

```html
<button click.trigger="isOpen = !isOpen">Toggle</button>

<div if.bind="isOpen">
  <input focus.to-view="isOpen" placeholder="Type to search...">
  <!-- Focuses when opened; does not auto-close on blur -->
</div>
```

## Notes

* The target must be focusable. For non-input elements, you may need `tabindex="0"`.
* Focusing can only happen once the element is connected to the document; `focus` handles this automatically (it will apply focus after attach if needed).


# Advanced custom attributes

Advanced patterns for building custom attributes in Aurelia 2, including template controllers, complex bindings, and performance optimization.

This guide covers advanced patterns for building custom attributes in Aurelia 2, focusing on template controllers, complex binding scenarios, and performance optimization techniques.

## Template Controllers

Template controllers are custom attributes that control the rendering of their associated template. They're the mechanism behind built-in attributes like `if`, `repeat`, `with`, and `switch`.

### Basic Template Controller Structure

All template controllers follow this pattern:

```typescript
import { customAttribute, ICustomAttributeController, IViewFactory, IRenderLocation, ISyntheticView } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'my-controller',
  isTemplateController: true,
  bindables: ['value']
})
export class MyController {
  public readonly $controller!: ICustomAttributeController<this>;
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);
  private view?: ISyntheticView;

  public value: unknown;

  public valueChanged(newValue: unknown): void {
    this.updateView(newValue);
  }

  private updateView(show: boolean): void {
    if (show && !this.view) {
      this.view = this.factory.create().setLocation(this.location);
      this.view.activate(this.view, this.$controller, this.$controller.scope);
    } else if (!show && this.view) {
      this.view.deactivate(this.view, this.$controller);
      this.view = undefined;
    }
  }

  public attaching(): void {
    if (this.value) {
      this.updateView(true);
    }
  }

  public detaching(): void {
    if (this.view) {
      this.view.deactivate(this.view, this.$controller);
      this.view = undefined;
    }
  }
}
```

Usage:

```html
<div my-controller.bind="condition">
  This content is conditionally rendered
</div>
```

### Real-World Example: Visibility Controller

A practical template controller that shows/hides content based on user permissions:

```typescript
import { customAttribute, ICustomAttributeController, IViewFactory, IRenderLocation, ISyntheticView } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

interface IPermissionService {
  hasPermission(permission: string): boolean;
  hasAnyPermission(permissions: string[]): boolean;
}

@customAttribute({
  name: 'show-if-permitted',
  isTemplateController: true,
  bindables: ['permission', 'anyOf']
})
export class ShowIfPermitted {
  public readonly $controller!: ICustomAttributeController<this>;
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);
  private readonly permissionService = resolve(IPermissionService);
  private view?: ISyntheticView;

  public permission?: string;
  public anyOf?: string[];

  public permissionChanged(): void {
    this.updateVisibility();
  }

  public anyOfChanged(): void {
    this.updateVisibility();
  }

  private updateVisibility(): void {
    const hasPermission = this.permission 
      ? this.permissionService.hasPermission(this.permission)
      : this.anyOf 
        ? this.permissionService.hasAnyPermission(this.anyOf)
        : false;

    if (hasPermission && !this.view) {
      this.view = this.factory.create().setLocation(this.location);
      this.view.activate(this.view, this.$controller, this.$controller.scope);
    } else if (!hasPermission && this.view) {
      this.view.deactivate(this.view, this.$controller);
      this.view = undefined;
    }
  }

  public attaching(): void {
    this.updateVisibility();
  }

  public detaching(): void {
    if (this.view) {
      this.view.deactivate(this.view, this.$controller);
      this.view = undefined;
    }
  }
}
```

Usage:

```html
<div show-if-permitted.bind="'admin'">
  Admin-only content
</div>

<div show-if-permitted any-of.bind="['user', 'moderator']">
  User or moderator content
</div>
```

### Advanced Template Controller: Loading States

A template controller that manages loading states with caching:

```typescript
import { customAttribute, ICustomAttributeController, IViewFactory, IRenderLocation, ISyntheticView } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'loading-state',
  isTemplateController: true,
  bindables: ['isLoading', 'cache']
})
export class LoadingState {
  public readonly $controller!: ICustomAttributeController<this>;
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);
  private view?: ISyntheticView;
  private cachedView?: ISyntheticView;

  public isLoading: boolean = false;
  public cache: boolean = true;

  public isLoadingChanged(newValue: boolean): void {
    this.updateView(newValue);
  }

  private updateView(isLoading: boolean): void {
    if (!isLoading && !this.view) {
      // Show content
      if (this.cache && this.cachedView) {
        this.view = this.cachedView;
      } else {
        this.view = this.factory.create().setLocation(this.location);
        if (this.cache) {
          this.cachedView = this.view;
        }
      }
      this.view.activate(this.view, this.$controller, this.$controller.scope);
    } else if (isLoading && this.view) {
      // Hide content
      this.view.deactivate(this.view, this.$controller);
      if (!this.cache) {
        this.view = undefined;
      } else {
        this.view = undefined; // Keep cached view
      }
    }
  }

  public attaching(): void {
    this.updateView(this.isLoading);
  }

  public detaching(): void {
    if (this.view) {
      this.view.deactivate(this.view, this.$controller);
      this.view = undefined;
    }
    if (this.cachedView) {
      this.cachedView = undefined;
    }
  }
}
```

Usage:

```html
<div loading-state.bind="isLoading" cache.bind="true">
  <p>This content is hidden while loading</p>
</div>
```

## Complex Two-Way Binding Attributes

### Bi-directional Data Synchronization

Create attributes that can both read and write data:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'auto-save',
  bindables: ['value', 'debounce']
})
export class AutoSave {
  private element = resolve(INode) as HTMLInputElement;
  private debounceTimer?: number;

  public value: string = '';
  public debounce: number = 500;

  public valueChanged(newValue: string): void {
    if (this.element.value !== newValue) {
      this.element.value = newValue;
    }
  }

  public attached(): void {
    this.element.addEventListener('input', this.handleInput);
    this.element.addEventListener('blur', this.handleBlur);
  }

  public detaching(): void {
    this.element.removeEventListener('input', this.handleInput);
    this.element.removeEventListener('blur', this.handleBlur);
    this.clearTimer();
  }

  private handleInput = (): void => {
    this.clearTimer();
    this.debounceTimer = window.setTimeout(() => {
      this.updateValue();
    }, this.debounce);
  };

  private handleBlur = (): void => {
    this.clearTimer();
    this.updateValue();
  };

  private updateValue(): void {
    const newValue = this.element.value;
    if (this.value !== newValue) {
      this.value = newValue;
    }
  }

  private clearTimer(): void {
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
      this.debounceTimer = undefined;
    }
  }
}
```

Usage:

```html
<input auto-save.bind="document.title" debounce.bind="1000">
```

### Multi-Value Binding

Handle multiple bindable properties with complex interactions:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'slider-range',
  bindables: ['min', 'max', 'step', 'value']
})
export class SliderRange {
  private element = resolve(INode) as HTMLInputElement;

  public min: number = 0;
  public max: number = 100;
  public step: number = 1;
  public value: number = 0;

  public minChanged(newValue: number): void {
    this.element.min = String(newValue);
    this.validateValue();
  }

  public maxChanged(newValue: number): void {
    this.element.max = String(newValue);
    this.validateValue();
  }

  public stepChanged(newValue: number): void {
    this.element.step = String(newValue);
  }

  public valueChanged(newValue: number): void {
    const validValue = this.clampValue(newValue);
    if (this.element.value !== String(validValue)) {
      this.element.value = String(validValue);
    }
  }

  public attached(): void {
    this.element.type = 'range';
    this.element.addEventListener('input', this.handleInput);
    this.element.addEventListener('change', this.handleChange);
    this.updateElement();
  }

  public detaching(): void {
    this.element.removeEventListener('input', this.handleInput);
    this.element.removeEventListener('change', this.handleChange);
  }

  private handleInput = (): void => {
    this.value = Number(this.element.value);
  };

  private handleChange = (): void => {
    this.value = Number(this.element.value);
  };

  private validateValue(): void {
    const clampedValue = this.clampValue(this.value);
    if (clampedValue !== this.value) {
      this.value = clampedValue;
    }
  }

  private clampValue(value: number): number {
    return Math.max(this.min, Math.min(this.max, value));
  }

  private updateElement(): void {
    this.element.min = String(this.min);
    this.element.max = String(this.max);
    this.element.step = String(this.step);
    this.element.value = String(this.clampValue(this.value));
  }
}
```

Usage:

```html
<input slider-range min.bind="0" max.bind="100" step.bind="5" value.bind="currentValue">
```

## Performance Optimization Patterns

### Lazy Initialization

Defer expensive operations until needed:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'lazy-load',
  bindables: ['src', 'placeholder']
})
export class LazyLoad {
  private element = resolve(INode) as HTMLImageElement;
  private observer?: IntersectionObserver;

  public src: string = '';
  public placeholder: string = '';

  public attached(): void {
    this.element.src = this.placeholder;
    this.setupIntersectionObserver();
  }

  public detaching(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = undefined;
    }
  }

  private setupIntersectionObserver(): void {
    if (!('IntersectionObserver' in window)) {
      // Fallback for older browsers
      this.loadImage();
      return;
    }

    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadImage();
            this.observer?.disconnect();
          }
        });
      },
      { threshold: 0.1 }
    );

    this.observer.observe(this.element);
  }

  private loadImage(): void {
    if (this.src) {
      this.element.src = this.src;
    }
  }
}
```

### Batch Updates

Minimize DOM operations by batching updates:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'batch-class',
  bindables: ['classes']
})
export class BatchClass {
  private element = resolve(INode) as HTMLElement;
  private scheduledUpdate = false;
  private appliedClasses = new Set<string>();

  public classes: Record<string, boolean> = {};

  public classesChanged(): void {
    this.scheduleUpdate();
  }

  private scheduleUpdate(): void {
    if (!this.scheduledUpdate) {
      this.scheduledUpdate = true;
      requestAnimationFrame(() => {
        this.updateClasses();
        this.scheduledUpdate = false;
      });
    }
  }

  private updateClasses(): void {
    const newClasses = new Set<string>();
    
    // Collect classes that should be applied
    for (const [className, shouldApply] of Object.entries(this.classes)) {
      if (shouldApply) {
        newClasses.add(className);
      }
    }

    // Remove classes that are no longer needed
    for (const className of this.appliedClasses) {
      if (!newClasses.has(className)) {
        this.element.classList.remove(className);
      }
    }

    // Add new classes
    for (const className of newClasses) {
      if (!this.appliedClasses.has(className)) {
        this.element.classList.add(className);
      }
    }

    this.appliedClasses = newClasses;
  }
}
```

## Error Handling in Custom Attributes

### Graceful Degradation

Handle errors gracefully without breaking the application:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve, ILogger } from '@aurelia/kernel';

@customAttribute({
  name: 'safe-transform',
  bindables: ['transform', 'fallback']
})
export class SafeTransform {
  private element = resolve(INode) as HTMLElement;
  private logger = resolve(ILogger);

  public transform: string = '';
  public fallback: string = '';

  public transformChanged(newValue: string): void {
    this.applyTransform(newValue);
  }

  private applyTransform(transform: string): void {
    try {
      this.element.style.transform = transform;
    } catch (error) {
      this.logger.warn(`Invalid transform "${transform}":`, error);
      this.element.style.transform = this.fallback;
    }
  }

  public attached(): void {
    this.applyTransform(this.transform);
  }
}
```

### Validation and Sanitization

Validate inputs before applying them:

```typescript
import { customAttribute, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

@customAttribute({
  name: 'safe-html',
  bindables: ['content', 'allowedTags']
})
export class SafeHtml {
  private element = resolve(INode) as HTMLElement;

  public content: string = '';
  public allowedTags: string[] = ['b', 'i', 'em', 'strong', 'p', 'br'];

  public contentChanged(newValue: string): void {
    this.updateContent(newValue);
  }

  private updateContent(content: string): void {
    const sanitized = this.sanitizeHtml(content);
    this.element.innerHTML = sanitized;
  }

  private sanitizeHtml(html: string): string {
    // Simple sanitization - in production, use a proper library like DOMPurify
    const div = document.createElement('div');
    div.innerHTML = html;

    // Remove all elements not in allowed tags
    const elements = div.querySelectorAll('*');
    for (let i = elements.length - 1; i >= 0; i--) {
      const element = elements[i];
      if (!this.allowedTags.includes(element.tagName.toLowerCase())) {
        element.remove();
      }
    }

    return div.innerHTML;
  }
}
```

## Testing Custom Attributes

### Unit Testing Template Controllers

```typescript
import { TestContext } from '@aurelia/testing';
import { MyController } from './my-controller';

describe('MyController', () => {
  let ctx: TestContext;

  beforeEach(() => {
    ctx = TestContext.create();
  });

  afterEach(() => {
    ctx.dispose();
  });

  it('should show content when value is true', async () => {
    const { component, startPromise, tearDown } = ctx.createFixture(
      `<div my-controller.bind="showContent">Content</div>`,
      class {
        showContent = true;
      }
    );

    await startPromise;

    expect(component.textContent).toContain('Content');

    await tearDown();
  });

  it('should hide content when value is false', async () => {
    const { component, startPromise, tearDown } = ctx.createFixture(
      `<div my-controller.bind="showContent">Content</div>`,
      class {
        showContent = false;
      }
    );

    await startPromise;

    expect(component.textContent).not.toContain('Content');

    await tearDown();
  });
});
```

## Best Practices

### 1. **Resource Management**

Always clean up resources in `detaching()`:

```typescript
public detaching(): void {
  if (this.observer) {
    this.observer.disconnect();
  }
  if (this.subscription) {
    this.subscription.dispose();
  }
}
```

### 2. **Performance Considerations**

* Use `requestAnimationFrame` for DOM updates
* Batch operations when possible
* Avoid frequent DOM queries

### 3. **Error Handling**

* Validate inputs before applying changes
* Provide fallback behaviors
* Log errors for debugging

### 4. **Type Safety**

* Use TypeScript interfaces for bindable properties
* Implement proper type guards for runtime validation

These patterns provide a solid foundation for building robust, performant custom attributes that integrate well with Aurelia's architecture while handling edge cases gracefully.


# Value converters (pipes)

Master Aurelia's value converters for powerful data transformation. Learn formatting, localization, custom converters, performance optimization, and real-world patterns.

Value converters are a powerful feature in Aurelia 2 that transform data as it flows between your view model and view. They enable clean separation of concerns by moving data formatting logic out of your view models while keeping templates readable and maintainable.

## Overview

Value converters excel at:

* **Data formatting** - dates, numbers, currencies, text transformations
* **Localization** - dynamic content based on user locale
* **Display logic** - conditional formatting without cluttering view models
* **Two-way transformations** - handling both display and input conversion
* **Reactive updates** - automatic re-evaluation on global state changes
* **Performance optimization** - caching expensive transformations

### Key Advantages

* **Pure functions** - predictable, testable transformations
* **Reusable** - use the same converter across multiple components
* **Composable** - chain multiple converters for complex transformations
* **Framework integration** - seamless integration with Aurelia's binding system
* **TypeScript support** - full type safety and intellisense

## Data Flow

Converters work in two directions:

* **toView**: Prepares model data for display.
* **fromView**: Adjusts view data before updating the model (useful with two-way binding).

Both methods receive the primary value as the first argument, with any extra arguments used as configuration.

### Example Methods

```typescript
// toView: from model to view
toView(value, ...args) { /* transform value for display */ }

// fromView: from view to model
fromView(value, ...args) { /* transform value for the model */ }
```

## Basic Usage

### Template Syntax

Use the pipe symbol (`|`) to apply a converter in templates:

```html
<!-- String interpolation -->
<h1>${userName | capitalize}</h1>
<p>${price | currency:'USD'}</p>

<!-- Property binding -->
<input value.bind="searchTerm | normalize">

<!-- Attribute binding -->
<div class.bind="status | statusClass">
```

### Simple Converter Example

```typescript
import { valueConverter } from 'aurelia';

@valueConverter('capitalize')
export class CapitalizeConverter {
  toView(value: string): string {
    if (!value) return '';
    return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
  }
}
```

Usage in template:

```html
<span>${'hello world' | capitalize}</span>
<!-- Output: "Hello world" -->
```

### Parameter Passing

Converters accept parameters using colons (`:`) for configuration:

#### Static Parameters

```html
<!-- Fixed locale -->
<span>${date | dateFormat:'en-GB'}</span>

<!-- Multiple parameters -->
<span>${price | currency:'EUR':'symbol':'1.2-2'}</span>
```

#### Bound Parameters

```html
<span>${date | dateFormat:userLocale}</span>
<span>${text | truncate:maxLength:appendEllipsis}</span>
```

```typescript
export class MyComponent {
  userLocale = 'fr-FR';
  maxLength = 50;
  appendEllipsis = true;
}
```

#### Object Parameters

```html
<div repeat.for="item of items | sort:sortConfig">
  ${item.name}
</div>
```

```typescript
export class MyComponent {
  sortConfig = {
    property: 'name',
    direction: 'asc',
    caseSensitive: false
  };
}
```

### Chaining Converters

Chain multiple converters for complex transformations:

```html
<!-- Apply multiple transformations in sequence -->
<span>${userInput | sanitize | capitalize | truncate:100}</span>

<!-- With parameters -->
<span>${rawText | normalize | highlight:searchTerm | capitalize}</span>
```

**Chain execution order**: Left to right, where each converter receives the output of the previous one.

### Advanced Template Patterns

#### Conditional Formatting

```html
<span class.bind="status | statusToClass">
  ${status | statusToDisplay}
</span>
```

#### Dynamic Parameter Selection

```html
<span>${date | dateFormat:(isDetailed ? 'long' : 'short')}</span>
```

#### Nested Object Access

```html
<span>${user.profile | formatProfile:user.preferences}</span>
```

## Receiving the Caller Context

By default, value converters receive only the value to transform and any configuration parameters. In some advanced scenarios, you may need to know more about the *binding* or *calling context* that invoked the converter—for example, to adjust the transformation based on the host element, attributes, or other binding-specific state.

Aurelia 2 provides an **opt-in** mechanism to receive the binding instance itself as an additional parameter. To enable this feature:

1. Add `withContext: true` to your value converter class:

   ```typescript
   import { valueConverter } from 'aurelia';

   @valueConverter({ name: 'myConverter' })
   export class MyConverter {
     public readonly withContext = true;

     public toView(value, caller, ...args) {
       // `caller` is an object with:
       // - `source`: The closest custom element view-model, if any.
       // - `binding`: The binding instance (e.g., PropertyBinding, InterpolationPartBinding).
       console.log('Converter called by binding:', caller.binding);
       console.log('Source/Component VM:', caller.source);

       // Use binding-specific state if needed, then return transformed value
       return /* your transformation logic */;
     }

     public fromView?(value, caller, ...args) {
       // For two-way binding scenarios, you can similarly access the caller properties
       return /* reverse transformation logic */;
     }
   }
   ```

Then use your converter in templates as usual:

```html
<import from="./my-converter"></import>
<p>${ someValue | myConverter }</p>
```

At runtime, Aurelia will detect `withContext: true` in the value converter and pass the binding instance as the second parameter. Depending on how the converter is used:

* **Property Binding** (`foo.bind` or `attr.bind`): the caller is a `PropertyBinding` instance
* **Interpolation** (`${ }` with converters): the caller is an `InterpolationPartBinding` instance
* **Other Bindings**: the caller corresponds to the specific binding type in use

### Common Use Cases

* Logging or debugging which binding invoked the converter
* Applying different formatting based on binding context
* Accessing binding metadata or context not available through standard converter parameters

Use this feature sparingly, only when you truly need insights into the calling context. For most formatting scenarios, simple converter parameters and camelCase converter names are sufficient.

### Accessing the View Model and Binding Context

Once `withContext: true` is enabled, your converter receives a `caller` parameter with direct access to the view model and binding information:

```typescript
import { valueConverter, type ICallerContext } from '@aurelia/runtime-html';

@valueConverter('vmAware')
export class ViewModelAwareConverter {
  readonly withContext = true;

  toView(value: unknown, caller: ICallerContext): string {
    // Direct access to the view model instance
    const viewModel = caller.source as MyComponent;
    
    // Access view model properties and methods
    if (viewModel.isAdmin) {
      return `Admin: ${value}`;
    }
    
    // Use view model data for transformation
    return `${value} (${viewModel.userName})`;
  }
}
```

#### Caller Context Properties

* **`caller.source`**: The **view model instance** of the component where the converter is used
  * This is the actual component class instance with all its properties and methods
  * Allows converters to access component state, computed properties, and methods
  * Always available when converter is used within a component
* **`caller.binding`**: The **binding instance** that invoked the converter
  * Contains binding-specific information and metadata
  * Useful for debugging or advanced binding manipulation
  * Type varies: `PropertyBinding`, `InterpolationPartBinding`, etc.

#### Real-World Example: User Permission Converter

```typescript
import { valueConverter, type ICallerContext } from '@aurelia/runtime-html';

interface UserComponent {
  currentUser: { role: string; permissions: string[] };
  isOwner(itemId: string): boolean;
}

@valueConverter('userPermission')
export class UserPermissionConverter {
  readonly withContext = true;

  toView(
    action: string,
    caller: ICallerContext,
    requiredPermission?: string
  ): boolean {
    const component = caller.source as UserComponent;
    
    // Access view model properties
    const user = component.currentUser;
    if (!user) return false;
    
    // Use view model methods
    if (action === 'delete' && component.isOwner) {
      return component.isOwner(requiredPermission || '');
    }
    
    // Check permissions
    return user.permissions.includes(requiredPermission || action);
  }
}
```

Usage in template:

```html
<button if.bind="'edit' | userPermission:'edit-posts'">
  Edit Post
</button>

<button if.bind="'delete' | userPermission:post.id">
  Delete Post
</button>
```

## Registration Patterns

Aurelia 2 provides flexible registration patterns for different use cases and architectural preferences.

### 1. Decorator Registration (Recommended)

The most common and straightforward approach:

```typescript
import { valueConverter } from 'aurelia';

// Simple registration
@valueConverter('capitalize')
export class CapitalizeConverter {
  toView(value: string): string {
    return value?.charAt(0).toUpperCase() + value?.slice(1).toLowerCase() || '';
  }
}
```

### 2. Configuration Object Registration

For advanced options including aliases:

```typescript
@valueConverter({ 
  name: 'currency', 
  aliases: ['money', 'cash'] 
})
export class CurrencyConverter {
  toView(value: number, locale = 'en-US', currency = 'USD'): string {
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency
    }).format(value);
  }
  
  fromView(value: string): number {
    // Parse currency string back to number for two-way binding
    const numericValue = parseFloat(value.replace(/[^\d.-]/g, ''));
    return isNaN(numericValue) ? 0 : numericValue;
  }
}
```

Usage with aliases:

```html
<span>${price | currency}</span>
<span>${price | money:'en-GB':'GBP'}</span>
<span>${price | cash}</span>
```

### 3. Static Definition

Using the static `$au` property (alternative registration approach):

```typescript
export class DateFormatConverter {
  static readonly $au: ValueConverterStaticAuDefinition = {
    type: 'value-converter',
    name: 'dateFormat',
    aliases: ['df']
  };

  toView(value: Date, format: string = 'short'): string {
    return new Intl.DateTimeFormat('en-US', 
      format === 'short' ? { dateStyle: 'short' } : { dateStyle: 'full' }
    ).format(value);
  }
}
```

### 4. Manual Registration

For dynamic or runtime registration scenarios:

```typescript
import { ValueConverter, IContainer } from 'aurelia';

// Method 1: ValueConverter.define()
const DynamicConverter = ValueConverter.define('dynamic', class {
  toView(value: unknown): string {
    return `[Dynamic: ${value}]`;
  }
});

// Method 2: Container registration
export class RuntimeConverter {
  toView(value: unknown): string {
    return String(value);
  }
}

// Register manually in main.ts or configure function
container.register(ValueConverter.define('runtime', RuntimeConverter));
```

### 5. Local vs Global Registration

#### Global Registration (Application-wide)

```typescript
// Available throughout the entire application
@valueConverter('global')
export class GlobalConverter {
  toView(value: string): string {
    return value.toUpperCase();
  }
}
```

#### Local Registration (Component-specific)

```typescript
import { LocalConverter } from './local-converter';

@customElement({
  name: 'my-element',
  template: '<span>${data | localConverter}</span>',
  dependencies: [LocalConverter] // Only available in this component tree
})
export class MyElement {
  data = 'hello world';
}
```

#### Scoped Registration (Feature Module)

```typescript
// feature-module.ts
import { IContainer } from 'aurelia';

export function configure(container: IContainer) {
  container.register(
    ValueConverter.define('featureSpecific', FeatureConverter)
  );
}
```

### 6. Conditional Registration

Register converters based on environment or feature flags:

```typescript
// main.ts
import { IContainer } from 'aurelia';

export function configure(container: IContainer) {
  // Production vs Development converters
  if (process.env.NODE_ENV === 'development') {
    container.register(DebugConverter);
  }
  
  // Feature flag based registration
  if (featureFlags.enableAdvancedFormatting) {
    container.register(AdvancedFormattingConverter);
  }
}
```

### Best Practices for Registration

1. **Use decorators for most cases** - Simple and straightforward
2. **Group related converters** - Organize by feature or domain
3. **Consider lazy loading** - Register heavy converters only when needed
4. **Document aliases** - Make alternative names clear to team members
5. **Avoid global pollution** - Use local registration for component-specific logic

## Creating Custom Value Converters

Custom value converters are classes that implement transformation logic. They provide a clean way to handle data formatting throughout your application.

### Basic Structure

```typescript
import { valueConverter } from 'aurelia';

@valueConverter('converterName')
export class ConverterNameValueConverter {
  // Required: transform data for display
  toView(value: InputType, ...args: unknown[]): OutputType {
    // Transform value for display
    return transformedValue;
  }

  // Optional: transform data from user input back to model
  fromView?(value: InputType, ...args: unknown[]): OutputType {
    // Transform user input back to model format
    return transformedValue;
  }

  // Optional: signals for automatic re-evaluation
  readonly signals?: string[] = ['signal-name'];

  // Optional: enables binding context access
  readonly withContext?: boolean = false;
}
```

### TypeScript Best Practices

#### Strong Typing

```typescript
interface FormattingOptions {
  locale?: string;
  style?: 'decimal' | 'currency' | 'percent';
  minimumFractionDigits?: number;
  maximumFractionDigits?: number;
}

@valueConverter('numberFormat')
export class NumberFormatConverter {
  toView(value: number | null | undefined, options: FormattingOptions = {}): string {
    if (value == null || isNaN(value)) return '';
    
    const {
      locale = 'en-US',
      style = 'decimal',
      minimumFractionDigits,
      maximumFractionDigits
    } = options;

    return new Intl.NumberFormat(locale, {
      style,
      minimumFractionDigits,
      maximumFractionDigits
    }).format(value);
  }

  fromView(value: string, options: FormattingOptions = {}): number {
    const numericValue = parseFloat(value.replace(/[^\d.-]/g, ''));
    return isNaN(numericValue) ? 0 : numericValue;
  }
}
```

#### Generic Converters

```typescript
@valueConverter('arrayJoin')
export class ArrayJoinConverter<T = unknown> {
  toView(array: T[] | null | undefined, separator = ', ', formatter?: (item: T) => string): string {
    if (!Array.isArray(array)) return '';
    
    const items = formatter 
      ? array.map(formatter)
      : array.map(String);
      
    return items.join(separator);
  }
}
```

### Bidirectional Converters (Two-Way Binding)

Perfect for form inputs that need both display formatting and input parsing:

#### Phone Number Formatter

```typescript
@valueConverter('phoneNumber')
export class PhoneNumberConverter {
  toView(value: string | null | undefined): string {
    if (!value) return '';
    
    // Remove all non-digits
    const digits = value.replace(/\D/g, '');
    
    // Format as (XXX) XXX-XXXX for US numbers
    if (digits.length >= 10) {
      return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
    }
    
    return digits;
  }

  fromView(value: string): string {
    // Store only digits in the model
    return value.replace(/\D/g, '');
  }
}
```

Usage with two-way binding:

```html
<input value.two-way="user.phone | phoneNumber" placeholder="Phone number">
```

#### Credit Card Formatter

```typescript
@valueConverter('creditCard')
export class CreditCardConverter {
  toView(value: string | null | undefined): string {
    if (!value) return '';
    
    const digits = value.replace(/\D/g, '');
    
    // Format as XXXX XXXX XXXX XXXX
    return digits.replace(/(.{4})/g, '$1 ').trim();
  }

  fromView(value: string): string {
    return value.replace(/\D/g, '');
  }
}
```

### Error Handling and Validation

```typescript
@valueConverter('safeJson')
export class SafeJsonConverter {
  toView(value: unknown, pretty = false): string {
    try {
      return JSON.stringify(value, null, pretty ? 2 : undefined);
    } catch (error) {
      console.warn('SafeJsonConverter: Invalid JSON value', error);
      return '[Invalid JSON]';
    }
  }

  fromView(value: string): unknown {
    if (!value.trim()) return null;
    
    try {
      return JSON.parse(value);
    } catch (error) {
      console.warn('SafeJsonConverter: Invalid JSON string', error);
      return value; // Return original string if parsing fails
    }
  }
}
```

### Performance Optimization

#### Memoized Converter

```typescript
@valueConverter('expensiveTransform')
export class ExpensiveTransformConverter {
  private cache = new Map<string, string>();
  
  toView(value: string, config: TransformConfig): string {
    const cacheKey = `${value}-${JSON.stringify(config)}`;
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)!;
    }
    
    const result = this.performExpensiveTransformation(value, config);
    this.cache.set(cacheKey, result);
    
    // Prevent memory leaks
    if (this.cache.size > 1000) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    return result;
  }
  
  private performExpensiveTransformation(value: string, config: TransformConfig): string {
    // Expensive operation here
    return value;
  }
}
```

### Utility Converters

#### Null-Safe Converter

```typescript
@valueConverter('nullSafe')
export class NullSafeConverter {
  toView(value: unknown, fallback = ''): string {
    if (value == null || value === '') return String(fallback);
    return String(value);
  }
}
```

#### Debug Converter

```typescript
@valueConverter('debug')
export class DebugConverter {
  toView(value: unknown, label = 'Debug'): unknown {
    console.log(`${label}:`, value);
    return value;
  }
}
```

Usage:

```html
<span>${complexData | debug:'User Data' | format}</span>
```

## Signals-Based Reactivity

Value converters can automatically re-evaluate when specific signals are dispatched, perfect for locale changes, theme updates, or global state changes.

```typescript
import { valueConverter, ISignaler, resolve } from 'aurelia';

@valueConverter('localeDate')
export class LocaleDateConverter {
  private signaler = resolve(ISignaler);
  public readonly signals = ['locale-changed', 'timezone-changed'];

  toView(value: string, locale?: string) {
    const currentLocale = locale || this.getCurrentLocale();
    return new Intl.DateTimeFormat(currentLocale, {
      month: 'long',
      day: 'numeric',
      year: 'numeric'
    }).format(new Date(value));
  }

  private getCurrentLocale() {
    // Get current locale from your app state
    return 'en-US';
  }
}
```

To trigger re-evaluation from anywhere in your app:

```typescript
import { resolve, ISignaler } from 'aurelia';

export class LocaleService {
  private signaler = resolve(ISignaler);

  changeLocale(newLocale: string) {
    // Update your locale
    this.signaler.dispatchSignal('locale-changed');
  }
}
```

Now all `localeDate` converters automatically update when the locale changes, without needing to manually refresh bindings.

### Built-in Signal-Aware Converters

Aurelia 2 includes several built-in converters that leverage signals:

```html
<!-- Automatically updates when locale changes -->
<p>${message | t}</p> <!-- Translation -->
<p>${date | df}</p> <!-- Date format -->
<p>${number | nf}</p> <!-- Number format -->
<p>${date | rt}</p> <!-- Relative time -->
```

## Built-in Value Converters

Aurelia 2 includes several built-in converters ready for use:

### Sanitize Converter

Aurelia 2 includes a `sanitize` converter, but it requires you to provide your own sanitizer implementation:

```typescript
import { ISanitizer } from 'aurelia';

// You must register your own sanitizer implementation
export class MyHtmlSanitizer implements ISanitizer {
  sanitize(input: string): string {
    // Implement your sanitization logic
    // You might use a library like DOMPurify here
    return input; // This is just an example - implement proper sanitization!
  }
}

// Register it in your main configuration
container.register(singletonRegistration(ISanitizer, MyHtmlSanitizer));
```

Then you can use the sanitize converter:

```html
<div innerHTML.bind="userContent | sanitize"></div>
```

**Note**: The built-in sanitize converter throws an error by default. You must provide your own `ISanitizer` implementation for it to work.

### I18n Converters (when @aurelia/i18n is installed)

```html
<!-- Translation -->
<p>${'welcome.message' | t}</p>
<p>${'welcome.user' | t:{ name: userName }}</p>

<!-- Date formatting -->
<p>${date | df}</p>
<p>${date | df:{ year: 'numeric', month: 'long' }}</p>

<!-- Number formatting -->
<p>${price | nf:{ style: 'currency', currency: 'USD' }}</p>

<!-- Relative time -->
<p>${timestamp | rt}</p>
```

## Advanced Configuration Options

### Date Formatter Example

This converter formats dates based on locale:

```typescript
import { valueConverter } from 'aurelia';

@valueConverter('date')
export class FormatDate {
  toView(value: string, locale = 'en-US') {
    const date = new Date(value);
    if (Number.isNaN(date.valueOf())) {
      return 'Invalid Date';
    }
    return new Intl.DateTimeFormat(locale, {
      month: 'long',
      day: 'numeric',
      year: 'numeric',
      timeZone: 'UTC'
    }).format(date);
  }
}
```

Import it in your view:

```html
<import from="./date-value-converter" />
```

Usage examples:

```html
<p>${'2021-06-22T09:21:26.699Z' | date}</p>
<p>${'2021-06-22T09:21:26.699Z' | date:'en-GB'}</p>
```

View this in action on [StackBlitz](https://stackblitz.com/edit/aurelia-date-value-converter?embed=1\&file=my-app.html\&hideExplorer=1\&view=preview).

## Real-World Converter Examples

### File Size Converter

Convert bytes to human-readable file sizes:

```typescript
@valueConverter('fileSize')
export class FileSizeConverter {
  private units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
  
  toView(bytes: number | null | undefined, precision = 1): string {
    if (bytes == null || bytes === 0) return '0 B';
    if (bytes < 0) return 'Invalid size';
    
    const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
    const value = bytes / Math.pow(1024, unitIndex);
    const unit = this.units[unitIndex] || 'XX';
    
    return `${value.toFixed(precision)} ${unit}`;
  }
}
```

```html
<span>File size: ${fileSize | fileSize:2}</span>
<!-- Output: "File size: 1.24 MB" -->
```

### Relative Time Converter

Display time relative to now (e.g., "2 hours ago"):

```typescript
@valueConverter('timeAgo')
export class TimeAgoConverter {
  readonly signals = ['time-tick'];
  
  private units = [
    { name: 'year', seconds: 31536000 },
    { name: 'month', seconds: 2592000 },
    { name: 'week', seconds: 604800 },
    { name: 'day', seconds: 86400 },
    { name: 'hour', seconds: 3600 },
    { name: 'minute', seconds: 60 },
    { name: 'second', seconds: 1 }
  ];

  toView(date: Date | string | number | null | undefined): string {
    if (!date) return '';
    
    const now = Date.now();
    const targetTime = new Date(date).getTime();
    const diffInSeconds = Math.floor((now - targetTime) / 1000);
    
    if (diffInSeconds < 0) return 'in the future';
    if (diffInSeconds < 30) return 'just now';
    
    for (const unit of this.units) {
      const count = Math.floor(diffInSeconds / unit.seconds);
      if (count >= 1) {
        return `${count} ${unit.name}${count > 1 ? 's' : ''} ago`;
      }
    }
    
    return 'just now';
  }
}
```

### Truncate with Tooltip Converter

Truncate text with full text available on hover:

```typescript
@valueConverter('truncate')
export class TruncateConverter {
  readonly withContext = true;
  
  toView(
    text: string | null | undefined, 
    caller: { binding: any, source: unknown }, 
    maxLength = 50, 
    suffix = '...'
  ): string {
    if (!text || text.length <= maxLength) return text || '';
    
    const truncated = text.substring(0, maxLength - suffix.length) + suffix;
    
    // Add full text as tooltip if binding target supports it
    if (caller.binding?.target && 'title' in caller.binding.target) {
      caller.binding.target.title = text;
    }
    
    return truncated;
  }
}
```

### Markdown to HTML Converter

Convert markdown text to HTML (using marked library):

```typescript
import { marked } from 'marked';

@valueConverter('markdown')
export class MarkdownConverter {
  private renderer = new marked.Renderer();
  
  constructor() {
    // Configure marked for security
    marked.setOptions({
      breaks: true,
      sanitize: true
    });
  }
  
  toView(markdown: string | null | undefined): string {
    if (!markdown) return '';
    
    try {
      return marked(markdown);
    } catch (error) {
      console.error('MarkdownConverter error:', error);
      return markdown; // Fallback to original text
    }
  }
}
```

### Search Highlight Converter

Highlight search terms in text:

```typescript
@valueConverter('highlight')
export class HighlightConverter {
  toView(
    text: string | null | undefined, 
    searchTerm: string | null | undefined, 
    className = 'highlight'
  ): string {
    if (!text || !searchTerm) return text || '';
    
    const regex = new RegExp(`(${this.escapeRegex(searchTerm)})`, 'gi');
    return text.replace(regex, `<span class="${className}">$1</span>`);
  }
  
  private escapeRegex(str: string): string {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }
}
```

### Sort Array Converter

Sort arrays by property or custom function:

```typescript
interface SortConfig<T = unknown> {
  property?: keyof T;
  direction?: 'asc' | 'desc';
  compareFunction?: (a: T, b: T) => number;
  caseSensitive?: boolean;
}

@valueConverter('sort')
export class SortConverter {
  toView<T>(
    array: T[] | null | undefined, 
    config: SortConfig<T> | string = {}
  ): T[] {
    if (!Array.isArray(array)) return [];
    
    // Handle string property shorthand
    const sortConfig = typeof config === 'string' 
      ? { property: config as keyof T } 
      : config;
      
    const { 
      property, 
      direction = 'asc', 
      compareFunction, 
      caseSensitive = true 
    } = sortConfig;
    
    const sorted = [...array];
    
    if (compareFunction) {
      sorted.sort(compareFunction);
    } else if (property) {
      sorted.sort((a, b) => {
        let aVal = a[property] as any;
        let bVal = b[property] as any;
        
        // Handle string case sensitivity
        if (typeof aVal === 'string' && typeof bVal === 'string' && !caseSensitive) {
          aVal = aVal.toLowerCase();
          bVal = bVal.toLowerCase();
        }
        
        if (aVal < bVal) return direction === 'asc' ? -1 : 1;
        if (aVal > bVal) return direction === 'asc' ? 1 : -1;
        return 0;
      });
    }
    
    return direction === 'desc' ? sorted.reverse() : sorted;
  }
}
```

### Color Converter

Convert between color formats:

```typescript
@valueConverter('color')
export class ColorConverter {
  toView(
    color: string | null | undefined, 
    format: 'hex' | 'rgb' | 'hsl' = 'hex'
  ): string {
    if (!color) return '';
    
    try {
      const rgb = this.parseColor(color);
      
      switch (format) {
        case 'rgb':
          return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
        case 'hsl':
          return this.rgbToHsl(rgb);
        case 'hex':
        default:
          return this.rgbToHex(rgb);
      }
    } catch (error) {
      console.warn('ColorConverter: Invalid color format', color);
      return color;
    }
  }
  
  private parseColor(color: string): { r: number; g: number; b: number } {
    // Implementation for parsing various color formats
    // This is simplified - you'd want a more robust color parsing library
    if (color.startsWith('#')) {
      const hex = color.slice(1);
      return {
        r: parseInt(hex.slice(0, 2), 16),
        g: parseInt(hex.slice(2, 4), 16),
        b: parseInt(hex.slice(4, 6), 16)
      };
    }
    throw new Error(`Unsupported color format: ${color}`);
  }
  
  private rgbToHex({ r, g, b }: { r: number; g: number; b: number }): string {
    return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
  }
  
  private rgbToHsl({ r, g, b }: { r: number; g: number; b: number }): string {
    // HSL conversion logic
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h = 0, s = 0, l = (max + min) / 2;
    
    if (max !== min) {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }
    
    return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
  }
}
```

## Performance Optimization

### Caching Strategies

Implement intelligent caching for expensive operations:

```typescript
@valueConverter('expensiveFormat')
export class ExpensiveFormatConverter {
  private cache = new Map<string, string>();
  private maxCacheSize = 1000;
  
  toView(value: string, config: ComplexConfig): string {
    const cacheKey = this.createCacheKey(value, config);
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)!;
    }
    
    const result = this.performExpensiveTransformation(value, config);
    
    // Implement LRU cache behavior
    if (this.cache.size >= this.maxCacheSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(cacheKey, result);
    return result;
  }
  
  private createCacheKey(value: string, config: ComplexConfig): string {
    return `${value}:${JSON.stringify(config)}`;
  }
}
```

### Lazy Evaluation

Defer expensive operations until actually needed:

```typescript
@valueConverter('lazyTransform')
export class LazyTransformConverter {
  private transformPromises = new WeakMap<object, Promise<string>>();
  
  toView(data: ComplexData): string | Promise<string> {
    if (this.transformPromises.has(data)) {
      return this.transformPromises.get(data)!;
    }
    
    const promise = this.performAsyncTransformation(data);
    this.transformPromises.set(data, promise);
    
    return promise;
  }
  
  private async performAsyncTransformation(data: ComplexData): Promise<string> {
    // Expensive async operation
    return 'transformed result';
  }
}
```

### Memory Management

Prevent memory leaks in converters:

```typescript
@valueConverter('memoryAware')
export class MemoryAwareConverter {
  private observers = new Set<() => void>();
  private cache = new Map();
  
  toView(value: string): string {
    // Clean up old observers
    this.cleanup();
    
    // Your transformation logic
    return this.transform(value);
  }
  
  private cleanup(): void {
    // Dispose observers and clear caches periodically
    if (this.observers.size > 100) {
      this.observers.forEach(cleanup => cleanup());
      this.observers.clear();
      this.cache.clear();
    }
  }
}
```

### Benchmark and Profile

Use performance measurement for optimization:

```typescript
@valueConverter('profiled')
export class ProfiledConverter {
  private performanceMetrics = new Map<string, number>();
  
  toView(value: string, operation: string): string {
    const start = performance.now();
    const result = this.performTransformation(value, operation);
    const duration = performance.now() - start;
    
    // Track performance metrics
    const key = `${operation}-${typeof value}`;
    const existing = this.performanceMetrics.get(key) || 0;
    this.performanceMetrics.set(key, (existing + duration) / 2);
    
    return result;
  }
  
  getPerformanceReport(): Record<string, number> {
    return Object.fromEntries(this.performanceMetrics);
  }
}
```

## Best Practices

### 1. Design Principles

**Single Responsibility**

```typescript
// ✅ Good - focused on one transformation
@valueConverter('capitalize')
export class CapitalizeConverter {
  toView(text: string): string {
    return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
  }
}

// ❌ Bad - doing too many things
@valueConverter('formatEverything')
export class FormatEverythingConverter {
  toView(value: unknown, type: string): string {
    // This converter tries to handle too many different cases
  }
}
```

**Pure Functions**

```typescript
// ✅ Good - no side effects
@valueConverter('multiply')
export class MultiplyConverter {
  toView(value: number, factor: number): number {
    return value * factor;
  }
}

// ❌ Bad - side effects
@valueConverter('logAndMultiply')  
export class LogAndMultiplyConverter {
  toView(value: number, factor: number): number {
    console.log('Processing:', value); // Side effect
    this.updateGlobalCounter(); // Side effect
    return value * factor;
  }
}
```

### 2. TypeScript Integration

**Strong Typing**

```typescript
interface DateFormatOptions {
  locale?: string;
  dateStyle?: 'full' | 'long' | 'medium' | 'short';
  timeStyle?: 'full' | 'long' | 'medium' | 'short';
}

@valueConverter('dateFormat')
export class DateFormatConverter {
  toView(
    date: Date | string | number | null | undefined,
    options: DateFormatOptions = {}
  ): string {
    if (!date) return '';
    
    const dateObj = new Date(date);
    if (isNaN(dateObj.getTime())) return 'Invalid Date';
    
    const { locale = 'en-US', ...formatOptions } = options;
    return new Intl.DateTimeFormat(locale, formatOptions).format(dateObj);
  }
}
```

**Generic Constraints**

```typescript
interface Filterable {
  [key: string]: unknown;
}

@valueConverter('filter')
export class FilterConverter {
  toView<T extends Filterable>(
    items: T[] | null | undefined,
    predicate: (item: T) => boolean
  ): T[] {
    if (!Array.isArray(items)) return [];
    return items.filter(predicate);
  }
}
```

### 3. Error Handling

**Graceful Degradation**

```typescript
@valueConverter('resilient')
export class ResilientConverter {
  toView(value: unknown, options: ConversionOptions = {}): string {
    try {
      return this.performConversion(value, options);
    } catch (error) {
      // Log for debugging but don't break the UI
      console.warn(`ResilientConverter failed for value:`, value, error);
      
      // Return safe fallback
      return options.fallback || String(value) || '';
    }
  }
  
  private performConversion(value: unknown, options: ConversionOptions): string {
    // Potentially throwing conversion logic
    throw new Error('Conversion failed');
  }
}
```

### 4. Testing Strategies

**Unit Testing**

```typescript
describe('CurrencyConverter', () => {
  let converter: CurrencyConverter;
  
  beforeEach(() => {
    converter = new CurrencyConverter();
  });
  
  it('should format USD currency correctly', () => {
    const result = converter.toView(1234.56, { locale: 'en-US', currency: 'USD' });
    expect(result).toBe('$1,234.56');
  });
  
  it('should handle null values gracefully', () => {
    const result = converter.toView(null);
    expect(result).toBe('');
  });
  
  it('should parse formatted currency back to number', () => {
    const result = converter.fromView('$1,234.56');
    expect(result).toBe(1234.56);
  });
});
```

## Troubleshooting Common Issues

### Issue: Converter Not Found

**Problem**: Template shows error "No ValueConverter named 'myConverter' was found"

**Solutions**:

1. **Import the converter**:

   ```html
   <import from="./my-converter"></import>
   ```
2. **Check decorator name**:

   ```typescript
   @valueConverter('myConverter') // Must match template usage
   export class MyConverter { }
   ```
3. **Global registration**:

   ```typescript
   // In main.ts
   import { MyConverter } from './my-converter';

   Aurelia.register(MyConverter).app(MyApp).start();
   ```

### Issue: Performance Problems

**Problem**: Page becomes slow with converters in loops

**Solutions**:

1. **Implement caching**:

   ```typescript
   private cache = new Map();
   toView(value: string): string {
     if (this.cache.has(value)) return this.cache.get(value);
     // ... expensive operation
   }
   ```
2. **Use signals for global updates**:

   ```typescript
   readonly signals = ['data-changed'];
   // Update only when signal is dispatched
   ```
3. **Optimize template usage**:

   ```html
   <!-- ❌ Bad - converter called for every item -->
   <div repeat.for="item of items">
     ${expensiveData | expensiveConverter}
   </div>

   <!-- ✅ Good - converter called once -->
   <div repeat.for="item of items">
     ${item.name}
   </div>
   <div>${expensiveData | expensiveConverter}</div>
   ```

### Issue: Context Access Not Working

**Problem**: `caller` parameter is undefined in `toView`

**Solutions**:

1. **Enable context access**:

   ```typescript
   readonly withContext = true; // Required property
   ```
2. **Correct parameter order**:

   ```typescript
   toView(value: unknown, caller: ICallerContext, ...args: unknown[]): unknown {
     // caller is always second parameter when withContext = true
   }
   ```

### Issue: Signals Not Triggering

**Problem**: Converter doesn't update when signal is dispatched

**Solutions**:

1. **Declare signals array**:

   ```typescript
   readonly signals = ['my-signal']; // Array of signal names
   ```
2. **Dispatch signals correctly**:

   ```typescript
   import { resolve } from '@aurelia/kernel';
   import { ISignaler } from '@aurelia/runtime-html';

   private signaler = resolve(ISignaler);

   updateData(): void {
     // Update data first
     this.signaler.dispatchSignal('my-signal');
   }
   ```

## Built-in Converters Reference

### Core Converters

| Converter  | Purpose           | Package                 | Parameters                                |
| ---------- | ----------------- | ----------------------- | ----------------------------------------- |
| `sanitize` | HTML sanitization | `@aurelia/runtime-html` | None (requires ISanitizer implementation) |

### I18n Converters (when `@aurelia/i18n` is installed)

| Converter | Purpose           | Parameters       | Example       |
| --------- | ----------------- | ---------------- | ------------- |
| `t`       | Translation       | `key`, `options` | \`${'hello'   |
| `df`      | Date formatting   | `options`        | \`${date      |
| `nf`      | Number formatting | `options`        | \`${price     |
| `rt`      | Relative time     | None             | \`${timestamp |

### Usage Examples

```html
<!-- Translation with parameters -->
<span>${'welcome.message' | t:{ name: userName }}</span>

<!-- Date formatting -->
<span>${createdDate | df:{ dateStyle: 'full', timeStyle: 'short' }}</span>

<!-- Currency formatting -->
<span>${price | nf:{ style: 'currency', currency: 'EUR' }}</span>

<!-- Relative time -->
<span>Posted ${postDate | rt}</span>
```

## Summary

Value converters in Aurelia 2 provide a powerful, flexible system for data transformation:

* **Bidirectional support** - Handle both display formatting and input parsing
* **Signal-based reactivity** - Automatic updates on global state changes
* **Context awareness** - Access binding context when needed
* **Performance optimization** - Built-in caching and lazy evaluation support
* **Type safety** - Full TypeScript support with strong typing
* **Flexible registration** - Multiple registration patterns for different needs
* **Extensibility** - Easy to create custom converters for specific requirements

Use value converters to keep your templates clean and maintainable while providing rich data formatting capabilities throughout your application.


# Binding behaviors

Binding behaviors are a powerful category of view resources in Aurelia 2 that modify how bindings operate. Unlike value converters which transform data, binding behaviors have complete access to the binding instance throughout its entire lifecycle, allowing them to fundamentally alter binding behavior.

## Overview

Binding behaviors enable you to:

* **Control timing** - throttle, debounce, or trigger updates at specific intervals
* **Modify binding modes** - force one-way, two-way, or one-time binding behavior
* **Customize event handling** - filter events or change which events trigger updates
* **Add debugging capabilities** - inspect, log, or visualize binding behavior
* **Implement complex logic** - create reusable binding modifications

### Syntax

Binding behaviors use the `&` operator and follow similar syntax to value converters:

```html
<!-- Basic usage -->
<input value.bind="searchQuery & debounce">

<!-- With parameters -->
<input value.bind="query & throttle:500">

<!-- Multiple parameters -->
<input value.bind="data & throttle:200:'signalName'">

<!-- Chaining behaviors -->
<input value.bind="text & debounce:300 & signal:'update'">

<!-- Combined with value converters -->
<span>${price | currency:'USD' & signal:'refresh'}</span>
```

**Parameter syntax flexibility:**

```html
<!-- All of these are equivalent -->
<input value.bind="data & throttle:200:'signal'">
<input value.bind="data & throttle :200 : 'signal'">
<input value.bind="data & throttle: 200 : 'signal'">
```

## Throttle

Aurelia provides several built-in binding behaviors to address common scenarios. The `throttle` behavior is designed to limit the rate at which updates propagate. This can apply to updates from the view-model to the view (in `to-view` or `one-way` bindings) or from the view to the view-model (in `two-way` bindings).

By default, `throttle` enforces a minimum time interval of 200ms between updates. You can easily customize this interval.

Here are some practical examples:

**Limiting property updates to a maximum of once every 200ms**

```html
<input type="text" value.bind="searchQuery & throttle">
<p>Searching for: ${searchQuery}</p>
```

In this example, the `searchQuery` property in your view model will update at most every 200ms, even if the user types more rapidly in the input field. This is especially useful for search inputs or other scenarios where frequent updates can be inefficient or overwhelming.

You'll notice the `&` symbol, which is used to introduce binding behavior expressions. The syntax for binding behaviors mirrors that of value converters:

* **Arguments**: Binding behaviors can accept arguments, separated by colons: `propertyName & behaviorName:arg1:arg2`.
* **Chaining**: Multiple binding behaviors can be chained together: `propertyName & behavior1 & behavior2:arg1`.
* **Combined with Value Converters**: Binding expressions can include both value converters and binding behaviors: `${data | valueConverter:arg & bindingBehavior:arg2}`.

Let's see how to customize the throttling interval:

**Limiting property updates to a maximum of once every 850ms**

```html
<input type="text" value.bind="query & throttle:850">
```

The `throttle` behavior is particularly valuable when used with event bindings, especially for events that fire frequently, such as `mousemove`.

**Handling `mousemove` events at most every 200ms**

```html
<div mousemove.trigger="mouseMoveHandler($event) & throttle"></div>
```

In this case, the `mouseMoveHandler` method in your view model will be invoked at most every 200ms, regardless of how frequently the `mousemove` event is triggered as the user moves their mouse.

### Flushing Pending Throttled Updates

In certain situations, you might need to immediately apply any pending throttled updates. Consider a form with throttled input fields. When a user tabs out of a field after typing, you might want to ensure the latest value is immediately processed, even if the throttle interval hasn't elapsed yet.

The `throttle` binding behavior supports this via a "signal". You can specify a signal name as the second argument to `throttle`. Then, using Aurelia's `ISignaler`, you can dispatch this signal to force a flush of the throttled update.

```html
<input value.bind="formValue & throttle:200:'flushInput'" blur.trigger="signaler.dispatchSignal('flushInput')">
```

```typescript
import { ISignaler } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class MyApp {
  formValue = '';
  signaler = resolve(ISignaler); // Inject ISignaler

  constructor() {}
}
```

In this example:

* `value.bind="formValue & throttle:200:'flushInput'"`: The `formValue` binding is throttled to 200ms and associated with the signal `'flushInput'`.
* `blur.trigger="signaler.dispatchSignal('flushInput')"`: When the input loses focus (`blur` event), `signaler.dispatchSignal('flushInput')` is called. This immediately triggers any pending throttled update associated with the `'flushInput'` signal, ensuring the `formValue` is updated in the view model right away.

You can also specify multiple signals using an array:

```html
<input value.bind="value & throttle:200:['finishTyping', 'urgentUpdate']">
```

This allows multiple different signals to trigger the same throttled update, providing flexibility in complex scenarios where updates might need to be flushed from different parts of your application.

## Debounce

The `debounce` binding behavior is another rate-limiting tool. `debounce` delays updates until a specified time interval has passed *without any further changes*. This is ideal for scenarios where you want to react only after a user has paused interacting.

A classic use case is a search input that triggers an autocomplete or search operation. Making an API call with every keystroke is inefficient. `debounce` ensures the search logic is invoked only after the user has stopped typing for a moment.

**Updating a property after typing has stopped for 200ms**

```html
<input type="text" value.bind="searchQuery & debounce">
```

**Updating a property after typing has stopped for 850ms**

```html
<input type="text" value.bind="searchQuery & debounce:850">
```

Similar to `throttle`, `debounce` is highly effective with event bindings.

**Calling `mouseMoveHandler` after the mouse stops moving for 500ms**

```html
<div mousemove.trigger="mouseMoveHandler($event) & debounce:500"></div>
```

### Flushing Pending Debounced Calls

Like `throttle`, `debounce` also supports flushing pending updates using signals. This is useful in scenarios like form submission where you want to ensure the most recent debounced values are processed immediately, even if the debounce interval hasn't elapsed.

```html
<input value.bind="formValue & debounce:300:'validateInput'" blur.trigger="signaler.dispatchSignal('validateInput')">
```

```typescript
import { ISignaler } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class MyApp {
  formValue = '';
  signaler = resolve(ISignaler); // Inject ISignaler

  constructor() {}

  validateInput() {
    console.log('Input validated:', this.formValue);
    // Perform validation logic here
  }
}
```

In this example, the `validateInput` method (which could perform input validation or other actions) will be called when the input field loses focus, even if the 300ms debounce interval isn't fully over, ensuring timely validation.

As with `throttle`, you can also provide multiple signal names to `debounce`:

```html
<input value.bind="searchQuery & debounce:500:['search', 'validate']">
```

## UpdateTrigger

The `updateTrigger` binding behavior allows you to customize which DOM events trigger updates from the view to the view model for input elements. By default, Aurelia uses the `change` and `input` events for most input types.

However, you can override this default behavior. For example, you might want to update the view model only when an input field loses focus (`blur` event).

**Updating the view model only on `blur`**

```html
<input value.bind="firstName & updateTrigger:'blur'">
```

You can specify multiple events that should trigger updates:

**Updating the view model on `blur` or `paste` events**

```html
<input value.bind="firstName & updateTrigger:'blur':'paste'">
```

This is useful in scenarios where you need fine-grained control over when view-model updates occur based on specific user interactions with input elements.

## Signal

The `signal` binding behavior provides a mechanism to explicitly tell a binding to refresh itself. This is particularly useful when a binding's result depends on external factors or global state changes that Aurelia's observation system might not automatically detect.

Consider a "translate" value converter that translates keys into localized strings, e.g., `${'greeting.key' | translate}`. If your application allows users to change the language dynamically, how do you refresh all the translation bindings to reflect the new language?

Another example is a value converter that displays a "time ago" string relative to the current time, e.g., `Posted ${post.date | timeAgo}`. As time progresses, this binding needs to refresh periodically to show updated relative times like "5 minutes ago," "an hour ago," etc.

`signal` binding behavior solves these refresh scenarios:

**Using a Signal to Refresh Bindings**

```html
<p>Last updated: ${lastUpdated | timeAgo & signal:'time-update'}</p>
```

In this example, `signal:'time-update'` assigns the signal name `'time-update'` to this binding. Multiple bindings can share the same signal name.

To trigger a refresh of all bindings with the signal name `'time-update'`, you use the `ISignaler`:

**Dispatching a Signal to Refresh Bindings**

```typescript
import { ISignaler } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class MyApp {
  lastUpdated = new Date();
  signaler = resolve(ISignaler);

  constructor() {
    setInterval(() => {
      this.lastUpdated = new Date(); // Update the time
      this.signaler.dispatchSignal('time-update'); // Signal bindings to refresh
    }, 5000); // Refresh every 5 seconds
  }
}
```

Every 5 seconds, the `setInterval` function updates `lastUpdated` and then calls `signaler.dispatchSignal('time-update')`. This tells Aurelia to re-evaluate all bindings that are configured with `& signal:'time-update'`, causing them to refresh and display the updated "time ago" value.

## Binding Mode Behaviors

Aurelia exposes four mode behaviors in `@aurelia/runtime-html` (`oneTime`, `toView`, `fromView`, `twoWay`). Each one derives from the shared `BindingModeBehavior` base class which simply assigns a different `binding.mode` during `bind` and restores it on `unbind`. They are especially handy when:

* You are consuming a component whose bindable defaults to `two-way`, but a specific usage should stay strictly view-model → view.
* You want to keep `.bind` syntax but override the direction inside `repeat.for`, `if/else`, or other inline templates without changing the child API.
* You need to chain additional behaviors/value converters and prefer not to switch to the `.one-time` command mid-expression.

```html
<!-- Force read-only values on a child component that defaults to two-way -->
<order-line line.bind="line & toView"></order-line>

<!-- Keep track of pending edits but stop pushing DOM mutations back up -->
<textarea value.bind="draft.summary & fromView"></textarea>

<!-- Kick off an expensive computation once, never re-run -->
<span class="snapshot">${report.total & oneTime}</span>
```

`StandardConfiguration` registers these behaviors for you. If you need something more custom—say, a behavior that forces `BindingMode.twoWay` only when the target implements a particular interface—you can extend `BindingModeBehavior` yourself:

```typescript
import { BindingMode, BindingModeBehavior } from '@aurelia/runtime-html';

export class DirtyCheckedBindingBehavior extends BindingModeBehavior {
  public static readonly $au = { type: 'binding-behavior', name: 'dirtyChecked' } as const;
  public get mode() {
    return BindingMode.twoWay;
  }
}

Aurelia.register(DirtyCheckedBindingBehavior);
```

After registration you can apply `&dirtyChecked` in any binding expression.

Aurelia provides binding behaviors that explicitly specify binding modes. While binding commands (`.bind`, `.to-view`, `.two-way`) are more commonly used, these behaviors offer programmatic control over binding modes.

### oneTime

The `oneTime` binding behavior creates the most efficient binding by evaluating the expression only once and never observing it for changes.

```html
<!-- Perfect for static content -->
<span>${appVersion & oneTime}</span>
<img src.bind="logoUrl & oneTime" alt="Company Logo">

<!-- Useful in repeaters for static data -->
<div repeat.for="item of items">
  <span>${item.id & oneTime}</span> <!-- ID never changes -->
  <span>${item.name}</span> <!-- Name might change -->
</div>
```

`oneTime` bindings eliminate observation overhead entirely, making them ideal for:

* Static configuration values
* IDs and other immutable data
* Large lists where some properties never change
* Performance-critical rendering scenarios

### toView (One-Way)

Forces one-way data flow from view-model to view only.

```html
<!-- Equivalent syntaxes -->
<input value.bind="dataItem & toView">
<input value.to-view="dataItem">
```

### fromView

Forces one-way data flow from view to view-model only. The view-model property will be updated when the view changes, but view-model changes won't update the view.

```html
<!-- Input updates view-model, but view-model changes don't update input -->
<input value.bind="userInput & fromView">
<!-- Equivalent to -->
<input value.from-view="userInput">
```

This is useful for scenarios like:

* Collecting user input without reflecting programmatic changes back to the UI
* One-way form submission scenarios
* Performance optimization when you don't need view updates

### twoWay

Forces bidirectional data synchronization between view and view-model.

```html
<!-- Equivalent syntaxes -->
<input value.bind="userInput & twoWay">
<input value.two-way="userInput">
```

### Binding Mode Summary

| Behavior   | Direction     | Use Case                    | Command Equivalent |
| ---------- | ------------- | --------------------------- | ------------------ |
| `oneTime`  | None (static) | Static content, performance | N/A                |
| `toView`   | VM → View     | Display-only data           | `.to-view`         |
| `fromView` | View → VM     | Input-only scenarios        | `.from-view`       |
| `twoWay`   | VM ↔ View     | Interactive forms           | `.two-way`         |

{% hint style="info" %}
**Naming Convention**: Binding mode behaviors use camelCase (`toView`, `fromView`, `twoWay`) because they're JavaScript expressions, while binding commands use dash-case (`.to-view`, `.from-view`, `.two-way`) due to HTML's case-insensitive nature.
{% endhint %}

## Self

The `self` binding behavior is used in event bindings to ensure that the event handler only responds to events dispatched directly from the element the listener is attached to, and not from any of its descendant elements due to event bubbling.

Consider a scenario with a panel component:

**Scenario without `self` binding behavior**

```html
<panel>
  <header mousedown.trigger="onMouseDown($event)" ref="headerElement">
    <button>Settings</button>
    <button>Close</button>
  </header>
</panel>
```

Without `self`, the `onMouseDown` handler will be invoked not only when the user mousedown on the `<header>` element itself, but also on any element *inside* the header, such as the "Settings" and "Close" buttons, due to event bubbling. This might not be the desired behavior if you want the panel to react only to direct interactions with the header, not its contents.

You could handle this in your event handler by checking the `event.target`:

**Event Handler without `self` binding behavior (manual check)**

```typescript
export class PanelComponent {
  headerElement?: HTMLElement; // Set via ref="headerElement"

  onMouseDown(event: MouseEvent) {
    if (event.target !== this.headerElement) {
      return; // Ignore events from header's descendants
    }
    // Mouse down directly on the header, start panel dragging logic...
    // ...
  }
}
```

However, this mixes DOM event handling logic with component-specific behavior. The `self` binding behavior offers a cleaner, more declarative solution:

**Using `self` binding behavior**

```html
<panel>
  <header mousedown.trigger='onMouseDown($event) & self'>
    <button class='settings'></button>
    <button class='close'></button>
  </header>
</panel>
```

**Event Handler with `self` binding behavior**

```typescript
export class PanelComponent {
  onMouseDown(event: MouseEvent) {
    // No need to check event.target, 'self' behavior ensures
    // this handler is only called for events directly on the header element.
    // Mouse down on header, start panel dragging logic...
    // ...
  }
}
```

By adding `& self` to the event binding, Aurelia ensures that `onMouseDown` is only called when the `mousedown` event originates directly from the `<header>` element, simplifying your event handler logic and separating concerns.

## Attr

The `attr` binding behavior forces a binding to use attribute accessor instead of property accessor. This is particularly useful when working with custom attributes or when you need to ensure the HTML attribute is set rather than just the property.

**Forcing attribute binding:**

```html
<!-- Forces setting the 'data-value' attribute -->
<div data-value.bind="itemValue & attr">

<!-- Useful for custom attributes that need actual HTML attributes -->
<custom-element custom-attr.bind="value & attr">

<!-- CSS class binding as attribute -->
<div class.bind="cssClasses & attr">
```

**When to use `attr`:**

* Custom attributes that require actual HTML attributes to be set
* Interoperability with third-party libraries that read HTML attributes
* SEO considerations where attributes need to be present in the DOM
* Cases where you need the attribute to be visible in browser dev tools

**Example with custom attribute:**

```typescript
// Custom attribute that reads from HTML attribute
export class TooltipCustomAttribute {
  attached() {
    // This requires the actual HTML attribute to be set
    const tooltipText = this.element.getAttribute('tooltip');
    // Setup tooltip with tooltipText
  }
}
```

```html
<!-- Without attr - might not work -->
<div tooltip.bind="helpText">Content</div>

<!-- With attr - ensures HTML attribute is set -->
<div tooltip.bind="helpText & attr">Content</div>
```

## Custom Binding Behaviors

You can create your own custom binding behaviors to encapsulate reusable binding modifications. Like value converters, custom binding behaviors are view resources.

Custom binding behaviors implement `bind(scope, binding, [...args])` and `unbind(scope, binding)` methods:

* **`bind(scope, binding, [...args])`**: Called when the binding is created and attached to the DOM. This is where you implement the behavior modification.
  * `scope`: The binding's scope, providing access to the view model (`scope.bindingContext`) and override context (`scope.overrideContext`)
  * `binding`: The binding instance whose behavior you want to alter (implements `IBinding` interface)
  * `[...args]`: Any arguments passed to the binding behavior in the template (e.g., `& myBehavior:arg1:arg2`)
* **`unbind(scope, binding)`**: Called when the binding is detached from the DOM. Clean up any changes made in the `bind` method to restore the binding to its original state and prevent memory leaks.

**Important:** Note the parameter order - `scope` comes **first**, then `binding`. This is different from some other Aurelia lifecycle methods.

Let's look at some practical examples of custom binding behaviors.

### Log Binding Context Behavior

This behavior logs the current binding context to the browser's console every time the binding updates its target (view). This is invaluable for debugging and understanding data flow in your Aurelia application.

```typescript
import { bindingBehavior, type IBinding } from '@aurelia/runtime-html';
import type { Scope } from '@aurelia/runtime';

@bindingBehavior('logBindingContext')
export class LogBindingContextBehavior {
  private originalUpdateTarget = new WeakMap<IBinding, Function>();

  public bind(scope: Scope, binding: IBinding) {
    // Store the original updateTarget method
    const original = binding.updateTarget;
    this.originalUpdateTarget.set(binding, original);

    // Override updateTarget to add logging
    binding.updateTarget = (value) => {
      console.log('Binding context:', scope.bindingContext);
      console.log('Binding value:', value);
      original.call(binding, value);
    };
  }

  public unbind(scope: Scope, binding: IBinding) {
    // Restore original updateTarget method
    const original = this.originalUpdateTarget.get(binding);
    if (original) {
      binding.updateTarget = original;
      this.originalUpdateTarget.delete(binding);
    }
  }
}
```

**Usage in Template:**

```html
<import from="./log-binding-context-behavior.ts"></import>
<input value.bind="userName & logBindingContext">
```

Now, whenever the `userName` binding updates the input element, you'll see the current binding context logged to the console, helping you inspect the data available at that point.

### Inspect Value Binding Behavior (Tooltip)

This behavior adds a temporary tooltip to the element displaying the binding's current value whenever it updates. This offers a quick way to inspect binding values directly in the UI without resorting to console logs.

```typescript
import { bindingBehavior, type IBinding } from '@aurelia/runtime-html';
import type { Scope } from '@aurelia/runtime';

@bindingBehavior('inspect')
export class InspectBindingBehavior {
  private originalMethods = new WeakMap<IBinding, Function>();

  public bind(scope: Scope, binding: IBinding) {
    const original = binding.updateTarget;
    this.originalMethods.set(binding, original);

    binding.updateTarget = (value) => {
      original.call(binding, value);
      // Add tooltip showing current value
      if (binding.target && 'title' in binding.target) {
        binding.target.title = `Current value: ${JSON.stringify(value)}`;
      }
    };
  }

  public unbind(scope: Scope, binding: IBinding) {
    // Restore original method
    const original = this.originalMethods.get(binding);
    if (original) {
      binding.updateTarget = original;
      this.originalMethods.delete(binding);
    }

    // Clear tooltip
    if (binding.target && 'title' in binding.target) {
      binding.target.title = '';
    }
  }
}
```

**Usage in Template:**

```html
<import from="./inspect-binding-behavior.ts"></import>
<input value.bind="itemName & inspect">
```

As the `itemName` binding updates, the input element will temporarily display a tooltip showing the current value, providing immediate visual feedback for debugging.

### Highlight Updates Binding Behavior

This behavior visually highlights an element by briefly changing its background color whenever the binding updates the element's target property. This visual cue helps quickly identify which parts of the UI are reacting to data changes, particularly useful during development and debugging complex views.

```typescript
import { bindingBehavior, type IBinding } from '@aurelia/runtime-html';
import type { Scope } from '@aurelia/runtime';

@bindingBehavior('highlightUpdates')
export class HighlightUpdatesBindingBehavior {
  private originalMethods = new WeakMap<IBinding, Function>();
  private timeouts = new WeakMap<IBinding, number>();

  public bind(scope: Scope, binding: IBinding, highlightColor: string = 'yellow', duration: number = 500) {
    const original = binding.updateTarget;
    this.originalMethods.set(binding, original);

    binding.updateTarget = (value) => {
      original.call(binding, value);

      // Clear any existing timeout
      const existingTimeout = this.timeouts.get(binding);
      if (existingTimeout) {
        clearTimeout(existingTimeout);
      }

      if (binding.target && binding.target.style) {
        const originalBg = binding.target.style.backgroundColor;
        binding.target.style.backgroundColor = highlightColor;

        const timeout = setTimeout(() => {
          binding.target.style.backgroundColor = originalBg;
          this.timeouts.delete(binding);
        }, duration);

        this.timeouts.set(binding, timeout);
      }
    };
  }

  public unbind(scope: Scope, binding: IBinding) {
    // Restore original method
    const original = this.originalMethods.get(binding);
    if (original) {
      binding.updateTarget = original;
      this.originalMethods.delete(binding);
    }

    // Clear any pending timeouts
    const timeout = this.timeouts.get(binding);
    if (timeout) {
      clearTimeout(timeout);
      this.timeouts.delete(binding);
    }

    // Reset background color
    if (binding.target && binding.target.style) {
      binding.target.style.backgroundColor = '';
    }
  }
}
```

**Usage in Template:**

```html
<import from="./highlight-updates-binding-behavior.ts"></import>
<div textContent.bind="message & highlightUpdates:'lightblue':'1000'"></div>
```

Whenever the `message` binding updates the `textContent` of the `div`, the div's background will briefly flash light blue for 1 second (1000ms), visually indicating the update. You can customize the highlight color and duration by passing arguments to the binding behavior in the template.

## Built-in Behaviors Reference

### Rate Limiting

| Behavior   | Purpose                 | Default | Parameters        | Signals |
| ---------- | ----------------------- | ------- | ----------------- | ------- |
| `throttle` | Limit update frequency  | 200ms   | `delay`, `signal` | ✅       |
| `debounce` | Delay until input stops | 200ms   | `delay`, `signal` | ✅       |

### Binding Modes

| Behavior   | Direction | Use Case                    |
| ---------- | --------- | --------------------------- |
| `oneTime`  | None      | Static content, performance |
| `toView`   | VM → View | Display-only data           |
| `fromView` | View → VM | Input-only scenarios        |
| `twoWay`   | VM ↔ View | Interactive forms           |

### Event & DOM

| Behavior        | Purpose                | Use Case                   |
| --------------- | ---------------------- | -------------------------- |
| `self`          | Filter event source    | Prevent event bubbling     |
| `updateTrigger` | Custom DOM events      | Control when updates occur |
| `attr`          | Force attribute access | Custom attributes, SEO     |

### Utility

| Behavior | Purpose        | Use Case                      |
| -------- | -------------- | ----------------------------- |
| `signal` | Manual refresh | Dynamic content, translations |

## Best Practices

### Performance Considerations

**Rate limiting for expensive operations:**

```html
<!-- API calls -->
<input value.bind="searchTerm & debounce:500">

<!-- DOM updates -->
<div scroll.trigger="onScroll($event) & throttle:16">
```

**Static content optimization:**

```html
<!-- Use oneTime for truly static content -->
<span>${config.version & oneTime}</span>
<img src.bind="staticLogoUrl & oneTime">
```

### Memory Management

**Proper cleanup in custom behaviors:**

```typescript
export class MyBehavior {
  private cleanupMethods = new WeakMap();

  bind(scope: Scope, binding: IBinding) {
    // Setup with cleanup tracking
  }

  unbind(scope: Scope, binding: IBinding) {
    // Always clean up to prevent memory leaks
    const cleanup = this.cleanupMethods.get(binding);
    cleanup?.();
    this.cleanupMethods.delete(binding);
  }
}
```

### Debugging and Development

**Progressive enhancement approach:**

```html
<!-- Development: with debugging -->
<input value.bind="data & logBindingContext & highlightUpdates">

<!-- Production: optimized -->
<input value.bind="data & debounce:300">
```

### Common Patterns

**Form handling:**

```html
<!-- Real-time validation with debounce -->
<input value.bind="email & debounce:300 & signal:'validate'">

<!-- Immediate validation on blur -->
<input value.bind="email & updateTrigger:'blur'"
       blur.trigger="signaler.dispatchSignal('validate')">
```

**Search functionality:**

```html
<!-- Debounced search -->
<input value.bind="searchQuery & debounce:400">

<!-- Immediate search button -->
<button click.trigger="search() & signal:'search-now'">Search</button>
```

**Dynamic content:**

```html
<!-- Time-sensitive content -->
<span>${timestamp | timeAgo & signal:'time-update'}</span>

<!-- Localized content -->
<span>${'greeting.hello' | translate & signal:'locale-change'}</span>
```

## Summary

Binding behaviors provide powerful ways to customize Aurelia's binding system:

* **Built-in behaviors** cover common scenarios like rate limiting, binding modes, and event handling
* **Custom behaviors** enable unlimited extensibility for specialized requirements
* **Proper cleanup** is essential to prevent memory leaks in custom implementations
* **Performance benefits** come from using appropriate behaviors for different use cases
* **Debugging capabilities** make development and troubleshooting easier

Use binding behaviors to create more efficient, maintainable, and user-friendly applications by controlling exactly how your data flows between view and view-model.


# @slotted Decorator

The `@slotted` decorator provides a declarative way to observe and react to changes in slotted content within your custom elements. This decorator automatically tracks which elements are projected into specific slots and provides your component with an up-to-date array of matching nodes.

## Overview

When building custom elements that accept slotted content, you often need to:

* Know which elements were projected into a specific slot
* Filter projected elements by CSS selector
* React when the projected content changes

The `@slotted` decorator handles all of this automatically, creating a reactive property that updates whenever the slotted content changes.

## Basic Usage

```typescript
import { slotted } from '@aurelia/runtime-html';

export class TabContainer {
  // Watch all elements in the default slot
  @slotted() tabs: Element[];

  tabsChanged(newTabs: Element[], oldTabs: Element[]) {
    console.log('Tabs changed:', newTabs);
  }
}
```

```html
<!-- tab-container.html -->
<div class="tab-container">
  <au-slot></au-slot>
</div>
```

**Usage:**

```html
<tab-container>
  <div class="tab">Tab 1</div>
  <div class="tab">Tab 2</div>
  <div class="tab">Tab 3</div>
</tab-container>
```

## Filtering with CSS Selectors

Use a CSS selector to filter which slotted elements are tracked:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class Accordion {
  // Only watch elements with class 'accordion-item'
  @slotted('.accordion-item') items: Element[];

  itemsChanged(newItems: Element[], oldItems: Element[]) {
    console.log(`Accordion now has ${newItems.length} items`);
  }
}
```

```html
<!-- accordion.html -->
<div class="accordion">
  <au-slot></au-slot>
</div>
```

**Usage:**

```html
<accordion>
  <div class="accordion-item">Item 1</div>
  <div class="accordion-item">Item 2</div>
  <div>This won't be tracked</div>
  <div class="accordion-item">Item 3</div>
</accordion>
```

## Targeting Specific Slots

When your component has multiple named slots, you can target specific slots:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class Dashboard {
  // Watch elements in the 'header' slot
  @slotted('*', 'header') headerItems: Element[];

  // Watch elements in the 'sidebar' slot
  @slotted('*', 'sidebar') sidebarItems: Element[];

  // Watch only buttons in the 'footer' slot
  @slotted('button', 'footer') footerButtons: Element[];
}
```

```html
<!-- dashboard.html -->
<div class="dashboard">
  <header>
    <au-slot name="header"></au-slot>
  </header>

  <aside>
    <au-slot name="sidebar"></au-slot>
  </aside>

  <main>
    <au-slot></au-slot> <!-- default slot -->
  </main>

  <footer>
    <au-slot name="footer"></au-slot>
  </footer>
</div>
```

**Usage:**

```html
<dashboard>
  <h1 au-slot="header">Dashboard Title</h1>
  <nav au-slot="sidebar">Sidebar Nav</nav>
  <p>Main content</p>
  <button au-slot="footer">Save</button>
  <button au-slot="footer">Cancel</button>
</dashboard>
```

## Watching All Slots

Use `'*'` as the slot name to watch all slots simultaneously:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class MultiSlotComponent {
  // Watch all div elements across all slots
  @slotted('div', '*') allDivs: Element[];

  allDivsChanged(newDivs: Element[], oldDivs: Element[]) {
    console.log(`Total div elements across all slots: ${newDivs.length}`);
  }
}
```

## Change Callbacks

The `@slotted` decorator automatically looks for a callback method following the naming convention `{propertyName}Changed`:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class CardList {
  @slotted('.card') cards: Element[];

  // This method is automatically called when cards change
  cardsChanged(newCards: Element[], oldCards: Element[]) {
    console.log(`Cards changed from ${oldCards.length} to ${newCards.length}`);
    this.updateCardIndexes();
  }

  private updateCardIndexes() {
    this.cards.forEach((card, index) => {
      card.setAttribute('data-index', String(index));
    });
  }
}
```

### Custom Callback Names

You can specify a custom callback method name:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class Gallery {
  @slotted({
    query: 'img',
    callback: 'handleImageChange'
  }) images: Element[];

  handleImageChange(newImages: Element[], oldImages: Element[]) {
    console.log('Images changed:', newImages);
  }
}
```

## Advanced Configuration

The `@slotted` decorator accepts a configuration object for fine-grained control:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class AdvancedComponent {
  @slotted({
    query: '.special-item',      // CSS selector to filter elements
    slotName: 'content',          // Name of the slot to watch
    callback: 'onItemsChanged'    // Custom callback method name
  }) specialItems: Element[];

  onItemsChanged(newItems: Element[], oldItems: Element[]) {
    console.log('Special items updated');
  }
}
```

### Configuration Options

| Property   | Type          | Default               | Description                                                                                              |
| ---------- | ------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
| `query`    | `string`      | `'*'`                 | CSS selector to filter slotted elements. Use `'*'` to match all elements, `'$all'` to include text nodes |
| `slotName` | `string`      | `'default'`           | Name of the slot to watch. Use `'*'` to watch all slots                                                  |
| `callback` | `PropertyKey` | `'{property}Changed'` | Name of the callback method to invoke when slotted content changes                                       |

## Querying All Nodes Including Text Nodes

By default, `@slotted` only tracks element nodes. To include text nodes, use the special query `'$all'`:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class TextAwareComponent {
  // Track all nodes including text nodes
  @slotted('$all') allNodes: Node[];

  allNodesChanged(newNodes: Node[], oldNodes: Node[]) {
    const textContent = newNodes
      .filter(node => node.nodeType === Node.TEXT_NODE)
      .map(node => node.textContent?.trim())
      .filter(Boolean)
      .join(' ');

    console.log('Text content:', textContent);
  }
}
```

## Complex Selectors

The `query` parameter accepts any valid CSS selector:

```typescript
import { slotted } from '@aurelia/runtime-html';

export class ComplexSelectors {
  // Only direct children with specific class
  @slotted('> .direct-child') directChildren: Element[];

  // Elements with specific data attribute
  @slotted('[data-type="widget"]') widgets: Element[];

  // Multiple selectors
  @slotted('button, a, input') interactiveElements: Element[];

  // Pseudo-selectors
  @slotted(':not(.excluded)') includedElements: Element[];
}
```

## Complete Example: Dynamic Tab Component

Here's a comprehensive example showing how to build a tab component using `@slotted`:

```typescript
// tab-panel.ts
import { slotted } from '@aurelia/runtime-html';

export class TabPanel {
  @slotted('.tab-header') tabHeaders: Element[];
  @slotted('.tab-content') tabContents: Element[];

  private activeIndex: number = 0;

  tabHeadersChanged(newHeaders: Element[]) {
    this.setupTabs();
  }

  tabContentsChanged(newContents: Element[]) {
    this.setupTabs();
  }

  private setupTabs() {
    if (this.tabHeaders.length === 0 || this.tabContents.length === 0) return;

    // Setup click handlers on headers
    this.tabHeaders.forEach((header, index) => {
      header.addEventListener('click', () => this.activateTab(index));
      header.setAttribute('role', 'tab');
      header.setAttribute('tabindex', index === this.activeIndex ? '0' : '-1');
    });

    // Setup content panels
    this.tabContents.forEach((content, index) => {
      content.setAttribute('role', 'tabpanel');
    });

    this.activateTab(this.activeIndex);
  }

  private activateTab(index: number) {
    if (index < 0 || index >= this.tabHeaders.length) return;

    this.activeIndex = index;

    // Update headers
    this.tabHeaders.forEach((header, i) => {
      header.classList.toggle('active', i === index);
      header.setAttribute('aria-selected', String(i === index));
      header.setAttribute('tabindex', i === index ? '0' : '-1');
    });

    // Update content
    this.tabContents.forEach((content, i) => {
      content.classList.toggle('active', i === index);
      content.setAttribute('aria-hidden', String(i !== index));
    });
  }
}
```

```html
<!-- tab-panel.html -->
<div class="tab-panel" role="tablist">
  <au-slot></au-slot>
</div>
```

**Usage:**

```html
<tab-panel>
  <div class="tab-header">Profile</div>
  <div class="tab-content">
    <h2>User Profile</h2>
    <p>Profile information goes here...</p>
  </div>

  <div class="tab-header">Settings</div>
  <div class="tab-content">
    <h2>Settings</h2>
    <p>User settings go here...</p>
  </div>

  <div class="tab-header">Messages</div>
  <div class="tab-content">
    <h2>Messages</h2>
    <p>User messages go here...</p>
  </div>
</tab-panel>
```

## Subscribing to Changes Programmatically

The property decorated with `@slotted` has a special `getObserver()` method that returns a subscriber collection:

```typescript
import { slotted } from '@aurelia/runtime-html';
import { ICustomElementController } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export class ObservableSlots {
  private controller = resolve(ICustomElementController);

  @slotted('.item') items: Element[];

  bound() {
    // Get the observer for the slotted property
    const observer = (this.items as any).getObserver?.();

    if (observer) {
      observer.subscribe({
        handleSlotChange: (nodes: Node[]) => {
          console.log('Items changed via subscription:', nodes);
        }
      });
    }
  }
}
```

## Lifecycle and Timing

The `@slotted` decorator integrates with Aurelia's lifecycle:

* **Activation**: The watcher starts observing during the `binding` lifecycle
* **Deactivation**: The watcher stops observing during the `unbinding` lifecycle
* **Initial Callback**: The change callback is invoked after `bound` with the initial slotted elements
* **Updates**: The callback is invoked whenever slotted content changes (elements added, removed, or reordered)

```typescript
import { slotted } from '@aurelia/runtime-html';

export class LifecycleExample {
  @slotted('.item') items: Element[];

  binding() {
    console.log('Component binding - watcher will start soon');
  }

  bound() {
    console.log('Component bound - initial items:', this.items);
  }

  itemsChanged(newItems: Element[]) {
    console.log('Items changed:', newItems);
    // This will be called:
    // 1. After bound() with initial elements
    // 2. Whenever slotted content changes
  }

  unbinding() {
    console.log('Component unbinding - watcher will stop');
  }
}
```

## Comparison with @children Decorator

Both `@slotted` and `@children` decorators watch for changes in child elements, but they serve different purposes:

| Feature        | @slotted                                         | @children                                |
| -------------- | ------------------------------------------------ | ---------------------------------------- |
| **Purpose**    | Watch slotted content projected into `<au-slot>` | Watch direct child elements of the host  |
| **Use with**   | Shadow DOM or `<au-slot>` components             | Any component                            |
| **Filters by** | CSS selector + slot name                         | CSS selector only                        |
| **Tracks**     | Content from parent component                    | Direct children only                     |
| **Best for**   | Content projection scenarios                     | Observing component's immediate children |

**Use `@slotted` when:**

* You're using `<au-slot>` for content projection
* You need to track which content was projected into which slot
* You want to filter projected content by selector

**Use `@children` when:**

* You need to observe the direct children of your component's host element
* You're not using slots
* You need access to child component view models (via the `filter` and `map` options)

## Important Notes

* The `@slotted` decorator only works with `<au-slot>`, not native `<slot>` elements
* The decorated property becomes read-only; attempting to set it manually has no effect
* Changes to the slotted content are detected via `MutationObserver`, so deep changes within slotted elements aren't automatically detected
* The query selector is evaluated against each slotted node; complex selectors may impact performance with many slotted elements

## See Also

* [Slotted Content](/components/shadow-dom-and-slots) - Overview of slots in Aurelia
* [@children Decorator](/components/shadow-dom-and-slots#at-the-projection-source-custom-element-host-with-the-children-decorator) - Alternative for watching child elements
* [Custom Elements](/components/components) - Building custom elements
* [Lifecycle Hooks](/components/component-lifecycles) - Component lifecycle integration


# Spread operators

Aurelia supports two different spread features in templates:

1. **Bindables spreading**: bind multiple properties from an object onto a custom element’s bindable properties.
2. **Attribute transferring**: forward captured attributes/bindings from a custom element usage to an element inside its template.

This page focuses on the **template syntax** and common gotchas. For the deeper conceptual guide, see:

* [Bindable Properties](/components/bindable-properties) (Bindables spreading, Attributes Transferring)
* [Attribute Transferring](/getting-to-know-aurelia/introduction/attribute-transferring)

***

## 1) Bindables spreading

Bindables spreading is for **custom element usage**. It creates one-way (`to-view`) bindings from an object’s properties to matching bindable properties on the custom element.

### Syntax options

```html
<!-- Recommended: put the expression in the attribute value -->
<user-card ...$bindables="user"></user-card>

<!-- Equivalent: explicit binding command form -->
<user-card $bindables.spread="user"></user-card>

<!-- Shorthand: put the expression in the attribute name (no spaces!) -->
<user-card ...user></user-card>
```

### Example

```ts
import { bindable } from 'aurelia';

export class UserCard {
  @bindable name!: string;
  @bindable email!: string;
  @bindable avatarUrl!: string;
}
```

```ts
export class MyApp {
  user = {
    name: 'Jane Doe',
    email: 'jane@example.com',
    avatarUrl: 'https://example.com/avatar.jpg',
    extra: 'ignored'
  };
}
```

```html
<user-card ...$bindables="user"></user-card>
```

Only keys that match bindable **property names** (`name`, `email`, `avatarUrl`) create bindings. Extra keys are ignored.

### Important gotchas

* **Keys are property names, not attribute names**: `firstName` matches `@bindable firstName`, but `first-name` does not.
* **Shorthand is HTML-case-sensitive in a bad way**: HTML lowercases attribute names, so `...firstName` becomes `...firstname` and won’t match your view-model property. Prefer `...$bindables="firstName"` when case matters.
* **No spaces in shorthand**: `...a + b` becomes multiple attributes (`...a`, `+`, `b`) and will not work. Use `...$bindables="a + b"`.
* **Direction is always one-way (`to-view`)**: bindables spreading does not support `two-way`/`from-view`. Binding behaviors can’t change direction, but can still affect evaluation (e.g. `...$bindables="user & debounce:200"`).
* **Bindings are based on existing keys**: if the object did not have a key at the time it was evaluated, no binding is created for that property. To “add” bindings, assign a **new object**.

### Avoid double-binding the same property

You can technically combine spread syntax with explicit bindings, but it creates multiple bindings targeting the same property and can be confusing.

Prefer one approach, or ensure the intent is obvious and well-documented. If you do mix them, the last declared binding typically “wins” at update time.

***

## 2) Attribute transferring (`...$attrs`)

Attribute transferring is for **inside a component template**: it forwards “captured” attributes and bindings from the component’s usage to an element inside the component.

### Minimal example

Enable capturing on the custom element definition:

```ts
import { customElement, bindable } from 'aurelia';

@customElement({
  name: 'form-input',
  capture: true,
  template: `
    <label>
      \${label}
      <input ...$attrs>
    </label>
  `
})
export class FormInput {
  @bindable label!: string;
  @bindable value!: string;
}
```

Use it like a normal input:

```html
<form-input
  label="Email"
  value.bind="email"
  placeholder="name@example.com"
  input.trigger="validate($event)">
</form-input>
```

Everything that is captured from `<form-input ...>` will be applied to the inner `<input ...$attrs>`.

### What gets captured?

When `capture: true` is enabled, Aurelia captures (for later spreading) **everything except**:

* **custom element bindables** (those are handled as bindables, not captured)
* **template controllers** (like `if`, `repeat.for`, `with`, `portal`, etc.)

You can also supply a capture filter function if you only want to capture certain attributes.

### Restrictions

* You **cannot** transfer template controllers via `...$attrs` (you’ll get a compile error).
* Don’t overuse deep “pass-through” chains; one or two layers is usually plenty.


# Forms

Master Aurelia 2 forms with comprehensive coverage of binding patterns, advanced  collections, validation integration, and performance optimization for production applications.


# Form Basics

Forms are the cornerstone of interactive web applications. Whether you're building simple contact forms, complex data-entry systems, or dynamic configuration interfaces, Aurelia provides a comprehensive and performant forms system.

{% hint style="info" %}
This guide assumes familiarity with Aurelia's binding system and template syntax. For fundamentals, see [Template Syntax & Features](/templates/overview) first.
{% endhint %}

## Quick Navigation

* [**Basic Inputs**](#basic-input-binding) - Text, textarea, number, date inputs
* [**Collections**](/templates/forms/collections) - Checkboxes, radios, multi-select, arrays
* [**Form Submission**](/templates/forms/submission) - Submit forms, handle events
* [**File Uploads**](/templates/forms/file-uploads) - Handle file inputs and uploads
* [**Validation Plugin**](/aurelia-packages/validation) - Integrate with @aurelia/validation

## Understanding Aurelia's Form Architecture

Aurelia's forms system is built on sophisticated observer patterns that provide automatic synchronization between your view models and form controls.

### Data Flow Architecture

```
User Input → DOM Event → Observer → Binding → View Model → Reactive Updates
     ↑                                                            ↓
Form Element ← DOM Update ← Binding ← Property Change ← View Model
```

**Key Components:**

1. **Observers**: Monitor DOM events and property changes
2. **Bindings**: Connect observers to view model properties
3. **Collection Observers**: Handle arrays, Sets, and Maps efficiently
4. **Mutation Observers**: Track dynamic DOM changes
5. **Value Converters & Binding Behaviors**: Transform and control data flow

### Automatic Change Detection

Aurelia automatically observes:

* **Text inputs**: `input`, `change`, `keyup` events
* **Checkboxes/Radio**: `change` events with array synchronization
* **Select elements**: `change` events with mutation observation
* **Collections**: Array mutations, Set/Map changes
* **Object properties**: Deep property observation

This means you typically don't need manual event handlers—Aurelia handles the complexity automatically while providing hooks for customization when needed.

## Basic Input Binding

Aurelia provides intuitive two-way binding for all standard form elements. Let's start with the fundamentals.

### Simple Text Inputs

The foundation of most forms is text input binding:

```html
<form submit.trigger="handleSubmit()">
  <div class="form-group">
    <label for="email">Email:</label>
    <input id="email"
           type="email"
           value.bind="email"
           placeholder.bind="emailPlaceholder" />
  </div>
  <div class="form-group">
    <label for="password">Password:</label>
    <input id="password"
           type="password"
           value.bind="password" />
  </div>
  <button type="submit" disabled.bind="!isFormValid">Login</button>
</form>
```

```typescript
export class LoginComponent {
  email = '';
  password = '';
  emailPlaceholder = 'Enter your email address';

  get isFormValid(): boolean {
    return this.email.length > 0 && this.password.length >= 8;
  }

  handleSubmit() {
    if (this.isFormValid) {
      console.log('Submitting:', { email: this.email, password: this.password });
    }
  }
}
```

**Key points:**

* Use `value.bind` for two-way binding
* Form inputs default to two-way binding automatically
* Computed properties (like `isFormValid`) automatically update

### Textarea Binding

Textareas work identically to text inputs:

```html
<div class="form-group">
  <label for="comments">Comments:</label>
  <textarea id="comments"
            value.bind="comments"
            rows="4"
            maxlength.bind="maxCommentLength"></textarea>
  <small>${comments.length}/${maxCommentLength} characters</small>
</div>
```

```typescript
export class FeedbackForm {
  comments = '';
  maxCommentLength = 500;
}
```

### Number and Date Inputs

Browser form controls always provide string values unless you bind to their typed DOM properties. Use Aurelia's `value-as-*` bindings when you need numbers or dates in your view-model.

```html
<div class="form-group">
  <label for="age">Age:</label>
  <input id="age"
         type="number"
         value-as-number.bind="age"
         min="18"
         max="120" />
</div>

<div class="form-group">
  <label for="birthdate">Birth Date:</label>
  <input id="birthdate"
         type="date"
         value-as-date.bind="birthDate" />
</div>

<div class="form-group">
  <label for="appointment">Appointment Time:</label>
  <input id="appointment"
         type="datetime-local"
         value-as-date.bind="appointmentTime" />
</div>
```

```typescript
export class ProfileForm {
  age = 25;
  birthDate = new Date('1998-01-01');
  appointmentTime = new Date();

  get isAdult(): boolean {
    return this.age >= 18;
  }
}
```

`value-as-number` binds to the input's `valueAsNumber`, so `age` is a `number` (or `NaN` when the field is empty/invalid). `value-as-date` binds to `valueAsDate`, giving you a `Date | null`. If you keep `value.bind`, the value remains a string—convert it before serializing to JSON for APIs.

### Input Types and Binding

Aurelia supports all HTML5 input types:

| Type             | Value                                       | Common Use         |
| ---------------- | ------------------------------------------- | ------------------ |
| `text`           | string                                      | General text input |
| `email`          | string                                      | Email addresses    |
| `password`       | string                                      | Password fields    |
| `number`         | string (use `value-as-number` for `number`) | Numeric input      |
| `tel`            | string                                      | Phone numbers      |
| `url`            | string                                      | Website URLs       |
| `search`         | string                                      | Search queries     |
| `date`           | string (use `value-as-date` for `Date`)     | Date selection     |
| `time`           | string                                      | Time selection     |
| `datetime-local` | string (use `value-as-date` for `Date`)     | Date and time      |
| `color`          | string                                      | Color picker       |
| `range`          | string (use `value-as-number` for `number`) | Slider input       |

### Binding Modes

While `value.bind` is automatic two-way binding, you can be explicit:

```html
<!-- Two-way binding (default for inputs) -->
<input value.two-way="username">

<!-- To-view (view model → view) -->
<input value.to-view="displayName">

<!-- From view (view → view model) -->
<input value.from-view="searchQuery">

<!-- One-time (set once, no updates) -->
<input value.one-time="initialValue">
```

**When to use each:**

* `.bind` - Default, use for most form inputs
* `.two-way` - Explicit two-way, same as `.bind` for inputs
* `.to-view` - Read-only inputs, display-only values
* `.from-view` - Capture input without updating view
* `.one-time` - Static initial values

### Real-World Example

Here's a complete user registration form:

```html
<form submit.trigger="register()" class="registration-form">
  <h2>Create Account</h2>

  <!-- Username -->
  <div class="form-group">
    <label for="username">Username *</label>
    <input id="username"
           type="text"
           value.bind="form.username"
           required
           minlength="3"
           maxlength="20">
    <small>3-20 characters</small>
  </div>

  <!-- Email -->
  <div class="form-group">
    <label for="email">Email *</label>
    <input id="email"
           type="email"
           value.bind="form.email"
           required>
  </div>

  <!-- Password -->
  <div class="form-group">
    <label for="password">Password *</label>
    <input id="password"
           type="password"
           value.bind="form.password"
           required
           minlength="8">
    <small>At least 8 characters</small>
  </div>

  <!-- Confirm Password -->
  <div class="form-group">
    <label for="confirmPassword">Confirm Password *</label>
    <input id="confirmPassword"
           type="password"
           value.bind="form.confirmPassword"
           required>
    <span if.bind="form.password !== form.confirmPassword" class="error">
      Passwords must match
    </span>
  </div>

  <!-- Age -->
  <div class="form-group">
    <label for="age">Age</label>
    <input id="age"
           type="number"
           value.bind="form.age"
           min="13"
           max="120">
  </div>

  <!-- Bio -->
  <div class="form-group">
    <label for="bio">Bio</label>
    <textarea id="bio"
              value.bind="form.bio"
              maxlength="500"
              rows="4"></textarea>
    <small>${form.bio.length}/500 characters</small>
  </div>

  <!-- Submit -->
  <button type="submit"
          disabled.bind="!isFormValid"
          class="btn-primary">
    Create Account
  </button>
</form>
```

```typescript
export class Registration {
  form = {
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    age: null,
    bio: ''
  };

  get isFormValid(): boolean {
    return this.form.username.length >= 3 &&
           this.form.email.includes('@') &&
           this.form.password.length >= 8 &&
           this.form.password === this.form.confirmPassword;
  }

  register() {
    if (this.isFormValid) {
      console.log('Registering user:', this.form);
      // API call here
    }
  }
}
```

## Next Steps

Now that you understand basic form inputs, explore:

* [**Collections**](/templates/forms/collections) - Checkboxes, radio buttons, and multi-select
* [**Validation Plugin**](/aurelia-packages/validation) - Integrate form validation
* [**File Uploads**](/templates/forms/file-uploads) - Handle file inputs
* [**Form Submission**](/templates/forms/submission) - Submit and process forms
* [**Template Recipes**](/templates/real-world-recipes/recipes) - Real-world examples

## Quick Tips

1. **Always use labels** - Associate labels with inputs using `for` attribute
2. **Validate on submit** - Don't validate every keystroke unless needed
3. **Provide feedback** - Show errors clearly after user completes input
4. **Use computed properties** - Let Aurelia handle form state reactively
5. **Keep it simple** - Don't overcomplicate with manual DOM manipulation

## Common Pitfalls

* **Forgetting `.bind`** - Must use `.bind` for two-way binding
* **Type mismatches** - Number inputs return strings, convert if needed
* **Direct object mutation** - Use `this.form.prop = value`, not `form[prop] = value`
* **Missing labels** - Always include labels for accessibility

## Related Documentation

* [Template Syntax Overview](/templates/overview)
* [Attribute Binding](/templates/overview/attribute-binding)
* [Event Binding](/templates/overview/event-binding)
* [Validation Plugin](/aurelia-packages/validation)


# Collections (Checkboxes, Radios, Select)

Learn how to work with checkboxes, radio buttons, select elements, and advanced collection patterns in Aurelia forms.

## Overview

Aurelia provides sophisticated support for collection-based form controls, going beyond simple arrays to support Sets, Maps, and custom collection types with optimal performance.

## Checkboxes

### Boolean Checkboxes

The simplest checkbox pattern binds to boolean properties:

```typescript
export class PreferencesForm {
  emailNotifications = false;
  smsNotifications = true;
  pushNotifications = false;

  get hasValidNotificationPrefs(): boolean {
    return this.emailNotifications || this.smsNotifications || this.pushNotifications;
  }
}
```

```html
<form>
  <fieldset>
    <legend>Notification Preferences</legend>
    <label>
      <input type="checkbox" checked.bind="emailNotifications" />
      Email notifications
    </label>
    <label>
      <input type="checkbox" checked.bind="smsNotifications" />
      SMS notifications
    </label>
    <label>
      <input type="checkbox" checked.bind="pushNotifications" />
      Push notifications
    </label>
  </fieldset>

  <div if.bind="!hasValidNotificationPrefs" class="warning">
    Please select at least one notification method.
  </div>
</form>
```

**Key Points:**

* Use `checked.bind` for boolean checkboxes
* Works with any boolean property
* Great for independent on/off toggles

### Array-Based Multi-Select

For multi-select scenarios, bind arrays to checkbox groups using `model.bind`:

```typescript
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

export class ProductSelectionForm {
  products: Product[] = [
    { id: 1, name: "Gaming Mouse", category: "Peripherals", price: 89.99 },
    { id: 2, name: "Mechanical Keyboard", category: "Peripherals", price: 159.99 },
    { id: 3, name: "4K Monitor", category: "Display", price: 399.99 },
    { id: 4, name: "Graphics Card", category: "Components", price: 599.99 }
  ];

  // Array of selected product IDs
  selectedProductIds: number[] = [];

  // Array of selected product objects
  selectedProducts: Product[] = [];

  get totalValue(): number {
    return this.selectedProducts.reduce((sum, product) => sum + product.price, 0);
  }
}
```

```html
<form>
  <h3>Select Products</h3>

  <!-- ID-based selection -->
  <div class="product-grid">
    <div repeat.for="product of products" class="product-card">
      <label>
        <input type="checkbox"
               model.bind="product.id"
               checked.bind="selectedProductIds" />
        <strong>${product.name}</strong>
        <span class="category">${product.category}</span>
        <span class="price">$${product.price}</span>
      </label>
    </div>
  </div>

  <!-- Object-based selection (more flexible) -->
  <h4>Or select complete product objects:</h4>
  <div class="product-list">
    <label repeat.for="product of products" class="product-item">
      <input type="checkbox"
             model.bind="product"
             checked.bind="selectedProducts" />
      ${product.name} - $${product.price}
    </label>
  </div>

  <div class="summary" if.bind="selectedProducts.length">
    <h4>Selected Items (${selectedProducts.length})</h4>
    <ul>
      <li repeat.for="product of selectedProducts">
        ${product.name} - $${product.price}
      </li>
    </ul>
    <strong>Total: $${totalValue}</strong>
  </div>
</form>
```

**How It Works:**

1. `model.bind` tells Aurelia what value to add to the array
2. `checked.bind` points to the array that holds selected values
3. Aurelia automatically adds/removes values when checkboxes are toggled

**Use Cases:**

* Multi-select forms (select multiple skills, interests, tags)
* Batch operations (select multiple items for deletion)
* Filter selections (select multiple categories to filter by)

### Set-Based Collections

For high-performance scenarios with frequent additions/removals, use Set collections:

```typescript
export class TagSelectionForm {
  availableTags = [
    { id: 'frontend', name: 'Frontend Development', color: '#blue' },
    { id: 'backend', name: 'Backend Development', color: '#green' },
    { id: 'database', name: 'Database Design', color: '#orange' },
    { id: 'devops', name: 'DevOps', color: '#purple' },
    { id: 'mobile', name: 'Mobile Development', color: '#red' }
  ];

  // Set-based selection for O(1) lookups
  selectedTags: Set<string> = new Set(['frontend', 'database']);

  // Custom matcher for Set operations
  tagMatcher = (a: any, b: any) => {
    if (typeof a === 'string' && typeof b === 'object') return a === b.id;
    if (typeof b === 'string' && typeof a === 'object') return b === a.id;
    return a === b;
  };

  get selectedTagList() {
    return this.availableTags.filter(tag => this.selectedTags.has(tag.id));
  }

  toggleTag(tagId: string) {
    if (this.selectedTags.has(tagId)) {
      this.selectedTags.delete(tagId);
    } else {
      this.selectedTags.add(tagId);
    }
  }
}
```

```html
<form>
  <h3>Select Your Skills</h3>
  <div class="tag-container">
    <label repeat.for="tag of availableTags"
           class="tag-label">
      <input type="checkbox"
             model.bind="tag.id"
             checked.bind="selectedTags"
             matcher.bind="tagMatcher" />
      <span class="tag-text">${tag.name}</span>
    </label>
  </div>

  <div if.bind="selectedTags.size > 0" class="selected-tags">
    <h4>Selected Skills (${selectedTags.size})</h4>
    <div class="tag-chips">
      <span repeat.for="tag of selectedTagList" class="tag-chip">
        ${tag.name}
        <button type="button"
                click.trigger="toggleTag(tag.id)"
                class="remove-tag">×</button>
      </span>
    </div>
  </div>
</form>
```

**Why Use Sets:**

* O(1) lookup performance with `.has()`
* Efficient for large collections
* Natural for unique value storage
* Better performance for frequent add/remove operations

### Map-Based Collections

For complex key-value selections, Maps provide the most flexibility:

```typescript
interface Permission {
  resource: string;
  actions: string[];
  description: string;
}

export class PermissionForm {
  permissions: Permission[] = [
    {
      resource: 'users',
      actions: ['create', 'read', 'update', 'delete'],
      description: 'User management operations'
    },
    {
      resource: 'posts',
      actions: ['create', 'read', 'update', 'delete', 'publish'],
      description: 'Content management operations'
    },
    {
      resource: 'settings',
      actions: ['read', 'update'],
      description: 'System configuration'
    }
  ];

  // Map: resource -> Set<action>
  selectedPermissions: Map<string, Set<string>> = new Map();

  constructor() {
    // Initialize with default permissions
    this.selectedPermissions.set('users', new Set(['read']));
    this.selectedPermissions.set('posts', new Set(['read', 'create']));
  }

  hasPermission(resource: string, action: string): boolean {
    return this.selectedPermissions.get(resource)?.has(action) ?? false;
  }

  togglePermission(resource: string, action: string) {
    if (!this.selectedPermissions.has(resource)) {
      this.selectedPermissions.set(resource, new Set());
    }

    const resourcePerms = this.selectedPermissions.get(resource)!;
    if (resourcePerms.has(action)) {
      resourcePerms.delete(action);
    } else {
      resourcePerms.add(action);
    }
  }

  get permissionSummary() {
    const summary: Array<{ resource: string; actions: string[] }> = [];
    this.selectedPermissions.forEach((actions, resource) => {
      if (actions.size > 0) {
        summary.push({ resource, actions: Array.from(actions) });
      }
    });
    return summary;
  }
}
```

```html
<form>
  <h3>Configure Permissions</h3>
  <div class="permission-matrix">
    <div repeat.for="permission of permissions" class="permission-group">
      <h4>${permission.resource}</h4>
      <p class="description">${permission.description}</p>
      <div class="action-checkboxes">
        <label repeat.for="action of permission.actions" class="action-label">
          <input type="checkbox"
                 checked.bind="hasPermission(permission.resource, action)"
                 change.trigger="togglePermission(permission.resource, action)" />
          ${action}
        </label>
      </div>
    </div>
  </div>

  <div if.bind="permissionSummary.length > 0" class="permission-summary">
    <h4>Selected Permissions</h4>
    <ul>
      <li repeat.for="perm of permissionSummary">
        <strong>${perm.resource}</strong>: ${perm.actions.join(', ')}
      </li>
    </ul>
  </div>
</form>
```

**When to Use Maps:**

* Nested selection scenarios (resource → actions)
* Complex key-value relationships
* Grouped permissions or settings
* Multi-dimensional selections

## Radio Buttons

Radio buttons are for single-selection from multiple options.

### Basic Radio Buttons

```typescript
export class ShippingForm {
  shippingMethods = ['Standard', 'Express', 'Overnight'];
  selectedMethod = 'Standard';
}
```

```html
<fieldset>
  <legend>Shipping Method</legend>
  <label repeat.for="method of shippingMethods">
    <input type="radio"
           name="shipping"
           model.bind="method"
           checked.bind="selectedMethod" />
    ${method}
  </label>
</fieldset>

<p>Selected: ${selectedMethod}</p>
```

### Radio Buttons with Objects

```typescript
interface PaymentMethod {
  id: string;
  type: 'credit' | 'debit' | 'paypal' | 'crypto';
  name: string;
  fee: number;
  processingTime: string;
  requiresVerification: boolean;
}

export class PaymentSelectionForm {
  paymentMethods: PaymentMethod[] = [
    {
      id: 'cc-visa',
      type: 'credit',
      name: 'Visa Credit Card',
      fee: 0,
      processingTime: 'Instant',
      requiresVerification: false
    },
    {
      id: 'pp-account',
      type: 'paypal',
      name: 'PayPal Account',
      fee: 2.50,
      processingTime: '1-2 business days',
      requiresVerification: true
    },
    {
      id: 'btc-wallet',
      type: 'crypto',
      name: 'Bitcoin Wallet',
      fee: 0.0001,
      processingTime: '10-60 minutes',
      requiresVerification: true
    }
  ];

  selectedPaymentMethod: PaymentMethod | null = null;

  // Custom matcher for complex object comparison
  paymentMethodMatcher = (a: PaymentMethod, b: PaymentMethod) => {
    return a?.id === b?.id;
  };

  get totalFee(): number {
    return this.selectedPaymentMethod?.fee || 0;
  }

  get requiresUserVerification(): boolean {
    return this.selectedPaymentMethod?.requiresVerification || false;
  }
}
```

```html
<form class="payment-selection-form">
  <h3>Select Payment Method</h3>

  <div class="payment-options">
    <div repeat.for="method of paymentMethods" class="payment-option">
      <label class="payment-card"
             class.bind="{ 'selected': selectedPaymentMethod?.id === method.id }">
        <input type="radio"
               name="paymentMethod"
               model.bind="method"
               checked.bind="selectedPaymentMethod"
               matcher.bind="paymentMethodMatcher" />

        <div class="payment-info">
          <div class="payment-header">
            <span class="payment-name">${method.name}</span>
            <span class="payment-type badge">${method.type}</span>
          </div>

          <div class="payment-details">
            <div class="processing-time">⏱️ ${method.processingTime}</div>
            <div class="fee-info">
              💵 ${method.fee === 0 ? 'No fees' : '$' + method.fee.toFixed(2)}
            </div>
            <div if.bind="method.requiresVerification" class="verification-required">
              🛡️ Verification required
            </div>
          </div>
        </div>
      </label>
    </div>
  </div>

  <!-- Selection Summary -->
  <div if.bind="selectedPaymentMethod" class="selection-summary">
    <h4>Payment Summary</h4>
    <p>Method: ${selectedPaymentMethod.name}</p>
    <p>Processing: ${selectedPaymentMethod.processingTime}</p>
    <p>Fee: ${totalFee === 0 ? 'Free' : '$' + totalFee.toFixed(2)}</p>
    <div if.bind="requiresUserVerification" class="warning">
      ⚠️ This payment method requires account verification
    </div>
  </div>
</form>
```

**Key Points:**

* Use same `name` attribute for all radios in a group
* `model.bind` defines the value when selected
* `checked.bind` holds the currently selected value
* Use `matcher.bind` for complex object comparison

## Select Elements

### Basic Select

```typescript
export class CountryForm {
  countries = ['USA', 'Canada', 'Mexico', 'UK', 'France', 'Germany'];
  selectedCountry = 'USA';
}
```

```html
<select value.bind="selectedCountry">
  <option repeat.for="country of countries" value.bind="country">
    ${country}
  </option>
</select>
```

### Select with Objects

```typescript
interface Country {
  code: string;
  name: string;
  region: string;
}

export class AdvancedCountryForm {
  countries: Country[] = [
    { code: 'US', name: 'United States', region: 'North America' },
    { code: 'CA', name: 'Canada', region: 'North America' },
    { code: 'MX', name: 'Mexico', region: 'North America' },
    { code: 'UK', name: 'United Kingdom', region: 'Europe' },
    { code: 'FR', name: 'France', region: 'Europe' },
    { code: 'DE', name: 'Germany', region: 'Europe' }
  ];

  selectedCountry: Country | null = null;

  // Custom matcher
  countryMatcher = (a: Country, b: Country) => a?.code === b?.code;
}
```

```html
<!-- Using model.bind for objects -->
<select value.bind="selectedCountry" matcher.bind="countryMatcher">
  <option model.bind="null">-- Select Country --</option>
  <option repeat.for="country of countries" model.bind="country">
    ${country.name}
  </option>
</select>

<p if.bind="selectedCountry">
  Selected: ${selectedCountry.name} (${selectedCountry.region})
</p>
```

### Select with Optgroups

```html
<select value.bind="selectedCountry" matcher.bind="countryMatcher">
  <option model.bind="null">-- Select Country --</option>
  <optgroup label="North America">
    <option repeat.for="country of countries | filter:isNorthAmerica"
            model.bind="country">
      ${country.name}
    </option>
  </optgroup>
  <optgroup label="Europe">
    <option repeat.for="country of countries | filter:isEurope"
            model.bind="country">
      ${country.name}
    </option>
  </optgroup>
</select>
```

### Multi-Select

```typescript
export class MultiSelectForm {
  availableSkills = ['JavaScript', 'TypeScript', 'Python', 'Java', 'C#', 'Go'];
  selectedSkills: string[] = ['JavaScript', 'TypeScript'];
}
```

```html
<select multiple value.bind="selectedSkills">
  <option repeat.for="skill of availableSkills" value.bind="skill">
    ${skill}
  </option>
</select>

<div if.bind="selectedSkills.length">
  <h4>Selected Skills (${selectedSkills.length})</h4>
  <ul>
    <li repeat.for="skill of selectedSkills">${skill}</li>
  </ul>
</div>
```

## Performance Considerations

Choose the right collection type for your use case:

| Collection Type | Best For                                  | Performance              |
| --------------- | ----------------------------------------- | ------------------------ |
| **Array**       | General purpose, small-medium collections | Good                     |
| **Set**         | Frequent additions/removals, uniqueness   | Excellent (O(1) lookups) |
| **Map**         | Key-value pairs, nested selections        | Excellent (O(1) lookups) |

**Performance Tips:**

* Use Set for large collections with frequent changes
* Implement efficient matcher functions for object comparison
* Avoid creating new objects in templates—use computed properties
* Consider virtualization for very large checkbox/radio lists

## Matchers Explained

By default, Aurelia compares values with strict equality (`===`). That works for primitives and for objects that are the exact same instance. It does not work when your selected value and your option values are different instances that represent the same logical entity (for example, data reloaded from an API). A matcher lets you define what "equal" means so selections stay in sync.

Matchers tell Aurelia how to compare values:

```typescript
// Simple matcher for objects with id property
simpleMatcher = (a, b) => a?.id === b?.id;

// Type-safe matcher
typedMatcher = (a: Product, b: Product) => a?.id === b?.id;

// Complex matcher with multiple criteria
complexMatcher = (a, b) => {
  if (!a || !b) return false;
  return a.id === b.id && a.version === b.version;
};

// Mixed type matcher (for Sets with objects)
mixedMatcher = (a: any, b: any) => {
  if (typeof a === 'string' && typeof b === 'object') return a === b.id;
  if (typeof b === 'string' && typeof a === 'object') return b === a.id;
  return a === b;
};
```

**When to use matchers:**

* Binding objects to checkboxes/radios
* Working with Sets containing objects
* Need custom equality logic
* Comparing by properties other than reference

## Common Patterns

### Select All / Deselect All

```typescript
export class BulkSelectionForm {
  items = [/* array of items */];
  selectedItems: any[] = [];

  get allSelected(): boolean {
    return this.selectedItems.length === this.items.length;
  }

  get someSelected(): boolean {
    return this.selectedItems.length > 0 && !this.allSelected;
  }

  toggleAll() {
    if (this.allSelected) {
      this.selectedItems = [];
    } else {
      this.selectedItems = [...this.items];
    }
  }
}
```

```html
<label>
  <input type="checkbox"
         checked.bind="allSelected"
         click.trigger="toggleAll()"
         indeterminate.bind="someSelected" />
  Select All
</label>

<label repeat.for="item of items">
  <input type="checkbox"
         model.bind="item"
         checked.bind="selectedItems" />
  ${item.name}
</label>
```

### Conditional Options

```html
<select value.bind="selectedOption">
  <option repeat.for="option of options"
          model.bind="option"
          disabled.bind="option.disabled">
    ${option.name}
    ${option.disabled ? '(unavailable)' : ''}
  </option>
</select>
```

## Related

* [Form Basics](/templates/forms/forms) - Basic form inputs
* [Validation](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/forms/validation.md) - Validate form inputs
* [Form Examples](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/forms/examples.md) - Complete examples
* [List Rendering](/templates/repeats-and-list-rendering) - Using repeat.for


# Form Submission

Learn how to handle form submissions with proper state management, error handling, and user feedback.

## Basic Form Submission

```typescript
export class ContactForm {
  formData = {
    name: '',
    email: '',
    message: ''
  };

  isSubmitting = false;
  successMessage = '';
  errorMessage = '';

  async handleSubmit() {
    this.isSubmitting = true;
    this.errorMessage = '';
    this.successMessage = '';

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(this.formData)
      });

      if (!response.ok) {
        throw new Error('Submission failed');
      }

      this.successMessage = 'Form submitted successfully!';
      this.resetForm();
    } catch (error) {
      this.errorMessage = 'Failed to submit form. Please try again.';
    } finally {
      this.isSubmitting = false;
    }
  }

  resetForm() {
    this.formData = { name: '', email: '', message: '' };
  }
}
```

```html
<form submit.trigger="handleSubmit()">
  <div class="form-group">
    <label>Name</label>
    <input value.bind="formData.name" required />
  </div>

  <div class="form-group">
    <label>Email</label>
    <input type="email" value.bind="formData.email" required />
  </div>

  <div class="form-group">
    <label>Message</label>
    <textarea value.bind="formData.message" required></textarea>
  </div>

  <div if.bind="successMessage" class="alert alert-success">
    ${successMessage}
  </div>

  <div if.bind="errorMessage" class="alert alert-error">
    ${errorMessage}
  </div>

  <button type="submit" disabled.bind="isSubmitting">
    ${isSubmitting ? 'Submitting...' : 'Submit'}
  </button>
</form>
```

## Preventing Default Submission

```html
<!-- Method 1: Use submit.trigger (recommended) -->
<form submit.trigger="handleSubmit()">
  <!-- form fields -->
</form>

<!-- Method 2: Prevent default in handler -->
<form submit.trigger="handleSubmit($event)">
  <!-- form fields -->
</form>
```

```typescript
handleSubmit(event?: Event) {
  event?.preventDefault();
  // your logic
}
```

## Validation Before Submission

```typescript
export class ValidatedForm {
  formData = { name: '', email: '' };

  get isValid(): boolean {
    return this.formData.name.length > 0 &&
           this.formData.email.includes('@');
  }

  handleSubmit() {
    if (!this.isValid) {
      alert('Please fill out all required fields');
      return;
    }

    // Submit form
  }
}
```

```html
<form submit.trigger="handleSubmit()">
  <!-- fields -->
  <button type="submit" disabled.bind="!isValid">Submit</button>
</form>
```

## Submission State Management

```typescript
interface SubmissionState {
  isSubmitting: boolean;
  success: boolean;
  error: string | null;
  attempts: number;
}

export class StatefulForm {
  formData = { /* ... */ };

  state: SubmissionState = {
    isSubmitting: false,
    success: false,
    error: null,
    attempts: 0
  };

  get canSubmit(): boolean {
    return !this.state.isSubmitting && this.state.attempts < 3;
  }

  async handleSubmit() {
    if (!this.canSubmit) return;

    this.state.isSubmitting = true;
    this.state.error = null;
    this.state.success = false;

    try {
      await this.submitData();
      this.state.success = true;

      setTimeout(() => this.resetForm(), 2000);
    } catch (error) {
      this.state.error = error.message;
      this.state.attempts++;
    } finally {
      this.state.isSubmitting = false;
    }
  }

  private async submitData() {
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.formData)
    });

    if (!response.ok) {
      throw new Error('Submission failed');
    }

    return response.json();
  }

  resetForm() {
    this.formData = { /* reset */ };
    this.state = {
      isSubmitting: false,
      success: false,
      error: null,
      attempts: 0
    };
  }
}
```

## Optimistic UI Updates

```typescript
export class OptimisticForm {
  items: Item[] = [];
  optimisticItem: Item | null = null;

  async addItem(item: Item) {
    // Add optimistically
    this.optimisticItem = { ...item, id: 'temp-' + Date.now() };
    this.items.push(this.optimisticItem);

    try {
      const result = await this.saveItem(item);

      // Replace optimistic item with real one
      const index = this.items.indexOf(this.optimisticItem);
      this.items[index] = result;
      this.optimisticItem = null;
    } catch (error) {
      // Remove optimistic item on error
      this.items = this.items.filter(i => i !== this.optimisticItem);
      this.optimisticItem = null;
      alert('Failed to add item');
    }
  }
}
```

## Debounced Auto-Save

```typescript
export class AutoSaveForm {
  formData = { title: '', content: '' };
  saveStatus: 'saved' | 'saving' | 'unsaved' = 'saved';
  saveTimer: any = null;

  formDataChanged() {
    this.saveStatus = 'unsaved';

    // Clear existing timer
    clearTimeout(this.saveTimer);

    // Set new timer for auto-save
    this.saveTimer = setTimeout(() => {
      this.autoSave();
    }, 2000);
  }

  async autoSave() {
    this.saveStatus = 'saving';

    try {
      await fetch('/api/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(this.formData)
      });

      this.saveStatus = 'saved';
    } catch (error) {
      this.saveStatus = 'unsaved';
    }
  }

  detaching() {
    clearTimeout(this.saveTimer);
  }
}
```

```html
<form>
  <input value.bind="formData.title" input.trigger="formDataChanged()" />
  <textarea value.bind="formData.content" input.trigger="formDataChanged()"></textarea>

  <span class="save-status">
    <span if.bind="saveStatus === 'saved'">✓ Saved</span>
    <span if.bind="saveStatus === 'saving'">Saving...</span>
    <span if.bind="saveStatus === 'unsaved'">Unsaved changes</span>
  </span>
</form>
```

## Rate Limiting

```typescript
export class RateLimitedForm {
  lastSubmission: Date | null = null;
  cooldownMs = 30000; // 30 seconds

  get canSubmit(): boolean {
    if (!this.lastSubmission) return true;

    const timeSince = Date.now() - this.lastSubmission.getTime();
    return timeSince > this.cooldownMs;
  }

  get cooldownRemaining(): number {
    if (!this.lastSubmission) return 0;

    const timeSince = Date.now() - this.lastSubmission.getTime();
    const remaining = this.cooldownMs - timeSince;
    return Math.max(0, Math.ceil(remaining / 1000));
  }

  async handleSubmit() {
    if (!this.canSubmit) {
      alert(`Please wait ${this.cooldownRemaining} seconds before submitting again`);
      return;
    }

    // Submit form
    await this.submitData();
    this.lastSubmission = new Date();
  }
}
```

## Multi-Step Forms

See the [Template Recipes collection](/templates/real-world-recipes/recipes) for complete examples, including community-contributed multi-step flows.

## Best Practices

1. **Always provide feedback** - Show loading, success, and error states
2. **Disable submit button** - Prevent multiple submissions
3. **Handle errors gracefully** - Show user-friendly error messages
4. **Validate before submitting** - Client-side validation for UX
5. **Reset form after success** - Clear form or redirect user
6. **Implement rate limiting** - Prevent spam submissions
7. **Use proper HTTP methods** - POST for creation, PUT/PATCH for updates

## Common Patterns

### Submit on Enter Key

```html
<form submit.trigger="handleSubmit()">
  <input value.bind="query" keydown.trigger:enter="handleSubmit()" />
</form>
```

### Confirm Before Submit

```typescript
handleSubmit() {
  if (!confirm('Are you sure you want to submit?')) {
    return;
  }
  // Proceed with submission
}
```

### Redirect After Success

```typescript
import { resolve } from '@aurelia/kernel';
import { IRouter } from '@aurelia/router';

export class FormWithRedirect {
  private readonly router = resolve(IRouter);

  async handleSubmit() {
    await this.submitData();
    this.router.load('/success');
  }
}
```

## Related

* [Form Basics](/templates/forms/forms)
* [Validation](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/forms/validation.md)
* [File Uploads](/templates/forms/file-uploads)
* [Form Examples](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/forms/examples.md)


# File Uploads

Learn how to handle file inputs and uploads in Aurelia forms.

## Basic File Input

```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>
```

```typescript
export class FileUploadComponent {
  selectedFiles: File[] = [];

  handleFileSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    if (!input.files?.length) return;

    this.selectedFiles = Array.from(input.files);
  }

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

    const formData = new FormData();
    for (const file of this.selectedFiles) {
      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);
      this.selectedFiles = [];
    } catch (error) {
      console.error('Error uploading files:', error);
    }
  }
}
```

## Single File Upload

```html
<input type="file" accept="image/*" change.trigger="handleFileSelect($event)" />
```

```typescript
handleFileSelect(event: Event) {
  const input = event.target as HTMLInputElement;
  this.selectedFiles = input.files?.length ? [input.files[0]] : [];
}
```

## File Preview

```typescript
export class FilePreviewComponent {
  selectedFile: File | null = null;
  previewUrl: string | null = null;

  handleFileSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];

    if (file) {
      this.selectedFile = file;
      this.createPreview(file);
    }
  }

  private createPreview(file: File) {
    if (this.previewUrl) {
      URL.revokeObjectURL(this.previewUrl);
    }

    this.previewUrl = URL.createObjectURL(file);
  }

  detaching() {
    if (this.previewUrl) {
      URL.revokeObjectURL(this.previewUrl);
    }
  }
}
```

```html
<input type="file" accept="image/*" change.trigger="handleFileSelect($event)" />

<div if.bind="previewUrl" class="preview">
  <img src.bind="previewUrl" alt="Preview" />
  <p>${selectedFile.name} (${(selectedFile.size / 1024).toFixed(2)} KB)</p>
</div>
```

## Validation

```typescript
export class ValidatedFileUpload {
  selectedFile: File | null = null;
  error: string | null = null;

  maxSize = 5 * 1024 * 1024; // 5MB
  allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

  handleFileSelect(event: Event) {
    this.error = null;
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];

    if (!file) return;

    if (!this.allowedTypes.includes(file.type)) {
      this.error = 'Only JPEG, PNG, and GIF images are allowed';
      input.value = '';
      return;
    }

    if (file.size > this.maxSize) {
      this.error = `File size must be less than ${this.maxSize / (1024 * 1024)}MB`;
      input.value = '';
      return;
    }

    this.selectedFile = file;
  }
}
```

## Progress Tracking

```typescript
export class FileUploadWithProgress {
  uploadProgress = 0;
  isUploading = false;

  async uploadWithProgress(file: File) {
    this.isUploading = true;
    this.uploadProgress = 0;

    const xhr = new XMLHttpRequest();

    return new Promise((resolve, reject) => {
      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
          this.uploadProgress = (e.loaded / e.total) * 100;
        }
      });

      xhr.addEventListener('load', () => {
        this.isUploading = false;
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(`Upload failed: ${xhr.status}`));
        }
      });

      xhr.addEventListener('error', () => {
        this.isUploading = false;
        reject(new Error('Upload failed'));
      });

      const formData = new FormData();
      formData.append('file', file);

      xhr.open('POST', '/api/upload');
      xhr.send(formData);
    });
  }
}
```

```html
<input type="file" change.trigger="handleFileSelect($event)" />

<div if.bind="isUploading" class="progress">
  <div class="progress-bar" css="width: ${uploadProgress}%"></div>
  <span>${uploadProgress.toFixed(0)}%</span>
</div>
```

## Best Practices

1. **Validate on both client and server** - Always verify file types and sizes server-side
2. **Clean up object URLs** - Revoke object URLs in `detaching()` to prevent memory leaks
3. **Show progress for large files** - Use XMLHttpRequest for progress tracking
4. **Provide clear feedback** - Show file names, sizes, and upload status
5. **Handle errors gracefully** - Display meaningful error messages

## Security Considerations

* Validate file types server-side (don't trust `accept` attribute)
* Check file sizes to prevent DoS attacks
* Scan uploaded files for malware
* Store files outside web root
* Use unique filenames to prevent overwrites
* Implement rate limiting

## Related

* [Form Basics](/templates/forms/forms)
* [Form Submission](/templates/forms/submission)
* [Form Examples](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/forms/examples.md)


# Advanced Patterns

Complex form scenarios with multi-step wizards, dynamic fields, conditional validation, and more.

{% hint style="success" %}
**What you'll learn...**

* Multi-step wizard forms with progress tracking
* Dynamic forms (add/remove fields at runtime)
* Conditional validation based on field dependencies
* Form state management (dirty, pristine, touched)
* Autosave and draft management
* Complex file uploads with preview and progress
* Form arrays (repeating field groups)
  {% endhint %}

## Prerequisites

All examples assume you have the validation plugin installed and configured:

```bash
npm install @aurelia/validation @aurelia/validation-html
```

```typescript
// src/main.ts
import Aurelia from 'aurelia';
import { ValidationHtmlConfiguration } from '@aurelia/validation-html';

Aurelia.register(ValidationHtmlConfiguration)
  .app(component)
  .start();
```

See [Validation documentation](/aurelia-packages/validation) for setup details.

***

## Multi-Step Wizard Forms

Multi-step forms break complex forms into manageable steps, improving user experience and completion rates.

### Complete Example: User Onboarding Wizard

```typescript
// src/components/onboarding-wizard.ts
import { newInstanceForScope, resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface UserProfile {
  // Step 1: Account
  email: string;
  password: string;
  confirmPassword: string;

  // Step 2: Personal Info
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  phone: string;

  // Step 3: Preferences
  newsletter: boolean;
  notifications: boolean;
  theme: 'light' | 'dark';
  language: string;
}

export class OnboardingWizard {
  private currentStep = 1;
  private readonly totalSteps = 3;

  private profile: UserProfile = {
    email: '',
    password: '',
    confirmPassword: '',
    firstName: '',
    lastName: '',
    dateOfBirth: '',
    phone: '',
    newsletter: true,
    notifications: true,
    theme: 'light',
    language: 'en'
  };

  private validation = resolve(newInstanceForScope(IValidationController));
  private validationRules = resolve(IValidationRules);

  constructor() {
    this.setupValidation();
  }

  private setupValidation() {
    // Step 1 validation rules
    this.validationRules
      .on(this.profile)
      .ensure('email')
        .required()
        .email()
        .when(() => this.currentStep === 1)
      .ensure('password')
        .required()
        .minLength(8)
        .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
        .withMessage('Password must contain uppercase, lowercase, and number')
        .when(() => this.currentStep === 1)
      .ensure('confirmPassword')
        .required()
        .satisfies((value: string) => value === this.profile.password)
        .withMessage('Passwords must match')
        .when(() => this.currentStep === 1)

      // Step 2 validation rules
      .ensure('firstName')
        .required()
        .minLength(2)
        .when(() => this.currentStep === 2)
      .ensure('lastName')
        .required()
        .minLength(2)
        .when(() => this.currentStep === 2)
      .ensure('dateOfBirth')
        .required()
        .satisfies((value: string) => {
          const age = this.calculateAge(new Date(value));
          return age >= 18 && age <= 120;
        })
        .withMessage('You must be at least 18 years old')
        .when(() => this.currentStep === 2)
      .ensure('phone')
        .required()
        .matches(/^\+?[\d\s\-()]+$/)
        .withMessage('Please enter a valid phone number')
        .when(() => this.currentStep === 2);
  }

  private calculateAge(birthDate: Date): number {
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
      age--;
    }

    return age;
  }

  async next() {
    // Validate current step before proceeding
    const result = await this.validation.validate();

    if (!result.valid) {
      return; // Stay on current step if validation fails
    }

    if (this.currentStep < this.totalSteps) {
      this.currentStep++;
    }
  }

  previous() {
    if (this.currentStep > 1) {
      this.currentStep--;
    }
  }

  async submit() {
    // Validate all steps
    const result = await this.validation.validate();

    if (!result.valid) {
      // Find first step with errors
      const firstErrorStep = this.findFirstErrorStep(result.results);
      this.currentStep = firstErrorStep;
      return;
    }

    // Submit the form
    try {
      await this.saveProfile(this.profile);
      console.log('Profile saved successfully!');
    } catch (error) {
      console.error('Failed to save profile:', error);
    }
  }

  private findFirstErrorStep(results: any[]): number {
    const step1Fields = ['email', 'password', 'confirmPassword'];
    const step2Fields = ['firstName', 'lastName', 'dateOfBirth', 'phone'];

    for (const result of results) {
      if (!result.valid) {
        if (step1Fields.includes(result.propertyName)) return 1;
        if (step2Fields.includes(result.propertyName)) return 2;
      }
    }

    return this.currentStep;
  }

  private async saveProfile(profile: UserProfile): Promise<void> {
    // API call to save profile
    const response = await fetch('/api/onboarding', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(profile)
    });

    if (!response.ok) {
      throw new Error('Failed to save profile');
    }
  }

  get progress(): number {
    return (this.currentStep / this.totalSteps) * 100;
  }

  get isFirstStep(): boolean {
    return this.currentStep === 1;
  }

  get isLastStep(): boolean {
    return this.currentStep === this.totalSteps;
  }
}
```

```html
<!-- src/components/onboarding-wizard.html -->
  <div class="wizard">
    <!-- Progress bar -->
    <div class="wizard-progress">
      <div class="wizard-progress-bar" style="width: ${progress}%"></div>
      <div class="wizard-steps">
        <div class="wizard-step ${currentStep >= 1 ? 'active' : ''} ${currentStep > 1 ? 'completed' : ''}">
          <span class="step-number">1</span>
          <span class="step-label">Account</span>
        </div>
        <div class="wizard-step ${currentStep >= 2 ? 'active' : ''} ${currentStep > 2 ? 'completed' : ''}">
          <span class="step-number">2</span>
          <span class="step-label">Personal Info</span>
        </div>
        <div class="wizard-step ${currentStep >= 3 ? 'active' : ''}">
          <span class="step-number">3</span>
          <span class="step-label">Preferences</span>
        </div>
      </div>
    </div>

    <!-- Step 1: Account -->
    <div class="wizard-content" if.bind="currentStep === 1">
      <h2>Create Your Account</h2>

      <div class="form-field">
        <label for="email">Email</label>
        <input
          type="email"
          id="email"
          value.bind="profile.email & validate"
          placeholder="you@example.com">
      </div>

      <div class="form-field">
        <label for="password">Password</label>
        <input
          type="password"
          id="password"
          value.bind="profile.password & validate"
          placeholder="Min. 8 characters">
      </div>

      <div class="form-field">
        <label for="confirmPassword">Confirm Password</label>
        <input
          type="password"
          id="confirmPassword"
          value.bind="profile.confirmPassword & validate">
      </div>
    </div>

    <!-- Step 2: Personal Info -->
    <div class="wizard-content" if.bind="currentStep === 2">
      <h2>Tell Us About Yourself</h2>

      <div class="form-row">
        <div class="form-field">
          <label for="firstName">First Name</label>
          <input
            type="text"
            id="firstName"
            value.bind="profile.firstName & validate">
        </div>

        <div class="form-field">
          <label for="lastName">Last Name</label>
          <input
            type="text"
            id="lastName"
            value.bind="profile.lastName & validate">
        </div>
      </div>

      <div class="form-field">
        <label for="dateOfBirth">Date of Birth</label>
        <input
          type="date"
          id="dateOfBirth"
          value.bind="profile.dateOfBirth & validate">
      </div>

      <div class="form-field">
        <label for="phone">Phone Number</label>
        <input
          type="tel"
          id="phone"
          value.bind="profile.phone & validate"
          placeholder="+1 (555) 123-4567">
      </div>
    </div>

    <!-- Step 3: Preferences -->
    <div class="wizard-content" if.bind="currentStep === 3">
      <h2>Customize Your Experience</h2>

      <div class="form-field">
        <label>
          <input type="checkbox" checked.bind="profile.newsletter">
          Subscribe to newsletter
        </label>
      </div>

      <div class="form-field">
        <label>
          <input type="checkbox" checked.bind="profile.notifications">
          Enable notifications
        </label>
      </div>

      <div class="form-field">
        <label for="theme">Theme</label>
        <select id="theme" value.bind="profile.theme">
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </div>

      <div class="form-field">
        <label for="language">Language</label>
        <select id="language" value.bind="profile.language">
          <option value="en">English</option>
          <option value="es">Español</option>
          <option value="fr">Français</option>
          <option value="de">Deutsch</option>
        </select>
      </div>
    </div>

    <!-- Navigation -->
    <div class="wizard-actions">
      <button
        type="button"
        click.trigger="previous()"
        disabled.bind="isFirstStep"
        class="btn btn-secondary">
        Previous
      </button>

      <button
        if.bind="!isLastStep"
        type="button"
        click.trigger="next()"
        class="btn btn-primary">
        Next
      </button>

      <button
        if.bind="isLastStep"
        type="button"
        click.trigger="submit()"
        class="btn btn-success">
        Complete
      </button>
    </div>
  </div>
```

**Key Features**:

* Step-by-step validation (only validate current step)
* Progress indicator
* Conditional validation rules with `.when()`
* Navigate to first step with errors on final submit
* Accessible navigation buttons

***

## Dynamic Forms (Add/Remove Fields)

Forms where users can add or remove fields at runtime, like adding multiple email addresses or phone numbers.

### Complete Example: Contact Form with Dynamic Emails

```typescript
// src/components/dynamic-contact-form.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface EmailEntry {
  id: string;
  address: string;
  label: string;
  isPrimary: boolean;
}

interface ContactForm {
  name: string;
  company: string;
  emails: EmailEntry[];
  notes: string;
}

export class DynamicContactForm {
  private form: ContactForm = {
    name: '',
    company: '',
    emails: [this.createEmailEntry(true)],
    notes: ''
  };

  private validation = resolve(newInstanceForScope(IValidationController));
  private nextId = 1;
  private validationRules = resolve(IValidationRules);

  constructor() {
    this.setupValidation();
  }

  private createEmailEntry(isPrimary = false): EmailEntry {
    return {
      id: `email-${this.nextId++}`,
      address: '',
      label: isPrimary ? 'Primary' : 'Secondary',
      isPrimary
    };
  }

  private setupValidation() {
    this.validationRules
      .on(this.form)
      .ensure('name')
        .required()
        .minLength(2)
      .ensure('company')
        .required()
      .ensure('emails')
        .required()
        .minItems(1)
        .withMessage('At least one email is required')
        .satisfies((emails: EmailEntry[]) =>
          emails.every(e => this.isValidEmail(e.address))
        )
        .withMessage('All email addresses must be valid')
        .satisfies((emails: EmailEntry[]) =>
          emails.filter(e => e.isPrimary).length === 1
        )
        .withMessage('Exactly one primary email is required');
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  addEmail() {
    this.form.emails.push(this.createEmailEntry());
  }

  removeEmail(id: string) {
    // Don't allow removing the last email
    if (this.form.emails.length <= 1) {
      return;
    }

    const index = this.form.emails.findIndex(e => e.id === id);
    if (index === -1) return;

    const wasRemoved = this.form.emails[index];
    this.form.emails.splice(index, 1);

    // If we removed the primary, make the first one primary
    if (wasRemoved.isPrimary && this.form.emails.length > 0) {
      this.form.emails[0].isPrimary = true;
      this.form.emails[0].label = 'Primary';
    }

    // Revalidate after removal
    this.validation.validate();
  }

  setPrimary(id: string) {
    // Only one email can be primary
    this.form.emails.forEach(email => {
      email.isPrimary = email.id === id;
      email.label = email.isPrimary ? 'Primary' : 'Secondary';
    });
  }

  async submit() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    console.log('Form submitted:', this.form);
    // API call here
  }

  get canRemoveEmail(): boolean {
    return this.form.emails.length > 1;
  }
}
```

```html
<!-- src/components/dynamic-contact-form.html -->
  <form submit.trigger="submit()">
    <h2>Contact Information</h2>

    <div class="form-field">
      <label for="name">Name *</label>
      <input
        type="text"
        id="name"
        value.bind="form.name & validate">
    </div>

    <div class="form-field">
      <label for="company">Company *</label>
      <input
        type="text"
        id="company"
        value.bind="form.company & validate">
    </div>

    <!-- Dynamic email fields -->
    <div class="form-section">
      <div class="section-header">
        <h3>Email Addresses *</h3>
        <button
          type="button"
          click.trigger="addEmail()"
          class="btn btn-small btn-secondary">
          + Add Email
        </button>
      </div>

      <div
        repeat.for="email of form.emails"
        class="email-entry"
        id.bind="email.id">

        <div class="form-row">
          <div class="form-field flex-grow">
            <label for="${email.id}-address">${email.label}</label>
            <input
              type="email"
              id="${email.id}-address"
              value.bind="email.address"
              placeholder="email@example.com">
          </div>

          <div class="form-field-actions">
            <label class="radio-label">
              <input
                type="radio"
                name="primaryEmail"
                checked.bind="email.isPrimary"
                change.trigger="setPrimary(email.id)">
              Primary
            </label>

            <button
              type="button"
              click.trigger="removeEmail(email.id)"
              disabled.bind="!canRemoveEmail"
              class="btn btn-small btn-danger"
              aria-label="Remove email">
              ×
            </button>
          </div>
        </div>
      </div>
    </div>

    <div class="form-field">
      <label for="notes">Notes</label>
      <textarea
        id="notes"
        value.bind="form.notes"
        rows="4"></textarea>
    </div>

    <button type="submit" class="btn btn-primary">Save Contact</button>
  </form>
```

**Key Features**:

* Add/remove email entries dynamically
* Ensure at least one email exists
* Automatically handle primary email when removing
* Validate all emails in the array
* Unique IDs for each entry (accessibility and key binding)

***

## Conditional Validation (Field Dependencies)

Validation rules that change based on the values of other fields.

### Complete Example: Shipping Form

```typescript
// src/components/shipping-form.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface ShippingForm {
  sameAsBilling: boolean;

  // Billing address
  billingStreet: string;
  billingCity: string;
  billingState: string;
  billingZip: string;
  billingCountry: string;

  // Shipping address (only required if different)
  shippingStreet: string;
  shippingCity: string;
  shippingState: string;
  shippingZip: string;
  shippingCountry: string;

  // Shipping method
  shippingMethod: 'standard' | 'express' | 'overnight' | '';

  // Signature required (only for overnight)
  signatureRequired: boolean;

  // International customs (only for international)
  customsValue: number;
  customsDescription: string;
}

export class ShippingFormComponent {
  private form: ShippingForm = {
    sameAsBilling: true,
    billingStreet: '',
    billingCity: '',
    billingState: '',
    billingZip: '',
    billingCountry: 'US',
    shippingStreet: '',
    shippingCity: '',
    shippingState: '',
    shippingZip: '',
    shippingCountry: 'US',
    shippingMethod: '',
    signatureRequired: false,
    customsValue: 0,
    customsDescription: ''
  };

  private validation = resolve(newInstanceForScope(IValidationController));
  private validationRules = resolve(IValidationRules);

  constructor() {
    this.setupValidation();
  }

  private setupValidation() {
    this.validationRules
      .on(this.form)
      // Billing address (always required)
      .ensure('billingStreet')
        .required()
      .ensure('billingCity')
        .required()
      .ensure('billingState')
        .required()
      .ensure('billingZip')
        .required()
        .matches(/^\d{5}(-\d{4})?$/)
        .withMessage('Please enter a valid ZIP code')
      .ensure('billingCountry')
        .required()

      // Shipping address (required only if different from billing)
      .ensure('shippingStreet')
        .required()
        .when(() => !this.form.sameAsBilling)
      .ensure('shippingCity')
        .required()
        .when(() => !this.form.sameAsBilling)
      .ensure('shippingState')
        .required()
        .when(() => !this.form.sameAsBilling)
      .ensure('shippingZip')
        .required()
        .when(() => !this.form.sameAsBilling)
        .matches(/^\d{5}(-\d{4})?$/)
        .withMessage('Please enter a valid ZIP code')
        .when(() => !this.form.sameAsBilling)
      .ensure('shippingCountry')
        .required()
        .when(() => !this.form.sameAsBilling)

      // Shipping method
      .ensure('shippingMethod')
        .required()
        .withMessage('Please select a shipping method')

      // Customs info (required for international shipments)
      .ensure('customsValue')
        .required()
        .min(0.01)
        .withMessage('Customs value must be greater than 0')
        .when(() => this.isInternationalShipment)
      .ensure('customsDescription')
        .required()
        .minLength(10)
        .withMessage('Please provide a detailed description for customs')
        .when(() => this.isInternationalShipment);
  }

  get isInternationalShipment(): boolean {
    const destCountry = this.form.sameAsBilling
      ? this.form.billingCountry
      : this.form.shippingCountry;

    return destCountry !== 'US';
  }

  get isOvernightShipping(): boolean {
    return this.form.shippingMethod === 'overnight';
  }

  sameAsBillingChanged() {
    if (this.form.sameAsBilling) {
      // Clear shipping address when using billing address
      this.form.shippingStreet = '';
      this.form.shippingCity = '';
      this.form.shippingState = '';
      this.form.shippingZip = '';
      this.form.shippingCountry = this.form.billingCountry;
    }

    // Revalidate after toggling
    this.validation.validate();
  }

  async submit() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    console.log('Shipping form submitted:', this.form);
  }
}
```

```html
<!-- src/components/shipping-form.html -->
  <form submit.trigger="submit()">
    <h2>Billing Address</h2>

    <div class="form-field">
      <label for="billingStreet">Street Address *</label>
      <input
        type="text"
        id="billingStreet"
        value.bind="form.billingStreet & validate">
    </div>

    <div class="form-row">
      <div class="form-field">
        <label for="billingCity">City *</label>
        <input
          type="text"
          id="billingCity"
          value.bind="form.billingCity & validate">
      </div>

      <div class="form-field">
        <label for="billingState">State *</label>
        <input
          type="text"
          id="billingState"
          value.bind="form.billingState & validate">
      </div>

      <div class="form-field">
        <label for="billingZip">ZIP Code *</label>
        <input
          type="text"
          id="billingZip"
          value.bind="form.billingZip & validate">
      </div>
    </div>

    <div class="form-field">
      <label for="billingCountry">Country *</label>
      <select id="billingCountry" value.bind="form.billingCountry & validate">
        <option value="US">United States</option>
        <option value="CA">Canada</option>
        <option value="MX">Mexico</option>
        <option value="UK">United Kingdom</option>
        <option value="FR">France</option>
      </select>
    </div>

    <hr>

    <h2>Shipping Address</h2>

    <div class="form-field">
      <label>
        <input
          type="checkbox"
          checked.bind="form.sameAsBilling"
          change.trigger="sameAsBillingChanged()">
        Same as billing address
      </label>
    </div>

    <!-- Only show shipping address fields if different from billing -->
    <div if.bind="!form.sameAsBilling">
      <div class="form-field">
        <label for="shippingStreet">Street Address *</label>
        <input
          type="text"
          id="shippingStreet"
          value.bind="form.shippingStreet & validate">
      </div>

      <div class="form-row">
        <div class="form-field">
          <label for="shippingCity">City *</label>
          <input
            type="text"
            id="shippingCity"
            value.bind="form.shippingCity & validate">
        </div>

        <div class="form-field">
          <label for="shippingState">State *</label>
          <input
            type="text"
            id="shippingState"
            value.bind="form.shippingState & validate">
        </div>

        <div class="form-field">
          <label for="shippingZip">ZIP Code *</label>
          <input
            type="text"
            id="shippingZip"
            value.bind="form.shippingZip & validate">
        </div>
      </div>

      <div class="form-field">
        <label for="shippingCountry">Country *</label>
        <select id="shippingCountry" value.bind="form.shippingCountry & validate">
          <option value="US">United States</option>
          <option value="CA">Canada</option>
          <option value="MX">Mexico</option>
          <option value="UK">United Kingdom</option>
          <option value="FR">France</option>
        </select>
      </div>
    </div>

    <hr>

    <h2>Shipping Method</h2>

    <div class="form-field">
      <label for="shippingMethod">Method *</label>
      <select id="shippingMethod" value.bind="form.shippingMethod & validate">
        <option value="">Select shipping method</option>
        <option value="standard">Standard (5-7 days) - $5.99</option>
        <option value="express">Express (2-3 days) - $14.99</option>
        <option value="overnight">Overnight - $29.99</option>
      </select>
    </div>

    <!-- Only show signature option for overnight shipping -->
    <div if.bind="isOvernightShipping" class="form-field">
      <label>
        <input type="checkbox" checked.bind="form.signatureRequired">
        Signature required upon delivery
      </label>
    </div>

    <!-- Only show customs fields for international shipments -->
    <div if.bind="isInternationalShipment">
      <hr>
      <h2>Customs Information</h2>

      <div class="form-field">
        <label for="customsValue">Declared Value (USD) *</label>
        <input
          type="number"
          id="customsValue"
          value.bind="form.customsValue & validate"
          step="0.01"
          min="0">
      </div>

      <div class="form-field">
        <label for="customsDescription">Description *</label>
        <textarea
          id="customsDescription"
          value.bind="form.customsDescription & validate"
          rows="3"
          placeholder="Detailed description of contents for customs"></textarea>
      </div>
    </div>

    <button type="submit" class="btn btn-primary">Continue to Payment</button>
  </form>
```

**Key Features**:

* Conditional field visibility with `if.bind`
* Conditional validation with `.when()`
* Fields depend on checkbox state
* Fields depend on select values
* Automatic revalidation when dependencies change

***

## Form State Management (Dirty, Pristine, Touched)

Track whether forms have been modified and warn users before losing changes.

### Complete Example: Article Editor with Unsaved Changes Warning

```typescript
// src/components/article-editor.ts
import { IRouter, RouteNode } from '@aurelia/router';
import { newInstanceForScope, resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface Article {
  id: string | null;
  title: string;
  content: string;
  tags: string[];
  published: boolean;
}

export class ArticleEditor {
  private article: Article = {
    id: null,
    title: '',
    content: '',
    tags: [],
    published: false
  };

  private originalArticle: Article;
  private isDirty = false;
  private isSaving = false;
  private lastSaved: Date | null = null;
  private autosaveTimer: any = null;

  private validation = resolve(newInstanceForScope(IValidationController));
  private router = resolve(IRouter);
  private validationRules = resolve(IValidationRules);

  constructor() {
    this.validationRules
      .on(this.article)
      .ensure('title')
        .required()
        .minLength(5)
      .ensure('content')
        .required()
        .minLength(50);

    // Store original state
    this.originalArticle = JSON.parse(JSON.stringify(this.article));

    // Setup autosave
    this.setupAutosave();
  }

  binding() {
    // Track changes to mark form as dirty
    this.watchForChanges();
  }

  detaching() {
    // Clean up autosave timer
    if (this.autosaveTimer) {
      clearInterval(this.autosaveTimer);
    }
  }

  private watchForChanges() {
    // Simple dirty checking - compare current to original
    // In production, consider using a more robust solution
    const checkDirty = () => {
      this.isDirty = JSON.stringify(this.article) !== JSON.stringify(this.originalArticle);
    };

    // Check after each property change
    // You could use @observable or watch the properties more elegantly
    setInterval(checkDirty, 500);
  }

  private setupAutosave() {
    // Autosave every 30 seconds if dirty
    this.autosaveTimer = setInterval(() => {
      if (this.isDirty && !this.isSaving) {
        this.saveDraft();
      }
    }, 30000);
  }

  async saveDraft() {
    if (this.isSaving) return;

    this.isSaving = true;

    try {
      // Save to localStorage as draft
      localStorage.setItem('article-draft', JSON.stringify(this.article));
      this.lastSaved = new Date();

      console.log('Draft saved');
    } catch (error) {
      console.error('Failed to save draft:', error);
    } finally {
      this.isSaving = false;
    }
  }

  async publish() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    this.isSaving = true;

    try {
      this.article.published = true;
      await this.saveArticle();

      // Update original state
      this.originalArticle = JSON.parse(JSON.stringify(this.article));
      this.isDirty = false;

      // Clear draft
      localStorage.removeItem('article-draft');

      // Navigate away
      await this.router.load('/articles');
    } catch (error) {
      console.error('Failed to publish:', error);
    } finally {
      this.isSaving = false;
    }
  }

  async save() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    this.isSaving = true;

    try {
      this.article.published = false;
      await this.saveArticle();

      // Update original state
      this.originalArticle = JSON.parse(JSON.stringify(this.article));
      this.isDirty = false;

      // Clear draft
      localStorage.removeItem('article-draft');
    } catch (error) {
      console.error('Failed to save:', error);
    } finally {
      this.isSaving = false;
    }
  }

  private async saveArticle(): Promise<void> {
    const response = await fetch('/api/articles', {
      method: this.article.id ? 'PUT' : 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.article)
    });

    if (!response.ok) {
      throw new Error('Failed to save article');
    }

    const saved = await response.json();
    this.article.id = saved.id;
  }

  // Router lifecycle hook - prevent navigation if dirty
  canUnload(next: RouteNode | null, current: RouteNode): boolean {
    if (!this.isDirty) {
      return true;
    }

    // Show confirmation dialog
    return confirm('You have unsaved changes. Are you sure you want to leave?');
  }

  get lastSavedText(): string {
    if (!this.lastSaved) {
      return 'Never saved';
    }

    const seconds = Math.floor((Date.now() - this.lastSaved.getTime()) / 1000);

    if (seconds < 60) {
      return 'Saved just now';
    } else if (seconds < 3600) {
      const minutes = Math.floor(seconds / 60);
      return `Saved ${minutes} minute${minutes > 1 ? 's' : ''} ago`;
    } else {
      const hours = Math.floor(seconds / 3600);
      return `Saved ${hours} hour${hours > 1 ? 's' : ''} ago`;
    }
  }
}
```

```html
<!-- src/components/article-editor.html -->
  <div class="article-editor">
    <!-- Status bar -->
    <div class="editor-status">
      <span class="status-indicator ${isDirty ? 'dirty' : 'clean'}">
        ${isDirty ? 'Unsaved changes' : 'All changes saved'}
      </span>
      <span class="last-saved">${lastSavedText}</span>
      <span if.bind="isSaving" class="saving-indicator">Saving...</span>
    </div>

    <!-- Editor form -->
    <div class="form-field">
      <label for="title">Title *</label>
      <input
        type="text"
        id="title"
        value.bind="article.title & validate"
        placeholder="Enter article title">
    </div>

    <div class="form-field">
      <label for="content">Content *</label>
      <textarea
        id="content"
        value.bind="article.content & validate"
        rows="20"
        placeholder="Write your article..."></textarea>
    </div>

    <div class="form-field">
      <label for="tags">Tags (comma-separated)</label>
      <input
        type="text"
        id="tags"
        value.bind="article.tags"
        placeholder="javascript, aurelia, web development">
    </div>

    <!-- Actions -->
    <div class="editor-actions">
      <button
        type="button"
        click.trigger="saveDraft()"
        disabled.bind="!isDirty || isSaving"
        class="btn btn-secondary">
        Save Draft
      </button>

      <button
        type="button"
        click.trigger="save()"
        disabled.bind="isSaving"
        class="btn btn-primary">
        ${isSaving ? 'Saving...' : 'Save'}
      </button>

      <button
        type="button"
        click.trigger="publish()"
        disabled.bind="isSaving"
        class="btn btn-success">
        ${isSaving ? 'Publishing...' : 'Publish'}
      </button>
    </div>
  </div>
```

**Key Features**:

* Dirty state tracking (compare current vs original)
* Autosave to localStorage every 30 seconds
* Last saved timestamp with human-readable formatting
* Prevent navigation with `canUnload` router hook
* Visual indicators for save state
* Disable actions while saving

***

## Form Arrays (Repeating Field Groups)

Form arrays allow users to add/remove entire groups of fields, like invoice line items or multiple addresses.

### Complete Example: Invoice Form with Line Items

```typescript
// src/components/invoice-form.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface LineItem {
  id: string;
  description: string;
  quantity: number;
  unitPrice: number;
  total: number;
}

interface Invoice {
  invoiceNumber: string;
  customerName: string;
  customerEmail: string;
  invoiceDate: string;
  dueDate: string;
  items: LineItem[];
  subtotal: number;
  tax: number;
  total: number;
}

export class InvoiceForm {
  private invoice: Invoice = {
    invoiceNumber: this.generateInvoiceNumber(),
    customerName: '',
    customerEmail: '',
    invoiceDate: new Date().toISOString().split('T')[0],
    dueDate: '',
    items: [this.createLineItem()],
    subtotal: 0,
    tax: 0,
    total: 0
  };

  private readonly taxRate = 0.08; // 8% tax
  private nextItemId = 1;

  private validation = resolve(newInstanceForScope(IValidationController));
  private validationRules = resolve(IValidationRules);

  constructor() {
    this.setupValidation();
  }

  private generateInvoiceNumber(): string {
    return `INV-${Date.now()}`;
  }

  private createLineItem(): LineItem {
    return {
      id: `item-${this.nextItemId++}`,
      description: '',
      quantity: 1,
      unitPrice: 0,
      total: 0
    };
  }

  private setupValidation() {
    this.validationRules
      .on(this.invoice)
      .ensure('invoiceNumber')
        .required()
      .ensure('customerName')
        .required()
        .minLength(2)
      .ensure('customerEmail')
        .required()
        .email()
      .ensure('invoiceDate')
        .required()
      .ensure('dueDate')
        .required()
        .satisfies((value: string) => {
          if (!value || !this.invoice.invoiceDate) return true;
          return new Date(value) >= new Date(this.invoice.invoiceDate);
        })
        .withMessage('Due date must be after invoice date')
      .ensure('items')
        .required()
        .minItems(1)
        .withMessage('At least one line item is required')
        .satisfies((items: LineItem[]) =>
          items.every(item =>
            item.description.trim().length > 0 &&
            item.quantity > 0 &&
            item.unitPrice >= 0
          )
        )
        .withMessage('All line items must be complete');
  }

  addLineItem() {
    this.invoice.items.push(this.createLineItem());
  }

  removeLineItem(id: string) {
    if (this.invoice.items.length <= 1) {
      return; // Must have at least one item
    }

    const index = this.invoice.items.findIndex(item => item.id === id);
    if (index !== -1) {
      this.invoice.items.splice(index, 1);
      this.calculateTotals();
    }
  }

  duplicateLineItem(id: string) {
    const index = this.invoice.items.findIndex(item => item.id === id);
    if (index !== -1) {
      const original = this.invoice.items[index];
      const duplicate = {
        ...original,
        id: `item-${this.nextItemId++}`
      };
      this.invoice.items.splice(index + 1, 0, duplicate);
      this.calculateTotals();
    }
  }

  updateLineItem(item: LineItem) {
    item.total = item.quantity * item.unitPrice;
    this.calculateTotals();
  }

  private calculateTotals() {
    this.invoice.subtotal = this.invoice.items.reduce(
      (sum, item) => sum + item.total,
      0
    );

    this.invoice.tax = this.invoice.subtotal * this.taxRate;
    this.invoice.total = this.invoice.subtotal + this.invoice.tax;
  }

  async submit() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    console.log('Invoice submitted:', this.invoice);
    // API call to save invoice
  }

  get canRemoveItem(): boolean {
    return this.invoice.items.length > 1;
  }
}
```

```html
<!-- src/components/invoice-form.html -->
  <form submit.trigger="submit()" class="invoice-form">
    <h2>Create Invoice</h2>

    <!-- Invoice Header -->
    <div class="invoice-header">
      <div class="form-row">
        <div class="form-field">
          <label for="invoiceNumber">Invoice Number *</label>
          <input
            type="text"
            id="invoiceNumber"
            value.bind="invoice.invoiceNumber & validate"
            readonly>
        </div>

        <div class="form-field">
          <label for="invoiceDate">Invoice Date *</label>
          <input
            type="date"
            id="invoiceDate"
            value.bind="invoice.invoiceDate & validate">
        </div>

        <div class="form-field">
          <label for="dueDate">Due Date *</label>
          <input
            type="date"
            id="dueDate"
            value.bind="invoice.dueDate & validate">
        </div>
      </div>

      <div class="form-row">
        <div class="form-field">
          <label for="customerName">Customer Name *</label>
          <input
            type="text"
            id="customerName"
            value.bind="invoice.customerName & validate">
        </div>

        <div class="form-field">
          <label for="customerEmail">Customer Email *</label>
          <input
            type="email"
            id="customerEmail"
            value.bind="invoice.customerEmail & validate">
        </div>
      </div>
    </div>

    <!-- Line Items -->
    <div class="invoice-items">
      <div class="items-header">
        <h3>Line Items</h3>
        <button
          type="button"
          click.trigger="addLineItem()"
          class="btn btn-secondary btn-small">
          + Add Item
        </button>
      </div>

      <div class="items-table">
        <div class="items-table-header">
          <div class="col-description">Description</div>
          <div class="col-quantity">Qty</div>
          <div class="col-price">Unit Price</div>
          <div class="col-total">Total</div>
          <div class="col-actions">Actions</div>
        </div>

        <div
          repeat.for="item of invoice.items"
          class="line-item"
          id.bind="item.id">

          <div class="col-description">
            <input
              type="text"
              value.bind="item.description"
              change.trigger="updateLineItem(item)"
              placeholder="Item description"
              aria-label="Description for item ${$index + 1}">
          </div>

          <div class="col-quantity">
            <input
              type="number"
              value.bind="item.quantity"
              change.trigger="updateLineItem(item)"
              min="1"
              step="1"
              aria-label="Quantity for item ${$index + 1}">
          </div>

          <div class="col-price">
            <input
              type="number"
              value.bind="item.unitPrice"
              change.trigger="updateLineItem(item)"
              min="0"
              step="0.01"
              aria-label="Unit price for item ${$index + 1}">
          </div>

          <div class="col-total">
            $${item.total | numberFormat:'0.00'}
          </div>

          <div class="col-actions">
            <button
              type="button"
              click.trigger="duplicateLineItem(item.id)"
              class="btn btn-icon"
              title="Duplicate"
              aria-label="Duplicate item ${$index + 1}">
              📋
            </button>

            <button
              type="button"
              click.trigger="removeLineItem(item.id)"
              disabled.bind="!canRemoveItem"
              class="btn btn-icon btn-danger"
              title="Remove"
              aria-label="Remove item ${$index + 1}">
              ×
            </button>
          </div>
        </div>
      </div>
    </div>

    <!-- Totals -->
    <div class="invoice-totals">
      <div class="total-row">
        <span>Subtotal:</span>
        <span>$${invoice.subtotal | numberFormat:'0.00'}</span>
      </div>
      <div class="total-row">
        <span>Tax (${taxRate * 100}%):</span>
        <span>$${invoice.tax | numberFormat:'0.00'}</span>
      </div>
      <div class="total-row total-row-grand">
        <span>Total:</span>
        <span>$${invoice.total | numberFormat:'0.00'}</span>
      </div>
    </div>

    <!-- Actions -->
    <div class="form-actions">
      <button type="submit" class="btn btn-primary">
        Save Invoice
      </button>
    </div>
  </form>
```

**Key Features**:

* Add/remove line items dynamically
* Duplicate line items
* Auto-calculate line totals and invoice totals
* Validate entire array of items
* Prevent removing last item
* Unique IDs for each line item
* Accessible labels for screen readers

***

## Complex File Uploads with Preview & Progress

Handle multiple file uploads with image previews, progress tracking, and validation.

### Complete Example: Image Gallery Upload

```typescript
// src/components/image-upload.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface UploadedFile {
  id: string;
  file: File;
  preview: string;
  progress: number;
  status: 'pending' | 'uploading' | 'complete' | 'error';
  error?: string;
}

export class ImageUpload {
  private files: UploadedFile[] = [];
  private dragOver = false;
  private nextId = 1;

  private readonly maxFileSize = 5 * 1024 * 1024; // 5MB
  private readonly maxFiles = 10;
  private readonly allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];

  private validation = resolve(newInstanceForScope(IValidationController));

  constructor(
    @IValidationRules validationRules: IValidationRules = resolve(IValidationRules)
  ) {
    validationRules
      .on(this)
      .ensure('files')
        .required()
        .minItems(1)
        .withMessage('Please upload at least one image')
        .satisfies((files: UploadedFile[]) => files.length <= this.maxFiles)
        .withMessage(`Maximum ${this.maxFiles} images allowed`);
  }

  handleFileSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    const files = Array.from(input.files || []);

    this.addFiles(files);

    // Clear input so same file can be selected again
    input.value = '';
  }

  handleDrop(event: DragEvent) {
    event.preventDefault();
    this.dragOver = false;

    const files = Array.from(event.dataTransfer?.files || []);
    this.addFiles(files);
  }

  handleDragOver(event: DragEvent) {
    event.preventDefault();
    this.dragOver = true;
  }

  handleDragLeave() {
    this.dragOver = false;
  }

  private addFiles(files: File[]) {
    for (const file of files) {
      // Check if we've reached the limit
      if (this.files.length >= this.maxFiles) {
        alert(`Maximum ${this.maxFiles} files allowed`);
        break;
      }

      // Validate file type
      if (!this.allowedTypes.includes(file.type)) {
        alert(`${file.name}: Invalid file type. Only images allowed.`);
        continue;
      }

      // Validate file size
      if (file.size > this.maxFileSize) {
        alert(`${file.name}: File too large. Maximum ${this.maxFileSize / 1024 / 1024}MB.`);
        continue;
      }

      // Create uploaded file entry
      const uploadedFile: UploadedFile = {
        id: `file-${this.nextId++}`,
        file,
        preview: '',
        progress: 0,
        status: 'pending'
      };

      this.files.push(uploadedFile);

      // Generate preview
      this.generatePreview(uploadedFile);
    }
  }

  private generatePreview(uploadedFile: UploadedFile) {
    const reader = new FileReader();

    reader.onload = (e) => {
      uploadedFile.preview = e.target?.result as string;
    };

    reader.readAsDataURL(uploadedFile.file);
  }

  removeFile(id: string) {
    const index = this.files.findIndex(f => f.id === id);
    if (index !== -1) {
      this.files.splice(index, 1);
    }
  }

  async uploadFile(uploadedFile: UploadedFile) {
    if (uploadedFile.status === 'uploading' || uploadedFile.status === 'complete') {
      return;
    }

    uploadedFile.status = 'uploading';
    uploadedFile.progress = 0;

    try {
      const formData = new FormData();
      formData.append('file', uploadedFile.file);

      // Simulate upload with progress
      await this.simulateUpload(uploadedFile);

      uploadedFile.status = 'complete';
      uploadedFile.progress = 100;
    } catch (error) {
      uploadedFile.status = 'error';
      uploadedFile.error = error.message || 'Upload failed';
    }
  }

  private async simulateUpload(uploadedFile: UploadedFile): Promise<void> {
    // In real implementation, use XMLHttpRequest or fetch with progress
    return new Promise((resolve) => {
      const duration = 2000; // 2 seconds
      const interval = 100; // Update every 100ms
      const increment = (interval / duration) * 100;

      const timer = setInterval(() => {
        uploadedFile.progress += increment;

        if (uploadedFile.progress >= 100) {
          clearInterval(timer);
          uploadedFile.progress = 100;
          resolve();
        }
      }, interval);
    });
  }

  async uploadAll() {
    const pending = this.files.filter(f => f.status === 'pending' || f.status === 'error');

    for (const file of pending) {
      await this.uploadFile(file);
    }
  }

  async submit() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    // Upload any pending files
    await this.uploadAll();

    // Check if all uploaded successfully
    const hasErrors = this.files.some(f => f.status === 'error');
    if (hasErrors) {
      alert('Some files failed to upload. Please try again.');
      return;
    }

    console.log('All files uploaded successfully!', this.files);
  }

  get uploadedCount(): number {
    return this.files.filter(f => f.status === 'complete').length;
  }

  get totalSize(): string {
    const bytes = this.files.reduce((sum, f) => sum + f.file.size, 0);
    const mb = bytes / 1024 / 1024;
    return `${mb.toFixed(2)} MB`;
  }
}
```

```html
<!-- src/components/image-upload.html -->
  <div class="image-upload">
    <h2>Upload Images</h2>

    <!-- Drop Zone -->
    <div
      class="drop-zone ${dragOver ? 'drag-over' : ''}"
      drop.trigger="handleDrop($event)"
      dragover.trigger="handleDragOver($event)"
      dragleave.trigger="handleDragLeave()">

      <div class="drop-zone-content">
        <p class="drop-zone-icon">📁</p>
        <p class="drop-zone-text">Drag & drop images here</p>
        <p class="drop-zone-or">or</p>

        <label for="fileInput" class="btn btn-primary">
          Choose Files
        </label>
        <input
          type="file"
          id="fileInput"
          change.trigger="handleFileSelect($event)"
          multiple
          accept="${allowedTypes.join(',')}"
          style="display: none;">

        <p class="drop-zone-hint">
          Maximum ${maxFiles} files, ${maxFileSize / 1024 / 1024}MB each
        </p>
      </div>
    </div>

    <!-- File List -->
    <div if.bind="files.length > 0" class="file-list">
      <div class="file-list-header">
        <h3>Selected Files (${files.length}/${maxFiles})</h3>
        <div class="file-list-stats">
          <span>${uploadedCount} uploaded</span>
          <span>${totalSize} total</span>
        </div>
      </div>

      <div class="file-grid">
        <div
          repeat.for="file of files"
          class="file-item file-item-${file.status}">

          <!-- Preview -->
          <div class="file-preview">
            <img
              if.bind="file.preview"
              src.bind="file.preview"
              alt="${file.file.name}">
            <div if.bind="!file.preview" class="file-preview-loading">
              Loading...
            </div>
          </div>

          <!-- Info -->
          <div class="file-info">
            <div class="file-name" title.bind="file.file.name">
              ${file.file.name}
            </div>
            <div class="file-size">
              ${file.file.size / 1024 | numberFormat:'0.0'} KB
            </div>
          </div>

          <!-- Progress -->
          <div if.bind="file.status === 'uploading'" class="file-progress">
            <div class="progress-bar">
              <div
                class="progress-fill"
                style="width: ${file.progress}%"></div>
            </div>
            <div class="progress-text">${file.progress | numberFormat:'0'}%</div>
          </div>

          <!-- Status -->
          <div class="file-status">
            <span if.bind="file.status === 'pending'" class="status-badge status-pending">
              Pending
            </span>
            <span if.bind="file.status === 'uploading'" class="status-badge status-uploading">
              Uploading...
            </span>
            <span if.bind="file.status === 'complete'" class="status-badge status-complete">
              ✓ Complete
            </span>
            <span if.bind="file.status === 'error'" class="status-badge status-error">
              ✕ ${file.error}
            </span>
          </div>

          <!-- Actions -->
          <div class="file-actions">
            <button
              if.bind="file.status === 'pending' || file.status === 'error'"
              type="button"
              click.trigger="uploadFile(file)"
              class="btn btn-icon btn-small">
              ↑
            </button>

            <button
              type="button"
              click.trigger="removeFile(file.id)"
              class="btn btn-icon btn-small btn-danger">
              ×
            </button>
          </div>
        </div>
      </div>

      <!-- Bulk Actions -->
      <div class="file-list-actions">
        <button
          type="button"
          click.trigger="uploadAll()"
          class="btn btn-secondary">
          Upload All
        </button>

        <button
          type="button"
          click.trigger="submit()"
          class="btn btn-primary">
          Complete Upload
        </button>
      </div>
    </div>
  </div>
```

**Key Features**:

* Drag & drop support
* Image preview generation
* Progress tracking for each file
* File type and size validation
* Multiple file selection
* Individual or bulk upload
* Error handling per file
* Visual status indicators

***

## Dependent Dropdowns (Cascading Selects)

Dropdowns where options depend on previous selections, like country → state → city.

### Complete Example: Location Selector

```typescript
// src/components/location-selector.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface Country {
  code: string;
  name: string;
}

interface State {
  code: string;
  name: string;
  countryCode: string;
}

interface City {
  id: string;
  name: string;
  stateCode: string;
}

interface LocationForm {
  country: string;
  state: string;
  city: string;
  address: string;
  zipCode: string;
}

export class LocationSelector {
  private form: LocationForm = {
    country: '',
    state: '',
    city: '',
    address: '',
    zipCode: ''
  };

  // Mock data (in real app, load from API)
  private allCountries: Country[] = [
    { code: 'US', name: 'United States' },
    { code: 'CA', name: 'Canada' },
    { code: 'MX', name: 'Mexico' }
  ];

  private allStates: State[] = [
    { code: 'CA', name: 'California', countryCode: 'US' },
    { code: 'NY', name: 'New York', countryCode: 'US' },
    { code: 'TX', name: 'Texas', countryCode: 'US' },
    { code: 'ON', name: 'Ontario', countryCode: 'CA' },
    { code: 'BC', name: 'British Columbia', countryCode: 'CA' },
    { code: 'JA', name: 'Jalisco', countryCode: 'MX' }
  ];

  private allCities: City[] = [
    { id: '1', name: 'Los Angeles', stateCode: 'CA' },
    { id: '2', name: 'San Francisco', stateCode: 'CA' },
    { id: '3', name: 'New York City', stateCode: 'NY' },
    { id: '4', name: 'Buffalo', stateCode: 'NY' },
    { id: '5', name: 'Houston', stateCode: 'TX' },
    { id: '6', name: 'Dallas', stateCode: 'TX' },
    { id: '7', name: 'Toronto', stateCode: 'ON' },
    { id: '8', name: 'Vancouver', stateCode: 'BC' },
    { id: '9', name: 'Guadalajara', stateCode: 'JA' }
  ];

  private isLoadingStates = false;
  private isLoadingCities = false;

  private validation = resolve(newInstanceForScope(IValidationController));

  constructor(
    @IValidationRules validationRules: IValidationRules = resolve(IValidationRules)
  ) {
    validationRules
      .on(this.form)
      .ensure('country')
        .required()
      .ensure('state')
        .required()
      .ensure('city')
        .required()
      .ensure('address')
        .required()
        .minLength(5)
      .ensure('zipCode')
        .required()
        .matches(/^\d{5}(-\d{4})?$/)
        .withMessage('Please enter a valid ZIP code');
  }

  // Computed: Available states based on selected country
  get availableStates(): State[] {
    if (!this.form.country) return [];
    return this.allStates.filter(s => s.countryCode === this.form.country);
  }

  // Computed: Available cities based on selected state
  get availableCities(): City[] {
    if (!this.form.state) return [];
    return this.allCities.filter(c => c.stateCode === this.form.state);
  }

  // When country changes, reset dependent fields
  async countryChanged(newValue: string, oldValue: string) {
    if (newValue !== oldValue) {
      this.form.state = '';
      this.form.city = '';

      // In real app, load states from API
      if (newValue) {
        this.isLoadingStates = true;
        await this.loadStates(newValue);
        this.isLoadingStates = false;
      }
    }
  }

  // When state changes, reset city
  async stateChanged(newValue: string, oldValue: string) {
    if (newValue !== oldValue) {
      this.form.city = '';

      // In real app, load cities from API
      if (newValue) {
        this.isLoadingCities = true;
        await this.loadCities(newValue);
        this.isLoadingCities = false;
      }
    }
  }

  private async loadStates(countryCode: string): Promise<void> {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 500));
    // States are already filtered by computed property
  }

  private async loadCities(stateCode: string): Promise<void> {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 500));
    // Cities are already filtered by computed property
  }

  async submit() {
    const result = await this.validation.validate();

    if (!result.valid) {
      return;
    }

    console.log('Location submitted:', this.form);
  }
}
```

```html
<!-- src/components/location-selector.html -->
  <form submit.trigger="submit()" class="location-form">
    <h2>Enter Your Location</h2>

    <div class="form-field">
      <label for="country">Country *</label>
      <select
        id="country"
        value.bind="form.country & validate">
        <option value="">Select a country</option>
        <option
          repeat.for="country of allCountries"
          value.bind="country.code">
          ${country.name}
        </option>
      </select>
    </div>

    <div class="form-field">
      <label for="state">State/Province *</label>
      <select
        id="state"
        value.bind="form.state & validate"
        disabled.bind="!form.country || isLoadingStates">
        <option value="">
          ${isLoadingStates ? 'Loading...' : 'Select a state'}
        </option>
        <option
          repeat.for="state of availableStates"
          value.bind="state.code">
          ${state.name}
        </option>
      </select>
      <div if.bind="!form.country" class="field-hint">
        Please select a country first
      </div>
    </div>

    <div class="form-field">
      <label for="city">City *</label>
      <select
        id="city"
        value.bind="form.city & validate"
        disabled.bind="!form.state || isLoadingCities">
        <option value="">
          ${isLoadingCities ? 'Loading...' : 'Select a city'}
        </option>
        <option
          repeat.for="city of availableCities"
          value.bind="city.id">
          ${city.name}
        </option>
      </select>
      <div if.bind="!form.state" class="field-hint">
        Please select a state first
      </div>
    </div>

    <div class="form-field">
      <label for="address">Street Address *</label>
      <input
        type="text"
        id="address"
        value.bind="form.address & validate"
        placeholder="123 Main St">
    </div>

    <div class="form-field">
      <label for="zipCode">ZIP/Postal Code *</label>
      <input
        type="text"
        id="zipCode"
        value.bind="form.zipCode & validate"
        placeholder="12345">
    </div>

    <button type="submit" class="btn btn-primary">
      Continue
    </button>
  </form>
```

**Key Features**:

* Cascading selects (country → state → city)
* Computed properties for filtered options
* Auto-reset dependent fields when parent changes
* Loading states while fetching options
* Disabled state until parent is selected
* Helpful hints for users

***

## Reusable Form Field Components

Create reusable form field components that encapsulate label, input, validation, and error display.

### Complete Example: Validated Text Field Component

```typescript
// src/components/validated-field.ts
import { bindable, INode } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
import { IValidationController } from '@aurelia/validation-html';

export class ValidatedField {
  @bindable label: string;
  @bindable value: any;
  @bindable type: string = 'text';
  @bindable placeholder: string = '';
  @bindable required: boolean = false;
  @bindable disabled: boolean = false;
  @bindable hint: string = '';
  @bindable validation: IValidationController;

  private fieldId: string;
  private inputElement: HTMLInputElement;
  private element = resolve(INode);

  constructor() {
    this.fieldId = `field-${Math.random().toString(36).substr(2, 9)}`;
  }

  get errors(): string[] {
    if (!this.validation) return [];

    const results = this.validation.results || [];
    return results
      .filter(r => !r.valid && r.propertyName === this.getPropertyName())
      .map(r => r.message);
  }

  get hasError(): boolean {
    return this.errors.length > 0;
  }

  private getPropertyName(): string {
    // Extract property name from binding expression
    // This is a simplified version
    const binding = this.element.getAttribute('value.bind');
    return binding?.split('&')[0].trim() || '';
  }

  focus() {
    this.inputElement?.focus();
  }
}
```

```html
<!-- src/components/validated-field.html -->
  <div class="form-field ${hasError ? 'has-error' : ''}">
    <label for.bind="fieldId">
      ${label}
      <span if.bind="required" class="required-indicator">*</span>
    </label>

    <input
      ref="inputElement"
      type.bind="type"
      id.bind="fieldId"
      value.bind="value & validate"
      placeholder.bind="placeholder"
      disabled.bind="disabled"
      aria-invalid.bind="hasError"
      aria-describedby="${fieldId}-hint ${hasError ? `${fieldId}-error` : ''}">

    <div
      if.bind="hint && !hasError"
      id="${fieldId}-hint"
      class="field-hint">
      ${hint}
    </div>

    <div
      if.bind="hasError"
      id="${fieldId}-error"
      class="field-error"
      role="alert">
      ${errors[0]}
    </div>
  </div>
```

### Usage Example

```typescript
// src/pages/signup.ts
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class Signup {
  private user = {
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  };

  private validation = resolve(newInstanceForScope(IValidationController));

  constructor(
    @IValidationRules validationRules: IValidationRules = resolve(IValidationRules)
  ) {
    validationRules
      .on(this.user)
      .ensure('username')
        .required()
        .minLength(3)
        .matches(/^[a-zA-Z0-9_]+$/)
        .withMessage('Username can only contain letters, numbers, and underscores')
      .ensure('email')
        .required()
        .email()
      .ensure('password')
        .required()
        .minLength(8)
      .ensure('confirmPassword')
        .required()
        .satisfies((value: string) => value === this.user.password)
        .withMessage('Passwords must match');
  }

  async submit() {
    const result = await this.validation.validate();
    if (!result.valid) return;

    console.log('Signup:', this.user);
  }
}
```

```html
<!-- src/pages/signup.html -->
  <form submit.trigger="submit()">
    <h2>Sign Up</h2>

    <validated-field
      label="Username"
      value.bind="user.username"
      validation.bind="validation"
      required.bind="true"
      hint="Letters, numbers, and underscores only">
    </validated-field>

    <validated-field
      label="Email"
      type="email"
      value.bind="user.email"
      validation.bind="validation"
      required.bind="true">
    </validated-field>

    <validated-field
      label="Password"
      type="password"
      value.bind="user.password"
      validation.bind="validation"
      required.bind="true"
      hint="Minimum 8 characters">
    </validated-field>

    <validated-field
      label="Confirm Password"
      type="password"
      value.bind="user.confirmPassword"
      validation.bind="validation"
      required.bind="true">
    </validated-field>

    <button type="submit" class="btn btn-primary">
      Create Account
    </button>
  </form>
```

**Key Features**:

* Encapsulates label, input, validation, error display
* Reusable across entire application
* Consistent styling and behavior
* Accessible (proper ARIA attributes)
* Reduced boilerplate in forms

***

## Related Documentation

* [Form Basics](/templates/forms/forms) - Basic form concepts
* [Validation Plugin](/aurelia-packages/validation) - Complete validation guide
* [Form Submission](/templates/forms/submission) - Handling form submission
* [Collections (Checkboxes, Radios, Select)](/templates/forms/collections) - Working with form collections
* [File Uploads](/templates/forms/file-uploads) - File upload patterns

***

## Summary

These advanced patterns handle complex real-world scenarios:

1. **Multi-step wizards** - Break complex forms into manageable steps with conditional validation and progress tracking
2. **Dynamic forms** - Add/remove individual fields at runtime with validation
3. **Conditional validation** - Validation rules that depend on other field values
4. **Form state management** - Track changes, prevent data loss, implement autosave with router guards
5. **Form arrays** - Repeating field groups (like invoice line items) with add/remove/duplicate functionality
6. **Complex file uploads** - Multiple file uploads with drag & drop, previews, progress tracking, and per-file validation
7. **Dependent dropdowns** - Cascading selects (country → state → city) with auto-reset and loading states
8. **Reusable form fields** - Encapsulated field components with built-in validation display

All examples use proper Aurelia 2 syntax with the validation plugin and follow accessibility best practices. Each pattern includes complete, production-ready code with TypeScript interfaces, validation rules, and accessible HTML templates.


# Comprehensive Reference

Master Aurelia 2 forms with comprehensive coverage of binding patterns, advanced  collections, validation integration, and performance optimization for production applications.

Forms are the cornerstone of interactive web applications. Whether you're building simple contact forms, complex data-entry systems, or dynamic configuration interfaces, Aurelia provides a comprehensive and performant forms system. This guide covers everything from basic input binding to advanced patterns like collection-based form controls, dynamic form generation, and seamless validation integration.

{% hint style="success" %}
**Looking for focused guides?** This is a comprehensive reference covering all form concepts. For more digestible, task-focused guides, check out:

* [**Form Basics**](/templates/forms/forms) - Text inputs, textareas, number/date inputs
* [**Collections**](/templates/forms/collections) - Checkboxes, radio buttons, select elements, Sets, Maps
* [**Form Submission**](/templates/forms/submission) - Handling form submission, state management, auto-save
* [**File Uploads**](/templates/forms/file-uploads) - File input handling, validation, progress tracking
* [**Advanced Patterns**](/templates/forms/advanced-patterns) - Multi-step wizards, dynamic forms, conditional validation, form state management
  {% endhint %}

{% hint style="info" %}
This guide assumes familiarity with Aurelia's binding system and template syntax. For fundamentals, see [Template Syntax & Features](/templates/overview) first.
{% endhint %}

## Table of Contents

1. [Understanding Aurelia's Form Architecture](#understanding-aurelias-form-architecture)
2. [Basic Input Binding](#basic-input-binding)
3. [Advanced Collection Patterns](#advanced-collection-patterns)
4. [Dynamic Forms and Performance](#dynamic-forms-and-performance)
5. [Event Handling and Binding Behaviors](#event-handling-and-binding-behaviors)
6. [Validation Integration](#validation-integration)
7. [File Upload Handling](#file-upload-handling)
8. [Form Submission Patterns](#form-submission-patterns)
9. [Security and Best Practices](#security-and-best-practices)
10. [Accessibility Considerations](#accessibility-considerations)

***

## Understanding Aurelia's Form Architecture

Aurelia's forms system is built on sophisticated observer patterns that provide automatic synchronization between your view models and form controls. Understanding this architecture helps you build more efficient and maintainable forms.

### Data Flow Architecture

```
User Input → DOM Event → Observer → Binding → View Model → Reactive Updates
     ↑                                                            ↓
Form Element ← DOM Update ← Binding ← Property Change ← View Model
```

**Key Components:**

1. **Observers**: Monitor DOM events and property changes
2. **Bindings**: Connect observers to view model properties
3. **Collection Observers**: Handle arrays, Sets, and Maps efficiently
4. **Mutation Observers**: Track dynamic DOM changes
5. **Value Converters & Binding Behaviors**: Transform and control data flow

### Automatic Change Detection

Aurelia automatically observes:

* **Text inputs**: `input`, `change`, `keyup` events
* **Checkboxes/Radio**: `change` events with array synchronization
* **Select elements**: `change` events with mutation observation
* **Collections**: Array mutations, Set/Map changes
* **Object properties**: Deep property observation

This means you typically don't need manual event handlers—Aurelia handles the complexity automatically while providing hooks for customization when needed.

***

## Basic Input Binding

Aurelia provides intuitive two-way binding for all standard form elements. Let's start with the fundamentals and build toward advanced patterns.

### Simple Text Inputs

The foundation of most forms is text input binding:

```html
<form submit.trigger="handleSubmit()">
  <div class="form-group">
    <label for="email">Email:</label>
    <input id="email" 
           type="email" 
           value.bind="email" 
           placeholder.bind="emailPlaceholder" />
  </div>
  <div class="form-group">
    <label for="password">Password:</label>
    <input id="password" 
           type="password" 
           value.bind="password" />
  </div>
  <button type="submit" disabled.bind="!isFormValid">Login</button>
</form>
```

```typescript
export class LoginComponent {
  email = '';
  password = '';
  emailPlaceholder = 'Enter your email address';

  get isFormValid(): boolean {
    return this.email.length > 0 && this.password.length >= 8;
  }

  handleSubmit() {
    if (this.isFormValid) {
      // Process form submission
      console.log('Submitting:', { email: this.email, password: this.password });
    }
  }
}
```

### Textarea Binding

Textareas work identically to text inputs:

```html
<div class="form-group">
  <label for="comments">Comments:</label>
  <textarea id="comments" 
            value.bind="comments"
            rows="4"
            maxlength.bind="maxCommentLength"></textarea>
  <small>${comments.length}/${maxCommentLength} characters</small>
</div>
```

```typescript
export class FeedbackForm {
  comments = '';
  maxCommentLength = 500;
}
```

### Number and Date Inputs

Browser form controls always provide string values unless you bind to their typed DOM properties. Use Aurelia's `value-as-*` bindings when you need numbers or dates in your view-model.

```html
<div class="form-group">
  <label for="age">Age:</label>
  <input id="age"
         type="number"
         value-as-number.bind="age"
         min="18"
         max="120" />
</div>
<div class="form-group">
  <label for="birthdate">Birth Date:</label>
  <input id="birthdate"
         type="date"
         value-as-date.bind="birthDate" />
</div>
<div class="form-group">
  <label for="appointment">Appointment Time:</label>
  <input id="appointment"
         type="datetime-local"
         value-as-date.bind="appointmentTime" />
</div>
```

```typescript
export class ProfileForm {
  age = 25;
  birthDate = new Date('1998-01-01');
  appointmentTime = new Date();
  
  // Computed property demonstrating reactive updates
  get isAdult(): boolean {
    return this.age >= 18;
  }
}
```

`value-as-number` binds to the input's `valueAsNumber`, so `age` is a `number` (or `NaN` when the field is empty/invalid). `value-as-date` binds to `valueAsDate`, giving you a `Date | null`. If you keep `value.bind`, the value remains a string—be sure to convert it before serializing to JSON for APIs.

***

## Binding With Text and Textarea Inputs

### Text Input

Binding to text inputs in Aurelia is straightforward:

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

You can also bind other attributes like placeholder:

```html
<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:

```html
<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.

***

## Advanced Collection Patterns

One of Aurelia's most powerful features is its sophisticated support for collection-based form controls. Beyond simple arrays, Aurelia supports Sets, Maps, and custom collection types with optimal performance characteristics.

### Boolean Checkboxes

The simplest checkbox pattern binds to boolean properties:

```typescript
export class PreferencesForm {
  emailNotifications = false;
  smsNotifications = true;
  pushNotifications = false;
  
  // Computed property for form validation
  get hasValidNotificationPrefs(): boolean {
    return this.emailNotifications || this.smsNotifications || this.pushNotifications;
  }
}
```

```html
<form>
  <fieldset>
    <legend>Notification Preferences</legend>
    <label>
      <input type="checkbox" checked.bind="emailNotifications" />
      Email notifications
    </label>
    <label>
      <input type="checkbox" checked.bind="smsNotifications" />
      SMS notifications
    </label>
    <label>
      <input type="checkbox" checked.bind="pushNotifications" />
      Push notifications
    </label>
  </fieldset>
  
  <div if.bind="!hasValidNotificationPrefs" class="warning">
    Please select at least one notification method.
  </div>
</form>
```

### Array-Based Multi-Select

For traditional multi-select scenarios, bind arrays to checkbox groups:

```typescript
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
}

export class ProductSelectionForm {
  products: Product[] = [
    { id: 1, name: "Gaming Mouse", category: "Peripherals", price: 89.99 },
    { id: 2, name: "Mechanical Keyboard", category: "Peripherals", price: 159.99 },
    { id: 3, name: "4K Monitor", category: "Display", price: 399.99 },
    { id: 4, name: "Graphics Card", category: "Components", price: 599.99 }
  ];

  // Array of selected product IDs
  selectedProductIds: number[] = [];
  
  // Array of selected product objects
  selectedProducts: Product[] = [];

  get totalValue(): number {
    return this.selectedProducts.reduce((sum, product) => sum + product.price, 0);
  }
}
```

```html
<form>
  <h3>Select Products</h3>
  
  <!-- ID-based selection -->
  <div class="product-grid">
    <div repeat.for="product of products" class="product-card">
      <label>
        <input type="checkbox" 
               model.bind="product.id" 
               checked.bind="selectedProductIds" />
        <strong>${product.name}</strong>
        <span class="category">${product.category}</span>
        <span class="price">$${product.price}</span>
      </label>
    </div>
  </div>

  <!-- Object-based selection (more flexible) -->
  <h4>Or select complete product objects:</h4>
  <div class="product-list">
    <label repeat.for="product of products" class="product-item">
      <input type="checkbox" 
             model.bind="product" 
             checked.bind="selectedProducts" />
      ${product.name} - $${product.price}
    </label>
  </div>

  <div class="summary" if.bind="selectedProducts.length">
    <h4>Selected Items (${selectedProducts.length})</h4>
    <ul>
      <li repeat.for="product of selectedProducts">
        ${product.name} - $${product.price}
      </li>
    </ul>
    <strong>Total: $${totalValue | number:'0.00'}</strong>
  </div>
</form>
```

### Set-Based Collections (Advanced)

For high-performance scenarios with frequent additions/removals, use Set collections:

```typescript
export class TagSelectionForm {
  availableTags = [
    { id: 'frontend', name: 'Frontend Development', color: '#blue' },
    { id: 'backend', name: 'Backend Development', color: '#green' },
    { id: 'database', name: 'Database Design', color: '#orange' },
    { id: 'devops', name: 'DevOps', color: '#purple' },
    { id: 'mobile', name: 'Mobile Development', color: '#red' }
  ];

  // Set-based selection for O(1) lookups
  selectedTags: Set<string> = new Set(['frontend', 'database']);

  get selectedTagList() {
    return this.availableTags.filter(tag => this.selectedTags.has(tag.id));
  }

  toggleTag(tagId: string) {
    if (this.selectedTags.has(tagId)) {
      this.selectedTags.delete(tagId);
    } else {
      this.selectedTags.add(tagId);
    }
  }
}
```

```html
<form>
  <h3>Select Your Skills</h3>
  <div class="tag-container">
    <label repeat.for="tag of availableTags" 
           class="tag-label" 
           css.bind="{ '--tag-color': tag.color }">
      <input type="checkbox" 
             model.bind="tag.id" 
             checked.bind="selectedTags" />
      <span class="tag-text">${tag.name}</span>
    </label>
  </div>

  <div if.bind="selectedTags.size > 0" class="selected-tags">
    <h4>Selected Skills (${selectedTags.size})</h4>
    <div class="tag-chips">
      <span repeat.for="tag of selectedTagList" class="tag-chip">
        ${tag.name}
        <button type="button" 
                click.trigger="toggleTag(tag.id)" 
                class="remove-tag">×</button>
      </span>
    </div>
  </div>
</form>
```

### Resource-Keyed Collections (Expert Level)

### Per-Resource Permission Sets (Expert Level)

For complex key-value selections (e.g., multiple actions per resource), keep a `Set` per resource so each checkbox can reuse Aurelia's built-in collection handling:

```typescript
interface Permission {
  resource: string;
  actions: string[];
  description: string;
}

export class PermissionForm {
  permissions: Permission[] = [
    {
      resource: 'users',
      actions: ['create', 'read', 'update', 'delete'],
      description: 'User management operations'
    },
    {
      resource: 'posts',
      actions: ['create', 'read', 'update', 'delete', 'publish'],
      description: 'Content management operations'
    },
    {
      resource: 'settings',
      actions: ['read', 'update'],
      description: 'System configuration'
    }
  ];

  // Record: resource -> Set<action>
  selectedPermissions: Record<string, Set<string>> = {};

  constructor() {
    for (const permission of this.permissions) {
      this.selectedPermissions[permission.resource] = new Set();
    }
    this.selectedPermissions['users'].add('read');
    this.selectedPermissions['posts'].add('read');
    this.selectedPermissions['posts'].add('create');
  }

  get permissionSummary() {
    const summary: Array<{ resource: string; actions: string[] }> = [];
    Object.entries(this.selectedPermissions).forEach(([resource, actions]) => {
      if (actions.size > 0) {
        summary.push({ resource, actions: Array.from(actions) });
      }
    });
    return summary;
  }
}
```

```html
<form>
  <h3>Configure Permissions</h3>
  <div class="permission-matrix">
    <div repeat.for="permission of permissions" class="permission-group">
      <h4>${permission.resource | capitalize}</h4>
      <p class="description">${permission.description}</p>
      <div class="action-checkboxes">
        <label repeat.for="action of permission.actions" class="action-label">
          <input type="checkbox" 
                 model.bind="action"
                 checked.bind="selectedPermissions[permission.resource]" />
          ${action | capitalize}
        </label>
      </div>
    </div>
  </div>

  <div if.bind="permissionSummary.length > 0" class="permission-summary">
    <h4>Selected Permissions</h4>
    <ul>
      <li repeat.for="perm of permissionSummary">
        <strong>${perm.resource}</strong>: ${perm.actions.join(', ')}
      </li>
    </ul>
  </div>
</form>
```

### Performance Considerations

**Choose the right collection type for your use case:**

* **Arrays**: General purpose, good for small to medium collections
* **Sets**: High-performance for frequent additions/removals, O(1) lookups
* **Record/Map of Sets**: Model resource → actions (or similar hierarchies) cleanly
* **Custom Matchers**: When object identity comparison isn't sufficient

**Performance Tips:**

* Use Set for large collections with frequent changes
* Implement efficient matcher functions for object comparison
* Avoid creating new objects in templates—use computed properties
* Consider virtualization for very large checkbox lists

***

## Event Handling and Binding Behaviors

Aurelia provides sophisticated event handling and binding behaviors that give you precise control over when and how form data synchronizes. These features are crucial for building responsive, performant forms.

### Advanced Event Timing with updateTrigger

By default, Aurelia uses appropriate events for each input type, but you can customize this behavior:

```typescript
export class AdvancedForm {
  searchQuery = '';
  username = '';
  description = '';

  // Debounced search handler
  performSearch = debounce((query: string) => {
    console.log('Searching for:', query);
    // Perform API call
  }, 300);

  searchQueryChanged(newValue: string) {
    this.performSearch(newValue);
  }
}
```

```html
<form>
  <!-- Update on every keystroke (input event) -->
  <input type="text" 
         value.bind="searchQuery & updateTrigger:'input'" 
         placeholder="Real-time search..." />

  <!-- Update on focus loss (blur event) -->
  <input type="text" 
         value.bind="username & updateTrigger:'blur'" 
         placeholder="Username (validates on blur)" />

  <!-- Multiple events -->
  <textarea value.bind="description & updateTrigger:['input', 'blur']"
            placeholder="Auto-save draft on input and blur"></textarea>

  <!-- Custom events -->
  <input type="text" 
         value.bind="customValue & updateTrigger:'keydown':'focus'"
         placeholder="Updates on keydown and focus" />
</form>
```

### Rate Limiting with Debounce and Throttle

Control the frequency of updates to improve performance and user experience:

```typescript
export class SearchForm {
  searchTerm = '';
  scrollPosition = 0;
  apiCallCount = 0;

  // This will be called max once per 300ms
  searchTermChanged(newTerm: string) {
    this.apiCallCount++;
    console.log(`API Call #${this.apiCallCount}: Searching for "${newTerm}"`);
  }

  // Throttled scroll tracking
  onScroll(position: number) {
    console.log('Scroll position:', position);
  }
}
```

```html
<form>
  <!-- Debounce: Wait for pause in typing -->
  <input type="search" 
         value.bind="searchTerm & debounce:300"
         placeholder="Search with 300ms debounce..." />

  <!-- Throttle: Maximum rate limiting -->
  <input type="range" 
         min="0" max="100" 
         value.bind="scrollPosition & throttle:100"
         input.trigger="onScroll(scrollPosition)" />

  <!-- Signal-based cache invalidation -->
  <input type="text" 
         value.bind="searchTerm & debounce:300:'searchSignal'"
         placeholder="Cache-aware search" />
</form>
```

### Signal-Based Reactive Updates

Signals provide cache invalidation and coordinated updates across components:

```typescript
import { resolve } from '@aurelia/kernel';
import { observable, computed } from '@aurelia/runtime';
import { ISignaler } from '@aurelia/runtime-html';

export class SignalDrivenForm {
  @observable searchCriteria = {
    term: '',
    category: 'all',
    priceRange: [0, 1000]
  };

  // Signal dispatcher
  private signaler = resolve(ISignaler);

  updateSearch(criteria: Partial<typeof this.searchCriteria>) {
    Object.assign(this.searchCriteria, criteria);
    // Notify all signal listeners
    this.signaler.dispatchSignal('searchUpdated');
  }

  // Expensive computed property with signal-based cache
  @computed('searchCriteria', 'searchUpdated')
  get searchResults() {
    // This will only recompute when 'searchUpdated' signal is dispatched
    return this.performExpensiveSearch(this.searchCriteria);
  }

  private performExpensiveSearch(criteria: any) {
    console.log('Performing expensive search operation...');
    // Simulate expensive computation
    return [];
  }
}
```

```html
<form>
  <!-- Signal-coordinated form fields -->
  <input type="search" 
         value.bind="searchCriteria.term & debounce:300:'searchUpdated'" />

  <select value.bind="searchCriteria.category & signal:'searchUpdated'">
    <option value="all">All Categories</option>
    <option value="electronics">Electronics</option>
    <option value="books">Books</option>
  </select>

  <input type="range" 
         min="0" max="1000"
         value.bind="searchCriteria.priceRange[1] & throttle:200:'searchUpdated'" />

  <!-- Results update automatically via signal -->
  <div class="results">
    <p>Found ${searchResults.length} results</p>
    <!-- Results rendered here -->
  </div>
</form>
```

***

## Dynamic Forms and Performance

Building performant, dynamic forms requires understanding Aurelia's observation system and applying optimization strategies for complex scenarios.

### Dynamic Field Generation

Create forms that adapt their structure based on configuration:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

interface FieldConfig {
  type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea';
  name: string;
  label: string;
  required?: boolean;
  options?: Array<{ value: any; label: string }>;
  validation?: string[];
  placeholder?: string;
  min?: number;
  max?: number;
}

export class DynamicFormGenerator {
  formConfig: FieldConfig[] = [];
  formData: Record<string, any> = {};
  formSchema: string = '';

  private readonly validationRules = resolve(IValidationRules);
  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  // Load form configuration from various sources
  async loadFormConfiguration(schemaId: string) {
    try {
      const response = await fetch(`/api/forms/schema/${schemaId}`);
      this.formConfig = await response.json();
      this.initializeFormData();
    } catch (error) {
      console.error('Failed to load form schema:', error);
    }
  }

  private initializeFormData() {
    this.formData = {};
    this.formConfig.forEach(field => {
      switch (field.type) {
        case 'checkbox':
          this.formData[field.name] = false;
          break;
        case 'number':
          this.formData[field.name] = field.min || 0;
          break;
        default:
          this.formData[field.name] = '';
      }
    });
  }

  // Dynamic validation rule setup
  setupDynamicValidation() {
    
    this.formConfig.forEach(fieldConfig => {
      let rule = this.validationRules.on(this.formData).ensure(fieldConfig.name);
      
      if (fieldConfig.required) {
        rule = rule.required().withMessage(`${fieldConfig.label} is required`);
      }
      
      if (fieldConfig.validation) {
        fieldConfig.validation.forEach(validationType => {
          switch (validationType) {
            case 'email':
              rule = rule.email().withMessage('Please enter a valid email address');
              break;
            case 'min-length-5':
              rule = rule.minLength(5).withMessage(`${fieldConfig.label} must be at least 5 characters`);
              break;
            // Add more validation types as needed
          }
        });
      }
      
      if (fieldConfig.type === 'number') {
        if (fieldConfig.min !== undefined) {
          rule = rule.min(fieldConfig.min);
        }
        if (fieldConfig.max !== undefined) {
          rule = rule.max(fieldConfig.max);
        }
      }
    });
  }

  addField(fieldConfig: FieldConfig) {
    this.formConfig.push(fieldConfig);
    // Initialize form data for new field
    this.formData[fieldConfig.name] = fieldConfig.type === 'checkbox' ? false : '';
    this.setupDynamicValidation();
  }

  removeField(fieldName: string) {
    this.formConfig = this.formConfig.filter(field => field.name !== fieldName);
    delete this.formData[fieldName];
  }

  async submitDynamicForm() {
    const validationResult = await this.validationController.validate();
    
    if (validationResult.valid) {
      const payload = {
        schemaId: this.formSchema,
        data: this.formData,
        timestamp: new Date().toISOString()
      };
      
      try {
        const response = await fetch('/api/forms/submit', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });
        
        if (response.ok) {
          console.log('Form submitted successfully');
        }
      } catch (error) {
        console.error('Form submission failed:', error);
      }
    }
  }
}
```

```html
<form submit.trigger="submitDynamicForm()" class="dynamic-form">
  <h2>Dynamic Form Generator</h2>
  
  <div class="form-controls">
    <label for="schema-select">Form Schema:</label>
    <select id="schema-select" 
            value.bind="formSchema" 
            change.trigger="loadFormConfiguration(formSchema)">
      <option value="">Select a form schema...</option>
      <option value="contact">Contact Form</option>
      <option value="survey">Survey Form</option>
      <option value="registration">Registration Form</option>
    </select>
  </div>

  <div if.bind="formConfig.length > 0" class="dynamic-fields">
    <div repeat.for="field of formConfig" class="form-group">
      
      <!-- Text Input -->
      <div if.bind="field.type === 'text'" class="field-container">
        <label for.bind="field.name">${field.label}</label>
        <input type="text"
               id.bind="field.name"
               value.bind="formData[field.name] & validate"
               placeholder.bind="field.placeholder"
               class="form-control" />
      </div>

      <!-- Number Input -->
      <div if.bind="field.type === 'number'" class="field-container">
        <label for.bind="field.name">${field.label}</label>
        <input type="number"
               id.bind="field.name"
               value.bind="formData[field.name] & validate"
               min.bind="field.min"
               max.bind="field.max"
               class="form-control" />
      </div>

      <!-- Select Dropdown -->
      <div if.bind="field.type === 'select'" class="field-container">
        <label for.bind="field.name">${field.label}</label>
        <select id.bind="field.name"
                value.bind="formData[field.name] & validate"
                class="form-control">
          <option value="">Choose...</option>
          <option repeat.for="option of field.options" 
                  model.bind="option.value">
            ${option.label}
          </option>
        </select>
      </div>

      <!-- Checkbox -->
      <div if.bind="field.type === 'checkbox'" class="field-container">
        <label class="checkbox-label">
          <input type="checkbox"
                 checked.bind="formData[field.name] & validate" />
          ${field.label}
        </label>
      </div>

      <!-- Textarea -->
      <div if.bind="field.type === 'textarea'" class="field-container">
        <label for.bind="field.name">${field.label}</label>
        <textarea id.bind="field.name"
                  value.bind="formData[field.name] & validate"
                  placeholder.bind="field.placeholder"
                  rows="4"
                  class="form-control"></textarea>
      </div>

      <!-- Field Management (Development Mode) -->
      <div class="field-actions" if.bind="developmentMode">
        <button type="button" 
                click.trigger="removeField(field.name)"
                class="btn btn-sm btn-danger">
          Remove Field
        </button>
      </div>
    </div>
  </div>

  <div class="form-actions" if.bind="formConfig.length > 0">
    <button type="submit" class="btn btn-primary">Submit Form</button>
    <button type="button" 
            click.trigger="formData = {}"
            class="btn btn-secondary">Clear Form</button>
  </div>

  <!-- Debug Information -->
  <div class="debug-panel" if.bind="debugMode">
    <h4>Form Data Debug</h4>
    <pre>${formData | json}</pre>
    
    <h4>Form Configuration Debug</h4>
    <pre>${formConfig | json}</pre>
  </div>
</form>
```

### Performance Optimization Strategies

Implement performance optimizations for large, complex forms:

```typescript
export class PerformantFormComponent {
  // Virtual scrolling for large option lists
  largeDataSet: any[] = [];
  virtualScrollOptions = {
    itemHeight: 40,
    containerHeight: 300,
    buffer: 10
  };

  // Lazy loading of form sections
  private loadedSections: Set<string> = new Set();
  
  // Debounced validation for expensive operations
  private debouncedValidations = new Map<string, Function>();

  // Efficient collection operations
  selectedItems: Set<any> = new Set();
  
  // Memoized computed properties
  @computed('formData.firstName', 'formData.lastName', 'formData.email')
  get computedSummary() {
    // Expensive computation that only runs when dependencies change
    return this.generateFormSummary();
  }

  // Optimize large collection updates
  updateLargeCollection(newItems: any[]) {
    // Use Set for O(1) lookups instead of Array.includes()
    const newItemsSet = new Set(newItems.map(item => item.id));
    
    // Batch updates to minimize observer notifications
    this.startBatch();
    
    this.largeDataSet = this.largeDataSet.filter(item => {
      if (newItemsSet.has(item.id)) {
        // Update existing item efficiently
        Object.assign(item, newItems.find(newItem => newItem.id === item.id));
        return true;
      }
      return false;
    });
    
    // Add new items
    newItems.forEach(item => {
      if (!this.largeDataSet.find(existing => existing.id === item.id)) {
        this.largeDataSet.push(item);
      }
    });
    
    this.endBatch();
  }

  // Efficient form section loading
  async loadFormSection(sectionName: string) {
    if (this.loadedSections.has(sectionName)) {
      return; // Already loaded
    }

    try {
      const sectionData = await fetch(`/api/forms/sections/${sectionName}`);
      const section = await sectionData.json();
      
      // Load section-specific validation rules
      this.loadSectionValidation(section);
      
      this.loadedSections.add(sectionName);
    } catch (error) {
      console.error(`Failed to load section ${sectionName}:`, error);
    }
  }

  // Memory-efficient validation
  createEfficientValidator(fieldName: string, validatorFn: Function, delay: number = 300) {
    if (this.debouncedValidations.has(fieldName)) {
      // Cleanup existing validator
      const existingValidator = this.debouncedValidations.get(fieldName);
      existingValidator.cancel?.();
    }

    const debouncedValidator = debounce(validatorFn, delay);
    this.debouncedValidations.set(fieldName, debouncedValidator);
    return debouncedValidator;
  }

  // Cleanup on disposal
  dispose() {
    // Cancel all pending validations
    this.debouncedValidations.forEach(validator => {
      validator.cancel?.();
    });
    this.debouncedValidations.clear();
  }

  private startBatch() {
    // Implementation depends on your observer system
    // This is a conceptual example
  }

  private endBatch() {
    // Implementation depends on your observer system
    // This is a conceptual example
  }

  private generateFormSummary() {
    // Expensive computation
    return {
      completionPercentage: this.calculateCompletionPercentage(),
      validationStatus: this.getOverallValidationStatus(),
      estimatedTimeToComplete: this.estimateTimeToComplete()
    };
  }

  private calculateCompletionPercentage(): number {
    // Calculate based on filled vs empty fields
    return 85; // Example value
  }

  private getOverallValidationStatus(): string {
    // Aggregate validation status
    return 'partial'; // Example value
  }

  private estimateTimeToComplete(): number {
    // Estimate in minutes
    return 5; // Example value
  }

  private loadSectionValidation(section: any) {
    // Load validation rules specific to the section
  }
}
```

***

## Radio Button and Select Element Patterns

Aurelia provides comprehensive support for single-selection controls with sophisticated object binding and custom matching logic.

### Advanced Radio Button Groups

Radio buttons with complex object handling and conditional logic:

```typescript
interface PaymentMethod {
  id: string;
  type: 'credit' | 'debit' | 'paypal' | 'crypto';
  name: string;
  fee: number;
  processingTime: string;
  requiresVerification: boolean;
}

export class PaymentSelectionForm {
  paymentMethods: PaymentMethod[] = [
    {
      id: 'cc-visa',
      type: 'credit',
      name: 'Visa Credit Card',
      fee: 0,
      processingTime: 'Instant',
      requiresVerification: false
    },
    {
      id: 'pp-account',
      type: 'paypal',
      name: 'PayPal Account',
      fee: 2.50,
      processingTime: '1-2 business days',
      requiresVerification: true
    },
    {
      id: 'btc-wallet',
      type: 'crypto',
      name: 'Bitcoin Wallet',
      fee: 0.0001,
      processingTime: '10-60 minutes',
      requiresVerification: true
    }
  ];

  selectedPaymentMethod: PaymentMethod | null = null;
  
  // Custom matcher for complex object comparison
  paymentMethodMatcher = (a: PaymentMethod, b: PaymentMethod) => {
    return a?.id === b?.id;
  };

  // Computed properties based on selection
  get totalFee(): number {
    return this.selectedPaymentMethod?.fee || 0;
  }

  get requiresUserVerification(): boolean {
    return this.selectedPaymentMethod?.requiresVerification || false;
  }

  get processingDetails(): string {
    if (!this.selectedPaymentMethod) return '';
    
    return `Processing time: ${this.selectedPaymentMethod.processingTime}
            ${this.totalFee > 0 ? `| Fee: $${this.totalFee.toFixed(2)}` : '| No additional fees'}`;
  }

  // Conditional validation based on selection
  validatePaymentSelection(): boolean {
    if (!this.selectedPaymentMethod) return false;
    
    if (this.requiresUserVerification && !this.isUserVerified()) {
      console.warn('User verification required for selected payment method');
      return false;
    }
    
    return true;
  }

  private isUserVerified(): boolean {
    // Check user verification status
    return false; // Placeholder
  }
}
```

```html
<form class="payment-selection-form">
  <h3>Select Payment Method</h3>
  
  <div class="payment-options">
    <div repeat.for="method of paymentMethods" class="payment-option">
      <label class="payment-card" 
             class.bind="{ 'selected': selectedPaymentMethod?.id === method.id }">
        <input type="radio"
               name="paymentMethod"
               model.bind="method"
               checked.bind="selectedPaymentMethod"
               matcher.bind="paymentMethodMatcher"
               class="payment-radio" />
        
        <div class="payment-info">
          <div class="payment-header">
            <span class="payment-name">${method.name}</span>
            <span class="payment-type badge" 
                  class.bind="method.type">${method.type}</span>
          </div>
          
          <div class="payment-details">
            <div class="processing-time">
              <i class="icon-clock"></i>
              ${method.processingTime}
            </div>
            <div class="fee-info">
              <i class="icon-dollar"></i>
              ${method.fee === 0 ? 'No fees' : '$' + method.fee.toFixed(2)}
            </div>
            <div if.bind="method.requiresVerification" class="verification-required">
              <i class="icon-shield"></i>
              Verification required
            </div>
          </div>
        </div>
      </label>
    </div>
  </div>

  <!-- Selection Summary -->
  <div if.bind="selectedPaymentMethod" class="selection-summary">
    <h4>Payment Summary</h4>
    <div class="summary-details">
      <div class="summary-row">
        <span>Method:</span>
        <span>${selectedPaymentMethod.name}</span>
      </div>
      <div class="summary-row">
        <span>Processing:</span>
        <span>${selectedPaymentMethod.processingTime}</span>
      </div>
      <div class="summary-row">
        <span>Fee:</span>
        <span>${totalFee === 0 ? 'Free' : '$' + totalFee.toFixed(2)}</span>
      </div>
      <div if.bind="requiresUserVerification" class="verification-notice">
        <i class="icon-warning"></i>
        This payment method requires account verification
      </div>
    </div>
  </div>
</form>
```

### Advanced Select Elements with Smart Filtering

Sophisticated select components with search, grouping, and virtual scrolling:

```typescript
interface SelectOption {
  value: any;
  label: string;
  group?: string;
  disabled?: boolean;
  metadata?: any;
}

export class AdvancedSelectComponent {
  countries: SelectOption[] = [];
  filteredCountries: SelectOption[] = [];
  selectedCountry: SelectOption | null = null;
  searchTerm = '';
  isLoading = false;
  showDropdown = false;
  
  // Grouping and filtering
  groupBy = 'region';
  sortBy = 'name';
  
  async loadCountries() {
    this.isLoading = true;
    try {
      const response = await fetch('/api/countries');
      const data = await response.json();
      
      this.countries = data.map(country => ({
        value: country.code,
        label: country.name,
        group: country.region,
        metadata: {
          population: country.population,
          currency: country.currency,
          flag: country.flag
        }
      }));
      
      this.applyFiltering();
    } catch (error) {
      console.error('Failed to load countries:', error);
    } finally {
      this.isLoading = false;
    }
  }

  searchTermChanged() {
    this.applyFiltering();
  }

  applyFiltering() {
    let filtered = this.countries;

    // Apply search filter
    if (this.searchTerm) {
      const term = this.searchTerm.toLowerCase();
      filtered = filtered.filter(country => 
        country.label.toLowerCase().includes(term) ||
        country.group?.toLowerCase().includes(term)
      );
    }

    // Apply grouping and sorting
    if (this.groupBy) {
      filtered = this.sortByGroup(filtered, this.groupBy);
    }

    this.filteredCountries = filtered;
  }

  private sortByGroup(options: SelectOption[], groupField: string): SelectOption[] {
    const groups = new Map<string, SelectOption[]>();
    
    // Group options
    options.forEach(option => {
      const groupKey = option.group || 'Other';
      if (!groups.has(groupKey)) {
        groups.set(groupKey, []);
      }
      groups.get(groupKey)!.push(option);
    });

    // Sort within groups and flatten
    const sortedOptions: SelectOption[] = [];
    Array.from(groups.keys()).sort().forEach(groupKey => {
      const groupOptions = groups.get(groupKey)!
        .sort((a, b) => a.label.localeCompare(b.label));
      sortedOptions.push(...groupOptions);
    });

    return sortedOptions;
  }

  selectOption(option: SelectOption) {
    if (option.disabled) return;
    
    this.selectedCountry = option;
    this.showDropdown = false;
    this.searchTerm = '';
  }

  // Custom matcher for complex object comparison
  countryMatcher = (a: SelectOption, b: SelectOption) => {
    return a?.value === b?.value;
  };

  // Virtual scrolling configuration for large lists
  virtualScrollConfig = {
    itemHeight: 48,
    containerHeight: 300,
    overscan: 10
  };
}
```

```html
<div class="advanced-select-container">
  <label for="country-select">Select Country</label>
  
  <!-- Custom Select with Search -->
  <div class="custom-select" class.bind="{ 'open': showDropdown }">
    <div class="select-trigger" 
         click.trigger="showDropdown = !showDropdown">
      <span class="selected-value">
        ${selectedCountry ? selectedCountry.label : 'Choose a country...'}
      </span>
      <i class="dropdown-arrow" 
         class.bind="{ 'rotated': showDropdown }"></i>
    </div>

    <div class="select-dropdown" if.bind="showDropdown">
      <!-- Search Input -->
      <div class="search-container">
        <input type="text"
               value.bind="searchTerm & debounce:200"
               placeholder="Search countries..."
               class="search-input"
               focus.bind="showDropdown" />
        <i class="search-icon"></i>
      </div>

      <!-- Loading State -->
      <div if.bind="isLoading" class="loading-state">
        <i class="spinner"></i>
        Loading countries...
      </div>

      <!-- Options List with Virtual Scrolling -->
      <div class="options-container" if.bind="!isLoading">
        <virtual-repeat.for="option of filteredCountries"
                          virtual-repeat-strategy="virtual-repeat-strategy-array">
          <div class="select-option"
               class.bind="{ 
                 'disabled': option.disabled,
                 'selected': selectedCountry?.value === option.value 
               }"
               click.trigger="selectOption(option)">
            
            <!-- Group Header -->
            <div if.bind="$index === 0 || filteredCountries[$index-1].group !== option.group" 
                 class="group-header">
              ${option.group || 'Other'}
            </div>

            <!-- Option Content -->
            <div class="option-content">
              <div class="option-main">
                <span if.bind="option.metadata?.flag" 
                      class="flag">${option.metadata.flag}</span>
                <span class="option-label">${option.label}</span>
              </div>
              <div class="option-details" if.bind="option.metadata">
                <small class="population">
                  Pop: ${option.metadata.population | number:'0,000'}
                </small>
                <small class="currency">
                  ${option.metadata.currency}
                </small>
              </div>
            </div>
          </div>
        </virtual-repeat>
      </div>

      <!-- No Results State -->
      <div if.bind="!isLoading && filteredCountries.length === 0" 
           class="no-results">
        No countries found matching "${searchTerm}"
      </div>
    </div>
  </div>

  <!-- Traditional Select (Fallback) -->
  <select if.bind="!useAdvancedSelect" 
          value.bind="selectedCountry"
          matcher.bind="countryMatcher"
          class="traditional-select">
    <option value="">Choose a country...</option>
    <optgroup repeat.for="group of groupedCountries" 
              label.bind="group.name">
      <option repeat.for="country of group.options" 
              model.bind="country"
              disabled.bind="country.disabled">
        ${country.label}
      </option>
    </optgroup>
  </select>
</div>
```

***

## Form Submission Patterns

Modern web applications require sophisticated form submission strategies that handle success, failure, loading states, and complex business logic. Aurelia provides flexible patterns for all scenarios.

### Basic Form Submission with State Management

Implement comprehensive submission state management for better user experience:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationController } from '@aurelia/validation-html';

interface SubmissionState {
  isSubmitting: boolean;
  success: boolean;
  error: string | null;
  attempts: number;
  lastSubmission: Date | null;
}

export class ComprehensiveFormSubmission {
  formData = {
    firstName: '',
    lastName: '',
    email: '',
    message: ''
  };

  submissionState: SubmissionState = {
    isSubmitting: false,
    success: false,
    error: null,
    attempts: 0,
    lastSubmission: null
  };

  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  // Rate limiting
  private readonly maxAttempts = 3;
  private readonly submissionCooldown = 30000; // 30 seconds

  get canSubmit(): boolean {
    return !this.submissionState.isSubmitting 
           && this.submissionState.attempts < this.maxAttempts
           && this.isWithinCooldown();
  }

  get submissionMessage(): string {
    if (this.submissionState.isSubmitting) {
      return 'Submitting your form...';
    }
    if (this.submissionState.success) {
      return 'Form submitted successfully!';
    }
    if (this.submissionState.error) {
      return `Submission failed: ${this.submissionState.error}`;
    }
    if (this.submissionState.attempts >= this.maxAttempts) {
      return `Maximum attempts reached. Please try again later.`;
    }
    return '';
  }

  async handleSubmit(event: Event) {
    event.preventDefault();

    if (!this.canSubmit) {
      return false; // Prevent form submission
    }

    // Reset previous state
    this.submissionState.error = null;
    this.submissionState.success = false;
    this.submissionState.isSubmitting = true;

    try {
      // Validate form before submission
      const validationResult = await this.validationController.validate();
      
      if (!validationResult.valid) {
        throw new Error('Please fix validation errors before submitting');
      }

      // Submit form data
      const response = await this.submitFormData(this.formData);
      
      // Handle success
      this.submissionState.success = true;
      this.submissionState.lastSubmission = new Date();
      
      // Optional: Redirect or show success message
      setTimeout(() => {
        this.resetForm();
      }, 2000);

    } catch (error) {
      // Handle submission error
      this.submissionState.error = error instanceof Error 
        ? error.message 
        : 'An unexpected error occurred';
      
      this.submissionState.attempts++;

      // Optional: Log error for debugging
      console.error('Form submission failed:', error);

    } finally {
      this.submissionState.isSubmitting = false;
    }

    return false; // Prevent default browser submission
  }

  private async submitFormData(data: any): Promise<any> {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify({
        ...data,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent
      })
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => null);
      throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  }

  private isWithinCooldown(): boolean {
    if (!this.submissionState.lastSubmission) return true;
    
    const timeSinceLastSubmission = Date.now() - this.submissionState.lastSubmission.getTime();
    return timeSinceLastSubmission > this.submissionCooldown;
  }

  resetForm() {
    this.formData = {
      firstName: '',
      lastName: '',
      email: '',
      message: ''
    };
    
    this.submissionState = {
      isSubmitting: false,
      success: false,
      error: null,
      attempts: 0,
      lastSubmission: null
    };

    // Clear validation errors
    this.validationController.reset();
  }

  retrySubmission() {
    if (this.submissionState.attempts < this.maxAttempts) {
      this.submissionState.error = null;
      // Allow retry by not resetting attempts - they'll try again
    }
  }
}
```

```html
<form submit.trigger="handleSubmit($event)" class="comprehensive-form">
  <h2>Contact Us</h2>
  
  <!-- Form Fields -->
  <div class="form-row">
    <div class="form-group col-md-6">
      <label for="firstName">First Name</label>
      <input id="firstName" 
             type="text"
             value.bind="formData.firstName & validate"
             disabled.bind="submissionState.isSubmitting"
             class="form-control" />
    </div>
    <div class="form-group col-md-6">
      <label for="lastName">Last Name</label>
      <input id="lastName" 
             type="text"
             value.bind="formData.lastName & validate"
             disabled.bind="submissionState.isSubmitting"
             class="form-control" />
    </div>
  </div>

  <div class="form-group">
    <label for="email">Email Address</label>
    <input id="email" 
           type="email"
           value.bind="formData.email & validate"
           disabled.bind="submissionState.isSubmitting"
           class="form-control" />
  </div>

  <div class="form-group">
    <label for="message">Message</label>
    <textarea id="message" 
              rows="5"
              value.bind="formData.message & validate"
              disabled.bind="submissionState.isSubmitting"
              class="form-control"></textarea>
  </div>

  <!-- Submission State Display -->
  <div class="submission-status">
    <div if.bind="submissionState.isSubmitting" class="alert alert-info">
      <div class="loading-spinner"></div>
      ${submissionMessage}
    </div>

    <div if.bind="submissionState.success" class="alert alert-success">
      <i class="icon-check"></i>
      ${submissionMessage}
    </div>

    <div if.bind="submissionState.error" class="alert alert-danger">
      <i class="icon-warning"></i>
      ${submissionMessage}
      <div class="retry-section">
        <button type="button" 
                click.trigger="retrySubmission()"
                class="btn btn-sm btn-outline-danger"
                if.bind="submissionState.attempts < maxAttempts">
          Try Again (${maxAttempts - submissionState.attempts} attempts remaining)
        </button>
      </div>
    </div>

    <div if.bind="submissionState.attempts >= maxAttempts" class="alert alert-warning">
      <i class="icon-clock"></i>
      Too many attempts. Please try again in 
      ${Math.ceil((submissionCooldown - (Date.now() - submissionState.lastSubmission?.getTime())) / 1000)} seconds.
    </div>
  </div>

  <!-- Form Actions -->
  <div class="form-actions">
    <button type="submit" 
            disabled.bind="!canSubmit"
            class="btn btn-primary"
            class.bind="{ 'btn-loading': submissionState.isSubmitting }">
      <span if.bind="submissionState.isSubmitting">Submitting...</span>
      <span if.bind="!submissionState.isSubmitting">Send Message</span>
    </button>
    
    <button type="button" 
            click.trigger="resetForm()"
            disabled.bind="submissionState.isSubmitting"
            class="btn btn-secondary">
      Reset Form
    </button>
  </div>

  <!-- Submission History (Development) -->
  <div if.bind="showDebugInfo" class="debug-section">
    <h4>Submission Debug Info</h4>
    <ul>
      <li>Attempts: ${submissionState.attempts}/${maxAttempts}</li>
      <li>Last Submission: ${submissionState.lastSubmission?.toLocaleString() || 'Never'}</li>
      <li>Can Submit: ${canSubmit ? 'Yes' : 'No'}</li>
      <li>Is Submitting: ${submissionState.isSubmitting ? 'Yes' : 'No'}</li>
    </ul>
  </div>
</form>
```

### Multi-Step Form Submission

Handle complex multi-step forms with progress tracking and validation:

```typescript
interface FormStep {
  id: string;
  title: string;
  component: string;
  isValid: boolean;
  isComplete: boolean;
  data: any;
}

export class MultiStepFormSubmission {
  currentStepIndex = 0;
  isSubmitting = false;
  submissionProgress = 0;

  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  steps: FormStep[] = [
    {
      id: 'personal',
      title: 'Personal Information',
      component: 'personal-info-step',
      isValid: false,
      isComplete: false,
      data: {}
    },
    {
      id: 'account',
      title: 'Account Details',
      component: 'account-details-step',
      isValid: false,
      isComplete: false,
      data: {}
    },
    {
      id: 'preferences',
      title: 'Preferences',
      component: 'preferences-step',
      isValid: false,
      isComplete: false,
      data: {}
    },
    {
      id: 'confirmation',
      title: 'Confirmation',
      component: 'confirmation-step',
      isValid: true,
      isComplete: false,
      data: {}
    }
  ];

  get currentStep(): FormStep {
    return this.steps[this.currentStepIndex];
  }

  get isFirstStep(): boolean {
    return this.currentStepIndex === 0;
  }

  get isLastStep(): boolean {
    return this.currentStepIndex === this.steps.length - 1;
  }

  get canProceed(): boolean {
    return this.currentStep.isValid && !this.isSubmitting;
  }

  get canGoBack(): boolean {
    return !this.isFirstStep && !this.isSubmitting;
  }

  get overallProgress(): number {
    const completedSteps = this.steps.filter(step => step.isComplete).length;
    return (completedSteps / this.steps.length) * 100;
  }

  async nextStep() {
    if (!this.canProceed) return;

    // Validate current step
    const isValid = await this.validateCurrentStep();
    if (!isValid) return;

    // Mark current step as complete
    this.currentStep.isComplete = true;

    if (this.isLastStep) {
      // Submit the form
      await this.submitCompleteForm();
    } else {
      // Move to next step
      this.currentStepIndex++;
    }
  }

  previousStep() {
    if (this.canGoBack) {
      this.currentStepIndex--;
    }
  }

  goToStep(stepIndex: number) {
    if (stepIndex >= 0 && stepIndex < this.steps.length) {
      // Only allow going to completed steps or next step
      const targetStep = this.steps[stepIndex];
      const canNavigateToStep = targetStep.isComplete || 
                               stepIndex === this.currentStepIndex + 1;
      
      if (canNavigateToStep) {
        this.currentStepIndex = stepIndex;
      }
    }
  }

  private async validateCurrentStep(): Promise<boolean> {
    const result = await this.validationController.validate();
    
    this.currentStep.isValid = result.valid;
    return result.valid;
  }

  private async submitCompleteForm() {
    this.isSubmitting = true;
    this.submissionProgress = 0;

    try {
      // Collect all form data
      const formData = this.steps.reduce((acc, step) => {
        return { ...acc, [step.id]: step.data };
      }, {});

      // Submit with progress tracking
      await this.submitWithProgress(formData);

      // Mark final step as complete
      this.currentStep.isComplete = true;
      this.submissionProgress = 100;

      // Show success message
      console.log('Multi-step form submitted successfully!');

    } catch (error) {
      console.error('Form submission failed:', error);
      // Handle error appropriately
    } finally {
      this.isSubmitting = false;
    }
  }

  private async submitWithProgress(data: any): Promise<void> {
    // Simulate progress updates
    const progressSteps = [
      { message: 'Validating data...', progress: 20 },
      { message: 'Processing payment...', progress: 50 },
      { message: 'Creating account...', progress: 75 },
      { message: 'Sending confirmation...', progress: 90 },
      { message: 'Complete!', progress: 100 }
    ];

    for (const step of progressSteps) {
      await new Promise(resolve => setTimeout(resolve, 800));
      this.submissionProgress = step.progress;
      console.log(step.message);
    }

    // Actual submission would happen here
    const response = await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error(`Registration failed: ${response.statusText}`);
    }
  }

  updateStepData(data: any) {
    Object.assign(this.currentStep.data, data);
  }

  resetForm() {
    this.currentStepIndex = 0;
    this.isSubmitting = false;
    this.submissionProgress = 0;
    
    this.steps.forEach(step => {
      step.isValid = step.id === 'confirmation'; // Only confirmation step is valid by default
      step.isComplete = false;
      step.data = {};
    });
  }
}
```

```html
<div class="multi-step-form-container">
  <div class="form-header">
    <h2>Account Registration</h2>
    
    <!-- Progress Indicator -->
    <div class="progress-container">
      <div class="progress-bar">
        <div class="progress-fill" 
             style="width: ${overallProgress}%"></div>
      </div>
      <div class="progress-text">${overallProgress.toFixed(0)}% Complete</div>
    </div>

    <!-- Step Navigation -->
    <nav class="step-navigation">
      <div repeat.for="step of steps" 
           class="step-indicator"
           class.bind="{
             'active': $index === currentStepIndex,
             'completed': step.isComplete,
             'valid': step.isValid
           }"
           click.trigger="goToStep($index)">
        <div class="step-number">${$index + 1}</div>
        <div class="step-title">${step.title}</div>
      </div>
    </nav>
  </div>

  <!-- Dynamic Step Content -->
  <div class="step-content">
    <au-compose 
      component.bind="currentStep.component"
      model.bind="{ 
        data: currentStep.data,
        updateData: updateStepData,
        isValid: currentStep.isValid
      }">
    </au-compose>
  </div>

  <!-- Submission Progress -->
  <div if.bind="isSubmitting" class="submission-progress">
    <h4>Processing Your Registration</h4>
    <div class="progress-bar">
      <div class="progress-fill" 
           style="width: ${submissionProgress}%"></div>
    </div>
    <div class="progress-text">${submissionProgress}% Complete</div>
  </div>

  <!-- Navigation Controls -->
  <div class="form-navigation" if.bind="!isSubmitting">
    <button type="button" 
            click.trigger="previousStep()"
            disabled.bind="!canGoBack"
            class="btn btn-secondary">
      ← Previous
    </button>

    <button type="button" 
            click.trigger="nextStep()"
            disabled.bind="!canProceed"
            class="btn btn-primary">
      <span if.bind="!isLastStep">Next →</span>
      <span if.bind="isLastStep">Submit Registration</span>
    </button>
  </div>

  <!-- Form Actions -->
  <div class="form-actions">
    <button type="button" 
            click.trigger="resetForm()"
            disabled.bind="isSubmitting"
            class="btn btn-outline-secondary">
      Start Over
    </button>
  </div>
</div>
```

***

## 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:

{% code title="file-upload-component.html" %}

```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>
```

{% endcode %}

* `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:

{% code title="file-upload-component.ts" %}

```typescript
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);
    }
  }
}
```

{% endcode %}

**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:

```html
<input type="file" accept="image/*" change.trigger="handleFileSelect($event)" />
```

```typescript
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.

***

## Validation Integration

Aurelia's validation system integrates seamlessly with forms through the `& validate` binding behavior and specialized validation components. This section covers practical validation patterns for production applications.

### Basic Validation with & validate

The `& validate` binding behavior automatically integrates form inputs with Aurelia's validation system.

{% hint style="info" %}
**Validation Controller Scope**: Use `newInstanceForScope(IValidationController)` to create a validation controller scoped to your component. This ensures each component gets its own isolated validation controller, preventing conflicts between different forms or components.
{% endhint %}

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class UserRegistrationForm {
  user = {
    email: '',
    password: '',
    confirmPassword: '',
    age: null as number | null,
    terms: false
  };

  private readonly validationRules = resolve(IValidationRules);
  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  constructor() {
    // Define validation rules
    this.setupValidationRules();
  }

  private setupValidationRules() {
    this.validationRules
      .on(this.user)
      .ensure('email')
        .required()
        .email()
        .withMessage('Please enter a valid email address')
      .ensure('password')
        .required()
        .minLength(8)
        .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
        .withMessage('Password must contain lowercase, uppercase, and number')
      .ensure('confirmPassword')
        .required()
        .satisfies((value, object) => value === object.password)
        .withMessage('Passwords must match')
      .ensure('age')
        .required()
        .range(13, 120)
        .withMessage('Age must be between 13 and 120')
      .ensure('terms')
        .satisfies(value => value === true)
        .withMessage('You must accept the terms and conditions');
  }

  async handleSubmit() {
    const result = await this.validationController.validate();
    
    if (result.valid) {
      console.log('Form is valid, submitting...', this.user);
      // Submit form
    } else {
      console.log('Validation failed:', result);
    }
  }
}
```

```html
<form submit.trigger="handleSubmit()">
  <div class="form-group">
    <label for="email">Email Address</label>
    <input id="email" 
           type="email" 
           value.bind="user.email & validate" 
           class="form-control" />
  </div>

  <div class="form-group">
    <label for="password">Password</label>
    <input id="password" 
           type="password" 
           value.bind="user.password & validate" 
           class="form-control" />
  </div>

  <div class="form-group">
    <label for="confirmPassword">Confirm Password</label>
    <input id="confirmPassword" 
           type="password" 
           value.bind="user.confirmPassword & validate" 
           class="form-control" />
  </div>

  <div class="form-group">
    <label for="age">Age</label>
    <input id="age" 
           type="number" 
           value.bind="user.age & validate" 
           class="form-control" />
  </div>

  <div class="form-group">
    <label>
      <input type="checkbox" checked.bind="user.terms & validate" />
      I accept the terms and conditions
    </label>
  </div>

  <button type="submit" class="btn btn-primary">Register</button>
</form>
```

### Advanced Validation Display

Use validation components for sophisticated error display:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController, ValidationTrigger } from '@aurelia/validation-html';

export class AdvancedValidationForm {
  contact = {
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    company: '',
    message: ''
  };

  // Validation controller for manual control
  private readonly validationController = resolve(newInstanceForScope(IValidationController));
  private readonly validationRules = resolve(IValidationRules);

  // Form-specific error tracking
  nameErrors: any[] = [];
  contactErrors: any[] = [];
  messageErrors: any[] = [];

  constructor() {
    this.setupValidation();
  }

  private setupValidation() {
    this.validationRules
      .on(this.contact)
      .ensure('firstName')
        .required()
        .minLength(2)
        .withMessage('First name must be at least 2 characters')
      .ensure('lastName')
        .required()
        .minLength(2)
        .withMessage('Last name must be at least 2 characters')
      .ensure('email')
        .required()
        .email()
        .withMessage('Please enter a valid email address')
      .ensure('phone')
        .required()
        .matches(/^[\d\s\-\+\(\)]+$/)
        .withMessage('Please enter a valid phone number')
      .ensure('company')
        .required()
        .withMessage('Company name is required')
      .ensure('message')
        .required()
        .minLength(10)
        .withMessage('Message must be at least 10 characters');

    // Configure validation triggers
    this.validationController.validateTrigger = ValidationTrigger.changeOrBlur;
  }

  async validateSection(sectionName: 'name' | 'contact' | 'message') {
    let properties: string[] = [];
    
    switch (sectionName) {
      case 'name':
        properties = ['firstName', 'lastName'];
        break;
      case 'contact':
        properties = ['email', 'phone', 'company'];
        break;
      case 'message':
        properties = ['message'];
        break;
    }

    const result = await this.validationController.validate({
      object: this.contact,
      propertyName: properties
    });

    // Update section-specific errors
    this[`${sectionName}Errors`] = result.results
      .filter(r => !r.valid)
      .map(r => ({ error: r, target: r.target }));

    return result.valid;
  }

  async submitForm() {
    const result = await this.validationController.validate();
    
    if (result.valid) {
      // Submit the form
      console.log('Submitting:', this.contact);
    }
  }
}
```

```html
<form class="advanced-form">
  <!-- Name Section with Validation Container -->
  <validation-container class="form-section">
    <h3>Personal Information</h3>
    <div validation-errors.bind="nameErrors" class="section-errors">
      <div repeat.for="error of nameErrors" class="alert alert-danger">
        ${error.error.message}
      </div>
    </div>

    <div class="form-row">
      <div class="form-group col-md-6">
        <label for="firstName">First Name</label>
        <input id="firstName" 
               value.bind="contact.firstName & validate" 
               class="form-control" />
      </div>
      <div class="form-group col-md-6">
        <label for="lastName">Last Name</label>
        <input id="lastName" 
               value.bind="contact.lastName & validate" 
               class="form-control" />
      </div>
    </div>

    <button type="button" 
            click.trigger="validateSection('name')"
            class="btn btn-outline-primary">
      Validate Name Section
    </button>
  </validation-container>

  <!-- Contact Section -->
  <validation-container class="form-section">
    <h3>Contact Information</h3>
    <div validation-errors.bind="contactErrors" class="section-errors">
      <div repeat.for="error of contactErrors" class="alert alert-danger">
        ${error.error.message}
      </div>
    </div>

    <div class="form-group">
      <label for="email">Email Address</label>
      <input id="email" 
             type="email"
             value.bind="contact.email & validate" 
             class="form-control" />
    </div>

    <div class="form-group">
      <label for="phone">Phone Number</label>
      <input id="phone" 
             type="tel"
             value.bind="contact.phone & validate" 
             class="form-control" />
    </div>

    <div class="form-group">
      <label for="company">Company</label>
      <input id="company" 
             value.bind="contact.company & validate" 
             class="form-control" />
    </div>
  </validation-container>

  <!-- Message Section -->
  <validation-container class="form-section">
    <h3>Message</h3>
    <div validation-errors.bind="messageErrors" class="section-errors">
      <div repeat.for="error of messageErrors" class="alert alert-danger">
        ${error.error.message}
      </div>
    </div>

    <div class="form-group">
      <label for="message">Your Message</label>
      <textarea id="message" 
                rows="5"
                value.bind="contact.message & validate" 
                class="form-control"
                placeholder="Tell us about your needs..."></textarea>
    </div>
  </validation-container>

  <!-- Form Actions -->
  <div class="form-actions">
    <button type="button" 
            click.trigger="submitForm()"
            class="btn btn-primary btn-lg">
      Send Message
    </button>
  </div>
</form>
```

### Dynamic Validation Rules

Create validation rules that adapt to changing form conditions:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class DynamicValidationForm {
  profile = {
    userType: 'individual' as 'individual' | 'business',
    firstName: '',
    lastName: '',
    businessName: '',
    taxId: '',
    email: '',
    phone: ''
  };

  private readonly validationRules = resolve(IValidationRules);
  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  constructor() {
    this.setupDynamicValidation();
  }

  private setupDynamicValidation() {
    this.validationRules
      .on(this.profile)
      .ensure('firstName')
        .required()
        .when(obj => obj.userType === 'individual')
        .withMessage('First name is required for individuals')
      .ensure('lastName')
        .required()
        .when(obj => obj.userType === 'individual')
        .withMessage('Last name is required for individuals')
      .ensure('businessName')
        .required()
        .when(obj => obj.userType === 'business')
        .withMessage('Business name is required for businesses')
      .ensure('taxId')
        .required()
        .matches(/^\d{2}-\d{7}$/)
        .when(obj => obj.userType === 'business')
        .withMessage('Tax ID must be in format XX-XXXXXXX')
      .ensure('email')
        .required()
        .email()
        .withMessage('Valid email address is required')
      .ensure('phone')
        .required()
        .matches(/^[\d\s\-\+\(\)]+$/)
        .withMessage('Valid phone number is required');
  }

  userTypeChanged() {
    // Re-validate when user type changes
    this.validationController.validate();
  }

  async handleSubmit() {
    const result = await this.validationController.validate();
    
    if (result.valid) {
      console.log('Submitting profile:', this.profile);
      // Handle successful validation
    } else {
      console.log('Validation errors:', result.results.filter(r => !r.valid));
    }
  }
}
```

```html
<form submit.trigger="handleSubmit()" class="dynamic-form">
  <div class="form-group">
    <label>Account Type</label>
    <div class="form-check-container">
      <label class="form-check">
        <input type="radio" 
               name="userType"
               model.bind="'individual'"
               checked.bind="profile.userType"
               change.trigger="userTypeChanged()" />
        Individual
      </label>
      <label class="form-check">
        <input type="radio" 
               name="userType"
               model.bind="'business'"
               checked.bind="profile.userType"
               change.trigger="userTypeChanged()" />
        Business
      </label>
    </div>
  </div>

  <!-- Individual Fields -->
  <div if.bind="profile.userType === 'individual'" class="user-type-section">
    <h4>Personal Information</h4>
    <div class="form-row">
      <div class="form-group col-md-6">
        <label for="firstName">First Name</label>
        <input id="firstName" 
               value.bind="profile.firstName & validate" 
               class="form-control" />
      </div>
      <div class="form-group col-md-6">
        <label for="lastName">Last Name</label>
        <input id="lastName" 
               value.bind="profile.lastName & validate" 
               class="form-control" />
      </div>
    </div>
  </div>

  <!-- Business Fields -->
  <div if.bind="profile.userType === 'business'" class="user-type-section">
    <h4>Business Information</h4>
    <div class="form-group">
      <label for="businessName">Business Name</label>
      <input id="businessName" 
             value.bind="profile.businessName & validate" 
             class="form-control" />
    </div>
    <div class="form-group">
      <label for="taxId">Tax ID (XX-XXXXXXX)</label>
      <input id="taxId" 
             value.bind="profile.taxId & validate" 
             class="form-control" 
             placeholder="12-3456789" />
    </div>
  </div>

  <!-- Common Fields -->
  <div class="common-fields">
    <h4>Contact Information</h4>
    <div class="form-group">
      <label for="email">Email Address</label>
      <input id="email" 
             type="email"
             value.bind="profile.email & validate" 
             class="form-control" />
    </div>
    <div class="form-group">
      <label for="phone">Phone Number</label>
      <input id="phone" 
             type="tel"
             value.bind="profile.phone & validate" 
             class="form-control" />
    </div>
  </div>

  <button type="submit" class="btn btn-primary">Submit Profile</button>
</form>
```

### Real-time Validation Feedback

Provide immediate feedback with sophisticated validation timing:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

// Utility function for debouncing (you would typically import this from a utility library)
function debounce(func: Function, wait: number) {
  let timeout: any;
  return function executedFunction(...args: any[]) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

export class RealTimeValidationForm {
  user = {
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  };

  validationStates = {
    username: { checking: false, available: false, message: '' },
    email: { checking: false, valid: false, message: '' },
    password: { strength: 0, message: '' },
    confirmPassword: { matches: false, message: '' }
  };

  private readonly validationRules = resolve(IValidationRules);
  private readonly validationController = resolve(newInstanceForScope(IValidationController));
  private debounceUsernameCheck = debounce(this.checkUsernameAvailability.bind(this), 500);
  private debounceEmailCheck = debounce(this.validateEmail.bind(this), 300);

  constructor() {
    this.setupValidation();
  }

  private setupValidation() {
    this.validationRules
      .on(this.user)
      .ensure('username')
        .required()
        .minLength(3)
        .matches(/^[a-zA-Z0-9_]+$/)
        .satisfies(async (username) => {
          if (username.length >= 3) {
            return await this.isUsernameAvailable(username);
          }
          return true;
        })
        .withMessage('Username must be available')
      .ensure('email')
        .required()
        .email()
        .satisfies(async (email) => await this.isEmailValid(email))
        .withMessage('Please enter a valid, verified email address')
      .ensure('password')
        .required()
        .minLength(8)
        .satisfies(password => this.calculatePasswordStrength(password) >= 3)
        .withMessage('Password must be strong (score 3+)')
      .ensure('confirmPassword')
        .required()
        .satisfies((value, obj) => value === obj.password)
        .withMessage('Passwords must match');
  }

  usernameChanged(newUsername: string) {
    if (newUsername.length >= 3) {
      this.validationStates.username.checking = true;
      this.debounceUsernameCheck(newUsername);
    }
  }

  emailChanged(newEmail: string) {
    if (newEmail.includes('@')) {
      this.validationStates.email.checking = true;
      this.debounceEmailCheck(newEmail);
    }
  }

  passwordChanged(newPassword: string) {
    const strength = this.calculatePasswordStrength(newPassword);
    this.validationStates.password.strength = strength;
    this.validationStates.password.message = this.getPasswordStrengthMessage(strength);
    
    // Re-validate confirm password
    if (this.user.confirmPassword) {
      this.confirmPasswordChanged(this.user.confirmPassword);
    }
  }

  confirmPasswordChanged(confirmPassword: string) {
    const matches = confirmPassword === this.user.password;
    this.validationStates.confirmPassword.matches = matches;
    this.validationStates.confirmPassword.message = matches 
      ? 'Passwords match' 
      : 'Passwords do not match';
  }

  private async checkUsernameAvailability(username: string) {
    try {
      const available = await this.isUsernameAvailable(username);
      this.validationStates.username = {
        checking: false,
        available,
        message: available ? 'Username is available' : 'Username is taken'
      };
    } catch (error) {
      this.validationStates.username = {
        checking: false,
        available: false,
        message: 'Error checking username availability'
      };
    }
  }

  private async validateEmail(email: string) {
    try {
      const valid = await this.isEmailValid(email);
      this.validationStates.email = {
        checking: false,
        valid,
        message: valid ? 'Email is valid' : 'Email format is invalid'
      };
    } catch (error) {
      this.validationStates.email = {
        checking: false,
        valid: false,
        message: 'Error validating email'
      };
    }
  }

  private calculatePasswordStrength(password: string): number {
    let strength = 0;
    if (password.length >= 8) strength++;
    if (/[a-z]/.test(password)) strength++;
    if (/[A-Z]/.test(password)) strength++;
    if (/\d/.test(password)) strength++;
    if (/[^a-zA-Z\d]/.test(password)) strength++;
    return strength;
  }

  private getPasswordStrengthMessage(strength: number): string {
    const messages = [
      'Very weak password',
      'Weak password',
      'Fair password',
      'Good password',
      'Strong password',
      'Very strong password'
    ];
    return messages[strength] || 'No password';
  }

  // Mock API calls (replace with real implementations)
  private async isUsernameAvailable(username: string): Promise<boolean> {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 500));
    return !['admin', 'test', 'user'].includes(username.toLowerCase());
  }

  private async isEmailValid(email: string): Promise<boolean> {
    // Simulate API call for email verification
    await new Promise(resolve => setTimeout(resolve, 300));
    return email.includes('@') && !email.includes('invalid');
  }
}
```

```html
<form class="realtime-validation-form">
  <div class="form-group">
    <label for="username">Username</label>
    <div class="input-with-feedback">
      <input id="username" 
             value.bind="user.username & validate & debounce:100"
             class="form-control"
             class.bind="{ 
               'is-valid': validationStates.username.available,
               'is-invalid': validationStates.username.message && !validationStates.username.available 
             }" />
      <div class="feedback-icons">
        <i if.bind="validationStates.username.checking" class="spinner"></i>
        <i if.bind="validationStates.username.available" class="success-icon">✓</i>
        <i if.bind="validationStates.username.message && !validationStates.username.available" class="error-icon">✗</i>
      </div>
    </div>
    <div class="feedback-text" 
         class.bind="{ 
           'text-success': validationStates.username.available,
           'text-danger': !validationStates.username.available && validationStates.username.message 
         }">
      ${validationStates.username.message}
    </div>
  </div>

  <div class="form-group">
    <label for="email">Email Address</label>
    <div class="input-with-feedback">
      <input id="email" 
             type="email"
             value.bind="user.email & validate & debounce:200"
             class="form-control"
             class.bind="{ 
               'is-valid': validationStates.email.valid,
               'is-invalid': validationStates.email.message && !validationStates.email.valid 
             }" />
      <div class="feedback-icons">
        <i if.bind="validationStates.email.checking" class="spinner"></i>
        <i if.bind="validationStates.email.valid" class="success-icon">✓</i>
        <i if.bind="validationStates.email.message && !validationStates.email.valid" class="error-icon">✗</i>
      </div>
    </div>
    <div class="feedback-text" 
         class.bind="{ 
           'text-success': validationStates.email.valid,
           'text-danger': !validationStates.email.valid && validationStates.email.message 
         }">
      ${validationStates.email.message}
    </div>
  </div>

  <div class="form-group">
    <label for="password">Password</label>
    <input id="password" 
           type="password"
           value.bind="user.password & validate" 
           class="form-control" />
    <div class="password-strength">
      <div class="strength-bar">
        <div repeat.for="i of 5" 
             class="strength-segment"
             class.bind="{ 
               'active': i < validationStates.password.strength,
               'weak': validationStates.password.strength <= 2,
               'medium': validationStates.password.strength === 3,
               'strong': validationStates.password.strength >= 4 
             }"></div>
      </div>
      <div class="strength-text">${validationStates.password.message}</div>
    </div>
  </div>

  <div class="form-group">
    <label for="confirmPassword">Confirm Password</label>
    <div class="input-with-feedback">
      <input id="confirmPassword" 
             type="password"
             value.bind="user.confirmPassword & validate" 
             class="form-control"
             class.bind="{ 
               'is-valid': validationStates.confirmPassword.matches && user.confirmPassword,
               'is-invalid': !validationStates.confirmPassword.matches && user.confirmPassword 
             }" />
      <div class="feedback-icons">
        <i if.bind="validationStates.confirmPassword.matches && user.confirmPassword" class="success-icon">✓</i>
        <i if.bind="!validationStates.confirmPassword.matches && user.confirmPassword" class="error-icon">✗</i>
      </div>
    </div>
    <div class="feedback-text" 
         class.bind="{ 
           'text-success': validationStates.confirmPassword.matches,
           'text-danger': !validationStates.confirmPassword.matches && user.confirmPassword 
         }">
      ${validationStates.confirmPassword.message}
    </div>
  </div>

  <button type="submit" class="btn btn-primary">Create Account</button>
</form>
```

For comprehensive validation documentation, see the dedicated [Validation Guide](/aurelia-packages/validation).

***

## Security and Best Practices

Security in forms is critical for protecting user data and preventing common web vulnerabilities. Aurelia provides the foundation for secure form implementations, but you must implement security best practices.

### Input Validation and Sanitization

Always validate and sanitize user input on both client and server sides:

```typescript
import { resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';

export class SecureFormComponent {
  private readonly maxFieldLength = 1000;
  private readonly allowedFileTypes = ['image/jpeg', 'image/png', 'image/webp'];
  private readonly maxFileSize = 5 * 1024 * 1024; // 5MB

  userData = {
    username: '',
    email: '',
    bio: '',
    website: ''
  };

  private readonly validationRules = resolve(IValidationRules);

  constructor() {
    this.setupSecureValidation();
  }

  private setupSecureValidation() {
    this.validationRules
      .on(this.userData)
      .ensure('username')
        .required()
        .minLength(3)
        .maxLength(20)
        .matches(/^[a-zA-Z0-9_-]+$/)
        .withMessage('Username can only contain letters, numbers, underscores, and hyphens')
        .satisfies(username => this.isUsernameSafe(username))
        .withMessage('Username contains prohibited content')
      .ensure('email')
        .required()
        .email()
        .maxLength(254) // RFC 5321 limit
        .satisfies(email => this.isEmailDomainAllowed(email))
        .withMessage('Email domain not allowed')
      .ensure('bio')
        .maxLength(this.maxFieldLength)
        .satisfies(bio => this.containsNoMaliciousContent(bio))
        .withMessage('Bio contains prohibited content')
      .ensure('website')
        .satisfies(url => !url || this.isUrlSafe(url))
        .withMessage('Website URL is not allowed');
  }

  // Input sanitization
  sanitizeInput(input: string): string {
    // Remove potentially dangerous characters
    let sanitized = input.trim();
    
    // Remove null bytes
    sanitized = sanitized.replace(/\0/g, '');
    
    // Limit length
    sanitized = sanitized.substring(0, this.maxFieldLength);
    
    // HTML encode for display (use a proper HTML sanitizer in production)
    sanitized = sanitized
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
    
    return sanitized;
  }

  // Security validation methods
  private isUsernameSafe(username: string): boolean {
    // Check against prohibited usernames
    const prohibitedUsernames = ['admin', 'root', 'administrator', 'system'];
    return !prohibitedUsernames.includes(username.toLowerCase());
  }

  private isEmailDomainAllowed(email: string): boolean {
    // Example: Block certain domains (implement your own logic)
    const blockedDomains = ['tempmail.com', 'guerrillamail.com'];
    const domain = email.split('@')[1]?.toLowerCase();
    return domain ? !blockedDomains.includes(domain) : false;
  }

  private containsNoMaliciousContent(text: string): boolean {
    // Check for common XSS patterns
    const dangerousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+\s*=/i,
      /<iframe/i,
      /<object/i,
      /<embed/i
    ];
    
    return !dangerousPatterns.some(pattern => pattern.test(text));
  }

  private isUrlSafe(url: string): boolean {
    try {
      const parsedUrl = new URL(url);
      
      // Only allow HTTP and HTTPS
      if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
        return false;
      }
      
      // Check for dangerous domains (implement your own logic)
      const dangerousDomains = ['malicious-site.com'];
      return !dangerousDomains.includes(parsedUrl.hostname.toLowerCase());
      
    } catch {
      return false; // Invalid URL
    }
  }

  // Secure file upload validation
  validateFile(file: File): { isValid: boolean; error?: string } {
    // Check file type
    if (!this.allowedFileTypes.includes(file.type)) {
      return {
        isValid: false,
        error: 'File type not allowed. Only JPEG, PNG, and WebP images are permitted.'
      };
    }

    // Check file size
    if (file.size > this.maxFileSize) {
      return {
        isValid: false,
        error: `File size exceeds limit. Maximum size is ${this.maxFileSize / (1024 * 1024)}MB.`
      };
    }

    // Check filename for dangerous characters
    if (/[<>:"\\|?*\x00-\x1f]/.test(file.name)) {
      return {
        isValid: false,
        error: 'Filename contains invalid characters.'
      };
    }

    return { isValid: true };
  }

  async handleSecureSubmit() {
    try {
      // Sanitize all inputs before submission
      const sanitizedData = {
        username: this.sanitizeInput(this.userData.username),
        email: this.userData.email.toLowerCase().trim(),
        bio: this.sanitizeInput(this.userData.bio),
        website: this.userData.website.trim()
      };

      // Add security headers and CSRF token
      const response = await fetch('/api/secure-form', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': this.getCSRFToken(),
          'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify(sanitizedData)
      });

      if (!response.ok) {
        throw new Error('Server validation failed');
      }

      const result = await response.json();
      console.log('Form submitted securely:', result);

    } catch (error) {
      console.error('Secure submission failed:', error);
      // Don't expose internal error details to user
      throw new Error('Submission failed. Please try again.');
    }
  }

  private getCSRFToken(): string {
    // Get CSRF token from meta tag or cookie
    const metaTag = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement;
    return metaTag?.content || '';
  }
}
```

### Rate Limiting and Abuse Prevention

Implement client-side rate limiting and abuse prevention:

```typescript
export class RateLimitedForm {
  private submissionAttempts: number = 0;
  private lastSubmissionTime: number = 0;
  private readonly maxAttempts: number = 5;
  private readonly timeWindow: number = 60000; // 1 minute
  private readonly baseDelay: number = 1000; // 1 second

  get currentDelay(): number {
    // Exponential backoff
    return this.baseDelay * Math.pow(2, this.submissionAttempts);
  }

  get canSubmit(): boolean {
    const now = Date.now();
    
    // Reset attempts if time window has passed
    if (now - this.lastSubmissionTime > this.timeWindow) {
      this.submissionAttempts = 0;
    }

    return this.submissionAttempts < this.maxAttempts;
  }

  async handleRateLimitedSubmit() {
    if (!this.canSubmit) {
      const waitTime = Math.ceil(this.currentDelay / 1000);
      throw new Error(`Too many attempts. Please wait ${waitTime} seconds.`);
    }

    this.submissionAttempts++;
    this.lastSubmissionTime = Date.now();

    try {
      await this.submitWithDelay();
      // Reset on successful submission
      this.submissionAttempts = 0;
    } catch (error) {
      // Keep attempt count for failed submissions
      throw error;
    }
  }

  private async submitWithDelay() {
    // Add artificial delay to prevent rapid-fire submissions
    if (this.submissionAttempts > 1) {
      await new Promise(resolve => setTimeout(resolve, this.currentDelay));
    }

    // Actual submission logic here
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.formData)
    });

    if (!response.ok) {
      throw new Error('Submission failed');
    }

    return response.json();
  }
}
```

### Content Security Policy (CSP) Considerations

Implement CSP-friendly form patterns:

```typescript
export class CSPFriendlyForm {
  // Avoid inline event handlers - use Aurelia's binding instead
  // BAD: <button onclick="handleClick()">
  // GOOD: <button click.trigger="handleClick()">

  // Use nonce for dynamic content if needed
  private nonce: string = this.generateNonce();

  private generateNonce(): string {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
  }

  // Safe dynamic script injection (if absolutely necessary)
  addScriptSecurely(scriptContent: string) {
    const script = document.createElement('script');
    script.nonce = this.nonce;
    script.textContent = scriptContent;
    document.head.appendChild(script);
  }
}
```

***

## Accessibility Considerations

Building accessible forms ensures your application works for users with disabilities and meets WCAG guidelines. Aurelia provides excellent support for accessibility features.

### Semantic Form Structure

Use proper semantic HTML and ARIA attributes:

```html
<form class="accessible-form" role="form" aria-labelledby="form-title">
  <h2 id="form-title">Contact Information Form</h2>
  <p id="form-description">Please provide your contact details. Required fields are marked with an asterisk (*).</p>

  <!-- Fieldset for grouping related fields -->
  <fieldset>
    <legend>Personal Information</legend>
    
    <div class="form-group">
      <label for="firstName" class="required">
        First Name *
        <span class="visually-hidden">(required)</span>
      </label>
      <input id="firstName"
             type="text"
             value.bind="contact.firstName & validate"
             required
             aria-describedby="firstName-help firstName-error"
             aria-invalid.bind="hasFirstNameError"
             class="form-control" />
      <div id="firstName-help" class="help-text">
        Enter your legal first name as it appears on official documents
      </div>
      <div id="firstName-error" 
           class="error-message"
           role="alert"
           if.bind="hasFirstNameError"
           aria-live="polite">
        ${firstNameError}
      </div>
    </div>

    <div class="form-group">
      <label for="email" class="required">
        Email Address *
        <span class="visually-hidden">(required)</span>
      </label>
      <input id="email"
             type="email"
             value.bind="contact.email & validate"
             required
             aria-describedby="email-help"
             autocomplete="email"
             class="form-control" />
      <div id="email-help" class="help-text">
        We'll use this to send you important updates
      </div>
    </div>

    <div class="form-group">
      <label for="phone">Phone Number (Optional)</label>
      <input id="phone"
             type="tel"
             value.bind="contact.phone & validate"
             aria-describedby="phone-help"
             autocomplete="tel"
             class="form-control" />
      <div id="phone-help" class="help-text">
        Include country code for international numbers
      </div>
    </div>
  </fieldset>

  <!-- Radio group with proper ARIA -->
  <fieldset>
    <legend>Preferred Contact Method</legend>
    <div class="radio-group" role="radiogroup" aria-required="true">
      <div class="form-check">
        <input id="contact-email"
               type="radio"
               name="contactMethod"
               model.bind="'email'"
               checked.bind="contact.preferredMethod"
               class="form-check-input" />
        <label for="contact-email" class="form-check-label">
          Email
        </label>
      </div>
      <div class="form-check">
        <input id="contact-phone"
               type="radio"
               name="contactMethod"
               model.bind="'phone'"
               checked.bind="contact.preferredMethod"
               class="form-check-input" />
        <label for="contact-phone" class="form-check-label">
          Phone
        </label>
      </div>
      <div class="form-check">
        <input id="contact-text"
               type="radio"
               name="contactMethod"
               model.bind="'text'"
               checked.bind="contact.preferredMethod"
               class="form-check-input" />
        <label for="contact-text" class="form-check-label">
          Text Message
        </label>
      </div>
    </div>
  </fieldset>

  <!-- Accessible checkbox with detailed description -->
  <div class="form-group">
    <div class="form-check">
      <input id="newsletter"
             type="checkbox"
             checked.bind="contact.subscribeNewsletter"
             aria-describedby="newsletter-description"
             class="form-check-input" />
      <label for="newsletter" class="form-check-label">
        Subscribe to newsletter
      </label>
    </div>
    <div id="newsletter-description" class="form-text">
      Receive weekly updates about new features, tips, and special offers. 
      You can unsubscribe at any time.
    </div>
  </div>

  <!-- Form submission with clear feedback -->
  <div class="form-actions">
    <button type="submit" 
            class="btn btn-primary"
            aria-describedby="submit-help">
      <span if.bind="!isSubmitting">Submit Contact Information</span>
      <span if.bind="isSubmitting">
        <span class="visually-hidden">Submitting form, please wait</span>
        <span aria-hidden="true">Submitting...</span>
      </span>
    </button>
    <div id="submit-help" class="form-text">
      Review your information before submitting
    </div>
  </div>

  <!-- Live region for dynamic updates -->
  <div aria-live="polite" aria-atomic="true" class="sr-only">
    <span if.bind="submissionMessage">${submissionMessage}</span>
  </div>
</form>
```

### Accessible Form Validation

Implement validation feedback that works with screen readers:

```typescript
import { resolve, newInstanceForScope } from '@aurelia/kernel';
import { IValidationController } from '@aurelia/validation-html';

export class AccessibleFormValidation {
  contact = {
    firstName: '',
    email: '',
    phone: '',
    preferredMethod: '',
    subscribeNewsletter: false
  };

  validationErrors: Map<string, string> = new Map();
  isSubmitting = false;
  submissionMessage = '';

  private readonly validationController = resolve(newInstanceForScope(IValidationController));

  get hasFirstNameError(): boolean {
    return this.validationErrors.has('firstName');
  }

  get firstNameError(): string {
    return this.validationErrors.get('firstName') || '';
  }

  // Focus management for form errors
  async handleSubmit() {
    this.validationErrors.clear();
    this.isSubmitting = true;

    try {
      const result = await this.validationController.validate();

      if (!result.valid) {
        // Collect validation errors
        result.results.forEach(error => {
          if (!error.valid) {
            this.validationErrors.set(error.propertyName, error.message);
          }
        });

        // Focus first error field
        this.focusFirstError();
        this.announceErrors();
        return;
      }

      // Submit form
      await this.submitForm();
      this.submissionMessage = 'Your contact information has been submitted successfully.';
      
    } catch (error) {
      this.submissionMessage = 'An error occurred. Please try again.';
    } finally {
      this.isSubmitting = false;
    }
  }

  private focusFirstError() {
    const firstErrorField = this.validationErrors.keys().next().value;
    if (firstErrorField) {
      const element = document.getElementById(firstErrorField);
      element?.focus();
    }
  }

  private announceErrors() {
    const errorCount = this.validationErrors.size;
    const announcement = errorCount === 1 
      ? 'There is 1 error in the form. Please review and correct it.'
      : `There are ${errorCount} errors in the form. Please review and correct them.`;
    
    this.announceToScreenReader(announcement);
  }

  private announceToScreenReader(message: string) {
    // Create a temporary live region for immediate announcements
    const announcement = document.createElement('div');
    announcement.setAttribute('aria-live', 'assertive');
    announcement.setAttribute('aria-atomic', 'true');
    announcement.className = 'sr-only';
    announcement.textContent = message;
    
    document.body.appendChild(announcement);
    
    // Remove after announcement
    setTimeout(() => {
      document.body.removeChild(announcement);
    }, 1000);
  }

  private async submitForm() {
    // Simulate form submission
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log('Form submitted:', this.contact);
  }
}
```

### CSS for Accessibility

Include essential accessibility styles:

```css
/* Screen reader only content */
.visually-hidden, .sr-only {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

/* Focus indicators */
.form-control:focus,
.form-check-input:focus,
.btn:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  .form-control,
  .form-check-input {
    border: 2px solid ButtonText;
  }
  
  .form-control:focus,
  .form-check-input:focus {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }
}

/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
  .loading-spinner,
  .progress-bar .progress-fill {
    animation: none;
  }
}

/* Error states */
.form-control[aria-invalid="true"] {
  border-color: #dc3545;
  box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

.error-message {
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/* Required field indicators */
.required::after {
  content: " *";
  color: #dc3545;
}
```

### Testing Accessibility

Include accessibility testing strategies:

```typescript
export class AccessibilityTester {
  // Programmatic accessibility testing
  testFormAccessibility() {
    const errors: string[] = [];

    // Check for required labels
    const inputs = document.querySelectorAll('input, textarea, select');
    inputs.forEach((input: HTMLInputElement) => {
      const label = document.querySelector(`label[for="${input.id}"]`);
      const ariaLabel = input.getAttribute('aria-label');
      const ariaLabelledBy = input.getAttribute('aria-labelledby');
      
      if (!label && !ariaLabel && !ariaLabelledBy) {
        errors.push(`Input with id "${input.id}" lacks proper labeling`);
      }
    });

    // Check for proper heading structure
    const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
    let previousLevel = 0;
    headings.forEach((heading: HTMLElement) => {
      const level = parseInt(heading.tagName.charAt(1));
      if (level > previousLevel + 1) {
        errors.push(`Heading level skipped: ${heading.tagName} after H${previousLevel}`);
      }
      previousLevel = level;
    });

    // Check for live regions
    const liveRegions = document.querySelectorAll('[aria-live]');
    if (liveRegions.length === 0) {
      errors.push('No live regions found for dynamic content announcements');
    }

    return {
      isAccessible: errors.length === 0,
      errors
    };
  }

  // Keyboard navigation testing
  testKeyboardNavigation() {
    const focusableElements = document.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    return {
      focusableCount: focusableElements.length,
      hasTrapFocus: this.checkFocusTrap(),
      hasSkipLinks: !!document.querySelector('[href="#main"], [href="#content"]')
    };
  }

  private checkFocusTrap(): boolean {
    // Implementation would check if focus is properly trapped in modals/dialogs
    return true; // Simplified
  }
}
```

This comprehensive forms documentation provides production-ready patterns for all aspects of form development in Aurelia 2, from basic binding to advanced security and accessibility considerations. Each section includes real-world examples that you can adapt to your specific use cases.


# CSS classes and styling

Learn how to style elements, components and other facets of an Aurelia application using classes and CSS. Strategies for different approaches are discussed in this section.

Aurelia makes it easy to modify an element inline class list and styles. You can work with not only strings but also objects to manipulate elements.

## Binding HTML Classes

The class binding allows you to bind one or more classes to an element and its native `class` attribute.

### Binding to a single class

Adding or removing a single class value from an element can be done using the `.class` binding. By prefixing the `.class` binding with the name of the class you want to display conditionally `selected.class="myBool"` you can add a selected class to an element. The value you pass into this binding is a boolean value (either true or false), if it is `true` the class will be added; otherwise, it will be removed.

```html
<p selected.class="isSelected">I am selected (I think)</p>
```

Inside of your view model, you would specify `isSelected` as a property and depending on the value, the class would be added or removed.

Here is a working example of a boolean value being toggled using `.class` bindings.

{% embed url="<https://stackblitz.com/edit/aurelia-conditional-class?embed=1&file=my-app.html&hideExplorer=1&hideNavigation=1&view=preview>" %}

### Binding to multiple classes

In addition to binding single classes conditionally, you can also bind multiple classes based on a single boolean expression using a comma-separated list in the `.class` binding syntax. This allows you to toggle a set of related classes together.

```html
<div alert,alert-danger,fade-in,bold-text.class="hasError">Something went wrong!</div>
```

In this example, if the `hasError` property in your view model is truthy, all four classes (`alert`, `alert-danger`, `fade-in`, and `bold-text`) will be added to the `div` element. If `hasError` is falsy, all four classes will be removed. **Important Note:** When using the comma-separated syntax for multiple classes, ensure there are no spaces around the commas. The parser expects a direct list of class names separated only by commas (e.g., `class1,class2,class3`).

Besides the `.class` syntax, there are other ways to achieve dynamic class binding, especially when dealing with complex logic or generating class strings:

| Syntax                    | Input Type | Example                    |
| ------------------------- | ---------- | -------------------------- |
| `class.bind="someString"` | `string`   | `'col-md-4 bg-${bgColor}'` |
| `class="${someString}"`   | `string`   | `col-md-4 ${someString}`   |

Once you have your CSS imported and ready to use in your components, there might be instances where you want to dynamically bind to the style attribute on an element (think setting dynamic widths or backgrounds).

## Binding Inline Styles

### Binding to a single style

You can dynamically add a CSS style value to an element using the `.style` binding in Aurelia.

```html
<p background.style="bg">My background is blue</p>
```

Inside of your view model, you would specify `bg` as a string value on your class.

Here is a working example of a style binding setting the background colour to blue:

{% embed url="<https://stackblitz.com/edit/aurelia-conditional-style?embed=1&file=my-app.html&hideExplorer=1&hideNavigation=1&view=preview>" %}

### Binding to multiple styles

To bind to one or more CSS style properties you can either use a string containing your style values (including dynamic values) or an object containing styles.

#### Style binding using strings

This is what a style string looks like, notice the interpolation here? It almost resembles just a plain native style attribute, with exception of the interpolation for certain values. Notice how you can also mix normal styles with interpolation as well?

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  private backgroundColor = 'black';
  private textColor = '#FFF';
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<p style="color: ${textColor}; font-weight: bold; background: ${backgroundColor};">Hello there</p>
```

{% endcode %}

You can also bind a string from your view model to the `style` property instead of inline string assignment by using `style.bind="myString"` where `myString` is a string of styles inside of your view model.

#### Style binding using objects

Styles can be passed into an element by binding to the styles property and using `.bind` to pass in an object of style properties. We can rewrite the above example to use style objects.

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  private styleObject = {
    background: 'black',
    color: '#FFF'
  };
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<p style.bind="styleObject">Hello there</p>
```

{% endcode %}

From a styling perspective, both examples above do the same thing. However, we are passing in an object and binding it to the `style` property instead of a string.


# Conditional Rendering

Learn about the various methods for conditionally rendering content in Aurelia 2, with detailed explanations and examples.

Conditional rendering allows you to dynamically show or hide parts of your view based on your application's state. Aurelia 2 provides three primary directives for conditional rendering, each suited for different scenarios.

## Quick Reference

| Directive     | Use Case                     | DOM Behavior          | Performance                 |
| ------------- | ---------------------------- | --------------------- | --------------------------- |
| `if.bind`     | Simple true/false conditions | Adds/removes elements | Best for infrequent changes |
| `show.bind`   | Toggle visibility            | Hides/shows elements  | Best for frequent changes   |
| `switch.bind` | Multiple conditions          | Adds/removes elements | Best for enum-like values   |

## Using `if.bind`

The `if.bind` directive conditionally adds or removes elements from the DOM based on a boolean expression. When the expression is `false`, Aurelia completely removes the element and its descendants, cleaning up resources, events, and custom elements.

### Basic Usage

```html
<div if.bind="isLoading">Loading...</div>
<div if.bind="user.isAuthenticated">Welcome back, ${user.name}!</div>
```

### If/Else Structures

Use `else` immediately after an `if.bind` element to create branching logic:

```html
<div if.bind="user.isAuthenticated">
  Welcome back, ${user.name}!
</div>
<div else>
  Please log in to continue.
</div>
```

### Caching Behavior

By default, `if.bind` caches views and view models for performance. Disable caching when you need fresh instances:

```html
<custom-element if="value.bind: canShow; cache: false"></custom-element>
```

With caching disabled, Aurelia deactivates and disposes a branch when a conditional change removes it. Returning to that branch creates a new view and view model.

{% hint style="warning" %}
**When to Use:** Use `if.bind` when elements change infrequently and you want to completely remove them from the DOM to save memory and improve performance.
{% endhint %}

## Using `show.bind`

The `show.bind` directive toggles element visibility without removing them from the DOM. This is equivalent to setting `display: none` in CSS.

### Basic Usage

```html
<div show.bind="isDataLoaded">Data loaded successfully!</div>
<div show.bind="!isLoading">Content is ready</div>
```

### `hide.bind` (inverse of `show.bind`)

`hide` is an alias of `show` with inverted logic:

```html
<div hide.bind="isHidden">Hidden when true</div>
```

This is equivalent to:

```html
<div show.bind="!isHidden">Hidden when true</div>
```

### When to Use show\.bind vs if.bind

```html
<!-- Use show.bind for frequent toggles -->
<div show.bind="isExpanded">
  <expensive-component></expensive-component>
</div>

<!-- Use if.bind for infrequent changes -->
<admin-panel if.bind="user.isAdmin"></admin-panel>
```

{% hint style="info" %}
**When to Use:** Use `show.bind` when you need to frequently toggle visibility and want to preserve element state, bindings, and avoid re-initialization costs.
{% endhint %}

## Using `switch.bind`

The `switch.bind` directive handles multiple conditions elegantly, similar to a JavaScript switch statement. It's ideal for enum values or when you have several mutually exclusive conditions.

### Basic Usage

```typescript
// Status.ts
enum OrderStatus {
  Received   = 'received',
  Processing = 'processing',
  Dispatched = 'dispatched',
  Delivered  = 'delivered'
}
```

```html
<!-- order-status.html -->
<template switch.bind="orderStatus">
  <span case="received">Order received</span>
  <span case="processing">Processing your order</span>
  <span case="dispatched">On the way</span>
  <span case="delivered">Delivered</span>
  <span default-case>Unknown status</span>
</template>
```

### Grouping Cases

Handle multiple values with a single case:

```html
<template switch.bind="orderStatus">
  <span case.bind="['received', 'processing']">
    Order is being processed
  </span>
  <span case="dispatched">On the way</span>
  <span case="delivered">Delivered</span>
</template>
```

### Fall-Through Behavior

Enable fall-through to show multiple cases:

```html
<template switch.bind="orderStatus">
  <span case="received" fall-through="true">Order received</span>
  <span case="processing">Processing your order</span>
</template>
```

{% hint style="info" %}
Fall-through is `false` by default. Set `fall-through="true"` or `fall-through.bind="true"` to enable it.
{% endhint %}

## Advanced Techniques

### Dynamic Switch Expressions

Use computed expressions with `switch.bind`:

```html
<template repeat.for="num of numbers">
  <template switch.bind="true">
    <span case.bind="num % 15 === 0">FizzBuzz</span>
    <span case.bind="num % 3 === 0">Fizz</span>
    <span case.bind="num % 5 === 0">Buzz</span>
    <span default-case>${num}</span>
  </template>
</template>
```

### Conditional Slot Projection

Combine `switch.bind` with slots for dynamic content projection:

```html
<template as-custom-element="status-card">
  <au-slot name="content"></au-slot>
</template>

<status-card>
  <template au-slot="content" switch.bind="status">
    <div case="loading">Loading...</div>
    <div case="error">Something went wrong</div>
    <div case="success">Operation completed</div>
  </template>
</status-card>
```

### Nested Switches

Handle complex conditional logic with nested switches:

```html
<template switch.bind="userRole">
  <div case="admin">
    <template switch.bind="adminSection">
      <admin-users case="users"></admin-users>
      <admin-settings case="settings"></admin-settings>
      <admin-dashboard default-case></admin-dashboard>
    </template>
  </div>
  <user-dashboard case="user"></user-dashboard>
  <guest-welcome default-case></guest-welcome>
</template>
```

## Performance Guidelines

### Choosing the Right Directive

* **Frequent toggles**: Use `show.bind` to avoid DOM manipulation overhead
* **Infrequent changes**: Use `if.bind` to remove elements and save memory
* **Multiple conditions**: Use `switch.bind` for cleaner, more maintainable code

### Optimization Tips

```html
<!-- Good: Group related conditions -->
<template switch.bind="appState">
  <loading-screen case="loading"></loading-screen>
  <error-screen case="error"></error-screen>
  <main-content case="ready"></main-content>
</template>

<!-- Avoid: Multiple separate if statements -->
<loading-screen if.bind="appState === 'loading'"></loading-screen>
<error-screen if.bind="appState === 'error'"></error-screen>
<main-content if.bind="appState === 'ready'"></main-content>
```

## Important Restrictions

### Case Usage Rules

The `case` attribute must be a direct child of `switch.bind`:

```html
<!-- ✅ Correct -->
<template switch.bind="status">
  <span case="active">Active</span>
</template>

<!-- ❌ Incorrect: case not direct child -->
<template switch.bind="status">
  <div if.bind="someCondition">
    <span case="active">Active</span>
  </div>
</template>
```

### Default Case Placement

Place `default-case` as the last option for best practices:

```html
<template switch.bind="status">
  <span case="received">Received</span>
  <span case="processing">Processing</span>
  <span default-case>Unknown</span> <!-- Last -->
</template>
```


# List Rendering

Master list rendering in Aurelia with repeat.for. Learn efficient data binding, performance optimization, advanced patterns, and real-world techniques for dynamic collections including arrays, maps, s

The `repeat.for` binding turns collection data into efficient, reactive UI. Aurelia observes changes and reuses row views, keeping DOM work focused on what changed. Built-in values such as `$index` and `$first` keep common list patterns straightforward in the template.

## Core Concepts

### The `repeat.for` Binding

`repeat.for` creates a template instance for each item in a collection, similar to a `for...of` loop but with intelligent DOM management:

```html
<ul>
  <li repeat.for="item of items">
    ${item.name}
  </li>
</ul>
```

**JavaScript Analogy:**

```js
for (let item of items) {
  // Aurelia creates DOM element for each item
  console.log(item.name);
}
```

### Change Detection and Updates

Aurelia automatically observes collection changes and updates the DOM efficiently:

```typescript
export class MyComponent {
  items = [{ name: 'John' }, { name: 'Jane' }];

  addItem() {
    // Aurelia detects this change and updates DOM
    this.items.push({ name: 'Bob' });
  }

  updateFirst() {
    // splice reports the replacement to the collection observer
    this.items.splice(0, 1, { name: 'Johnny' });
  }
}
```

Use observed array methods such as `push`, `pop`, `splice`, `reverse`, and `sort` for in-place updates. You can also assign a new array reference. Native index assignment bypasses the collection observer.

## Stable Item Identity with Keys

### Why Keys Matter

By default, `repeat.for` matches object items by reference and primitive items by value. When the same object moves, its DOM and component state move with it. Add a key when refreshed data represents the same logical item with a new object instance:

```html
<!-- The object reference identifies this row -->
<div repeat.for="user of users">
  <input value.bind="user.name">
</div>

<!-- The user id preserves the row across refreshed objects -->
<div repeat.for="user of users; key.bind: user.id">
  <input value.bind="user.name">
</div>
```

### Key Strategies

**Property-based keys (recommended):**

```html
<!-- Use stable, unique properties -->
<li repeat.for="product of products; key.bind: product.id">
  ${product.name}
</li>
```

A stable, unique key identifies one item across collection updates. When keys are duplicated, Aurelia matches their occurrences in order: the first new occurrence reuses the first old occurrence with that key.

**Literal property keys (more efficient):**

```html
<!-- Avoids expression evaluation -->
<li repeat.for="product of products; key: id">
  ${product.name}
</li>
```

**Expression-based keys (flexible but slower):**

```html
<!-- For complex key logic -->
<li repeat.for="item of items; key.bind: item.category + '-' + item.id">
  ${item.name}
</li>
```

### When to Use Keys

Use a stable key when a data refresh creates new object instances for existing records. The key lets form state and component state remain with the logical item across those replacements.

The default identity is sufficient when object references remain stable. Primitive values also provide their own identity.

## Contextual Properties

Every repeat iteration provides rich contextual information:

```html
<div repeat.for="item of items">
  <span class="index">Item ${$index + 1} of ${$length}</span>
  <span class="status">
    ${$first ? 'First' : $last ? 'Last' : $middle ? 'Middle' : ''}
  </span>
  <div class="item ${$even ? 'even' : 'odd'}">
    ${item.name}
  </div>
</div>
```

### Complete Property Reference

| Property    | Type      | Description                                |
| ----------- | --------- | ------------------------------------------ |
| `$index`    | `number`  | Zero-based index (0, 1, 2...)              |
| `$first`    | `boolean` | `true` for the first item                  |
| `$last`     | `boolean` | `true` for the last item                   |
| `$middle`   | `boolean` | `true` for items that aren't first or last |
| `$even`     | `boolean` | `true` for even indices (0, 2, 4...)       |
| `$odd`      | `boolean` | `true` for odd indices (1, 3, 5...)        |
| `$length`   | `number`  | Total number of items                      |
| `$previous` | `any`     | `null`                                     |
| `$parent`   | `object`  | Parent binding context                     |

### Nested Repeats and $parent

Access parent contexts in nested structures:

```html
<div repeat.for="department of departments">
  <h2>${department.name}</h2>
  <div repeat.for="employee of department.employees">
    <span>
      Dept: ${$parent.department.name},
      Employee #${$index + 1}: ${employee.name}
    </span>
    <!-- Access root context -->
    <span>Company: ${$parent.$parent.companyName}</span>
  </div>
</div>
```

### Accessing Previous Items with $previous

The `$previous` contextual property provides access to the previous iteration's item, enabling powerful comparison and rendering patterns. It is a computed property available by default as part of repeat's contextual values. You can disable all contextual computed values (including `$previous`) using the `contextual` option.

**Basic usage:**

```html
<!-- $previous is enabled by default (disable with contextual: false) -->
<div repeat.for="item of items">
  <div class="item">
    ${item.name}
    <span if.bind="$previous !== null">
      (Previous: ${$previous.name})
    </span>
  </div>
</div>
```

**Key characteristics:**

* `$previous` is `null` for the first item
* `$previous` is `undefined` when `contextual` is disabled
* Computed property with minimal overhead when enabled (contextual is enabled by default)
* Works with all collection types (arrays, Maps, Sets, etc.)
* Compatible with keyed repeats

#### Section Headers and Dividers

A common use case is rendering section headers only when data changes:

```typescript
export class ProductList {
  products = [
    { category: 'Electronics', name: 'Laptop' },
    { category: 'Electronics', name: 'Mouse' },
    { category: 'Books', name: 'JavaScript Guide' },
    { category: 'Books', name: 'TypeScript Handbook' }
  ];
}
```

```html
<!-- Show category header only when it changes -->
<div repeat.for="product of products">
  <h2 if.bind="product.category !== $previous?.category">
    ${product.category}
  </h2>
  <div class="product">${product.name}</div>
</div>
```

**Output:**

```
Electronics
  Laptop
  Mouse
Books
  JavaScript Guide
  TypeScript Handbook
```

#### Comparison and Change Indicators

Highlight changes from previous values:

```typescript
export class StockTracker {
  prices = [
    { time: '09:00', price: 100 },
    { time: '09:01', price: 102 },
    { time: '09:02', price: 98 },
    { time: '09:03', price: 98 }
  ];
}
```

```html
<table>
  <tr repeat.for="entry of prices">
    <td>${entry.time}</td>
    <td class="${entry.price > $previous?.price ? 'up' :
                  entry.price < $previous?.price ? 'down' : ''}">
      $${entry.price}
      <span if.bind="$previous && entry.price !== $previous.price">
        ${entry.price > $previous.price ? '↑' : '↓'}
      </span>
    </td>
  </tr>
</table>
```

#### Combining with Keys

`$previous` works seamlessly with keyed repeats:

```html
<!-- Multiple iterator properties separated by semicolons -->
<div repeat.for="item of items; key: id">
  <div class="item-${item.id}">
    ${item.name}
    <span if.bind="$previous">
      Changed from: ${$previous.name}
    </span>
  </div>
</div>
```

#### Conditional Contextual Properties

Control contextual computed properties (including `$previous`) based on view model properties:

```typescript
export class ConfigurableList {
  items = [...];
  showContextual = true; // Toggle contextual on/off
}
```

```html
<!-- Enable/disable contextual based on component state -->
<div repeat.for="item of items; contextual.bind: showContextual">
  <!-- $previous is only available when contextual is true -->
</div>
```

#### Performance Considerations

**When contextual is disabled:**

* Zero memory overhead - `$previous` is not computed
* Negligible CPU cost - single conditional check per item

**When contextual is enabled (default):**

* Computed on demand via contextual getter
* Minimal CPU cost

**Best practices:**

* Keep contextual enabled unless you have a strong reason to disable it
* If needed, disable per-instance with `contextual: false` or `contextual.bind: someBoolean`

## Data Types and Collections

### Arrays

The most common and optimized collection type:

```typescript
export class ProductList {
  products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 25 }
  ];

  sortByPrice() {
    // Aurelia detects and updates DOM
    this.products.sort((a, b) => a.price - b.price);
  }
}
```

```html
<div repeat.for="product of products; key.bind: product.id">
  <h3>${product.name}</h3>
  <span class="price">${product.price | currency}</span>
</div>
```

### Sets

Useful for unique collections:

```typescript
export class TagManager {
  selectedTags = new Set(['javascript', 'typescript']);

  toggleTag(tag: string) {
    if (this.selectedTags.has(tag)) {
      this.selectedTags.delete(tag);
    } else {
      this.selectedTags.add(tag);
    }
  }
}
```

```html
<div repeat.for="tag of selectedTags">
  <span class="tag">${tag}</span>
</div>
```

### Maps

Perfect for key-value pairs:

```typescript
export class LocalizationDemo {
  translations = new Map([
    ['en', 'Hello'],
    ['es', 'Hola'],
    ['fr', 'Bonjour']
  ]);
}
```

```html
<!-- Destructure map entries -->
<div repeat.for="[language, greeting] of translations">
  <strong>${language}:</strong> ${greeting}
</div>

<!-- Or access as entry object -->
<div repeat.for="entry of translations">
  <strong>${entry[0]}:</strong> ${entry[1]}
</div>
```

### Number Ranges

Generate sequences quickly:

```html
<!-- Create pagination -->
<nav>
  <a repeat.for="page of totalPages"
     href="/products?page=${page + 1}">
    ${page + 1}
  </a>
</nav>

<!-- Star ratings -->
<div class="rating">
  <span repeat.for="star of 5"
        class="star ${star < rating ? 'filled' : ''}">
    ★
  </span>
</div>
```

## Advanced Patterns

### Destructuring Declarations

Extract multiple values in the repeat declaration:

```typescript
export class OrderHistory {
  orders = [
    { id: 1, items: [{ name: 'Coffee', qty: 2 }] },
    { id: 2, items: [{ name: 'Tea', qty: 1 }] }
  ];
}
```

```html
<!-- Destructure objects -->
<div repeat.for="{ id, items } of orders">
  Order #${id}: ${items.length} items
</div>

<!-- Rename a property while destructuring -->
<div repeat.for="{ id: orderId, items } of orders">
  Order #${orderId}: ${items.length} items
</div>

<!-- Destructure arrays -->
<div repeat.for="[index, value] of arrayOfPairs">
  ${index}: ${value}
</div>
```

Object repeat declarations support shallow property names and aliases. Each local is a reactive, one-way projection: changing the selected property on an order updates the local, but assigning the local does not write back to the order.

Nested patterns, default values, rest properties, and expression targets are not supported. For more complex access, repeat the object itself:

```html
<div repeat.for="order of orders">
  Order #${order.id}: ${order.customer.name}
</div>
```

### Integration with Other Template Controllers

**Conditional rendering within repeats:**

```html
<div repeat.for="user of users">
  <div if.bind="user.isActive">
    <strong>${user.name}</strong> - Active
  </div>
  <div else>
    <em>${user.name}</em> - Inactive
  </div>
</div>
```

**Nested conditionals and repeats:**

```html
<div repeat.for="category of categories">
  <h2>${category.name}</h2>
  <div if.bind="category.products.length > 0">
    <div repeat.for="product of category.products; key.bind: product.id">
      ${product.name}
    </div>
  </div>
  <p else>No products in this category</p>
</div>
```

### Working with Async Data

Handle loading states and async operations:

```typescript
export class AsyncDataExample {
  items: Item[] = [];
  isLoading = true;
  error: string | null = null;

  async attached() {
    try {
      this.items = await this.dataService.getItems();
    } catch (err) {
      this.error = err.message;
    } finally {
      this.isLoading = false;
    }
  }
}
```

```html
<div if.bind="isLoading">
  <spinner></spinner> Loading...
</div>

<div else>
  <div if.bind="error">
    <div class="error">Error: ${error}</div>
  </div>

  <div else>
    <div if.bind="items.length === 0">
      <p>No items found</p>
    </div>

    <div else>
      <div repeat.for="item of items; key.bind: item.id">
        ${item.name}
      </div>
    </div>
  </div>
</div>
```

### Asynchronous Row Lifecycles

Components rendered by a repeat use their normal lifecycle hooks. When a row's activation or teardown returns a Promise, Aurelia finishes that list update before beginning the next one. Collection changes made in the meantime are applied from the latest collection once the row lifecycle settles. Rows with synchronous hooks still update immediately.

If a row hook rejects, Aurelia waits for row work already in progress and reports the original lifecycle error. That list update fails, and the affected `repeat` should be treated as terminal.

### Complex Object Iteration

Use value converters for non-standard collections:

```typescript
// Object keys converter
export class KeysValueConverter {
  toView(obj: Record<string, any>): string[] {
    return obj ? Object.keys(obj) : [];
  }
}

// Object entries converter
export class EntriesValueConverter {
  toView(obj: Record<string, any>): [string, any][] {
    return obj ? Object.entries(obj) : [];
  }
}
```

```html
<!-- Iterate object keys -->
<div repeat.for="key of settings | keys">
  <label>${key}:</label>
  <input value.bind="settings[key]">
</div>

<!-- Iterate object entries -->
<div repeat.for="[key, value] of configuration | entries">
  <strong>${key}:</strong> ${value}
</div>
```

## Performance Best Practices

### Optimizing Large Lists

**Use stable keys when refreshed records are new objects:**

```html
<!-- Reuse each row by logical item identity -->
<div repeat.for="item of largeList; key.bind: item.id">
  ${item.name}
</div>
```

**Consider virtual scrolling for very large lists:**

```html
<!-- Use ui-virtualization for very large collections of items -->
<div virtual-repeat.for="item of hugeList">
  ${item.name}
</div>
```

This requires using the virtual repeat plugin.

### Memory Management

**Avoid memory leaks in complex scenarios:**

```typescript
export class ListComponent {
  private subscription?: IDisposable;

  attached() {
    // Subscribe to external data changes
    this.subscription = this.dataService.changes.subscribe(
      items => this.items = items
    );
  }

  detaching() {
    // Clean up subscriptions
    this.subscription?.dispose();
  }
}
```

## Custom Collection Handlers

### Built-in Handlers

Aurelia includes handlers for:

* **Arrays** (`Array`, `[]`)
* **Sets** (`Set`)
* **Maps** (`Map`)
* **Numbers** (`5` → creates range 0-4)
* **Array-like objects** (NodeList, HTMLCollection, etc.)
* **Null/undefined** (renders nothing)

### Creating Custom Handlers

For specialized collections:

```typescript
import { IRepeatableHandler, Registration } from 'aurelia';

// Custom handler for immutable lists
class ImmutableListHandler implements IRepeatableHandler {
  handles(value: unknown): boolean {
    return value && typeof value === 'object' && 'size' in value && 'get' in value;
  }

  iterate(value: any, func: (item: unknown, index: number) => void): void {
    for (let i = 0; i < value.size; i++) {
      func(value.get(i), i);
    }
  }
}

// Register the handler
Aurelia.register(
  Registration.singleton(IRepeatableHandler, ImmutableListHandler)
).app(MyApp).start();
```

### Observable Collections

Create reactive custom collections:

```typescript
import { CollectionObserver, ICollectionObserver } from '@aurelia/runtime';

class ReactiveCustomCollection {
  private _items: any[] = [];
  private _observer?: ICollectionObserver;

  get items() { return this._items; }

  add(item: any) {
    this._items.push(item);
    this._observer?.handleCollectionChange(/* change details */);
  }

  // Implement observable pattern...
}
```

## Troubleshooting Common Issues

### Issue: Changes Not Reflecting

**Problem:** Direct array index assignment doesn't trigger updates

```typescript
// This won't update the DOM
this.items[0] = newItem;
```

**Solution:** Use array methods or replace the array

```typescript
// These will update the DOM
this.items.splice(0, 1, newItem);
// or
this.items = [...this.items.slice(0, 0), newItem, ...this.items.slice(1)];
```

### Issue: Form State Resets After a Data Refresh

**Problem:** A refresh replaces existing records with new object instances, resetting row state

```html
<!-- New objects have new reference identities -->
<div repeat.for="item of items">
  <input value.bind="item.name">
</div>
```

**Solution:** Use stable keys

```html
<!-- Row state stays with each item id -->
<div repeat.for="item of items; key.bind: item.id">
  <input value.bind="item.name">
</div>
```

### Issue: Performance with Large Lists

**Problem:** Slow rendering with 1000+ items

**Solutions:**

1. **Use virtual scrolling** for very large lists
2. **Implement pagination** or infinite scroll
3. **Optimize templates** - minimize complex expressions
4. **Preserve item references**, or add a key when refreshed records are new objects

### Issue: Memory Leaks

**Problem:** Components not disposing properly

**Solution:** Clean up in lifecycle hooks

```typescript
export class MyComponent {
  detaching() {
    // Dispose of subscriptions, timers, etc.
    this.cleanup();
  }
}
```

## Real-World Examples

### Dynamic Product Catalog

```typescript
export class ProductCatalog {
  products: Product[] = [];
  filteredProducts: Product[] = [];
  searchTerm = '';
  selectedCategory = '';

  searchTermChanged() {
    this.filterProducts();
  }

  categoryChanged() {
    this.filterProducts();
  }

  private filterProducts() {
    this.filteredProducts = this.products.filter(product => {
      const matchesSearch = !this.searchTerm ||
        product.name.toLowerCase().includes(this.searchTerm.toLowerCase());
      const matchesCategory = !this.selectedCategory ||
        product.category === this.selectedCategory;
      return matchesSearch && matchesCategory;
    });
  }
}
```

```html
<div class="filters">
  <input value.bind="searchTerm" placeholder="Search products...">
  <select value.bind="selectedCategory">
    <option value="">All Categories</option>
    <option repeat.for="category of categories"
            value.bind="category">${category}</option>
  </select>
</div>

<div class="product-grid">
  <div repeat.for="product of filteredProducts; key.bind: product.id"
       class="product-card">
    <img src.bind="product.image" alt.bind="product.name">
    <h3>${product.name}</h3>
    <p class="price">${product.price | currency}</p>
    <button click.trigger="addToCart(product)">Add to Cart</button>
  </div>
</div>

<div if.bind="filteredProducts.length === 0" class="no-results">
  No products found matching your criteria.
</div>
```

### Data Table with Sorting

```typescript
export class DataTable {
  data: TableRow[] = [];
  sortColumn = '';
  sortDirection: 'asc' | 'desc' = 'asc';

  sort(column: string) {
    if (this.sortColumn === column) {
      this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
    } else {
      this.sortColumn = column;
      this.sortDirection = 'asc';
    }

    this.data.sort((a, b) => {
      const aVal = a[column];
      const bVal = b[column];
      const modifier = this.sortDirection === 'asc' ? 1 : -1;

      return aVal < bVal ? -modifier : aVal > bVal ? modifier : 0;
    });
  }
}
```

```html
<table class="data-table">
  <thead>
    <tr>
      <th repeat.for="column of columns"
          click.trigger="sort(column.key)"
          class="${sortColumn === column.key ? 'sorted ' + sortDirection : ''}">
        ${column.title}
        <span if.bind="sortColumn === column.key"
              class="sort-indicator">
          ${sortDirection === 'asc' ? '↑' : '↓'}
        </span>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr repeat.for="row of data; key.bind: row.id">
      <td repeat.for="column of columns">
        ${row[column.key] | column.converter}
      </td>
    </tr>
  </tbody>
</table>
```

## TypeScript Integration

### Type-Safe Repeats

```typescript
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

export class UserList {
  users: User[] = [];

  // Type-safe filtering
  get activeUsers(): User[] {
    return this.users.filter(user => user.isActive);
  }

  // Type-safe operations
  toggleUserStatus(user: User): void {
    user.isActive = !user.isActive;
  }
}
```

```html
<!-- TypeScript provides intellisense and type checking -->
<div repeat.for="user of activeUsers; key.bind: user.id">
  <span>${user.name}</span> <!-- ✓ TypeScript knows user.name exists -->
  <span>${user.email}</span> <!-- ✓ Type safe -->
  <button click.trigger="toggleUserStatus(user)">
    ${user.isActive ? 'Deactivate' : 'Activate'}
  </button>
</div>
```


# Lambda Expressions

Master lambda expressions in Aurelia templates to write cleaner, more expressive code. Learn the supported syntax, array operations, event handling, and performance considerations with real examples f

Lambda expressions in Aurelia templates allow you to use arrow function syntax directly in your HTML bindings. This feature enables inline data transformations, filtering, and event handling without requiring additional methods in your view models.

## Table of Contents

* [What Are Lambda Expressions?](#what-are-lambda-expressions)
* [Basic Syntax](#basic-syntax)
* [Supported Patterns](#supported-patterns)
* [Restrictions and Limitations](#restrictions-and-limitations)
* [Array Operations](#array-operations)
* [Event Handling](#event-handling)
* [Text Interpolation](#text-interpolation)
* [Advanced Use Cases](#advanced-use-cases)
* [Performance and Best Practices](#performance-and-best-practices)
* [Common Pitfalls](#common-pitfalls)

## What Are Lambda Expressions?

Lambda expressions are arrow functions that you can write directly in Aurelia template bindings. They provide a way to perform inline data transformations, filtering, and calculations without defining separate methods in your view model.

**Key Benefits:**

* **Inline Logic:** Write simple functions directly in templates
* **Reduced Boilerplate:** Avoid creating view model methods for simple operations
* **Better Locality:** Keep related logic close to where it's used
* **Reactive Updates:** Automatically re-evaluate when dependencies change

## Basic Syntax

Aurelia supports these arrow function patterns:

```html
<!-- No parameters -->
${(() => 42)()}

<!-- Single parameter (parentheses optional) -->
${items.map(item => item.name)}
${items.map((item) => item.name)}

<!-- Multiple parameters -->
${items.reduce((sum, item) => sum + item.price, 0)}

<!-- Rest parameters -->
${((...args) => args[0] + args[1] + args[2])(1, 2, 3)}

<!-- Nested arrow functions -->
${((a => b => a + b)(1))(2)}
```

**Real Template Usage:**

```html
<div repeat.for="item of items.filter(item => item.isActive)">
  ${item.name}
</div>
```

## Supported Patterns

Lambda expressions work in multiple binding contexts:

```html
<!-- Text interpolation -->
${items.filter(item => item.active).length}

<!-- Repeat bindings -->
<div repeat.for="user of users.sort((a, b) => a.name.localeCompare(b.name))">
  ${user.name}
</div>

<!-- Event bindings -->
<button click.trigger="() => doSomething()">Click</button>

<!-- Attribute bindings -->
<div my-attr.bind="value => transform(value)"></div>

<!-- With binding behaviors and value converters -->
<div repeat.for="item of items.filter(i => i.active) & debounce:500">
  ${item.name}
</div>
<div repeat.for="item of items.sort((a, b) => a - b) | take:10">
  ${item.name}
</div>
```

## Restrictions and Limitations

Aurelia's lambda expressions support only **expression bodies**. The following JavaScript arrow function features are **not supported**:

```html
<!-- ❌ Block bodies with curly braces -->
${items.filter(item => { return item.active; })}

<!-- ❌ Default parameters -->
${items.map((item = {}) => item.name)}

<!-- ❌ Destructuring parameters -->
${items.map(([first]) => first)}
${items.map(({name}) => name)}
```

**Error Messages:** When you attempt to use unsupported features, you'll receive these specific error codes:

* Block bodies: `AUR0178: "arrow function with function body is not supported"`
* Default parameters: `AUR0174: "arrow function with default parameters is not supported"`
* Destructuring: `AUR0175: "arrow function with destructuring parameters is not supported"`

## Array Operations

Lambda expressions work with all standard JavaScript array methods. Aurelia automatically observes array changes and property access within lambda expressions.

### Basic Array Operations

Lambda expressions work with all standard JavaScript array methods:

```html
<!-- Filtering -->
<div repeat.for="item of items.filter(item => item.isVisible)">
  ${item.name}
</div>

<!-- Sorting numbers -->
<div repeat.for="num of numbers.sort((a, b) => a - b)">
  ${num}
</div>

<!-- Mapping and joining -->
${items.map(item => item.name.toUpperCase()).join(', ')}

<!-- Array search methods -->
${items.find(item => item.id === selectedId)?.name}
${items.findIndex(item => item.active)}
${items.indexOf(targetValue)}
${items.lastIndexOf(targetValue)}
${items.includes(searchValue)}

<!-- Array access -->
${items.at(-1)} <!-- Last item -->

<!-- Aggregation -->
${cartItems.reduce((total, item) => total + item.price, 0)}
${cartItems.reduceRight((acc, item) => acc + item.value)}

<!-- Array tests -->
${items.every(item => item.valid)}
${items.some(item => item.hasError)}

<!-- Array transformation -->
${nested.flat()}
${items.flatMap(item => item.tags)}
${items.slice(0, 5)}
```

### Chaining Operations

Combine multiple array methods for complex transformations:

```html
<div repeat.for="product of products
  .filter(p => p.inStock && p.category === currentCategory)
  .sort((a, b) => b.rating - a.rating)
  .slice(0, 10)">
  ${product.name} - ${product.rating}⭐
</div>
```

### Reactive Property Access

Aurelia automatically tracks property access in lambda expressions:

```html
<!-- Changes to item.visible will trigger re-evaluation -->
<div repeat.for="item of items.filter(item => item.visible)">
  ${item.name}
</div>
```

## Event Handling

Lambda expressions work with all Aurelia event bindings:

```html
<!-- Simple event handlers -->
<button click.trigger="() => count++">Increment</button>

<!-- Passing event data -->
<input input.trigger="event => search(event.target.value)">

<!-- Multiple parameters -->
<button click.trigger="event => deleteItem(event, item.id)">Delete</button>
```

**Custom Attributes with Lambdas:**

```html
<!-- Pass functions to custom attributes -->
<div validate.bind="value => value.length > 3">
  <input value.bind="inputValue">
</div>
```

## Text Interpolation

Use lambda expressions in text interpolation for inline calculations:

```html
<!-- Array transformations -->
<p>Tags: ${tags.map(tag => tag.toUpperCase()).join(', ')}</p>

<!-- Calculations -->
<p>Total: $${items.reduce((sum, item) => sum + item.price, 0).toFixed(2)}</p>

<!-- Conditional text -->
<p>Status: ${items.every(item => item.completed) ? 'All Done!' : 'In Progress'}</p>

<!-- String operations -->
<p>Names: ${users.map(u => u.name).join(' and ')}</p>
```

### Accessing Template Context

Lambda expressions can access `$this` and `$parent` for scope navigation, maintaining proper binding context:

```html
<!-- Access view model properties -->
${items.filter(item => item.userId === $this.currentUserId).length}

<!-- Access parent scope in nested contexts -->
<div with.bind="childData">
  ${items.find(item => item.id === $parent.selectedId)?.name}
</div>

<!-- Complex scope navigation -->
<div with.bind="{level: 1}">
  <div with.bind="{level: 2}">
    <div with.bind="{level: 3}">
      <!-- Access different scope levels -->
      ${(level => `Current: ${level}, Parent: ${$parent.level}, Root: ${$parent.$parent.level}`)($this.level)}
    </div>
  </div>
</div>
```

**Scope Access Patterns:**

* **`$this`**: References the current binding context (view model)
* **`$parent`**: References the parent binding context
* **`$parent.$parent`**: Chain to access higher-level scopes
* **Maintains reactivity**: Changes to referenced properties trigger updates

## Advanced Use Cases

### Nested Array Processing

Process complex nested data structures with proper scope handling:

```html
<!-- Flatten nested hierarchies -->
<div repeat.for="item of items.flatMap(x => 
  [x].concat(x.children.flatMap(y => [y].concat(y.children))))">
  ${item.name}
</div>

<!-- Access parent variables in nested operations -->
<div repeat.for="item of items.flatMap(x => 
  x.children.flatMap(y => ([x, y].concat(y.children))))">
  ${item.name}
</div>

<!-- Complex hierarchical flattening with metadata -->
<div repeat.for="item of categories.flatMap(category => 
  category.products
    .filter(p => p.active)
    .map(product => ({ ...product, categoryName: category.name })))">
  ${item.name} (${item.categoryName})
</div>
```

**Nested Processing Benefits:**

* **Maintains lexical scope**: Outer variables accessible in inner functions
* **Reactive updates**: Changes to nested properties trigger re-evaluation
* **Performance optimized**: Aurelia observes the right level of nesting

### Dynamic Search and Filtering

Create responsive search interfaces:

```html
<input value.bind="searchQuery" placeholder="Search products...">
<input value.bind="minPrice" type="number" placeholder="Min price">
<input value.bind="maxPrice" type="number" placeholder="Max price">

<div repeat.for="product of products
  .filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
  .filter(p => p.price >= (minPrice || 0))
  .filter(p => p.price <= (maxPrice || 999999))
  .sort((a, b) => b.rating - a.rating)">
  ${product.name} - $${product.price} (${product.rating}⭐)
</div>
```

### Immediate Invoked Arrow Functions (IIFE)

Execute functions immediately within templates using arrow function IIFE patterns:

```html
<!-- Simple IIFE -->
${(a => a)(42)}

<!-- Nested arrow functions -->
${(a => b => a + b)(1)(2)}

<!-- Rest parameters -->
${((...args) => args[0] + args[1] + args[2])(1, 2, 3)}

<!-- Complex object creation with property access -->
${(((e) => ({ a: e.value }))({ value: 'test' })).a}

<!-- Multi-step calculations -->
${((price, tax) => (price * (1 + tax)).toFixed(2))(100, 0.08)}
```

**IIFE Use Cases:**

* **Calculations**: Perform complex math without cluttering view model
* **Data transformation**: Transform values inline with specific logic
* **Scoped operations**: Execute multi-step operations in template context

## Performance and Best Practices

### Automatic Property Observation

Aurelia automatically observes properties accessed within lambda expressions:

```html
<!-- Will update when item.status or item.priority changes -->
<div repeat.for="item of items.filter(item => 
  item.status === 'active' && item.priority > 3)">
  ${item.name}
</div>
```

### Array Mutation Handling

Aurelia observes array mutations for most methods:

```html
<!-- These will automatically update when arrays change -->
${items.map(item => item.name)}           <!-- ✅ Observes array changes -->
${items.filter(item => item.active)}      <!-- ✅ Observes array changes -->
${items.reduce((sum, item) => sum + item.price, 0)}  <!-- ✅ Observes array changes -->
```

**Sorting Considerations:**

```html
<!-- ✅ Works perfectly for text interpolation -->
${items.sort((a, b) => a - b)}

<!-- ⚠️ Use slice() first for repeat.for to avoid mutation issues -->
<div repeat.for="item of items.slice().sort((a, b) => a.order - b.order)">
  ${item.name}
</div>

<!-- ⚠️ Direct sort in repeat.for can cause issues due to array mutation -->
<!-- This pattern is skipped in framework tests due to flush queue complications -->
<div repeat.for="item of items.sort((a, b) => a.order - b.order)">
  ${item.name} <!-- Can cause problems when items array is mutated -->
</div>
```

**Why slice() before sort()?**

* `sort()` mutates the original array, triggering multiple change notifications
* `slice()` creates a copy, preventing mutation conflicts with the repeater
* Ensures stable rendering when the source array changes

### Performance Guidelines

* **Keep expressions simple:** Complex logic should move to view model methods
* **Avoid deep nesting:** Limit chained operations for readability
* **Use specific property access:** Reference specific properties for optimal observation
* **Profile when needed:** Monitor performance in large lists with complex transformations

## Common Pitfalls

### Mutation vs. Immutable Operations

```html
<!-- ❌ Problematic: sort mutates original array -->
<div repeat.for="item of items.sort((a, b) => a.name.localeCompare(b.name))">

<!-- ✅ Better: use slice() to avoid mutation -->
<div repeat.for="item of items.slice().sort((a, b) => a.name.localeCompare(b.name))">
```

### Expression Complexity

```html
<!-- ❌ Too complex for templates -->
${items.filter(item => {
  const category = categories.find(c => c.id === item.categoryId);
  return category && category.active && item.stock > 0;
})}

<!-- ✅ Move complex logic to view model -->
${getAvailableItems(items, categories)}
```

### Debugging Tips

* **Isolate expressions:** Test lambda expressions in browser console first
* **Use intermediate variables:** Break complex chains into steps in your view model
* **Check property names:** Ensure referenced properties exist and are observable
* **Verify data structure:** Confirm arrays and objects have expected shape
* **Parser state corruption:** If experiencing strange errors, check for syntax issues in other expressions that might corrupt the parser state

### Framework Implementation Notes

**AST Structure:**

* Lambda expressions compile to `ArrowFunction` AST nodes
* Support `rest` parameters via boolean flag
* Body must be an expression (`IsAssign` type)
* Parameters are stored as `BindingIdentifier[]`

**Error Codes:**

* `AUR0173`: Invalid arrow parameter list
* `AUR0174`: Default parameters not supported
* `AUR0175`: Destructuring parameters not supported
* `AUR0176`: Rest parameter must be last
* `AUR0178`: Function body (block statements) not supported

## Summary

Lambda expressions in Aurelia templates provide a powerful way to write inline logic without cluttering your view models. They excel at array transformations, simple calculations, and event handling. Remember to:

* Use only **expression bodies** (no curly braces)
* Leverage **automatic property observation** for reactive updates
* Keep expressions **simple and readable**
* Move complex logic to **view model methods** when needed
* Use `slice()` before mutating operations like `sort()` for safety

With these guidelines, lambda expressions can significantly improve your template code's clarity and maintainability.


# Local templates (inline templates)

Learn how to define, use, and optimize local (inline) templates in Aurelia 2 to remove boilerplate and simplify your components.

## Introduction

Most of the time, when working with templated views in Aurelia, you want to create reusable components. However, there are scenarios where reusability isn’t necessary or might cause unnecessary overhead. Local (inline) templates allow you to define a template as a "one-off" custom element, usable **only** within the scope of its parent view. This helps reduce boilerplate and fosters clear, localized organization of your code.

```html
<template as-custom-element="person-info">
  <bindable name="person"></bindable>
  <div>
    <label>Name:</label>
    <span>${person.name}</span>
  </div>
  <div>
    <label>Address:</label>
    <span>${person.address}</span>
  </div>
</template>

<h2>Sleuths</h2>
<person-info repeat.for="sleuth of sleuths" person.bind="sleuth"></person-info>

<h2>Nemeses</h2>
<person-info
  repeat.for="nemesis of nemeses"
  person.bind="nemesis"
></person-info>
```

By defining `<template as-custom-element="person-info">`, you create a local component named person-info, which can only be used in this file (my-app.html). It accepts a bindable property person (specified via the `<bindable>` tag). You can now reuse `<person-info>` repeatedly in this view without creating a separate file or global custom element.

```typescript
export class MyApp {
  public readonly sleuths: Person[] = [
    new Person("Byomkesh Bakshi", "66, Harrison Road"),
    new Person("Sherlock Holmes", "221b Baker Street"),
  ];
  public readonly nemeses: Person[] = [
    new Person("Anukul Guha", "unknown"),
    new Person("James Moriarty", "unknown"),
  ];
}

class Person {
  public constructor(public name: string, public address: string) {}
}
```

***

## Why Use Local Templates?

Local templates are similar to HTML-Only Custom Elements, with the major difference that local templates are scoped to the file that defines them. They are ideal for:

* One-off Components: When you need a snippet repeated multiple times in a single view but have no intention of reusing it elsewhere.
* Reducing Boilerplate: You don’t have to create a new .html and .ts file for every small piece of UI logic.
* Maintain High Cohesion: Local templates can be optimized for a specific context without worrying about external usage. They can contain deeply nested markup or references to local data without polluting your global component space.

That said, if you find your local template would be useful across multiple views or components, consider extracting it into a shared component.

***

## Syntax and Basic Usage

A local template must be declared with `<template as-custom-element="your-element-name">`. Inside this


# SVG

A developer guide for enabling SVG binding in Aurelia 2.

Learn about enabling SVG binding in Aurelia templates.

## Adding SVG Registration

By default, Aurelia won't work with SVG elements since SVG elements and their attributes require different parsing rules (SVG uses XML namespaces and has different attribute handling than HTML). To teach Aurelia how to handle SVG element bindings, register the `SVGAnalyzer`:

```typescript
import { SVGAnalyzer } from '@aurelia/runtime-html';
import { Aurelia } from 'aurelia';

Aurelia
  .register(SVGAnalyzer) // <-- add this line
  .app(MyApp)
  .start();
```

After adding this registration, bindings on SVG elements will work as expected.

## Basic SVG Bindings

Once `SVGAnalyzer` is registered, you can bind to SVG attributes just like HTML attributes:

```html
<svg width="200" height="200">
  <!-- Bind to standard SVG attributes -->
  <circle cx.bind="circleX"
          cy.bind="circleY"
          r.bind="radius"
          fill.bind="fillColor" />

  <!-- Use interpolation for transform -->
  <rect x="10" y="10"
        width.bind="rectWidth"
        height.bind="rectHeight"
        transform="rotate(${rotation} 50 50)" />
</svg>
```

```typescript
export class SvgDemo {
  circleX = 100;
  circleY = 100;
  radius = 50;
  fillColor = '#3498db';
  rectWidth = 80;
  rectHeight = 60;
  rotation = 45;
}
```

## Dynamic SVG Charts

Create reactive charts with data binding:

```html
<svg width="400" height="300" class="bar-chart">
  <!-- Y-axis -->
  <line x1="50" y1="10" x2="50" y2="250" stroke="#333" />
  <!-- X-axis -->
  <line x1="50" y1="250" x2="390" y2="250" stroke="#333" />

  <!-- Dynamic bars -->
  <g repeat.for="item of chartData; let i = $index">
    <rect x.bind="60 + i * 70"
          y.bind="250 - item.value * 2"
          width="50"
          height.bind="item.value * 2"
          fill.bind="item.color" />
    <text x.bind="85 + i * 70"
          y="270"
          text-anchor="middle"
          font-size="12">
      ${item.label}
    </text>
  </g>
</svg>
```

```typescript
export class BarChart {
  chartData = [
    { label: 'Jan', value: 65, color: '#3498db' },
    { label: 'Feb', value: 89, color: '#2ecc71' },
    { label: 'Mar', value: 72, color: '#e74c3c' },
    { label: 'Apr', value: 95, color: '#9b59b6' }
  ];
}
```

## SVG Path Animations

Bind to path data for dynamic shapes:

```html
<svg width="300" height="200">
  <path d.bind="pathData"
        fill="none"
        stroke.bind="strokeColor"
        stroke-width.bind="strokeWidth" />
</svg>
```

```typescript
export class PathDemo {
  strokeColor = '#e74c3c';
  strokeWidth = 2;

  get pathData(): string {
    return `M 10 80 Q 95 10 180 80 T 350 80`;
  }
}
```

## Interactive SVG Elements

Handle events on SVG elements:

```html
<svg width="400" height="300">
  <circle repeat.for="circle of circles"
          cx.bind="circle.x"
          cy.bind="circle.y"
          r.bind="circle.r"
          fill.bind="circle.color"
          class.bind="circle.selected ? 'selected' : ''"
          click.trigger="selectCircle(circle)"
          mouseenter.trigger="highlightCircle(circle)"
          mouseleave.trigger="unhighlightCircle(circle)"
          style="cursor: pointer;" />
</svg>
```

## SVG with CSS Classes

Apply dynamic classes to SVG elements:

```html
<svg width="200" height="200">
  <rect x="50" y="50"
        width="100" height="100"
        class.bind="isActive ? 'shape-active' : 'shape-inactive'" />
</svg>
```

```css
.shape-active {
  fill: #2ecc71;
  stroke: #27ae60;
  stroke-width: 3;
}

.shape-inactive {
  fill: #bdc3c7;
  stroke: #95a5a6;
  stroke-width: 1;
}
```

## SVG Gradients with Bindings

Create dynamic gradients:

```html
<svg width="200" height="200">
  <defs>
    <linearGradient id="dynamicGradient" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" stop-color.bind="startColor" />
      <stop offset="100%" stop-color.bind="endColor" />
    </linearGradient>
  </defs>
  <rect x="10" y="10" width="180" height="180" fill="url(#dynamicGradient)" />
</svg>
```

## Supported SVG Elements

The `SVGAnalyzer` supports all standard SVG elements including:

* Basic shapes: `circle`, `rect`, `ellipse`, `line`, `polyline`, `polygon`, `path`
* Text: `text`, `tspan`, `textPath`
* Containers: `g`, `svg`, `defs`, `symbol`, `use`
* Gradients: `linearGradient`, `radialGradient`, `stop`
* Filters: `filter`, `feBlend`, `feColorMatrix`, `feGaussianBlur`, and more
* Animation: `animate`, `animateTransform`, `animateMotion`

## Important Notes

* **Always register `SVGAnalyzer`** before using SVG bindings
* SVG attributes are case-sensitive (e.g., `viewBox`, not `viewbox`)
* Use `xlink:href` for older SVG references, or just `href` for modern browsers
* For better performance with many SVG elements, consider using `one-time` bindings for static values

## Related Documentation

* [Attribute Binding](/templates/overview/attribute-binding) - Basic binding syntax
* [Class and Style Binding](/templates/class-and-style-bindings) - Dynamic styling
* [Repeat Rendering](/templates/repeats-and-list-rendering) - Iterating over data


# Real-World Recipes


# Recipes Overview

Real-world, production-ready examples showing Aurelia templates in action. Each recipe is a complete, working example demonstrating multiple templating features working together.

## Available Recipes

### E-Commerce & Shopping

* [**Product Catalog with Search & Filters**](/templates/real-world-recipes/product-catalog) - Search, filter, sort products with real-time updates
* [**Shopping Cart**](/templates/real-world-recipes/shopping-cart) - Add/remove items, update quantities, calculate totals

### Data Display & Tables

* [**Data Table with Sorting, Filtering & Pagination**](/templates/real-world-recipes/data-table) - Complete data table with sorting, filtering, pagination, row selection

### UI Components

* [**Notification/Toast System**](/templates/real-world-recipes/notification-system) - Global notifications with auto-dismiss, multiple types, and queue management
* [**Search with Autocomplete**](/templates/real-world-recipes/search-autocomplete) - Typeahead search with keyboard navigation and highlighting

## How to Use These Recipes

Each recipe includes:

* **Complete working code** - Copy and paste to get started
* **Feature breakdown** - What templating features are being used
* **Variations** - Common modifications and extensions
* **Related patterns** - Links to similar recipes

## Recipe Template

Looking to contribute a recipe? Follow this structure:

```markdown
# Recipe Name

Brief description of what this recipe demonstrates.

## Live Demo

[Open in StackBlitz](link-to-stackblitz)

## Features Demonstrated

- Feature 1
- Feature 2
- Feature 3

## Code

### View Model (TypeScript)

[code]

### Template (HTML)

[code]

### Styles (CSS) - Optional

[code]

## How It Works

Step-by-step explanation...

## Variations

- Variation 1: Description and code
- Variation 2: Description and code

## Related

- [Related Recipe](link)
- [Related Docs](link)
```

## Contributing

Have a great real-world example? We'd love to include it! Submit a PR with your recipe following the template above.


# Product Catalog

A complete product catalog featuring real-time search, category filtering, sorting, and responsive design. This recipe demonstrates how to build a performant, user-friendly product browsing experience.

## Features Demonstrated

* **Two-way data binding** - Search input with instant updates
* **Computed properties** - Filtered product list based on search and filters
* **`repeat.for` with keys** - Efficient list rendering
* **Event handling** - Sort buttons, filter checkboxes
* **Conditional rendering** - Empty states, loading states
* **Value converters** - Currency formatting
* **CSS class binding** - Active filters, selected sort order
* **Debouncing** - Optimize search performance

## Code

### View Model (product-catalog.ts)

```typescript
interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  category: string;
  image: string;
  inStock: boolean;
  rating: number;
}

type SortOption = 'name' | 'price-low' | 'price-high' | 'rating';

export class ProductCatalog {
  // Data
  products: Product[] = [
    {
      id: 1,
      name: 'Wireless Headphones',
      description: 'Premium noise-canceling headphones with 30-hour battery',
      price: 299.99,
      category: 'Audio',
      image: '/images/headphones.jpg',
      inStock: true,
      rating: 4.5
    },
    {
      id: 2,
      name: 'Smart Watch',
      description: 'Fitness tracking with heart rate monitor and GPS',
      price: 399.99,
      category: 'Wearables',
      image: '/images/smartwatch.jpg',
      inStock: true,
      rating: 4.2
    },
    {
      id: 3,
      name: 'Laptop Stand',
      description: 'Ergonomic aluminum stand for better posture',
      price: 49.99,
      category: 'Accessories',
      image: '/images/stand.jpg',
      inStock: false,
      rating: 4.8
    },
    {
      id: 4,
      name: 'Mechanical Keyboard',
      description: 'RGB backlit with customizable switches',
      price: 159.99,
      category: 'Accessories',
      image: '/images/keyboard.jpg',
      inStock: true,
      rating: 4.6
    },
    {
      id: 5,
      name: 'USB-C Hub',
      description: '7-in-1 adapter with 4K HDMI and SD card reader',
      price: 79.99,
      category: 'Accessories',
      image: '/images/hub.jpg',
      inStock: true,
      rating: 4.3
    },
    {
      id: 6,
      name: 'Wireless Earbuds',
      description: 'True wireless with active noise cancellation',
      price: 199.99,
      category: 'Audio',
      image: '/images/earbuds.jpg',
      inStock: true,
      rating: 4.4
    }
  ];

  // Filter state
  searchQuery = '';
  selectedCategories: string[] = [];
  sortBy: SortOption = 'name';
  showOutOfStock = true;

  // Computed property for unique categories
  get categories(): string[] {
    return [...new Set(this.products.map(p => p.category))].sort();
  }

  // Computed property for filtered and sorted products
  get filteredProducts(): Product[] {
    let filtered = this.products;

    // Filter by search query
    if (this.searchQuery.trim()) {
      const query = this.searchQuery.toLowerCase();
      filtered = filtered.filter(p =>
        p.name.toLowerCase().includes(query) ||
        p.description.toLowerCase().includes(query)
      );
    }

    // Filter by selected categories
    if (this.selectedCategories.length > 0) {
      filtered = filtered.filter(p =>
        this.selectedCategories.includes(p.category)
      );
    }

    // Filter out of stock if needed
    if (!this.showOutOfStock) {
      filtered = filtered.filter(p => p.inStock);
    }

    // Sort products
    return this.sortProducts(filtered);
  }

  get hasActiveFilters(): boolean {
    return this.searchQuery.trim() !== '' ||
           this.selectedCategories.length > 0 ||
           !this.showOutOfStock;
  }

  private sortProducts(products: Product[]): Product[] {
    const sorted = [...products];

    switch (this.sortBy) {
      case 'name':
        return sorted.sort((a, b) => a.name.localeCompare(b.name));
      case 'price-low':
        return sorted.sort((a, b) => a.price - b.price);
      case 'price-high':
        return sorted.sort((a, b) => b.price - a.price);
      case 'rating':
        return sorted.sort((a, b) => b.rating - a.rating);
      default:
        return sorted;
    }
  }

  clearFilters() {
    this.searchQuery = '';
    this.selectedCategories = [];
    this.showOutOfStock = true;
  }

  setSortOrder(sortOption: SortOption) {
    this.sortBy = sortOption;
  }
}
```

### Template (product-catalog.html)

```html
<div class="product-catalog">
  <!-- Header -->
  <header class="catalog-header">
    <h1>Product Catalog</h1>
    <p class="result-count">
      Showing ${filteredProducts.length} of ${products.length} products
    </p>
  </header>

  <!-- Search and Filters -->
  <div class="filters-section">
    <!-- Search Bar -->
    <div class="search-box">
      <input
        type="search"
        value.bind="searchQuery & debounce:300"
        placeholder="Search products..."
        class="search-input">
      <span class="search-icon">🔍</span>
    </div>

    <!-- Category Filters -->
    <div class="filter-group">
      <h3>Categories</h3>
      <label repeat.for="category of categories" class="filter-option">
        <input
          type="checkbox"
          model.bind="category"
          checked.bind="selectedCategories">
        ${category}
      </label>
    </div>

    <!-- Availability Filter -->
    <div class="filter-group">
      <label class="filter-option">
        <input type="checkbox" checked.bind="showOutOfStock">
        Show out of stock items
      </label>
    </div>

    <!-- Clear Filters -->
    <button
      if.bind="hasActiveFilters"
      click.trigger="clearFilters()"
      class="clear-filters-btn">
      Clear All Filters
    </button>
  </div>

  <!-- Sort Options -->
  <div class="sort-section">
    <label>Sort by:</label>
    <button
      click.trigger="setSortOrder('name')"
      class="sort-btn ${sortBy === 'name' ? 'active' : ''}">
      Name
    </button>
    <button
      click.trigger="setSortOrder('price-low')"
      class="sort-btn ${sortBy === 'price-low' ? 'active' : ''}">
      Price: Low to High
    </button>
    <button
      click.trigger="setSortOrder('price-high')"
      class="sort-btn ${sortBy === 'price-high' ? 'active' : ''}">
      Price: High to Low
    </button>
    <button
      click.trigger="setSortOrder('rating')"
      class="sort-btn ${sortBy === 'rating' ? 'active' : ''}">
      Rating
    </button>
  </div>

  <!-- Product Grid -->
  <div class="product-grid" if.bind="filteredProducts.length > 0">
    <div
      repeat.for="product of filteredProducts; key: id"
      class="product-card ${product.inStock ? '' : 'out-of-stock'}">

      <!-- Product Image -->
      <div class="product-image">
        <img src.bind="product.image" alt.bind="product.name">
        <span if.bind="!product.inStock" class="stock-badge">Out of Stock</span>
      </div>

      <!-- Product Info -->
      <div class="product-info">
        <h3 class="product-name">${product.name}</h3>
        <p class="product-description">${product.description}</p>

        <!-- Rating -->
        <div class="product-rating">
          <span repeat.for="star of 5" class="star ${star < product.rating ? 'filled' : ''}">
            ★
          </span>
          <span class="rating-value">${product.rating}</span>
        </div>

        <!-- Price and Actions -->
        <div class="product-footer">
          <span class="product-price">${product.price | currency:'USD'}</span>
          <button
            class="add-to-cart-btn"
            disabled.bind="!product.inStock"
            click.trigger="addToCart(product)">
            ${product.inStock ? 'Add to Cart' : 'Unavailable'}
          </button>
        </div>
      </div>
    </div>
  </div>

  <!-- Empty State -->
  <div if.bind="filteredProducts.length === 0" class="empty-state">
    <p class="empty-icon">📦</p>
    <h2>No products found</h2>
    <p>Try adjusting your search or filters</p>
    <button click.trigger="clearFilters()" class="btn-primary">
      Clear Filters
    </button>
  </div>
</div>
```

### Styles (product-catalog.css)

```css
.product-catalog {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.catalog-header {
  margin-bottom: 2rem;
}

.result-count {
  color: #666;
  margin-top: 0.5rem;
}

.filters-section {
  background: #f5f5f5;
  padding: 1.5rem;
  border-radius: 8px;
  margin-bottom: 2rem;
}

.search-box {
  position: relative;
  margin-bottom: 1.5rem;
}

.search-input {
  width: 100%;
  padding: 0.75rem 2.5rem 0.75rem 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.search-icon {
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}

.filter-group {
  margin-bottom: 1rem;
}

.filter-group h3 {
  font-size: 0.9rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
  color: #333;
}

.filter-option {
  display: block;
  margin-bottom: 0.5rem;
  cursor: pointer;
}

.filter-option input {
  margin-right: 0.5rem;
}

.clear-filters-btn {
  background: #fff;
  border: 1px solid #ddd;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
}

.clear-filters-btn:hover {
  background: #f0f0f0;
}

.sort-section {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 2rem;
  flex-wrap: wrap;
}

.sort-btn {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background: #fff;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.sort-btn:hover {
  border-color: #007bff;
}

.sort-btn.active {
  background: #007bff;
  color: white;
  border-color: #007bff;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.5rem;
}

.product-card {
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
}

.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.product-card.out-of-stock {
  opacity: 0.6;
}

.product-image {
  position: relative;
  height: 200px;
  background: #f5f5f5;
  overflow: hidden;
}

.product-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.stock-badge {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  background: #dc3545;
  color: white;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 600;
}

.product-info {
  padding: 1rem;
}

.product-name {
  font-size: 1.1rem;
  margin: 0 0 0.5rem 0;
  color: #333;
}

.product-description {
  color: #666;
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
  line-height: 1.4;
}

.product-rating {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  margin-bottom: 1rem;
}

.star {
  color: #ddd;
  font-size: 1rem;
}

.star.filled {
  color: #ffc107;
}

.rating-value {
  margin-left: 0.25rem;
  color: #666;
  font-size: 0.9rem;
}

.product-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 1.25rem;
  font-weight: 600;
  color: #007bff;
}

.add-to-cart-btn {
  padding: 0.5rem 1rem;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: background 0.2s;
}

.add-to-cart-btn:hover:not(:disabled) {
  background: #218838;
}

.add-to-cart-btn:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.empty-state {
  text-align: center;
  padding: 4rem 2rem;
}

.empty-icon {
  font-size: 4rem;
  margin-bottom: 1rem;
}

.empty-state h2 {
  color: #333;
  margin-bottom: 0.5rem;
}

.empty-state p {
  color: #666;
  margin-bottom: 1.5rem;
}

.btn-primary {
  padding: 0.75rem 1.5rem;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-primary:hover {
  background: #0056b3;
}

@media (max-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }

  .sort-section {
    font-size: 0.9rem;
  }

  .sort-btn {
    padding: 0.4rem 0.8rem;
    font-size: 0.85rem;
  }
}
```

## How It Works

### 1. Search with Debouncing

The search input uses debouncing to avoid excessive filtering operations:

```html
<input value.bind="searchQuery & debounce:300">
```

This waits 300ms after the user stops typing before updating `searchQuery`, which triggers the `filteredProducts` computed property.

### 2. Reactive Filtering

The `filteredProducts` getter automatically recalculates when any filter changes:

```typescript
get filteredProducts(): Product[] {
  // Filters are applied in sequence
  // Search → Categories → Stock availability → Sort
}
```

### 3. Multiple Checkbox Selection

Category filters use array binding:

```html
<input type="checkbox" model.bind="category" checked.bind="selectedCategories">
```

Aurelia automatically adds/removes items from the `selectedCategories` array.

### 4. Efficient List Rendering

Using `key: id` tells Aurelia to track products by ID, enabling efficient DOM updates when sorting or filtering:

```html
<div repeat.for="product of filteredProducts; key: id">
```

### 5. Dynamic CSS Classes

The active sort button and out-of-stock cards use class binding:

```html
<button class="sort-btn ${sortBy === 'name' ? 'active' : ''}">
<div class="product-card ${product.inStock ? '' : 'out-of-stock'}">
```

## Variations

### Add Price Range Filter

```typescript
minPrice = 0;
maxPrice = 500;

get filteredProducts(): Product[] {
  // ... existing filters
  filtered = filtered.filter(p =>
    p.price >= this.minPrice && p.price <= this.maxPrice
  );
  // ... sort
}
```

```html
<div class="filter-group">
  <h3>Price Range</h3>
  <input type="range" min="0" max="500" value.bind="minPrice">
  <input type="range" min="0" max="500" value.bind="maxPrice">
  <p>${minPrice | currency} - ${maxPrice | currency}</p>
</div>
```

### Add to Cart Functionality

```typescript
cart: Product[] = [];

addToCart(product: Product) {
  this.cart.push(product);
  // Show notification
  console.log(`Added ${product.name} to cart`);
}
```

### Persist Filters in URL

Use the router to save filter state:

```typescript
import { resolve } from 'aurelia';
import { IRouter } from '@aurelia/router';

export class ProductCatalog {
  private readonly router = resolve(IRouter);

  searchQueryChanged() {
    this.router.load({
      query: { search: this.searchQuery }
    });
  }
}
```

## Related

* [Shopping Cart Recipe](/templates/real-world-recipes/shopping-cart)
* [Data Table Recipe](/templates/real-world-recipes/data-table)
* [List Rendering Guide](/templates/repeats-and-list-rendering)
* [Conditional Rendering](/templates/conditional-rendering)
* [Value Converters](/templates/value-converters)


# Shopping Cart

A complete shopping cart implementation with add/remove items, quantity updates, and dynamic total calculations. Demonstrates reactive data management and user interaction patterns.

## Features Demonstrated

* **Array manipulation** - Add, remove, update cart items
* **Lambda expressions** - Complex calculations directly in templates using `reduce`, `filter`, etc.
* **Event handling** - Button clicks, quantity changes
* **Conditional rendering** - Empty cart state, checkout button
* **List rendering with keys** - Efficient cart item updates
* **Two-way binding** - Quantity inputs
* **Number formatting** - Currency display
* **Component state management** - Cart as a service

## Code

### View Model (shopping-cart.ts)

```typescript
export interface CartItem {
  id: number;
  productId: number;
  name: string;
  price: number;
  quantity: number;
  image: string;
  maxQuantity: number;
}

export class ShoppingCart {
  cartItems: CartItem[] = [];

  // Add item to cart
  addToCart(product: { id: number; name: string; price: number; image: string; maxQuantity: number }) {
    const existingItem = this.cartItems.find(item => item.productId === product.id);

    if (existingItem) {
      // Increase quantity if item already in cart
      if (existingItem.quantity < existingItem.maxQuantity) {
        existingItem.quantity++;
      } else {
        alert(`Maximum quantity (${existingItem.maxQuantity}) reached for ${existingItem.name}`);
      }
    } else {
      // Add new item
      this.cartItems.push({
        id: Date.now(), // Simple ID generation
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1,
        image: product.image,
        maxQuantity: product.maxQuantity
      });
    }
  }

  // Update item quantity
  updateQuantity(item: CartItem, newQuantity: number) {
    if (newQuantity <= 0) {
      this.removeItem(item);
    } else if (newQuantity <= item.maxQuantity) {
      item.quantity = newQuantity;
    } else {
      item.quantity = item.maxQuantity;
      alert(`Maximum quantity is ${item.maxQuantity}`);
    }
  }

  // Increase quantity
  increaseQuantity(item: CartItem) {
    if (item.quantity < item.maxQuantity) {
      item.quantity++;
    } else {
      alert(`Maximum quantity (${item.maxQuantity}) reached`);
    }
  }

  // Decrease quantity
  decreaseQuantity(item: CartItem) {
    if (item.quantity > 1) {
      item.quantity--;
    } else {
      this.removeItem(item);
    }
  }

  // Remove item from cart
  removeItem(item: CartItem) {
    const index = this.cartItems.indexOf(item);
    if (index > -1) {
      this.cartItems.splice(index, 1);
    }
  }

  // Clear entire cart
  clearCart() {
    if (confirm('Are you sure you want to clear your cart?')) {
      this.cartItems = [];
    }
  }

  // Proceed to checkout
  checkout() {
    console.log('Proceeding to checkout with:', this.cartItems);
    alert('Proceeding to checkout...');
    // In a real app, navigate to checkout page or open checkout modal
  }
}
```

### Currency Value Converter (currency-value-converter.ts)

```typescript
import { valueConverter } from 'aurelia';

@valueConverter('currency')
export class CurrencyValueConverter {
  toView(value: number, currencyCode = 'USD'): string {
    if (value == null) return '';

    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currencyCode
    }).format(value);
  }
}
```

### Template (shopping-cart.html)

```html
<import from="./currency-value-converter"></import>

<div class="shopping-cart">
  <!-- Cart Header -->
  <header class="cart-header">
    <h1>
      Shopping Cart
      <span class="item-count" if.bind="cartItems.length">
        (${cartItems.reduce((sum, item) => sum + item.quantity, 0)} ${cartItems.reduce((sum, item) => sum + item.quantity, 0) === 1 ? 'item' : 'items'})
      </span>
    </h1>
    <button
      if.bind="cartItems.length"
      click.trigger="clearCart()"
      class="clear-btn">
      Clear Cart
    </button>
  </header>

  <!-- Empty Cart State -->
  <div if.bind="!cartItems.length" class="empty-cart">
    <div class="empty-icon">🛒</div>
    <h2>Your cart is empty</h2>
    <p>Add some products to get started!</p>
  </div>

  <!-- Cart Items -->
  <div else class="cart-content">
    <!-- Cart Items List -->
    <div class="cart-items">
      <div
        repeat.for="item of cartItems; key: id"
        class="cart-item">

        <!-- Product Image -->
        <div class="item-image">
          <img src.bind="item.image" alt.bind="item.name">
        </div>

        <!-- Product Details -->
        <div class="item-details">
          <h3 class="item-name">${item.name}</h3>
          <p class="item-price">${item.price | currency:'USD'} each</p>

          <!-- Quantity Controls -->
          <div class="quantity-controls">
            <button
              click.trigger="decreaseQuantity(item)"
              class="qty-btn"
              title="Decrease quantity">
              −
            </button>

            <input
              type="number"
              value.bind="item.quantity"
              min="1"
              max.bind="item.maxQuantity"
              change.trigger="updateQuantity(item, item.quantity)"
              class="qty-input">

            <button
              click.trigger="increaseQuantity(item)"
              class="qty-btn"
              disabled.bind="item.quantity >= item.maxQuantity"
              title="Increase quantity">
              +
            </button>

            <span class="max-qty-label" if.bind="item.quantity >= item.maxQuantity">
              (max)
            </span>
          </div>
        </div>

        <!-- Item Total and Remove -->
        <div class="item-actions">
          <div class="item-total">
            ${(item.price * item.quantity) | currency:'USD'}
          </div>
          <button
            click.trigger="removeItem(item)"
            class="remove-btn"
            title="Remove item">
            ×
          </button>
        </div>
      </div>
    </div>

    <!-- Cart Summary -->
    <div class="cart-summary">
      <h2>Order Summary</h2>

      <div class="summary-row">
        <span>Subtotal:</span>
        <span>${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) | currency:'USD'}</span>
      </div>

      <div class="summary-row">
        <span>Tax (8%):</span>
        <span>${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) * 0.08 | currency:'USD'}</span>
      </div>

      <div class="summary-row">
        <span>Shipping:</span>
        <span>
          ${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) >= 50
            ? 'FREE'
            : (5.99 | currency:'USD')}
        </span>
      </div>

      <div if.bind="cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) < 50 && cartItems.length" class="shipping-notice">
        <small>💡 Add ${(50 - cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)) | currency:'USD'} more for free shipping!</small>
      </div>

      <hr class="summary-divider">

      <div class="summary-row total-row">
        <strong>Total:</strong>
        <strong class="total-amount">
          ${(cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) +
             cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) * 0.08 +
             (cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) >= 50 ? 0 : 5.99)) | currency:'USD'}
        </strong>
      </div>

      <button
        click.trigger="checkout()"
        disabled.bind="!cartItems.length"
        class="checkout-btn">
        Proceed to Checkout
      </button>
    </div>
  </div>
</div>
```

### Styles (shopping-cart.css)

```css
.shopping-cart {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.cart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.cart-header h1 {
  font-size: 2rem;
  color: #333;
  margin: 0;
}

.item-count {
  font-size: 1.2rem;
  color: #666;
  font-weight: normal;
}

.clear-btn {
  padding: 0.5rem 1rem;
  background: #fff;
  border: 1px solid #dc3545;
  color: #dc3545;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.clear-btn:hover {
  background: #dc3545;
  color: white;
}

.empty-cart {
  text-align: center;
  padding: 4rem 2rem;
}

.empty-icon {
  font-size: 5rem;
  margin-bottom: 1rem;
  opacity: 0.5;
}

.empty-cart h2 {
  color: #333;
  margin-bottom: 0.5rem;
}

.empty-cart p {
  color: #666;
}

.cart-content {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
}

.cart-items {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.cart-item {
  display: grid;
  grid-template-columns: 100px 1fr auto;
  gap: 1rem;
  padding: 1rem;
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  transition: box-shadow 0.2s;
}

.cart-item:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.item-image {
  width: 100px;
  height: 100px;
  overflow: hidden;
  border-radius: 4px;
  background: #f5f5f5;
}

.item-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.item-details {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.item-name {
  margin: 0;
  font-size: 1.1rem;
  color: #333;
}

.item-price {
  margin: 0;
  color: #666;
  font-size: 0.9rem;
}

.quantity-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.qty-btn {
  width: 32px;
  height: 32px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.qty-btn:hover:not(:disabled) {
  background: #f0f0f0;
  border-color: #007bff;
}

.qty-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.qty-input {
  width: 60px;
  height: 32px;
  border: 1px solid #ddd;
  border-radius: 4px;
  text-align: center;
  font-size: 1rem;
}

.max-qty-label {
  font-size: 0.85rem;
  color: #666;
}

.item-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
}

.item-total {
  font-size: 1.2rem;
  font-weight: 600;
  color: #007bff;
}

.remove-btn {
  width: 32px;
  height: 32px;
  border: 1px solid #dc3545;
  background: white;
  color: #dc3545;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  transition: all 0.2s;
}

.remove-btn:hover {
  background: #dc3545;
  color: white;
}

.cart-summary {
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 1.5rem;
  height: fit-content;
  position: sticky;
  top: 2rem;
}

.cart-summary h2 {
  margin: 0 0 1rem 0;
  font-size: 1.3rem;
  color: #333;
}

.summary-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.75rem;
  color: #666;
}

.shipping-notice {
  background: #e3f2fd;
  padding: 0.5rem;
  border-radius: 4px;
  margin: 0.5rem 0;
  text-align: center;
}

.shipping-notice small {
  color: #1976d2;
}

.summary-divider {
  border: none;
  border-top: 1px solid #e0e0e0;
  margin: 1rem 0;
}

.total-row {
  font-size: 1.2rem;
  color: #333;
  margin-bottom: 1.5rem;
}

.total-amount {
  color: #007bff;
  font-size: 1.5rem;
}

.checkout-btn {
  width: 100%;
  padding: 1rem;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1.1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.checkout-btn:hover:not(:disabled) {
  background: #218838;
}

.checkout-btn:disabled {
  background: #6c757d;
  cursor: not-allowed;
  opacity: 0.6;
}

@media (max-width: 768px) {
  .cart-content {
    grid-template-columns: 1fr;
  }

  .cart-item {
    grid-template-columns: 80px 1fr;
    gap: 0.75rem;
  }

  .item-actions {
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
  }

  .cart-summary {
    position: static;
  }
}
```

## How It Works

### 1. Lambda Expressions in Templates

Instead of computed properties in the view model, calculations are done directly in the template using lambda expressions:

```html
<!-- Item count using reduce -->
${cartItems.reduce((sum, item) => sum + item.quantity, 0)}

<!-- Subtotal calculation -->
${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) | currency:'USD'}

<!-- Conditional logic for free shipping -->
${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) >= 50 ? 'FREE' : (5.99 | currency:'USD')}
```

Aurelia's lambda expressions support complex operations like `reduce`, `filter`, `map`, `every`, and `some` directly in templates. The template automatically tracks dependencies and recalculates when `cartItems` changes.

### 2. Array Manipulation

Using array methods ensures change detection:

```typescript
// ✓ Aurelia detects this
this.cartItems.splice(index, 1);

// ✓ Aurelia detects this
this.cartItems.push(newItem);

// ✗ Aurelia won't detect this
this.cartItems[index] = newItem; // Use splice instead
```

### 3. Benefits of Lambda Expressions

Moving calculations to the template has several advantages:

* **Reduced boilerplate** - No need for getter methods in the view model
* **Clear intent** - Calculations are visible right where they're used
* **Single source of truth** - The template directly expresses what data it needs
* **Automatic reactivity** - Aurelia tracks all dependencies within lambda expressions

This approach is particularly useful for derived data that's only needed in the view.

### 4. Efficient List Updates

Using `key: id` allows Aurelia to track items efficiently:

```html
<div repeat.for="item of cartItems; key: id">
```

When items are removed or reordered, Aurelia reuses DOM elements.

### 5. Quantity Validation

Multiple ways to update quantity with validation:

```typescript
updateQuantity(item: CartItem, newQuantity: number) {
  if (newQuantity <= 0) {
    this.removeItem(item);
  } else if (newQuantity <= item.maxQuantity) {
    item.quantity = newQuantity;
  } else {
    item.quantity = item.maxQuantity;
    alert(`Maximum quantity is ${item.maxQuantity}`);
  }
}
```

### 6. Conditional Rendering

Show different UI based on cart state:

```html
<div if.bind="!cartItems.length" class="empty-cart">
  <!-- Empty state -->
</div>

<div else class="cart-content">
  <!-- Cart items and summary -->
</div>
```

Lambda expressions work seamlessly with conditionals: `if.bind="cartItems.length"` or `if.bind="!cartItems.length"`.

### 7. Currency Formatting with Value Converter

The currency value converter formats prices consistently:

```html
${product.price | currency:'USD'}
```

The converter uses `Intl.NumberFormat` for proper currency formatting including the currency symbol, decimal places, and thousands separators. This keeps formatting logic out of the view model.

### 8. When to Use Lambda Expressions vs Computed Properties

**Use lambda expressions in templates when:**

* The calculation is only needed in the view
* The logic is straightforward and readable inline
* You want to reduce view model boilerplate

**Use computed properties in the view model when:**

* The calculation is complex and would make the template hard to read
* The value is used in multiple places (template and view model logic)
* You need to unit test the calculation logic
* The calculation is expensive and you want explicit memoization

For this shopping cart example, the calculations are simple arithmetic operations that are only displayed to the user, making lambda expressions a great fit. They eliminate boilerplate while keeping the template clear and maintainable.

## Variations

### Persist Cart to LocalStorage

```typescript
export class ShoppingCart {
  cartItems: CartItem[] = [];

  constructor() {
    this.loadCart();
  }

  private loadCart() {
    const saved = localStorage.getItem('cart');
    if (saved) {
      this.cartItems = JSON.parse(saved);
    }
  }

  private saveCart() {
    localStorage.setItem('cart', JSON.stringify(this.cartItems));
  }

  addToCart(product: any) {
    // ... existing logic
    this.saveCart();
  }

  removeItem(item: CartItem) {
    // ... existing logic
    this.saveCart();
  }
}
```

### Add Discount Codes

```typescript
export class ShoppingCart {
  discountCode = '';
  discountPercentage = 0;

  applyDiscount() {
    const codes: Record<string, number> = {
      'SAVE10': 10,
      'SAVE20': 20,
      'FREESHIP': 0 // Handle free shipping separately
    };

    if (codes[this.discountCode.toUpperCase()]) {
      this.discountPercentage = codes[this.discountCode.toUpperCase()];
    } else {
      alert('Invalid discount code');
    }
  }
}
```

```html
<div class="discount-section">
  <input value.bind="discountCode" placeholder="Enter discount code">
  <button click.trigger="applyDiscount()">Apply</button>
</div>

<!-- In the summary, calculate discount using lambda -->
<div class="summary-row" if.bind="discountPercentage > 0">
  <span>Discount (${discountPercentage}%):</span>
  <span>-${cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) * (discountPercentage / 100) | currency:'USD'}</span>
</div>

<!-- Update total to include discount -->
<div class="summary-row total-row">
  <strong>Total:</strong>
  <strong class="total-amount">
    ${(cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) * (1 - discountPercentage / 100) +
       cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) * 0.08 +
       (cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) >= 50 ? 0 : 5.99)) | currency:'USD'}
  </strong>
</div>
```

### Cart as a Service

Make the cart available throughout the app:

```typescript
// cart.service.ts
import { DI } from 'aurelia';

export const ICartService = DI.createInterface<ICartService>(
  'ICartService',
  x => x.singleton(CartService)
);

export interface ICartService extends CartService {}

export class CartService {
  cartItems: CartItem[] = [];

  // ... all cart methods
}

// Use in components
import { resolve } from 'aurelia';
import { ICartService } from './cart.service';

export class ProductList {
  private readonly cart = resolve(ICartService);

  addToCart(product: Product) {
    this.cart.addToCart(product);
  }
}
```

## Related

* [Product Catalog Recipe](/templates/real-world-recipes/product-catalog)
* [Lambda Expressions Guide](/templates/lambda-expressions)
* [List Rendering Guide](/templates/repeats-and-list-rendering)
* [Computed Properties](/introduction/essentials/reactivity)
* [Value Converters](/templates/value-converters)


# Data Table

A complete, production-ready data table with sorting, filtering, pagination, row selection, and responsive design.

## Features Demonstrated

* **Two-way data binding** - Search input, filters, page size
* **Computed properties** - Filtered, sorted, and paginated data
* **`repeat.for` with keys** - Efficient list rendering with tracking
* **Event handling** - Sort, filter, pagination clicks
* **Conditional rendering** - Empty states, loading states
* **Value converters** - Date and number formatting
* **CSS class binding** - Active sort, selected rows
* **Debouncing** - Optimize search performance

## Code

### View Model (data-table.ts)

```typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive' | 'pending';
  lastLogin: Date;
  tasksCompleted: number;
}

type SortColumn = 'name' | 'email' | 'role' | 'status' | 'lastLogin' | 'tasksCompleted';
type SortDirection = 'asc' | 'desc';

export class DataTable {
  // Raw data (would normally come from API)
  private allUsers: User[] = [
    {
      id: 1,
      name: 'Alice Johnson',
      email: 'alice@example.com',
      role: 'Admin',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 127
    },
    {
      id: 2,
      name: 'Bob Smith',
      email: 'bob@example.com',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 89
    },
    {
      id: 3,
      name: 'Carol Williams',
      email: 'carol@example.com',
      role: 'Manager',
      status: 'inactive',
      lastLogin: new Date('2024-12-15'),
      tasksCompleted: 203
    },
    {
      id: 4,
      name: 'David Brown',
      email: 'david@example.com',
      role: 'User',
      status: 'pending',
      lastLogin: new Date('2025-01-07'),
      tasksCompleted: 45
    },
    {
      id: 5,
      name: 'Eve Davis',
      email: 'eve@example.com',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 156
    },
    // Add more sample data...
    {
      id: 6,
      name: 'Frank Miller',
      email: 'frank@example.com',
      role: 'Admin',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 312
    },
    {
      id: 7,
      name: 'Grace Wilson',
      email: 'grace@example.com',
      role: 'Manager',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 178
    },
    {
      id: 8,
      name: 'Henry Moore',
      email: 'henry@example.com',
      role: 'User',
      status: 'inactive',
      lastLogin: new Date('2024-11-20'),
      tasksCompleted: 67
    },
    {
      id: 9,
      name: 'Iris Taylor',
      email: 'iris@example.com',
      role: 'User',
      status: 'active',
      lastLogin: new Date('2025-01-09'),
      tasksCompleted: 234
    },
    {
      id: 10,
      name: 'Jack Anderson',
      email: 'jack@example.com',
      role: 'Manager',
      status: 'active',
      lastLogin: new Date('2025-01-08'),
      tasksCompleted: 189
    }
  ];

  // Filter state
  searchQuery = '';
  selectedRole: string = 'all';
  selectedStatus: string = 'all';

  // Sort state
  sortColumn: SortColumn = 'name';
  sortDirection: SortDirection = 'asc';

  // Pagination state
  currentPage = 1;
  pageSize = 5;

  // Selection state
  selectedRows = new Set<number>();

  // Loading state
  isLoading = false;

  // Computed: Filtered data
  get filteredUsers(): User[] {
    return this.allUsers.filter(user => {
      // Search filter
      const query = this.searchQuery.toLowerCase();
      const matchesSearch = !query ||
        user.name.toLowerCase().includes(query) ||
        user.email.toLowerCase().includes(query);

      // Role filter
      const matchesRole = this.selectedRole === 'all' ||
        user.role === this.selectedRole;

      // Status filter
      const matchesStatus = this.selectedStatus === 'all' ||
        user.status === this.selectedStatus;

      return matchesSearch && matchesRole && matchesStatus;
    });
  }

  // Computed: Sorted data
  get sortedUsers(): User[] {
    const sorted = [...this.filteredUsers];

    sorted.sort((a, b) => {
      let aVal: any = a[this.sortColumn];
      let bVal: any = b[this.sortColumn];

      // Handle dates
      if (aVal instanceof Date) {
        aVal = aVal.getTime();
        bVal = (bVal as Date).getTime();
      }

      // Handle strings (case-insensitive)
      if (typeof aVal === 'string') {
        aVal = aVal.toLowerCase();
        bVal = bVal.toLowerCase();
      }

      if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
      if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
      return 0;
    });

    return sorted;
  }

  // Computed: Paginated data
  get paginatedUsers(): User[] {
    const start = (this.currentPage - 1) * this.pageSize;
    const end = start + this.pageSize;
    return this.sortedUsers.slice(start, end);
  }

  // Computed: Pagination info
  get totalPages(): number {
    return Math.ceil(this.sortedUsers.length / this.pageSize);
  }

  get totalResults(): number {
    return this.sortedUsers.length;
  }

  get startResult(): number {
    if (this.totalResults === 0) return 0;
    return (this.currentPage - 1) * this.pageSize + 1;
  }

  get endResult(): number {
    return Math.min(this.currentPage * this.pageSize, this.totalResults);
  }

  pageSizeChanged(newValue: number | string) {
    const numeric = typeof newValue === 'string' ? Number(newValue) : newValue;
    if (typeof numeric === 'number' && !Number.isNaN(numeric) && numeric !== this.pageSize) {
      this.pageSize = numeric;
      return;
    }
    this.currentPage = 1;
  }

  get pages(): number[] {
    const pages: number[] = [];
    const maxVisible = 5;
    const half = Math.floor(maxVisible / 2);

    let start = Math.max(1, this.currentPage - half);
    let end = Math.min(this.totalPages, start + maxVisible - 1);

    // Adjust start if we're near the end
    if (end - start < maxVisible - 1) {
      start = Math.max(1, end - maxVisible + 1);
    }

    for (let i = start; i <= end; i++) {
      pages.push(i);
    }

    return pages;
  }

  // Computed: Selection state
  get allPageSelected(): boolean {
    if (this.paginatedUsers.length === 0) return false;
    return this.paginatedUsers.every(user => this.selectedRows.has(user.id));
  }

  get somePageSelected(): boolean {
    if (this.paginatedUsers.length === 0) return false;
    return this.paginatedUsers.some(user => this.selectedRows.has(user.id)) &&
      !this.allPageSelected;
  }

  // Actions
  sort(column: SortColumn) {
    if (this.sortColumn === column) {
      // Toggle direction
      this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
    } else {
      // New column, default to ascending
      this.sortColumn = column;
      this.sortDirection = 'asc';
    }
  }

  goToPage(page: number) {
    if (page < 1 || page > this.totalPages) return;
    this.currentPage = page;
  }

  nextPage() {
    this.goToPage(this.currentPage + 1);
  }

  previousPage() {
    this.goToPage(this.currentPage - 1);
  }

  toggleAllPageSelection() {
    if (this.allPageSelected) {
      // Deselect all on page
      this.paginatedUsers.forEach(user => this.selectedRows.delete(user.id));
    } else {
      // Select all on page
      this.paginatedUsers.forEach(user => this.selectedRows.add(user.id));
    }
  }

  clearSelection() {
    this.selectedRows.clear();
  }

  deleteSelected() {
    if (this.selectedRows.size === 0) return;

    const confirmed = confirm(`Delete ${this.selectedRows.size} user(s)?`);
    if (!confirmed) return;

    // Remove selected users
    this.allUsers = this.allUsers.filter(user => !this.selectedRows.has(user.id));

    // Clear selection
    this.selectedRows.clear();

    // Adjust page if needed
    if (this.currentPage > this.totalPages && this.totalPages > 0) {
      this.currentPage = this.totalPages;
    }
  }

  // Reset filters
  resetFilters() {
    this.searchQuery = '';
    this.selectedRole = 'all';
    this.selectedStatus = 'all';
    this.currentPage = 1;
  }

  // Watch for filter changes and reset to page 1
  searchQueryChanged() {
    this.currentPage = 1;
  }

  selectedRoleChanged() {
    this.currentPage = 1;
  }

  selectedStatusChanged() {
    this.currentPage = 1;
  }
}
```

### Template (data-table.html)

```html
<div class="data-table">
  <!-- Header with filters -->
  <div class="table-header">
      <h2>Users</h2>

      <div class="table-actions">
        <button
          type="button"
          click.trigger="deleteSelected()"
          disabled.bind="selectedRows.size === 0"
          class="btn btn-danger">
          Delete Selected (${selectedRows.size})
        </button>
      </div>
    </div>

    <!-- Filters -->
    <div class="table-filters">
      <div class="filter-group">
        <label for="search">Search</label>
        <input
          type="text"
          id="search"
          value.bind="searchQuery & debounce:300"
          placeholder="Search by name or email...">
      </div>

      <div class="filter-group">
        <label for="role">Role</label>
        <select id="role" value.bind="selectedRole">
          <option value="all">All Roles</option>
          <option value="Admin">Admin</option>
          <option value="Manager">Manager</option>
          <option value="User">User</option>
        </select>
      </div>

      <div class="filter-group">
        <label for="status">Status</label>
        <select id="status" value.bind="selectedStatus">
          <option value="all">All Statuses</option>
          <option value="active">Active</option>
          <option value="inactive">Inactive</option>
          <option value="pending">Pending</option>
        </select>
      </div>

      <div class="filter-group">
        <label for="pageSize">Per Page</label>
        <select id="pageSize" value.bind="pageSize">
          <option value="5">5</option>
          <option value="10">10</option>
          <option value="25">25</option>
          <option value="50">50</option>
        </select>
      </div>

      <button
        type="button"
        click.trigger="resetFilters()"
        class="btn btn-secondary">
        Reset Filters
      </button>
    </div>

    <!-- Results summary -->
    <div class="table-summary">
      Showing ${startResult}-${endResult} of ${totalResults} users
      <span if.bind="selectedRows.size > 0">
        (${selectedRows.size} selected)
      </span>
    </div>

    <!-- Data Table -->
    <div class="table-wrapper">
      <table class="table">
        <thead>
          <tr>
            <th class="col-checkbox">
              <input
                type="checkbox"
                checked.bind="allPageSelected"
                indeterminate.bind="somePageSelected"
                change.trigger="toggleAllPageSelection()"
                aria-label="Select all on page">
            </th>
            <th
              click.trigger="sort('name')"
              class="sortable ${sortColumn === 'name' ? 'sorted' : ''}">
              Name
              <span class="sort-icon" if.bind="sortColumn === 'name'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
            <th
              click.trigger="sort('email')"
              class="sortable ${sortColumn === 'email' ? 'sorted' : ''}">
              Email
              <span class="sort-icon" if.bind="sortColumn === 'email'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
            <th
              click.trigger="sort('role')"
              class="sortable ${sortColumn === 'role' ? 'sorted' : ''}">
              Role
              <span class="sort-icon" if.bind="sortColumn === 'role'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
            <th
              click.trigger="sort('status')"
              class="sortable ${sortColumn === 'status' ? 'sorted' : ''}">
              Status
              <span class="sort-icon" if.bind="sortColumn === 'status'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
            <th
              click.trigger="sort('lastLogin')"
              class="sortable ${sortColumn === 'lastLogin' ? 'sorted' : ''}">
              Last Login
              <span class="sort-icon" if.bind="sortColumn === 'lastLogin'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
            <th
              click.trigger="sort('tasksCompleted')"
              class="sortable ${sortColumn === 'tasksCompleted' ? 'sorted' : ''} col-number">
              Tasks
              <span class="sort-icon" if.bind="sortColumn === 'tasksCompleted'">
                ${sortDirection === 'asc' ? '↑' : '↓'}
              </span>
            </th>
          </tr>
        </thead>
        <tbody>
          <tr
            repeat.for="user of paginatedUsers"
            class="${selectedRows.has(user.id) ? 'selected' : ''}">
            <td class="col-checkbox">
              <input
                type="checkbox"
                model.bind="user.id"
                checked.bind="selectedRows"
                aria-label="Select ${user.name}">
            </td>
            <td>${user.name}</td>
            <td>${user.email}</td>
            <td>
              <span class="badge badge-${user.role.toLowerCase()}">
                ${user.role}
              </span>
            </td>
            <td>
              <span class="status-${user.status}">
                ${user.status}
              </span>
            </td>
            <td>${user.lastLogin | dateFormat:'MMM d, yyyy'}</td>
            <td class="col-number">${user.tasksCompleted}</td>
          </tr>
        </tbody>
      </table>

      <!-- Empty state -->
      <div if.bind="paginatedUsers.length === 0" class="empty-state">
        <p>No users found</p>
        <button
          type="button"
          click.trigger="resetFilters()"
          class="btn btn-primary">
          Clear Filters
        </button>
      </div>
    </div>

    <!-- Pagination -->
    <div if.bind="totalPages > 1" class="table-pagination">
      <button
        type="button"
        click.trigger="previousPage()"
        disabled.bind="currentPage === 1"
        class="btn btn-secondary"
        aria-label="Previous page">
        ← Previous
      </button>

      <div class="pagination-pages">
        <button
          if.bind="pages[0] > 1"
          type="button"
          click.trigger="goToPage(1)"
          class="btn btn-page">
          1
        </button>
        <span if.bind="pages[0] > 2" class="pagination-ellipsis">...</span>

        <button
          repeat.for="page of pages"
          type="button"
          click.trigger="goToPage(page)"
          class="btn btn-page ${page === currentPage ? 'active' : ''}"
          aria-label="Page ${page}"
          aria-current="${page === currentPage ? 'page' : undefined}">
          ${page}
        </button>

        <span if.bind="pages[pages.length - 1] < totalPages - 1" class="pagination-ellipsis">...</span>
        <button
          if.bind="pages[pages.length - 1] < totalPages"
          type="button"
          click.trigger="goToPage(totalPages)"
          class="btn btn-page">
          ${totalPages}
        </button>
      </div>

      <button
        type="button"
        click.trigger="nextPage()"
        disabled.bind="currentPage === totalPages"
        class="btn btn-secondary"
        aria-label="Next page">
        Next →
      </button>
    </div>
  </div>
```

### Styles (data-table.css)

```css
.data-table {
  width: 100%;
}

.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
}

.table-filters {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
  align-items: flex-end;
}

.filter-group {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.filter-group label {
  font-size: 0.875rem;
  font-weight: 500;
}

.table-summary {
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
  color: #666;
}

.table-wrapper {
  overflow-x: auto;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}

.table {
  width: 100%;
  border-collapse: collapse;
}

.table thead {
  background-color: #f5f5f5;
}

.table th,
.table td {
  padding: 0.75rem 1rem;
  text-align: left;
  border-bottom: 1px solid #e0e0e0;
}

.table th.sortable {
  cursor: pointer;
  user-select: none;
}

.table th.sortable:hover {
  background-color: #e8e8e8;
}

.table th.sorted {
  background-color: #e3f2fd;
}

.sort-icon {
  margin-left: 0.25rem;
  font-size: 0.75rem;
}

.col-checkbox {
  width: 40px;
  text-align: center;
}

.col-number {
  text-align: right;
}

.table tbody tr:hover {
  background-color: #f9f9f9;
}

.table tbody tr.selected {
  background-color: #e3f2fd;
}

.badge {
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 500;
}

.badge-admin {
  background-color: #ff5722;
  color: white;
}

.badge-manager {
  background-color: #2196f3;
  color: white;
}

.badge-user {
  background-color: #4caf50;
  color: white;
}

.status-active {
  color: #4caf50;
}

.status-inactive {
  color: #999;
}

.status-pending {
  color: #ff9800;
}

.empty-state {
  text-align: center;
  padding: 3rem;
  color: #999;
}

.table-pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 1rem;
  gap: 1rem;
}

.pagination-pages {
  display: flex;
  gap: 0.25rem;
}

.btn-page {
  min-width: 40px;
  padding: 0.5rem;
}

.btn-page.active {
  background-color: #2196f3;
  color: white;
}

.pagination-ellipsis {
  padding: 0.5rem;
  color: #999;
}

/* Responsive */
@media (max-width: 768px) {
  .table-filters {
    flex-direction: column;
    align-items: stretch;
  }

  .table-pagination {
    flex-direction: column;
  }

  .table {
    font-size: 0.875rem;
  }

  .table th,
  .table td {
    padding: 0.5rem;
  }
}
```

## How It Works

### Filtering Pipeline

Data flows through a pipeline:

1. **Raw data** (`allUsers`) → all records
2. **Filtered** (`filteredUsers`) → apply search and dropdown filters
3. **Sorted** (`sortedUsers`) → apply column sorting
4. **Paginated** (`paginatedUsers`) → slice for current page

Each computed property builds on the previous one, keeping the logic clean and testable.

### Sorting

Click column headers to sort. The first click sorts ascending, the second descending, and subsequent clicks toggle between the two. The active sort column is highlighted.

### Pagination

Smart pagination shows up to 5 page numbers with ellipsis for gaps. Always shows first and last pages. Automatically adjusts when filters reduce total pages.

### Selection

* Checkbox in header selects/deselects all rows on current page
* Individual row checkboxes for granular selection
* Selected rows track across pages
* Delete selected button removes all selected users

### Performance

* **Debounced search** (300ms) prevents excessive filtering
* **Keyed repeat** ensures efficient DOM updates
* **Computed properties** cache results until dependencies change

## Variations

### Server-Side Pagination

For large datasets, move filtering/sorting to the server:

```typescript
async loadUsers() {
  this.isLoading = true;

  const params = new URLSearchParams({
    page: this.currentPage.toString(),
    pageSize: this.pageSize.toString(),
    search: this.searchQuery,
    role: this.selectedRole,
    status: this.selectedStatus,
    sortColumn: this.sortColumn,
    sortDirection: this.sortDirection
  });

  try {
    const response = await fetch(`/api/users?${params}`);
    const data = await response.json();

    this.allUsers = data.users;
    this.totalResults = data.total; // Server provides total count
  } finally {
    this.isLoading = false;
  }
}
```

### Inline Editing

Add edit mode for quick updates:

```typescript
editingRow: number | null = null;

startEdit(userId: number) {
  this.editingRow = userId;
}

async saveEdit(user: User) {
  await fetch(`/api/users/${user.id}`, {
    method: 'PUT',
    body: JSON.stringify(user)
  });

  this.editingRow = null;
}

cancelEdit() {
  this.editingRow = null;
  // Restore original data
}
```

### Column Visibility Toggle

Let users show/hide columns:

```typescript
visibleColumns = {
  name: true,
  email: true,
  role: true,
  status: true,
  lastLogin: true,
  tasksCompleted: true
};
```

```html
<th if.bind="visibleColumns.email">Email</th>
```

## Related

* [Product Catalog](/templates/real-world-recipes/product-catalog) - Another filtering/sorting example
* [List Rendering](/templates/repeats-and-list-rendering) - `repeat.for` documentation
* [Conditional Rendering](/templates/conditional-rendering) - `if.bind` and `show.bind`
* [Value Converters](/templates/value-converters) - Date/number formatting


# Notification System

A complete notification system with auto-dismiss, multiple types, animations, and queue management.

## Features Demonstrated

* **Dependency Injection** - Singleton service pattern
* **Event Aggregator** - Global notification triggering
* **Animations** - CSS transitions for enter/leave
* **Timers** - Auto-dismiss with setTimeout
* **Array manipulation** - Add/remove notifications
* **Dynamic CSS classes** - Type-based styling
* **Conditional rendering** - Show/hide based on array length

## Code

### Service (notification-service.ts)

```typescript
// src/services/notification-service.ts
import { DI } from '@aurelia/kernel';

export interface Notification {
  id: string;
  type: 'success' | 'error' | 'warning' | 'info';
  title: string;
  message: string;
  duration: number; // milliseconds, 0 = no auto-dismiss
  dismissible: boolean;
  timestamp: Date;
  expiresAt?: number;
  remaining?: number;
}

export const INotificationService = DI.createInterface<INotificationService>(
  'INotificationService',
  x => x.singleton(NotificationService)
);

export interface INotificationService {
  readonly notifications: Notification[];
  show(options: Partial<Notification>): string;
  success(title: string, message: string, duration?: number): string;
  error(title: string, message: string, duration?: number): string;
  warning(title: string, message: string, duration?: number): string;
  info(title: string, message: string, duration?: number): string;
  dismiss(id: string): void;
  clear(): void;
}

class NotificationService implements INotificationService {
  notifications: Notification[] = [];
  private nextId = 1;
  private timers = new Map<string, number>();
  private progressTimers = new Map<string, number>();

  show(options: Partial<Notification>): string {
    const notification: Notification = {
      id: `notification-${this.nextId++}`,
      type: options.type || 'info',
      title: options.title || '',
      message: options.message || '',
      duration: options.duration !== undefined ? options.duration : 5000,
      dismissible: options.dismissible !== undefined ? options.dismissible : true,
      timestamp: new Date(),
      remaining: options.duration ?? 5000,
      expiresAt: options.duration ? Date.now() + options.duration : undefined
    };

    // Add to beginning of array (newest first)
    this.notifications.unshift(notification);

    // Auto-dismiss if duration > 0
    if (notification.duration > 0) {
      const timer = window.setTimeout(() => {
        this.dismiss(notification.id);
      }, notification.duration);

      this.timers.set(notification.id, timer);

      const progress = window.setInterval(() => {
        if (!notification.expiresAt) return;
        const remaining = Math.max(notification.expiresAt - Date.now(), 0);
        notification.remaining = remaining;
        if (remaining <= 0) {
          window.clearInterval(progress);
          this.progressTimers.delete(notification.id);
        }
      }, 100);
      this.progressTimers.set(notification.id, progress);
    }

    return notification.id;
  }

  success(title: string, message: string, duration = 5000): string {
    return this.show({ type: 'success', title, message, duration });
  }

  error(title: string, message: string, duration = 0): string {
    // Errors don't auto-dismiss by default
    return this.show({ type: 'error', title, message, duration });
  }

  warning(title: string, message: string, duration = 7000): string {
    return this.show({ type: 'warning', title, message, duration });
  }

  info(title: string, message: string, duration = 5000): string {
    return this.show({ type: 'info', title, message, duration });
  }

  dismiss(id: string): void {
    // Clear timer if exists
    const timer = this.timers.get(id);
    if (timer) {
      clearTimeout(timer);
      this.timers.delete(id);
    }
    const progress = this.progressTimers.get(id);
    if (progress) {
      clearInterval(progress);
      this.progressTimers.delete(id);
    }

    // Remove notification
    const index = this.notifications.findIndex(n => n.id === id);
    if (index !== -1) {
      this.notifications.splice(index, 1);
    }
  }

  clear(): void {
    // Clear all timers
    this.timers.forEach(timer => clearTimeout(timer));
    this.timers.clear();
    this.progressTimers.forEach(interval => clearInterval(interval));
    this.progressTimers.clear();

    // Clear all notifications
    this.notifications = [];
  }
}
```

### Component (notification-container.ts)

```typescript
// src/components/notification-container.ts
import { resolve } from '@aurelia/kernel';
import { INotificationService } from '../services/notification-service';

export class NotificationContainer {
  private notificationService = resolve(INotificationService);

  get notifications() {
    return this.notificationService.notifications;
  }

  dismiss(id: string) {
    this.notificationService.dismiss(id);
  }

  getIcon(type: string): string {
    switch (type) {
      case 'success': return '✓';
      case 'error': return '✕';
      case 'warning': return '⚠';
      case 'info': return 'ⓘ';
      default: return '';
    }
  }

  getProgressWidth(notification: any): number {
    if (notification.duration === 0) return 0;
    const remaining = notification.remaining ?? notification.duration;
    return Math.max((remaining / notification.duration) * 100, 0);
  }
}
```

### Template (notification-container.html)

```html
<!-- src/components/notification-container.html -->
<div class="notification-container">
  <div
    repeat.for="notification of notifications"
    class="notification notification-${notification.type}">

      <div class="notification-icon">
        ${getIcon(notification.type)}
      </div>

      <div class="notification-content">
        <div class="notification-title">${notification.title}</div>
        <div class="notification-message">${notification.message}</div>

        <!-- Progress bar for auto-dismiss -->
        <div
          if.bind="notification.duration > 0"
          class="notification-progress">
          <div
            class="notification-progress-bar"
            style.width.bind="getProgressWidth(notification) + '%'">
          </div>
        </div>
      </div>

      <button
        if.bind="notification.dismissible"
        type="button"
        click.trigger="dismiss(notification.id)"
        class="notification-close"
        aria-label="Dismiss notification">
        ×
      </button>
    </div>
  </div>
```

### Styles (notification-container.css)

```css
.notification-container {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-width: 400px;
  width: calc(100% - 2rem);
}

.notification {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: 8px;
  background: white;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  animation: slideIn 0.3s ease-out;
  position: relative;
  overflow: hidden;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.notification-icon {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  font-size: 16px;
  flex-shrink: 0;
}

.notification-success {
  border-left: 4px solid #4caf50;
}

.notification-success .notification-icon {
  background-color: #4caf50;
  color: white;
}

.notification-error {
  border-left: 4px solid #f44336;
}

.notification-error .notification-icon {
  background-color: #f44336;
  color: white;
}

.notification-warning {
  border-left: 4px solid #ff9800;
}

.notification-warning .notification-icon {
  background-color: #ff9800;
  color: white;
}

.notification-info {
  border-left: 4px solid #2196f3;
}

.notification-info .notification-icon {
  background-color: #2196f3;
  color: white;
}

.notification-content {
  flex-grow: 1;
}

.notification-title {
  font-weight: 600;
  margin-bottom: 0.25rem;
  color: #333;
}

.notification-message {
  font-size: 0.875rem;
  color: #666;
  line-height: 1.4;
}

.notification-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #999;
  padding: 0;
  width: 24px;
  height: 24px;
  line-height: 1;
  flex-shrink: 0;
}

.notification-close:hover {
  color: #333;
}

.notification-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 4px;
  background-color: rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.notification-progress-bar {
  height: 100%;
  background-color: currentColor;
  transition: width 0.1s linear;
}

.notification-success .notification-progress-bar {
  background-color: #4caf50;
}

.notification-error .notification-progress-bar {
  background-color: #f44336;
}

.notification-warning .notification-progress-bar {
  background-color: #ff9800;
}

.notification-info .notification-progress-bar {
  background-color: #2196f3;
}

/* Responsive */
@media (max-width: 640px) {
  .notification-container {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    max-width: 100%;
    width: 100%;
    border-radius: 0;
  }

  .notification {
    border-radius: 0;
    border-left: none;
    border-top: 4px solid;
  }
}
```

### Registration (main.ts)

```typescript
// src/main.ts
import Aurelia from 'aurelia';
import { NotificationContainer } from './components/notification-container';
import { INotificationService } from './services/notification-service';

Aurelia
  .register(NotificationContainer, INotificationService)
  .app(component)
  .start();
```

### Usage in Root Component (my-app.html)

```html
<!-- src/my-app.html -->
<notification-container></notification-container>

<!-- Your app content -->
<au-viewport></au-viewport>
```

### Usage in Any Component

```typescript
// src/pages/dashboard.ts
import { resolve } from '@aurelia/kernel';
import { INotificationService } from '../services/notification-service';

export class Dashboard {
  private notifications = resolve(INotificationService);

  async saveData() {
    try {
      await this.apiClient.save(this.data);

      this.notifications.success(
        'Saved!',
        'Your changes have been saved successfully.'
      );
    } catch (error) {
      this.notifications.error(
        'Error',
        'Failed to save changes. Please try again.',
        0 // Don't auto-dismiss errors
      );
    }
  }

  showWarning() {
    this.notifications.warning(
      'Low Storage',
      'You are running low on storage space.',
      7000
    );
  }

  showInfo() {
    this.notifications.info(
      'Tip',
      'You can use keyboard shortcuts to navigate faster.'
    );
  }
}
```

## How It Works

### Singleton Service Pattern

The `INotificationService` is registered as a singleton, so the same instance is shared across the entire application. Any component can inject it and trigger notifications.

### Auto-Dismiss Timer

When a notification is added with `duration > 0`, a timer is created that automatically dismisses it after the specified time. The timer is stored in a Map so it can be cleared if the user manually dismisses the notification.

### Reactive Array

The `notifications` array is a reactive property. When notifications are added or removed, Aurelia's binding system automatically updates the DOM.

### Progress Bar Animation

The progress bar uses a computed property (`getProgressWidth`) that calculates the percentage remaining based on elapsed time. This creates a smooth countdown animation.

## Variations

### Stacking vs Replacing

Current implementation stacks notifications. For "replacing" behavior (only show one at a time):

```typescript
show(options: Partial<Notification>): string {
  // Clear existing notifications of the same type
  this.notifications = this.notifications.filter(n => n.type !== options.type);

  // ... rest of implementation
}
```

### Position Options

Make position configurable:

```html
<div class="notification-container notification-container-${position}">
```

```css
.notification-container-top-right { top: 1rem; right: 1rem; }
.notification-container-top-left { top: 1rem; left: 1rem; }
.notification-container-bottom-right { bottom: 1rem; right: 1rem; }
.notification-container-bottom-left { bottom: 1rem; left: 1rem; }
```

### Action Buttons

Add action buttons to notifications:

```typescript
export interface NotificationAction {
  label: string;
  callback: () => void | Promise<void>;
}

export interface Notification {
  // ... existing properties
  actions?: NotificationAction[];
}
```

```html
<div if.bind="notification.actions" class="notification-actions">
  <button
    repeat.for="action of notification.actions"
    type="button"
    click.trigger="action.callback()"
    class="btn btn-small">
    ${action.label}
  </button>
</div>
```

### Pause on Hover

Pause the auto-dismiss timer when hovering:

```typescript
pauseTimer(id: string) {
  const timer = this.timers.get(id);
  if (timer) {
    clearTimeout(timer);
  }
}

resumeTimer(notification: Notification) {
  if (notification.duration > 0) {
    const elapsed = Date.now() - notification.timestamp.getTime();
    const remaining = Math.max(0, notification.duration - elapsed);

    const timer = setTimeout(() => {
      this.dismiss(notification.id);
    }, remaining);

    this.timers.set(notification.id, timer);
  }
}
```

```html
<div
  mouseover.trigger="notifications.pauseTimer(notification.id)"
  mouseout.trigger="notifications.resumeTimer(notification)">
  <!-- notification content -->
</div>
```

## Related

* [Dependency Injection](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection) - Singleton services
* [Event Aggregator](/aurelia-packages/event-aggregator) - Alternative global communication
* [Conditional Rendering](/templates/conditional-rendering) - `if.bind` documentation
* [List Rendering](/templates/repeats-and-list-rendering) - `repeat.for` documentation


# Search Autocomplete

A complete autocomplete/typeahead search component with keyboard navigation, highlighting, and debouncing.

## Features Demonstrated

* **Two-way data binding** - Search input
* **Debouncing** - Optimize API calls
* **Computed properties** - Filtered results
* **Keyboard navigation** - Arrow keys, Enter, Escape
* **Focus management** - Keep track of selected item
* **Click outside** - Close dropdown
* **Custom attributes** - Auto-focus
* **Template references** - Access DOM elements
* **Conditional rendering** - Loading states, empty states

## Code

### Component (search-autocomplete.ts)

```typescript
// src/components/search-autocomplete.ts
import { bindable, INode, IPlatform } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';

export interface SearchResult {
  id: string | number;
  title: string;
  description?: string;
  image?: string;
  category?: string;
}

export class SearchAutocomplete {
  @bindable placeholder = 'Search...';
  @bindable minLength = 2;
  @bindable debounceMs = 300;
  @bindable maxResults = 10;
  @bindable onSelect: (result: SearchResult) => void;
  @bindable onSearch: (query: string) => Promise<SearchResult[]>;

  private query = '';
  private results: SearchResult[] = [];
  private isOpen = false;
  private isLoading = false;
  private selectedIndex = -1;
  private searchTimeout: any = null;

  private inputElement?: HTMLInputElement;
  private dropdownElement?: HTMLElement;
  private clickOutsideListener?: (e: MouseEvent) => void;
  private readonly platform = resolve(IPlatform);
  private readonly element = resolve(INode);

  attached() {
    // Listen for clicks outside to close dropdown
    this.clickOutsideListener = (e: MouseEvent) => {
      if (!this.element.contains(e.target as Node)) {
        this.close();
      }
    };

    this.platform.document?.addEventListener('click', this.clickOutsideListener);
  }

  detaching() {
    // Clean up event listener
    if (this.clickOutsideListener) {
      this.platform.document?.removeEventListener('click', this.clickOutsideListener);
    }

    // Clean up timeout
    if (this.searchTimeout) {
      clearTimeout(this.searchTimeout);
    }
  }

  private async performSearch() {
    if (!this.query || this.query.length < this.minLength) {
      this.results = [];
      this.isOpen = false;
      return;
    }

    this.isLoading = true;
    this.isOpen = true;

    try {
      if (this.onSearch) {
        // Use custom search function
        this.results = await this.onSearch(this.query);
      } else {
        // Use default search (for demo purposes)
        this.results = await this.defaultSearch(this.query);
      }

      // Limit results
      this.results = this.results.slice(0, this.maxResults);

      // Reset selection
      this.selectedIndex = -1;
    } catch (error) {
      console.error('Search failed:', error);
      this.results = [];
    } finally {
      this.isLoading = false;
    }
  }

  // Default search implementation (replace with real API)
  private async defaultSearch(query: string): Promise<SearchResult[]> {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 500));

    const mockData: SearchResult[] = [
      { id: 1, title: 'Getting Started with Aurelia', category: 'Tutorial' },
      { id: 2, title: 'Advanced Routing', category: 'Guide' },
      { id: 3, title: 'Dependency Injection', category: 'Concept' },
      { id: 4, title: 'Template Syntax', category: 'Reference' },
      { id: 5, title: 'Validation Plugin', category: 'Plugin' },
    ];

    return mockData.filter(item =>
      item.title.toLowerCase().includes(query.toLowerCase()) ||
      item.category?.toLowerCase().includes(query.toLowerCase())
    );
  }

  queryChanged(newValue: string, oldValue: string) {
    // Clear existing timeout
    if (this.searchTimeout) {
      clearTimeout(this.searchTimeout);
    }

    // Debounce the search
    this.searchTimeout = setTimeout(() => {
      this.performSearch();
    }, this.debounceMs);
  }

  handleKeydown(event: KeyboardEvent) {
    if (!this.isOpen || this.results.length === 0) {
      return;
    }

    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1);
        this.scrollToSelected();
        break;

      case 'ArrowUp':
        event.preventDefault();
        this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
        this.scrollToSelected();
        break;

      case 'Enter':
        event.preventDefault();
        if (this.selectedIndex >= 0) {
          this.selectResult(this.results[this.selectedIndex]);
        }
        break;

      case 'Escape':
        event.preventDefault();
        this.close();
        break;
    }
  }

  private scrollToSelected() {
    if (!this.dropdownElement || this.selectedIndex < 0) {
      return;
    }

    const selectedElement = this.dropdownElement.querySelector(
      `.autocomplete-item[data-index="${this.selectedIndex}"]`
    ) as HTMLElement;

    if (selectedElement) {
      selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
    }
  }

  selectResult(result: SearchResult) {
    if (this.onSelect) {
      this.onSelect(result);
    }

    // Set input to selected title
    this.query = result.title;

    // Close dropdown
    this.close();
  }

  close() {
    this.isOpen = false;
    this.selectedIndex = -1;
  }

  highlightMatch(text: string, query: string): string {
    if (!query) return text;

    const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
  }

  private escapeRegex(str: string): string {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  get showEmpty(): boolean {
    return this.isOpen &&
      !this.isLoading &&
      this.query.length >= this.minLength &&
      this.results.length === 0;
  }
}
```

### Template (search-autocomplete.html)

```html
<!-- src/components/search-autocomplete.html -->
<div class="autocomplete">
  <!-- Search input -->
  <div class="autocomplete-input-wrapper">
      <input
        ref="inputElement"
        type="text"
        value.bind="query"
        keydown.trigger="handleKeydown($event)"
        placeholder.bind="placeholder"
        autocomplete="off"
        role="combobox"
        aria-autocomplete="list"
        aria-expanded.bind="isOpen"
        aria-controls="autocomplete-dropdown"
        aria-activedescendant.bind="selectedIndex >= 0 ? `result-${selectedIndex}` : undefined"
        class="autocomplete-input">

      <!-- Loading spinner -->
      <div if.bind="isLoading" class="autocomplete-spinner">
        <span class="spinner"></span>
      </div>

      <!-- Clear button -->
      <button
        if.bind="query && !isLoading"
        type="button"
        click.trigger="query = ''; close()"
        class="autocomplete-clear"
        aria-label="Clear search">
        ×
      </button>
    </div>

    <!-- Dropdown -->
    <div
      if.bind="isOpen"
      ref="dropdownElement"
      id="autocomplete-dropdown"
      role="listbox"
      class="autocomplete-dropdown">

      <!-- Results -->
      <div
        repeat.for="result of results"
        data-index.bind="$index"
        id="result-${$index}"
        role="option"
        aria-selected.bind="selectedIndex === $index"
        click.trigger="selectResult(result)"
        class="autocomplete-item ${selectedIndex === $index ? 'selected' : ''}">

        <!-- Image (if provided) -->
        <img
          if.bind="result.image"
          src.bind="result.image"
          alt=""
          class="autocomplete-item-image">

        <div class="autocomplete-item-content">
          <div
            class="autocomplete-item-title"
            innerhtml.bind="highlightMatch(result.title, query)"></div>

          <div
            if.bind="result.description"
            class="autocomplete-item-description">
            ${result.description}
          </div>

          <div
            if.bind="result.category"
            class="autocomplete-item-category">
            ${result.category}
          </div>
        </div>
      </div>

      <!-- Empty state -->
      <div if.bind="showEmpty" class="autocomplete-empty">
        No results found for "${query}"
      </div>
    </div>
  </div>
```

### Styles (search-autocomplete.css)

```css
.autocomplete {
  position: relative;
  width: 100%;
}

.autocomplete-input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.autocomplete-input {
  width: 100%;
  padding: 0.75rem 3rem 0.75rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  outline: none;
  transition: border-color 0.2s;
}

.autocomplete-input:focus {
  border-color: #2196f3;
}

.autocomplete-spinner {
  position: absolute;
  right: 1rem;
  display: flex;
  align-items: center;
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid #e0e0e0;
  border-top-color: #2196f3;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.autocomplete-clear {
  position: absolute;
  right: 0.75rem;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #999;
  padding: 0;
  width: 24px;
  height: 24px;
  line-height: 1;
  border-radius: 50%;
  transition: background-color 0.2s;
}

.autocomplete-clear:hover {
  background-color: #f5f5f5;
  color: #333;
}

.autocomplete-dropdown {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  right: 0;
  max-height: 400px;
  overflow-y: auto;
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  animation: fadeIn 0.2s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.autocomplete-item {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  cursor: pointer;
  transition: background-color 0.15s;
  border-bottom: 1px solid #f5f5f5;
}

.autocomplete-item:last-child {
  border-bottom: none;
}

.autocomplete-item:hover,
.autocomplete-item.selected {
  background-color: #f5f5f5;
}

.autocomplete-item-image {
  width: 40px;
  height: 40px;
  border-radius: 4px;
  object-fit: cover;
  flex-shrink: 0;
}

.autocomplete-item-content {
  flex-grow: 1;
  min-width: 0;
}

.autocomplete-item-title {
  font-weight: 500;
  color: #333;
  margin-bottom: 0.25rem;
}

.autocomplete-item-title mark {
  background-color: #ffeb3b;
  padding: 0 2px;
  border-radius: 2px;
}

.autocomplete-item-description {
  font-size: 0.875rem;
  color: #666;
  margin-bottom: 0.25rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.autocomplete-item-category {
  font-size: 0.75rem;
  color: #999;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.autocomplete-empty {
  padding: 2rem 1rem;
  text-align: center;
  color: #999;
}
```

### Usage Example

```typescript
// src/pages/search-page.ts
import { IRouter } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class SearchPage {
  private readonly router = resolve(IRouter);

  async searchProducts(query: string) {
    const response = await fetch(`/api/products/search?q=${encodeURIComponent(query)}`);
    return response.json();
  }

  handleSelect(result: any) {
    console.log('Selected:', result);
    this.router.load(`products/${result.id}`);
  }
}
```

```html
<!-- src/pages/search-page.html -->
<div class="search-page">
  <h1>Search Products</h1>

  <search-autocomplete
    placeholder="Search for products..."
    min-length.bind="2"
    debounce-ms.bind="300"
    max-results.bind="10"
    on-search.bind="searchProducts"
    on-select.bind="handleSelect">
  </search-autocomplete>
</div>
```

## How It Works

### Debouncing

The `queryChanged` callback uses setTimeout to debounce API calls. When the user types, previous timers are cleared, so only the final query triggers a search after the specified delay.

### Keyboard Navigation

The component handles arrow keys to navigate results, Enter to select, and Escape to close. The selected index tracks which item is highlighted, and `scrollIntoView` ensures it's visible.

### Click Outside

A global click listener detects clicks outside the component and closes the dropdown. The listener is added in `attached()` and cleaned up in `detaching()`.

### Highlighting Matches

The `highlightMatch` method uses regex to wrap matching text in `<mark>` tags. The result is bound with `innerhtml.bind` to render the HTML.

{% hint style="warning" %}
**Security Note**

Be careful with `innerhtml.bind`. In this case it's safe because we're only highlighting text we control. For user-generated content, use the `sanitize` value converter.
{% endhint %}

### Accessibility

* `role="combobox"` on input
* `role="listbox"` on dropdown
* `role="option"` on results
* `aria-expanded` indicates dropdown state
* `aria-activedescendant` points to selected item
* Keyboard navigation follows ARIA practices

## Variations

### Recent Searches

Store and show recent searches when input is focused but empty:

```typescript
private recentSearches: string[] = [];

attached() {
  // Load from localStorage
  const stored = localStorage.getItem('recent-searches');
  if (stored) {
    this.recentSearches = JSON.parse(stored);
  }
}

selectResult(result: SearchResult) {
  // Save to recent searches
  this.recentSearches = [
    result.title,
    ...this.recentSearches.filter(s => s !== result.title)
  ].slice(0, 5);

  localStorage.setItem('recent-searches', JSON.stringify(this.recentSearches));

  // ... rest of implementation
}
```

### Grouped Results

Group results by category:

```typescript
get groupedResults() {
  const groups = new Map<string, SearchResult[]>();

  this.results.forEach(result => {
    const category = result.category || 'Other';
    if (!groups.has(category)) {
      groups.set(category, []);
    }
    groups.get(category)!.push(result);
  });

  return Array.from(groups.entries());
}
```

```html
<div repeat.for="[category, items] of groupedResults">
  <div class="autocomplete-group-header">${category}</div>
  <div repeat.for="item of items" class="autocomplete-item">
    <!-- item content -->
  </div>
</div>
```

### Infinite Scroll

Load more results as user scrolls:

```typescript
handleScroll(event: Event) {
  const element = event.target as HTMLElement;
  const bottom = element.scrollHeight - element.scrollTop === element.clientHeight;

  if (bottom && !this.isLoading && this.hasMoreResults) {
    this.loadMore();
  }
}
```

## Related

* [Event Binding](/templates/overview/event-binding) - Keyboard events
* [Conditional Rendering](/templates/conditional-rendering) - `if.bind` documentation
* [Template References](/templates/overview/template-references) - `ref` attribute
* [Bindable Properties](/components/bindable-properties) - Component inputs
* [Value Converters](/templates/value-converters) - `sanitize` for HTML binding


# Component basics

Components are the building blocks of Aurelia applications. This guide covers creating, configuring, and using components effectively.

Components are the core building blocks of Aurelia applications. Each component typically consists of:

* A TypeScript class (view model)
* An HTML template (view)
* Optional CSS styling

{% hint style="info" %}
**Component Naming**

Component names must include a hyphen (e.g., `user-card`, `nav-menu`) to comply with Web Components standards. Use a consistent prefix like `app-` or your organization's initials for better organization.
{% endhint %}

## Creating Your First Component

The simplest way to create a component is with convention-based files:

{% tabs %}
{% tab title="user-card.ts" %}

```typescript
export class UserCard {
  name = 'John Doe';
  email = 'john@example.com';
}
```

{% endtab %}

{% tab title="user-card.html" %}

```html
<div class="user-card">
  <h3>${name}</h3>
  <p>${email}</p>
</div>
```

{% endtab %}
{% endtabs %}

Aurelia automatically pairs `user-card.ts` with `user-card.html` by convention, creating a `<user-card>` element you can use in templates.

## Component Configuration

Use the `@customElement` decorator for explicit configuration:

```typescript
import { customElement } from 'aurelia';

@customElement({
  name: 'user-card',
  template: `
    <div class="user-card">
      <h3>\${name}</h3>
      <p>\${email}</p>
    </div>
  `
})
export class UserCard {
  name = 'John Doe';
  email = 'john@example.com';
}
```

For simple naming, use the shorthand syntax:

```typescript
@customElement('user-card')
export class UserCard {
  // Component logic
}
```

### Configuration Options

Key `@customElement` options:

**Template Configuration:**

```typescript
import template from './custom-template.html?raw';

@customElement({
  name: 'data-widget',
  template, // External file
})
export class DataWidget {}

@customElement({
  name: 'inline-widget',
  template: '<div>Inline template</div>',
})
export class InlineWidget {}

@customElement({
  name: 'viewless-widget',
  template: null,
})
export class ViewlessWidget {}
```

### Importing external HTML templates with bundlers

When a component imports an `.html` file, the bundler must deliver that file as a plain string. Otherwise tools such as Vite, Webpack, and Parcel try to parse the file as an entry point and emit errors like `[vite:build-html] Unable to parse HTML; parse5 error code unexpected-character-in-unquoted-attribute-value` or `"template" is not exported by src/components/product-name-search.html`.

Configure your bundler using the option that best matches your stack:

* **Vite / esbuild (default Aurelia starter), Parcel 2, Rollup + `@rollup/plugin-string`** – append `?raw` to the import so the bundler treats the file as text:

  ```ts
  import template from './product-name-search.html?raw';
  ```

  Add a matching declaration so TypeScript understands these imports (the query string can be reused for other text assets):

  ```ts
  declare module '*.html?raw' {
    const content: string;
    export default content;
  }
  ```
* **Webpack 5** – mark `.html` files as `asset/source` (or keep using `raw-loader`). After that you can import without a query parameter:

  ```ts
  // webpack.config.cjs
  module.exports = {
    module: {
      rules: [
        { test: /\.html$/i, type: 'asset/source' },
      ],
    },
  };
  ```

  ```ts
  import template from './product-name-search.html';
  declare module '*.html' {
    const content: string;
    export default content;
  }
  ```
* **Other bundlers** – use the equivalent “treat this file as a string” hook (e.g., SystemJS `text` plugin).

Once the bundler understands `.html` files as text, both `npm start` and `npm run build` can reuse the same component source without inline templates. Keep the import pattern consistent across the project so contributors immediately know which loader configuration applies.

**Dependencies:**

```typescript
import { ChildComponent } from './child-component';

@customElement({
  name: 'parent-widget',
  dependencies: [ChildComponent] // Available without <import>
})
```

### Alternative Creation Methods

**Static Configuration:**

```typescript
export class UserCard {
  static $au = {
    type: 'custom-element',
    name: 'user-card'
  };
}
```

**Programmatic (mainly for testing):**

```typescript
import { CustomElement } from '@aurelia/runtime-html';

const MyComponent = CustomElement.define({
  name: 'test-component',
  template: '<span>\${message}</span>'
});
```

## HTML-Only Components

Create simple components with just HTML:

{% code title="status-badge.html" %}

```html
<bindable name="status"></bindable>
<bindable name="message"></bindable>

<span class="badge badge-\${status}">\${message}</span>
```

{% endcode %}

Usage:

```html
<import from="./status-badge.html"></import>

<status-badge status="success" message="Complete"></status-badge>
```

## Viewless Components

Components that handle DOM manipulation through third-party libraries:

```typescript
import { bindable, customElement } from 'aurelia';
import * as nprogress from 'nprogress';

@customElement({
  name: 'progress-indicator',
  template: null
})
export class ProgressIndicator {
  @bindable loading = false;

  loadingChanged(newValue: boolean) {
    newValue ? nprogress.start() : nprogress.done();
  }
}
```

## Using Components

**Global Registration (in main.ts):**

```typescript
import Aurelia from 'aurelia';
import { UserCard } from './components/user-card';

Aurelia
  .register(UserCard)
  .app(MyApp)
  .start();
```

**Local Import (in templates):**

```html
<import from="./user-card"></import>
<!-- or with alias -->
<import from="./user-card" as="profile-card"></import>

<user-card user.bind="currentUser"></user-card>
<profile-card user.bind="selectedUser"></profile-card>
```

## Containerless Components

Render component content without wrapper tags:

```typescript
import { customElement, containerless } from 'aurelia';

@customElement({ name: 'list-wrapper' })
@containerless
export class ListWrapper {
  // Component logic
}
```

Or configure inline:

```typescript
@customElement({
  name: 'list-wrapper',
  containerless: true
})
export class ListWrapper {}
```

{% hint style="warning" %}
**Use Sparingly**

Containerless components lose their wrapper element, which can complicate styling, testing, and third-party library integration.
{% endhint %}

## Component Lifecycle

Components follow a predictable lifecycle. Implement only the hooks you need:

```typescript
export class UserProfile {
  constructor() {
    // Component instantiation
  }

  binding() {
    // Before bindings are processed
  }

  bound() {
    // After bindings are set
  }

  attached() {
    // Component is in the DOM
  }

  detaching() {
    // Before removal from DOM
  }
}
```

{% hint style="info" %}
See [Component Lifecycles](/components/component-lifecycles) for comprehensive lifecycle documentation.
{% endhint %}

## Bindable Properties

Components accept data through bindable properties:

```typescript
import { bindable, BindingMode } from 'aurelia';

export class UserCard {
  @bindable user: User;
  @bindable isActive: boolean = false;
  @bindable({ mode: BindingMode.twoWay }) selectedId: string;

  userChanged(newUser: User, oldUser: User) {
    // Called when user property changes
  }
}
```

```html
<user-card 
  user.bind="currentUser" 
  is-active.bind="userIsActive"
  selected-id.two-way="selectedUserId">
</user-card>
```

See [Bindable Properties](/components/bindable-properties) for complete configuration options.

## Advanced Features

### Shadow DOM

Enable Shadow DOM for complete style and DOM encapsulation:

```typescript
import { customElement, useShadowDOM, shadowCSS } from 'aurelia';

@customElement({
  name: 'isolated-widget',
  template: '<div class="widget"><slot></slot></div>',
  dependencies: [
    shadowCSS(`
      .widget {
        border: 1px solid var(--widget-border, #ddd);
        padding: 16px;
      }
    `)
  ]
})
@useShadowDOM({ mode: 'open' })
export class IsolatedWidget {
  // Styles and DOM are fully encapsulated from outside
}
```

Shadow DOM is useful for:

* Complete style isolation (styles won't leak in or out)
* Creating reusable components with predictable styling
* Using native `<slot>` elements for content projection
* Building design systems and component libraries

See the [Shadow DOM guide](/components/shadow-dom) for detailed configuration, styling patterns, and best practices.

### Template Processing

Transform markup before compilation:

```typescript
import { customElement, processContent, INode } from 'aurelia';

@customElement({ name: 'card-grid' })
export class CardGrid {
  @processContent()
  static processContent(node: INode) {
    // Transform <card> elements into proper markup
    const cards = node.querySelectorAll('card');
    cards.forEach(card => {
      card.classList.add('card-item');
      // Additional transformations...
    });
  }
}
```

### Enhancing Existing DOM

Apply Aurelia to existing elements:

```typescript
import { resolve, Aurelia } from 'aurelia';

export class DynamicContent {
  private readonly au = resolve(Aurelia);

  async enhanceContent() {
    const element = document.getElementById('server-rendered');
    await this.au.enhance({
      host: element,
      component: { data: this.dynamicData }
    });
  }
}
```

### Reactive Properties

Watch for property changes:

```typescript
import { watch, bindable } from 'aurelia';

export class ChartWidget {
  @bindable data: ChartData[];
  @bindable config: ChartConfig;

  @watch('data')
  @watch('config') 
  onDataChange(newValue: any, oldValue: any, propertyName: string) {
    this.updateChart();
  }
}
```

### Child Element Observation

```typescript
import { children, slotted } from 'aurelia';

export class TabContainer {
  @children('tab-item') tabItems: TabItem[];
  @slotted('tab-panel') panels: TabPanel[];

  tabItemsChanged(newItems: TabItem[]) {
    this.syncTabs();
  }
}
```

### Component Configuration

**Attribute Capture:**

```typescript
import { capture, customElement } from 'aurelia';

@customElement({ name: 'flex-wrapper' })
@capture() // Captures all unrecognized attributes
export class FlexWrapper {}
```

**Aliases:**

```typescript
import { customElement } from 'aurelia';

@customElement({
  name: 'primary-button',
  aliases: ['btn-primary', 'p-btn']
})
export class PrimaryButton {}
```

## Best Practices

### Component Design

* **Single Responsibility**: Each component should have one clear purpose
* **Type Safety**: Use interfaces for complex data structures
* **Composition**: Favor composition over inheritance

```typescript
import { bindable, resolve } from 'aurelia';
import { ILogger } from '@aurelia/kernel';

interface User {
  id: string;
  name: string;
  email: string;
}

export class UserProfile {
  @bindable user: User;
  private readonly logger = resolve(ILogger);
  
  attached() {
    this.logger.info('Profile loaded', { userId: this.user.id });
  }
}
```

### Performance

* Use `attached()` for DOM-dependent initialization
* Clean up subscriptions in `detaching()`
* Prefer `@watch` over polling for reactive updates
* Consider Shadow DOM for style isolation

### Testing

* Mock dependencies properly
* Test lifecycle hooks and bindable properties
* Write tests for error scenarios

See [Testing Components](/developer-guides/overview/testing-components) for detailed guidance.

***

Components form the foundation of Aurelia applications. Start with simple convention-based components and add complexity as needed. The framework's flexibility allows you to adopt patterns that fit your project's requirements while maintaining clean, maintainable code.


# Component lifecycles

Aurelia components offer a rich **lifecycle** that lets you hook into specific moments of a component's existence—from construction, through activation, to eventual disposal. Understanding the order and intent of each hook will help you write components that are predictable, testable, and memory-leak-free.

{% hint style="info" %}
All lifecycle callbacks are **optional**. Implement only what you need. Hooks such as `binding`/`unbinding` or `attaching`/`detaching` are often implemented in pairs so you can clean up resources you set up in the first hook.
{% endhint %}

{% hint style="info" %}
Lifecycle hooks apply to **custom elements** and **custom attributes**. Synthetic views (created by template controllers like `if`, `repeat`) do not have lifecycle hooks, but their child components do.
{% endhint %}

## Quick reference

```mermaid
flowchart LR
  ctor["Constructor"] --> hydrating --> hydrated --> created --> binding --> bound --> attaching --> attached --> detaching --> unbinding --> dispose
```

| Phase        | Hook          | Runs                       | Child-parent order | Async?                          |
| ------------ | ------------- | -------------------------- | ------------------ | ------------------------------- |
| Construction | `constructor` | once                       | –                  | –                               |
|              | `hydrating`   | once                       | **top ➞ down**     | no                              |
|              | `hydrated`    | once                       | **top ➞ down**     | no                              |
|              | `created`     | once                       | **bottom ➞ up**    | no                              |
| Activation   | `binding`     | every activation           | **top ➞ down**     | yes (blocks children)           |
|              | `bound`       | every activation           | **top ➞ down**     | yes (awaits)                    |
|              | `attaching`   | every activation           | **top ➞ down**     | yes (awaits before `attached`)  |
|              | `attached`    | every activation           | **bottom ➞ up**    | yes (awaits)                    |
| Deactivation | `detaching`   | every deactivation         | **bottom ➞ up**    | yes (awaits before DOM removal) |
|              | `unbinding`   | every deactivation         | **bottom ➞ up**    | yes (awaits)                    |
| Cleanup      | `dispose`     | when permanently discarded | **top ➞ down**     | no                              |

Legend

* **top ➞ down** – parent executes before its children
* **bottom ➞ up** – children execute before their parent

## Detailed walkthrough

### 1. Constructor

Executed when the instance is created. Inject services here and perform work that **does not** depend on bindable values.

```typescript
import { resolve } from '@aurelia/kernel';
import { IRouter } from '@aurelia/router';

export class MyComponent {
  readonly router = resolve(IRouter);
}
```

### 2. Hydrating

```typescript
hydrating(controller: IContextualCustomElementController<this>): void {}
```

* Opportunity to **register dependencies** in `controller.container` that are needed while **compiling** the view template.
* Runs **synchronously**, parent before children.

### 3. Hydrated

```typescript
hydrated(controller: ICompiledCustomElementController<this>): void {}
```

* View template has been compiled, child components are **not** yet created.
* Last chance to influence how the soon-to-be-created child components resolve their dependencies.

### 4. Created

```typescript
created(controller: ICustomElementController<this> | ICustomAttributeController<this>): void {}
```

* All child components are now constructed and hydrated.
* Executes **once** per instance, **children before parent**.
* Great for logic that must run after the whole subtree is constructed but **before binding**.

### 5. Binding

```typescript
// Custom Elements
binding(initiator: IHydratedController, parent: IHydratedController | null): void | Promise<void> {}

// Custom Attributes
binding(initiator: IHydratedController, parent: IHydratedController): void | Promise<void> {}
```

* Bindable properties have been set but **bindings in the view are not yet connected**.
* Runs **parent ➞ child**.
* Return a `Promise` (or mark the method `async`) to **block** binding/attaching of children until resolved.

### 6. Bound

```typescript
// Custom Elements
bound(initiator: IHydratedController, parent: IHydratedController | null): void | Promise<void> {}

// Custom Attributes
bound(initiator: IHydratedController, parent: IHydratedController): void | Promise<void> {}
```

* View-to-view-model bindings are active; `ref`, `let`, and `from-view` values are available.
* Executes **parent ➞ child**.

### 7. Attaching

```typescript
// Custom Elements
attaching(initiator: IHydratedController, parent: IHydratedController | null): void | Promise<void> {}

// Custom Attributes
attaching(initiator: IHydratedController, parent: IHydratedController): void | Promise<void> {}
```

* The component's host element is now in the DOM but **child components may still be attaching**.
* Queue animations or setup 3rd-party libraries here.
* A returned `Promise` is awaited **before** `attached` is invoked on this component **but does not block children**.

### 8. Attached

```typescript
attached(initiator: IHydratedController): void | Promise<void> {}
```

* The entire component subtree is mounted; safe to measure elements or call libraries that need actual layout information.
* Executes **child ➞ parent**.
* Note: Only receives the `initiator` parameter, **not** the parent.

### 9. Detaching

```typescript
// Custom Elements
detaching(initiator: IHydratedController, parent: IHydratedController | null): void | Promise<void> {}

// Custom Attributes
detaching(initiator: IHydratedController, parent: IHydratedController): void | Promise<void> {}
```

* Called when the framework removes the component's element from the DOM.
* Executes **child ➞ parent**. Any returned `Promise` (e.g., an outgoing animation) is awaited **in parallel** with sibling promises.

### 10. Unbinding

```typescript
// Custom Elements
unbinding(initiator: IHydratedController, parent: IHydratedController | null): void | Promise<void> {}

// Custom Attributes
unbinding(initiator: IHydratedController, parent: IHydratedController): void | Promise<void> {}
```

* Runs after `detaching` finishes and before bindings are disconnected.
* Executes **child ➞ parent**.

### 11. Dispose

```typescript
dispose(): void {}
```

* Invoked when the instance is **permanently discarded**—typically when removed from a repeater and the view cache is full, or when the application shuts down.
* Use to tear down long-lived resources, subscriptions, or manual observers to prevent memory leaks.
* Runs synchronously, parent before children. `stop(true)` disposes an application root after deactivation.
* A thrown disposal error ends that disposal call and is reported directly.

## Lifecycle hooks decorator (`@lifecycleHooks`)

For **cross-cutting concerns** like logging, analytics, or debugging, implement lifecycle hooks in a separate class using the `@lifecycleHooks` decorator. This keeps your component code focused while adding shared behavior.

```typescript
import { lifecycleHooks, ILifecycleHooks, ICustomElementController, IHydratedController } from 'aurelia';

@lifecycleHooks()
export class ComponentLogger implements ILifecycleHooks<MyComponent> {
  bound(vm: MyComponent, initiator: IHydratedController, parent: IHydratedController | null) {
    console.log(`${vm.constructor.name} bound with data:`, vm.someProperty);
  }

  detaching(vm: MyComponent, initiator: IHydratedController, parent: IHydratedController | null) {
    console.log(`${vm.constructor.name} detaching`);
  }
}
```

Multiple lifecycle hook classes can be registered. Aurelia invokes their matching methods **in registration order**, followed by the component's matching hook. A successful phase completes after every returned Promise settles.

## Async completion and errors

Aurelia completes a successful lifecycle phase after the component and its descendants finish the work they started, including Promises returned by registered lifecycle hooks.

A hook failure ends the affected lifecycle transition and reaches the caller as the original thrown or rejected value. The affected component or application is then in a terminal state. Fix the hook before creating and activating a replacement.

Router navigation is different. The router controls both the current route and its replacement, so it can discard a failed candidate and keep the current route active. See the [Router lifecycle](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle) for its navigation behavior.

The low-level Controller API is not a general transition queue. Framework structural owners serialize their own updates, and custom integrations should await a Controller transition before requesting the next one. Deactivation requested during activation stops later activation phases and proceeds toward the inactive state.

Return the Promise created by your hook when Aurelia should wait for that work:

```typescript
export class AnimatedPanel {
  public attaching(): Promise<void> {
    return this.animation.enter();
  }

  public detaching(): Promise<void> {
    return this.animation.leave();
  }
}
```

For low-level controller integrations, the Promise returned by a hook should represent only work that hook started. Returning a controller transition that depends on the same hook creates a cycle that cannot settle. Await lifecycle completion before disposing the controller.

## Special cases

* Values rendered by **`<au-compose>`** can implement `activate(model)`. Composed custom elements retain their standard `detaching`, `unbinding`, and `dispose` hooks. See the [dynamic composition guide](/getting-to-know-aurelia/composition-patterns/dynamic-composition).
* **Router hooks** such as `canLoad`, `loading`, `canUnload`, `unloading`, etc., are documented in the [routing lifecycle section](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle) and are available even if you do not use the router.

## Best practices

1. **Prefer early exits**—perform checks at the start of hooks and `return` early to minimise nesting.
2. **Clean up** observers, timeouts, event listeners, or 3rd-party widgets **in the opposite hook** (`unbinding`/`detaching` or `dispose`).
3. **Avoid heavy work in the constructor.** Move anything needing bindables or DOM to later hooks.
4. **Return the complete Promise for work started by the hook** so Aurelia waits for all of it. An `async` hook provides this naturally when it awaits all of its work.
5. **Keep hooks fast**—expensive work can block the component hierarchy.


# Lifecycle Visual Diagrams

These diagrams show the public timing guarantees for a component tree. See [Component lifecycles](/components/component-lifecycles) for hook signatures and common uses.

## Activation

Activation proceeds from parent to child until the `attached` phase returns from the leaves.

```mermaid
sequenceDiagram
    participant Parent
    participant DOM
    participant Child

    Parent->>Parent: binding()
    Note over Parent: Await returned work
    Parent->>Parent: connect bindings
    Parent->>Parent: bound()
    Note over Parent: Await returned work
    Parent->>DOM: insert parent nodes

    par Parent attaching work
        Parent->>Parent: attaching()
    and Child activation
        Parent->>Child: activate
        Child->>Child: binding(), connect, bound()
        Child->>DOM: insert child nodes
        Child->>Child: attaching()
        Child->>Child: attached()
        Child-->>Parent: child subtree attached
    end

    Note over Parent,Child: Parent attaching and the child subtree have settled
    Parent->>Parent: attached()
```

For each controller:

1. `binding()` settles.
2. Aurelia connects that controller's bindings.
3. `bound()` settles.
4. Aurelia inserts the controller's nodes.
5. `attaching()` begins.
6. Child activation can proceed while the parent's `attaching()` Promise remains pending.
7. `attached()` runs after that controller's attaching work and descendant activation have settled. A descendant can reach `attached()` while an ancestor's `attaching()` Promise is still pending.

This produces the following tree order:

| Hook        | Invocation order across the tree | When Aurelia continues                                                                 |
| ----------- | -------------------------------- | -------------------------------------------------------------------------------------- |
| `binding`   | parent to child                  | each parent settles before its children begin                                          |
| `bound`     | parent to child                  | each controller settles before its attaching phase                                     |
| `attaching` | parent to child                  | parent work can overlap descendant activation                                          |
| `attached`  | child to parent                  | each subtree settles before its own parent runs; ancestor `attaching` work can overlap |

Registered lifecycle-hook providers run in registration order for a controller. The component's matching hook follows them. A successful phase completes after every returned Promise settles.

## Deactivation

Deactivation gives every component a cleanup opportunity before disconnecting its bindings.

```mermaid
sequenceDiagram
    participant Parent
    participant Child
    participant DOM

    Parent->>Child: begin deactivation
    Child->>Child: detaching()
    Parent->>Parent: detaching()
    Note over Parent,Child: All returned detaching work settles

    Parent->>DOM: remove component nodes

    Child->>Child: unbinding()
    Parent->>Parent: unbinding()
    Note over Parent,Child: All returned unbinding work settles

    Child->>Child: disconnect bindings
    Parent->>Parent: disconnect bindings
    Note over Parent,Child: Subtree is inactive
```

The phase order is stable:

1. Aurelia invokes `detaching()` from child to parent. The returned Promises can remain pending at the same time.
2. DOM removal begins after all returned detaching work settles.
3. Aurelia invokes `unbinding()` from child to parent while bindings remain connected.
4. Binding disconnection begins after all returned unbinding work settles.
5. The controller subtree reaches its inactive state.
6. Permanent cleanup invokes synchronous `dispose()` from parent to child.

An outgoing animation can retain the DOM by returning its Promise from `detaching()`:

```typescript
export class ToastMessage {
  public detaching(): Promise<void> {
    return this.animation.leave();
  }
}
```

## Error reporting

Lifecycle failures are terminal for the affected transition. Aurelia preserves the original application error and reports it to the caller.

```mermaid
flowchart LR
    A[Hook throws synchronously] --> B[Later providers do not start]
    B --> C[Report the original application error]
```

A failed hook leaves the affected component or application in a terminal state. Fix the hook before creating and activating a replacement. Router navigation is different: the router can discard a failed route candidate and keep the current route active.

## Low-level transition requests

The low-level Controller API is not a general transition queue. Framework features such as Repeat, `if`, Switch, and dynamic composition serialize their own structural updates. Custom integrations should await the current Controller result before requesting another transition.

| Request                               | Result                                                                                 |
| ------------------------------------- | -------------------------------------------------------------------------------------- |
| Deactivate during activation          | Aurelia stops entering later activation phases and proceeds toward the inactive state. |
| Repeat an active deactivation request | The caller receives the current deactivation result.                                   |
| Activate after deactivation settles   | Aurelia starts a new activation transition.                                            |

For low-level integrations, return the Promise for work started by the hook. Returning a controller transition that depends on the same hook creates a lifecycle dependency cycle that cannot settle.

## Practical lifecycle patterns

* **Prepare from bindables in `binding()`.** The constructor is suitable for dependency injection and field defaults. Bindable values are available when `binding()` runs.
* **Use mounted DOM at the appropriate phase.** Start an enter animation in `attaching()` when Aurelia should wait for it. Use `attached()` for measurements that need the component subtree in the document.
* **Return asynchronous hook work.** Aurelia keeps the DOM in place until a Promise returned by `detaching()` settles:

```typescript
public detaching(): Promise<void> {
  const animation = this.element.animate(
    [{ opacity: 1 }, { opacity: 0 }],
    { duration: 200 },
  );
  return animation.finished.then(() => void 0);
}
```

* **Pair resources with the active lifecycle.** Install DOM listeners in `attached()` and remove them in `detaching()` so cached components can reactivate cleanly:

```typescript
private readonly onResize = (): void => this.updateLayout();

public attached(): void {
  window.addEventListener('resize', this.onResize);
}

public detaching(): void {
  window.removeEventListener('resize', this.onResize);
}
```

Use synchronous `dispose()` for permanent cleanup of resources designed to survive ordinary deactivation.

## Waiting for startup and shutdown

Await application lifecycle calls in tests and in code that depends on the completed transition:

```typescript
await aurelia.start();

// Exercise the running application.

await aurelia.stop(true);
aurelia.dispose();
```

`start()` retains a synchronous fast path and therefore returns `void | Promise<void>`. The `await` expression handles both forms.


# Bindable properties

How to create components that accept one or more bindable properties. You might know these as "props" if you are coming from other frameworks and libraries.

### Bindable properties

When creating components, sometimes you will want the ability for data to be passed into them instead of their host elements. The `@bindable` decorator allows you to specify one or more bindable properties for a component.

The `@bindable` attribute also can be used with custom attributes as well as custom elements. The decorator denotes bindable properties on components on the view model of a component.

{% code title="loader-component.ts" %}

```typescript
import { bindable } from 'aurelia';

export class LoaderComponent {
    @bindable loading = false;
}
```

{% endcode %}

This will allow our component to be passed in values. Our specified bindable property here is called `loading` and can be used like this:

{% code title="loader-component.html" %}

```html
<loader loading.bind="true"></loader>
```

{% endcode %}

In the example above, we are binding the boolean literal `true` to the `loading` property.

Instead of literal, you can also bind another property (`loadingVal` in the following example) to the `loading` property.

{% code title="loader-component.html" %}

```html
<loader loading.bind="loadingVal"></loader>
```

{% endcode %}

As seen in the following example, you can also bind values without the `loading.bind` part.

```html
<loader loading="true"></loader>
```

{% hint style="warning" %}
Aurelia treats attribute values as strings. This means when working with primitives such as booleans or numbers, they won't come through in that way and need to be coerced into their primitive type using a [bindable setter](#bindable-setter) or specifying the bindable type explicitly using [bindable coercion](#bindable-coercion).
{% endhint %}

The `@bindable` decorator signals to Aurelia that a property is bindable in our custom element. Let's create a custom element where we define two bindable properties.

{% tabs %}
{% tab title="name-component.ts" %}

```typescript
import { bindable, BindingMode } from 'aurelia';

export class NameComponent {
    @bindable({ mode: BindingMode.toView }) firstName = '';
    @bindable({ mode: BindingMode.toView }) lastName  = '';
}
```

{% endtab %}

{% tab title="name-component.html" %}

```html
<p>Hello ${firstName} ${lastName}. How are you today?</p>
```

{% endtab %}
{% endtabs %}

You can then use the component in this way,\``<name-component first-name="John" last-name="Smith"></name-component>`

### Calling a change function when bindable is modified

By default, Aurelia will call a change callback (if it exists) which takes the bindable property name followed by `Changed` added to the end. For example, `firstNameChanged(newVal, previousVal)` would fire every time the `firstName` bindable property is changed.

{% hint style="warning" %}
Due to the way the Aurelia binding system works, change callbacks will not be fired upon initial component initialization. If you worked with Aurelia 1, this behavior differs from what you might expect.
{% endhint %}

If you would like to call your change handler functions when the component is initially bound (like v1), you can achieve this the following way:

```typescript
import { bindable } from 'aurelia';

export class NameComponent {
    @bindable firstName = '';
    @bindable lastName  = '';

    bound() {
        this.firstNameChanged(this.firstName, undefined);
    }

    firstNameChanged(newVal, oldVal) {
        console.log('Value changed');
    }
}
```

If you have multiple bindable properties like `firstName`/`lastName` in the above example, and want to use a single callback to react to those changes, you can use `propertyChanged` callback. `propertyChanged` callback will be called immediately after the targeted change callback. The parameters of this callback will be `key`/`newValue`/`oldValue`, similar like the following example:

```ts
export class NameComponent {
    @bindable firstName = '';
    @bindable lastName  = '';

    propertyChanged(key, newVal, oldVal) {
      if (key === 'firstName') {
        // Handle firstName change
      } else if (key === 'lastName') {
        // Handle lastName change
      }
    }
}
```

In the above example, even though `propertyChanged` can be used for multiple properties (like `firstName` and `lastName`), it's only called individually for each of those properties. If you wish to act on a group of changes, like both `firstName` and `lastName` at once in the above example, `propertiesChanged` callback can used instead, like the following example:

```ts
propertiesChanged({ firstName, lastName }) {
  if (firstName && lastName) {
    // both firstName and lastName were changed at the same time
    // apply first update strategy
    const { newValue: newFirstName, oldValue: oldFirstName } = firstName;
    const { newValue: newLastName, oldValue: oldLastName } = lastName;
  } else if (firstName) {
    // only firstName was changed - apply second update strategy
    // ...
  } else {
    // only lastName was changed - apply third update strategy
    // ...
  }
}
```

For the order of callbacks when there are multiple callbacks involved, refer to the following example: If we have a component class that looks like this:

```ts
class MyComponent {
  @bindable prop = 0

  propChanged() { console.log('prop changed'); }

  propertyChanged(name) { console.log(`property "${name}" changed`) }

  propertiesChanged(changes) {
    console.log('changes are:', changes)
  }
}
```

When we do

```ts
myComponent.prop = 1;
console.log('after assign');
```

the console logs will look like the following:

```
propChanged
property "prop" changed
after assign
changes are, { prop: { newValue: 1, oldValue: 0 } }
```

**Note**: The individual change callback (`propChanged`) and `propertyChanged` execute immediately when the property is set, while `propertiesChanged` is deferred and executes asynchronously in the next tick.

### Configuring bindable properties

Like almost everything in Aurelia, you can configure how bindable properties work.

#### Change the binding mode using mode

You can specify the binding mode using the `mode` property and passing in a valid `BindingMode` to it; `@bindable({ mode: BindingMode.twoWay})` - this determines which way changes flow in your binding. By default, this will be `BindingMode.oneWay`

{% hint style="info" %}
Please consult the [binding modes](#one-way-binding) documentation below to learn how to change the binding modes. By default, the binding mode for bindable properties will be `one-way`
{% endhint %}

#### Change the name of the change callback

You can change the name of the callback that is fired when a change is made `@bindable({ callback: 'propChanged' })`

{% tabs %}
{% tab title="name-component.ts" %}

```typescript
import { bindable, BindingMode } from 'aurelia';

export class NameComponent {
    @bindable({ mode: BindingMode.twoWay}) firstName = '';
    @bindable({ callback: 'lnameChanged' }) lastName  = '';

    lnameChanged(val) {}
}
```

{% endtab %}
{% endtabs %}

#### Change the attribute name using attribute

By default, Aurelia converts camelCase property names to kebab-case attribute names (e.g., `firstName` becomes `first-name`). You can override this with the `attribute` option:

```typescript
import { bindable } from 'aurelia';

export class UserCard {
    @bindable({ attribute: 'user-id' }) id = '';
}
```

This allows the component to be used with a custom attribute name:

```html
<user-card user-id="123"></user-card>
```

#### Set the default property for custom attributes

When creating custom attributes, you can specify which property receives the value when the attribute is used without explicitly naming a property. Use the `defaultProperty` option on the `@customAttribute` decorator:

```typescript
import { bindable, customAttribute } from 'aurelia';

@customAttribute({ name: 'tooltip', defaultProperty: 'message' })
export class TooltipCustomAttribute {
    @bindable message = '';
    @bindable position = 'top';
}
```

This allows a simpler usage syntax:

```html
<!-- The value "Hello" goes to the default property (message) -->
<div tooltip="Hello"></div>

<!-- Equivalent explicit syntax -->
<div tooltip="message: Hello"></div>

<!-- Using multiple bindables -->
<div tooltip="message: Hello; position: bottom"></div>
```

{% hint style="info" %}
If `defaultProperty` is not specified, it defaults to `'value'`. This allows custom attributes without any bindables defined to automatically receive values through a `value` property.
{% endhint %}

Bindable properties support many different binding modes determining the direction the data is bound in and how it is bound.

#### One way binding

By default, bindable properties will be one-way binding (also known as `toView`). This means values flow into your component but not back out of it (hence the name, one way).

{% hint style="info" %}
Bindable properties without a `mode` explicitly set will be `toView` (one-way) by default. You can also explicitly specify the binding mode.
{% endhint %}

```typescript
import { bindable, BindingMode } from 'aurelia';

export class Loader {
    @bindable({ mode: BindingMode.toView }) loading = false;
}
```

#### Two-way binding

Unlike the default, the two-way binding mode allows data to flow in both directions. If the value is changed with your component, it flows back out.

```typescript
import { bindable, BindingMode } from 'aurelia';

export class Loader {
    @bindable({ mode: BindingMode.twoWay }) loading = false;
}
```

#### One-time binding

The one-time binding mode binds a value once and never updates it again, even if the source value changes. This is useful for static values that won't change after initial binding.

```typescript
import { bindable, BindingMode } from 'aurelia';

export class Loader {
    @bindable({ mode: BindingMode.oneTime }) config = {};
}
```

#### From-view binding

The from-view binding mode allows data to flow from the view (target) to the view model (source), but not the other way. This is the opposite of `toView`.

```typescript
import { bindable, BindingMode } from 'aurelia';

export class Loader {
    @bindable({ mode: BindingMode.fromView }) userInput = '';
}
```

### Working with two-way binding

Much like most facets of binding in Aurelia, two-way binding is intuitive. Instead of `.bind` you use `.two-way` if you need to be explicit, but in most instances, you will specify the type of binding relationship a bindable property is using with `@bindable` instead.

**Explicit two-way binding looks like this:**

```html
<input type="text" value.two-way="myVal">
```

The `myVal` variable will get a new value whenever the text input is updated. Similarly, if `myVal` were updated from within the view model, the input would get the updated value.

{% hint style="info" %}
When using `.bind` for input/form control values such as text inputs, select dropdowns and other form elements. Aurelia will automatically create a two-way binding relationship. So, the above example using a text input can be rewritten to be `value.bind="myVal"` , and it would still be a two-way binding.
{% endhint %}

### Bindable setter

In some cases, you want to make an impact on the value that is binding. For such a scenario, you can use the possibility of new `set`.

```typescript
@bindable({
    set: value => someFunction(value),  /* HERE */
    // Or set: value => value,
    mode: /* ... */
})
```

Suppose you have a `carousel` component in which you want to enable `navigator` feature for it.

```html
<!-- Enable -->
<my-carousel navigator.bind="true">
<my-carousel navigator="true">
<my-carousel navigator=true>
<my-carousel navigator>

<!-- Disable -->
<my-carousel navigator.bind="false">
<my-carousel navigator="false">
<my-carousel navigator=false>
<my-carousel>
```

In version two, you can easily implement such a capability with the `set` feature.

Define your property like this:

```typescript
@bindable({ set: /* ? */, mode: BindingMode.toView }) public navigator: boolean = false;
```

For `set` part, we need functionality to check the input. If the value is one of the following, we want to return `true`, otherwise, we return the `false` value.

* `''`: No input for a standalone `navigator` property.
* `true`: When the `navigator` property set to `true`.
* `"true"`: When the `navigator` property set to `"true"`.

So our function will be like this

```typescript
export function truthyDetector(value: unknown) {
    return value === '' || value === true || value === "true";
}
```

Now, we should set `truthyDetector` function as follows:

```typescript
@bindable({ set: truthyDetector, mode: BindingMode.toView }) public navigator: boolean = false;
```

Although, there is another way to write the functionality too:

```typescript
@bindable({ set: v => v === '' || v === true || v === "true", mode: BindingMode.toView }) public navigator: boolean = false;
```

You can simply use any of the above four methods to enable/disable your feature. As you can see, `set` can be used to transform the values being bound into your bindable property and offer more predictable results when dealing with primitives like booleans and numbers.

### Bindable & getter/setter

By default you'll work with bindable fields most of the time, like the examples above. But there are cases where it makes sense to expose a bindable getter (or getter/setter pair) so you can compute the value on the fly.

For example, a component card nav that allow parent component to query its active status. With bindable on field, it would be written like this:

```ts
import { BindingMode, bindable, customElement, ICustomElementViewModel } from 'aurelia';

@customElement({ name: 'card-nav', template })
export class CardNav implements ICustomElementViewModel {
  @bindable routes: RouteLink[] = [];

  @bindable({ mode: BindingMode.fromView }) active?: string;

  bound() {
    this.setActive();
  }

  setActive() {
    this.active = this.routes.find((y) => y.isActive)?.path;
  }

  handleClick(route: RouteLink) {
    this.routes.forEach((x) => (x.isActive = x === route));
    this.setActive();
  }
}
```

Note that because `active` value needs to computed from other variables, we have to "actively" call `setActive`. It's not a big deal, but sometimes not desirable.

For cases like this, we can turn `active` into a getter, and decorate it with bindable, like the following:

```ts
import { BindingMode, bindable, customElement, ICustomElementViewModel } from 'aurelia';

@customElement({ name: 'card-nav', template })
export class CardNav implements ICustomElementViewModel {
  @bindable routes: RouteLink[] = [];

  @bindable({ mode: BindingMode.fromView }) get active() {
    return this.routes.find((y) => y.isActive)?.path;
  }

  handleClick(route: RouteLink) {
    this.routes.forEach((x) => (x.isActive = x === route));
  }
}
```

Simpler, since the value of `active` is computed, and observed based on the properties/values accessed inside the getter.

### Bindable coercion

The bindable setter section shows how to adapt the value is bound to a `@bindable` property. One common usage of the setter is to coerce the values that are bound from the view. Consider the following example.

{% tabs %}
{% tab title="my-el.ts" %}

```typescript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable public num: number;
}
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
@customElement({ name:'my-app', template: '<my-el num="42"></my-el>' })
export class MyApp { }
```

{% endtab %}
{% endtabs %}

Without any setter for the `@bindable` num we will end up with the string `'42'` as the value for `num` in `MyEl`. You can write a setter to coerce the value. However, it is a bit annoying to write setters for every `@bindable`.

#### Automatic type coercion

To address this issue, Aurelia 2 supports type coercion. To maintain backward compatibility, automatic type coercion is disabled by default and must be enabled explicitly.

```typescript
new Aurelia()
    .register(
      StandardConfiguration
        .customize((config) => {
          config.coercingOptions.enableCoercion = true;
          // config.coercingOptions.coerceNullish = true;
        }),
      ...
    );
```

There are two relevant configuration options.

**enableCoercion**

The default value is `false`; that is Aurelia 2 does not coerce the types of the `@bindable` by default. It can be set to `true` to enable the automatic type-coercion.

**coerceNullish**

The default value is `false`; that is Aurelia2 does not coerce the `null` and `undefined` values. It can be set to `true` to coerce the `null` and `undefined` values as well. This property can be thought of as the global counterpart of the `nullable` property in the bindable definition (see [Coercing nullable values](#coercing-nullable-values) section).

Additionally, depending on whether you are using TypeScript or JavaScript for your app, there can be several ways to use automatic type coercion.

#### Specify `type` in `@bindable`

You need to specify the explicit `type` in the `@bindable` definition.

```javascript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable({ type: Number }) num : number;
}
```

{% hint style="info" %}
The rest of the document is based on TypeScript examples. However, we trust that you can transfer that knowledge to your JavaScript codebase if necessary.
{% endhint %}

### Coercing primitive types

Currently, coercing four primitive types are supported out of the box. These are `number`, `string`, `boolean`, and `bigint`. The coercion functions for these types are respectively `Number(value)`, `String(value)`, `Boolean(value)`, and `BigInt(value)`.

{% hint style="warning" %}
Be mindful when dealing with `bigint` as the `BigInt(value)` will throw if the `value` cannot be converted to bigint; for example `null`, `undefined`, or non-numeric string literal.
{% endhint %}

### Coercing to instances of classes

It is also possible to coerce values into instances of classes. There are two ways how that can be done.

#### Using a static `coerce` method

You can define a static method named `coerce` in the class used as a `@bindable` type. This method will be called by Aurelia2 automatically to coerce the bound value.

This is shown in the following example with the `Person` class.

{% tabs %}
{% tab title="person.ts" %}

```typescript
export class Person {
  public constructor(
    public readonly name: string,
    public readonly age: number,
  ) { }
  public static coerce(value: unknown): Person {
    if (value instanceof Person) return value;
    if (typeof value === 'string') {
      try {
        const json = JSON.parse(value) as Person;
        return new this(json.name, json.age);
      } catch {
        return new this(value, null!);
      }
    }
    if (typeof value === 'number') {
      return new this(null!, value);
    }
    if (typeof value === 'object' && value != null) {
      return new this((value as any).name, (value as any).age);
    }
    return new this(null!, null!);
  }
}
```

{% endtab %}

{% tab title="my-el.ts" %}

```typescript
import { Person } from './person.ts';
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable public person: Person;
}
```

{% endtab %}

{% tab title="my-app.html" %}

```typescript
@customElement({ name:'my-app', template: '<my-el person="john"></my-el>' })
export class MyApp { }
```

{% endtab %}
{% endtabs %}

According to the `Person#coercer` implementation, for the example above `MyEl#person` will be assigned an instance of `Person` that is equivalent to `new Person('john', null)`.

#### Using the `@coercer` decorator

Aurelia2 also offers a `@coercer` decorator to declare a static method in the class as the coercer. The previous example can be rewritten as follows using the `@coercer` decorator.

{% tabs %}
{% tab title="person.ts" %}

```typescript
import { coercer } from '@aurelia/runtime-html';

export class Person {
  public constructor(
    public readonly name: string,
    public readonly age: number,
  ) { }

  @coercer
  public static createFrom(value: unknown): Person {
    if (value instanceof Person) return value;
    if (typeof value === 'string') {
      try {
        const json = JSON.parse(value) as Person;
        return new this(json.name, json.age);
      } catch {
        return new this(value, null!);
      }
    }
    if (typeof value === 'number') {
      return new this(null!, value);
    }
    if (typeof value === 'object' && value != null) {
      return new this((value as any).name, (value as any).age);
    }
    return new this(null!, null!);
  }
}
```

{% endtab %}

{% tab title="my-el.ts" %}

```typescript
import { Person } from './person.ts';

@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable public person: Person;
}
```

{% endtab %}

{% tab title="my-app.html" %}

```typescript
@customElement({ name:'my-app', template: '<my-el person="john"></my-el>' })
export class MyApp { }
```

{% endtab %}
{% endtabs %}

With the `@coercer` decorator, you are free to name the static method as you like.

### Coercing nullable values

To maintain backward compatibility, Aurelia2 does not attempt to coerce `null` and `undefined` values. We believe that this default choice should avoid unnecessary surprises and code breaks when migrating to newer versions of Aurelia.

However, you can explicitly mark a `@bindable` to be not nullable.

```typescript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable({ nullable: false }) public num: number;
}
```

When `nullable` is set to `false`, Aurelia2 will try to coerce the `null` and `undefined` values.

### `set` and auto-coercion

It is important to note that an explicit `set` (see [bindable setter](#bindable-setter)) function is always prioritized over the `type`. In fact, the auto-coercion is the fallback for the `set` function. Hence whenever `set` is defined, the auto-coercion becomes non-operational.

**However, this gives you an opportunity to:**

* Override any of the default primitive type coercing behavior, or
* Disable coercion selectively for a few selective `@bindable` by using a `noop` function for `set`.

{% hint style="info" %}
Aurelia2 already exposes a `noop` function saving your effort to write such boring functions.
{% endhint %}

### Union types

When using TypeScript, usages of union types are not rare. However, using union types for `@bindable` will deactivate the auto-coercion.

```typescript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable public num: number | string;
}
```

For the example above, the type metadata supplied by TypeScript will be `Object` disabling the auto-coercion.

To coerce union types, you can explicitly specify a `type`.

```typescript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable({type: String}) public num: number | string;
}
```

However, using a setter would be more straightforward to this end.

```typescript
@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable({set(v: unknown) {... return coercedV;}}) public num: number | string;
}
```

{% hint style="info" %}
Even though using a `noop` function for `set` function is a straightforward choice, `Object` can also be used for `type` in the bindable definition to disable the auto-coercion for selective `@bindable`s (that is when the automatic type-coercion is enabled).
{% endhint %}

### Bindables spreading

Spreading syntaxes are supported for simpler binding of multiple bindable properties.

Given the following component:

```typescript
export class NameTag {
  @bindable first
  @bindable last
}
```

with template:

```html
<b>${first.toUpperCase()}</b> ${last}
```

and its usage template:

```html
<name-tag ...$bindables="{ first: 'John', last: 'Doe' }"></name-tag>
```

The rendered html will be:

```html
<b>JOHN</b> Doe
```

Here we are using `...$bindables` to express that we want to bind all properties in the object `{ first: 'John', last: 'Doe' }` to bindable properties on `<name-tag>` component. The `...$bindables="..."` syntax will only connect properties that are matching with bindable properties on `<name-tag>`, so even if an object with hundreds of properties are given to a `...$bindables` binding, it will still resulted in 2 bindings for `first` and `last`.

`...$bindables` also work with any expression, rather than literal object, per the following examples:

```html
<name-tag $bindables.spread="customer1">
<name-tag $bindables.spread="customer.details">
<name-tag $bindables.spread="customer[this_that]">
<name-tag $bindables.spread="customer1 | mapDetails">
<name-tag $bindables.spread="customer.details | simplify">
<name-tag $bindables.spread="customer[this_that] | addDetails">
```

#### Shorthand syntax

Sometimes when the expression of the spread binding is simple, we can simplify the binding even further. Default templating syntax of Aurelia supports a shorter version of the above examples:

```html
<name-tag ...customer1>
<name-tag ...customer.details>
<name-tag ...customer[this_that]>

or if you need space in the expression:
<name-tag ...$bindables="customer1 | mapDetails">
<name-tag ...$bindables="customer.details | simplify">
<name-tag ...$bindables="customer[this_that] | addDetails">
```

{% hint style="warning" %}

* Remember that HTML is case insensitive, so `...firstName` actually will be seen as `...firstname`, for example
* Bindables properties will be tried to matched as is, which means a `firstName` bindable property will match an object `firstName` property, but not `first-name`
* If the expression contains space, it will result into multiple attributes and thus won't work as intended with spread syntax `...`. For example `...a + b` will be actually turned into 3 attributes: `...a`, `+` and `b`
  {% endhint %}

#### Binding orders

The order of the bindings created will be the same with the order declared in the template. For example, for the `NameTag` component above, if we have a usage

```html
<name-tag id="1" first="John" ...$bindables="{ first: 'Jane' }">
<name-tag id="2" ...$bindables="{ first: 'Jane' }" first="John">
```

Then the value of the `first` property in `NameTag` with `id=1` will be `Jane`, and the value of `first` property in `NameTag` with `id=2` will be `John`.

{% hint style="warning" %}

* An exception of this order is when bindables spreading is used together with [`...$attrs`](#attributes-transferring), `...$attrs` will always result in bindings after `...$bindables`/`$bindables.spread`/`...expression`.
  {% endhint %}

#### Observation behavior

Bindings will be created based on the keys available in the object evaluated from the `expression` of a spread binding. The following example illustrate the behavior:

For the `NameTag` component above:

```html
<let item.bind="{ first: 'John' }">
<name-tag ...item></name-tag>
<button click.trigger="item.last = 'Doe'">Change last name</button>
```

The rendered HTML of `<name-tag>` will be

```html
<b>JOHN</b>
```

When clicking on the button with text `Change last name`, the rendered html of `<name-tag>` won't be changed, as the original object given to `<name-tag>` doesn't contain `last`, hence it wasn't observed, which ignore our new value set from the button click. If it's desirable to reset the observation, give a new object to the spread binding, like the following example:

```html
<let item.bind="{ first: 'John' }">
<name-tag ...item></name-tag>
<button click.trigger="item = { first: item.name, last: 'Doe' }">Change last name</button>
```

{% hint style="success" %}

* With the above behavior of non-eager binding, applications can have the opportunity to leave some bindable properties untouched, while with the opposite behavior of always observing all properties on the given object based on the number of bindable properties, missing value (`null`/`undefined`) will start flowing in in an unwanted way.
  {% endhint %}

There are some other behaviors of the spread binding that are worth noting:

* All bindings created with `$bindables.spread` or `...` syntax will have binding mode equivalent to `to-view`, binding behavior cannot alter this. Though other binding behavior like `throttle`/`debounce` can still work.
* If the same object is returned from evaluating the expression, the spread binding won't try to rebind its inner bindings. This means mutating and then reassigning won't result in new binding, instead, give the spread binding a new object.

### Attributes Transferring

Attribute transferring is a way to relay the binding(s) on a custom element to its child element(s).

As an application grows, the components inside it also grow. Something that starts simple, like the following component

```typescript
export class FormInput {
  @bindable label
  @bindable value
}
```

with the template

```html
<label>${label}
  <input value.bind="value">
</label>
```

can quickly grow out of hand with a number of needs for configuration: aria, type, min, max, pattern, tooltip, validation etc...

After a while, the `FormInput` component above will become more and more like a relayer to transfer the bindings from outside, to the elements inside it. This often results in an increase in the number of `@bindable`. While this is fine, you end up with components that have a lot of boilerplate.

```typescript
export class FormInput {
  @bindable label
  @bindable value
  @bindable type
  @bindable tooltip
  @bindable arias
  @bindable etc
}
```

And the usage of our component would look like this:

```html
<form-input
  label.bind="label"
  value.bind="message"
  tooltip.bind="Did you know Aurelia syntax comes from an idea of an Angular community member? We greatly appreciate Angular and its community for this."
  validation.bind="...">
```

to be repeated like this inside:

```html
<label>${label}
  <input value.bind tooltip.bind validation.bind min.bind max.bind>
</label>
```

To juggle all the relevant pieces for such a task isn't difficult, but somewhat tedious. With attribute transferring, which is roughly close to object spreading in JavaScript, the above template should be as simple as:

```html
<label>${label}
  <input ...$attrs>
</label>
```

, which reads like this: for some bindings on `<form-input>`, change the targets of those bindings to the `<input>` element inside it.

#### Usage

To transfer attributes & bindings from a custom element, there are two steps:

* Set `capture` to `true` on a custom element via `@customElement` decorator:

```typescript
@customElement({
  ...,
  capture: true
})
```

Or use the `capture` decorator from `aurelia` package if you don't want to declare the `customElement` decorator and have to specify your name and template values.

```typescript
import { capture } from 'aurelia';

@capture
export class MyCustomElement {
  ...
}

// either form is valid
@capture()
export class MyCustomElement {
  ...
}
```

As the name suggests, this is to signal the template compiler that all the bindings & attributes, with some exceptions, should be captured for future usage.

**Spread the captured attributes onto an element**

Using the ellipsis syntax which you might be accustomed to from Javascript, we can spread our attributes onto an element proceeding the magic variable `$attrs`

```html
<input ...$attrs>
```

**Spread attributes and overriding specific ones**

In case you want to spread all attributes while explicitly overriding individual ones, make sure these come after the spread operator.

```html
<input value.bind="..." ...$attrs> spread wins
<input ...$attrs value.bind="..."> explicit wins
```

{% hint style="warning" %}
It's recommended that this feature should not be overused in multi-level capturing & transferring. This is often known as prop-drilling in React and could have a bad effect on the overall & long-term maintainability of an application. It's probably healthy to limit the max level of transferring to 2.
{% endhint %}

#### Usage with conventions

Aurelia conventions enable the setting of `capture` metadata from the template via `<capture>` tag, like the following example:

```html
<capture>

<input ...$attrs>
```

#### Attribute filtering

Sometimes it is desirable to capture only certain attributes on a custom element. Aurelia supports this via a function form of the custom element `capture` value: a function that takes 1 parameter (the attribute name) and returns a boolean to indicate whether it should be captured.

```typescript
@customElement({
  capture: attr => attr !== 'class'  // Captures all attributes except 'class'
})
```

**Note**: When using a capture filter function, you cannot use the standalone `@capture` decorator. You must specify the filter function within the `@customElement` decorator's `capture` property.

#### How it works

**What attributes are captured**

Everything except the template controller and custom element bindables are captured.

{% code title="form-input.ts" %}

```typescript
export class FormInput {
  @bindable label
}
```

{% endcode %}

A usage example is as follows:

{% code title="my-app.html" %}

```html
<form-input
  if.bind="needsComment"
  label.bind="label"
  value.bind="extraComment"
  class="form-control"
  style="background: var(--theme-purple)"
  tooltip="Hello, ${tooltip}">
```

{% endcode %}

**What is captured:**

* `value.bind="extraComment"`
* `class="form-control"`
* `style="background: var(--theme-purple)"`
* `tooltip="Hello, ${tooltip}"`

**What is not captured:**

* `if.bind="needsComment"` (`if` is a template controller)
* `label.bind="label"` (`label` is a bindable property)

**How will attributes be applied in ...$attrs**

Attributes that are spread onto an element will be compiled as if it was declared on that element.

This means `.bind` command will work as expected when it's transferred from some element onto some element that uses `.two-way` for `.bind`.

It also means that spreading onto a custom element will also work: if a captured attribute targets a bindable property of the applied custom element. An example:

```html
app.html
<input-field value.bind="message">

input-field.html
<my-input ...$attrs>
```

if `value` is a bindable property of `my-input`, the end result will be a binding that connects the `message` property of the corresponding `app.html` view model with `<my-input>` view model `value` property. The binding mode is also preserved like normal attributes.

### Performance Considerations

#### Spread vs Individual Bindings

When deciding between spread syntax and individual bindings, consider the following performance implications:

**Spread Syntax Benefits:**

* Reduces template verbosity and maintains cleaner code
* Automatically handles dynamic property sets
* Eliminates the need for manual bindable declarations for pass-through properties

**Individual Binding Benefits:**

* Slightly more efficient for small, known sets of properties
* Explicit property access provides better type safety
* Easier to debug specific binding issues

```typescript
// Performance comparison
export class ComponentWrapper {
  // Good for small, known property sets
  @bindable name: string;
  @bindable age: number;
  
  // Better for dynamic or large property sets
  @bindable userData: UserData;
}
```

#### Memory and Observation Overhead

Understanding the observation behavior helps optimize performance:

```typescript
// More efficient - limited observation
const userData = { name: 'John', age: 30 };
<user-profile ...userData>

// Less efficient - observes all properties
const userData = { name: 'John', age: 30, metadata: {...}, history: [...] };
<user-profile ...userData>
```

**Best Practice:** Use spread syntax when you need to pass through a focused set of properties, not entire large objects.

### Advanced Patterns

#### Complex Expression Patterns

Spread syntax supports complex expressions and transformations:

```html
<!-- Object transformation -->
<user-card ...user.profile>
<user-card ...user.permissions.admin>
<user-card ...getCurrentUser().settings>

<!-- With value converters -->
<user-card ...$bindables="user | formatUser">
<user-card ...$bindables="user.profile | selectFields:['name', 'email']">

<!-- Dynamic property selection -->
<user-card ...$bindables="user | pick:allowedFields">
```

#### Combining Spread with Other Binding Features

```html
<!-- Spread with binding behaviors -->
<input-field ...$bindables="formData | filterEmpty & debounce:500">

<!-- Spread with conditional logic -->
<user-form ...$bindables="user & if:isEditMode">

<!-- Mixed binding patterns -->
<input-field ...user 
             ...$attrs 
             id.bind="fieldId" 
             class="form-control"
             validation.bind="userValidation">
```

#### Error Handling Patterns

```typescript
// Null-safe spreading
@customElement({ name: 'safe-component' })
export class SafeComponent {
  @bindable data: any;
  
  get safeData() {
    return this.data || {};
  }
}
```

```html
<!-- Template with null safety -->
<safe-component ...safeData>
<!-- or -->
<safe-component ...$bindables="data || {}">
```

### Best Practices

#### When to Use Spread Syntax

**✅ Good Use Cases:**

* Component composition and wrapper components
* Dynamic forms with varying field sets
* Passing through configuration objects
* Creating reusable component libraries

**❌ Avoid When:**

* You need explicit control over individual bindings
* Working with large objects with many irrelevant properties
* Performance is critical and you're binding a small, known set of properties
* You need different binding modes for different properties

#### Maintainability Guidelines

```typescript
// Good: Clear, focused spreading
export class FormField {
  @bindable label: string;
  @bindable required: boolean;
  
  // Spread for input-specific attributes
  @customElement({ capture: true })
}
```

```html
<!-- Template shows clear intent -->
<label>${label}
  <input ...$attrs class="form-control">
</label>
```

**Recommended Limits:**

* Maximum 2 levels of attribute transferring to avoid prop-drilling
* Use spread for groups of related properties, not entire application state
* Document spread behavior in component interfaces

#### Type Safety Considerations

```typescript
// Define interfaces for spread objects
interface UserProfile {
  name: string;
  email: string;
  avatar?: string;
}

export class UserCard {
  @bindable profile: UserProfile;
}
```

```html
<!-- Type-safe spreading -->
<user-card ...user.profile>
```

### Common Patterns and Anti-Patterns

#### ✅ Recommended Patterns

```typescript
// Pattern 1: Wrapper Components
@customElement({ name: 'styled-input', capture: true })
export class StyledInput {
  @bindable label: string;
  @bindable theme: string;
}
```

```html
<div class="input-group ${theme}">
  <label>${label}</label>
  <input ...$attrs>
</div>
```

```typescript
// Pattern 2: Configuration Objects
export class DataTable {
  @bindable columns: Column[];
  @bindable options: TableOptions;
}
```

```html
<data-table columns.bind="userColumns" ...tableConfig>
```

#### ❌ Anti-Patterns to Avoid

```typescript
// Anti-pattern 1: Spreading entire application state
export class BadComponent {
  @bindable appState: ApplicationState; // Too broad
}
```

```html
<!-- Anti-pattern 2: Excessive nesting -->
<wrapper-1 ...data>
  <wrapper-2 ...data>
    <wrapper-3 ...data>
      <actual-component ...data>
    </wrapper-3>
  </wrapper-2>
</wrapper-1>
```

### Debugging and Troubleshooting

#### Common Issues

1. **HTML Case Sensitivity**

   ```html
   <!-- Problem: HTML converts to lowercase -->
   <component ...firstName> <!-- becomes ...firstname -->

   <!-- Solution: Use explicit binding -->
   <component ...$bindables="{ firstName: user.firstName }">
   ```
2. **Property Observation Not Working**

   ```typescript
   // Problem: Adding properties after initial binding
   this.userData.newProperty = 'value'; // Not observed

   // Solution: Replace the entire object
   this.userData = { ...this.userData, newProperty: 'value' };
   ```
3. **Binding Mode Conflicts**

   ```html
   <!-- Problem: Spread overrides explicit binding -->
   <input value.two-way="data.value" ...formConfig>

   <!-- Solution: Put explicit bindings after spread -->
   <input ...formConfig value.two-way="data.value">
   ```

#### Debugging Tips

```typescript
// Enable detailed binding information in development
@customElement({ 
  name: 'debug-component',
  capture: (attr: string) => {
    if (__DEV__) {
      console.log('Capturing attribute:', attr);
    }
    return !attr.startsWith('debug-');
  }
})
```

Understanding these patterns and considerations will help you use Aurelia's spread syntax effectively while maintaining good performance and code maintainability.


# Styling components

Master the art of dynamic styling in Aurelia 2. Learn everything from basic class toggling to advanced CSS custom properties, plus component styling strategies that will make your apps both beautiful

Dynamic styling is a fundamental aspect of modern web applications, and Aurelia 2 provides powerful, flexible mechanisms for binding CSS classes and styles to your elements. Whether you need to toggle an active state, implement a theming system, or create responsive layouts, Aurelia's binding system makes these tasks straightforward and maintainable.

This comprehensive guide covers everything from basic class toggling to advanced styling techniques, giving you the knowledge and tools to implement any styling requirement in your Aurelia 2 applications.

## Basic Class Binding

The most common use case for dynamic styling is conditionally applying CSS classes based on component state.

### Single Class Binding: The `.class` Syntax

The `.class` binding is the foundation of dynamic styling in Aurelia. The syntax is straightforward:

```html
<button submit.class="isFormValid">Submit Form</button>
<div loading.class="isLoading">Content here...</div>
<nav-item active.class="isCurrentPage">Home</nav-item>
```

```typescript
export class MyComponent {
  isFormValid = false;
  isLoading = true;
  isCurrentPage = false;

  // When isFormValid becomes true, the 'submit' class gets added
  // When isLoading is false, the 'loading' class gets removed
}
```

**How it works**: The syntax is `className.class="booleanExpression"`. When the expression is truthy, the class is added. When it's falsy, the class is removed.

{% hint style="info" %}
**Note**: You can use any valid CSS class name, including ones with special characters like `my-awesome-class.class="isAwesome"` or Unicode characters like `✓.class="isComplete"`.
{% endhint %}

{% hint style="warning" %}
**TailwindCSS note**: Tailwind’s content scanner won’t pick up class names that only appear in attribute names. For Tailwind classes that include special characters (for example `width-[360px]`), prefer the object form with `class.bind` so the class token appears in an attribute value:

```html
<div class.bind="{ 'width-[360px]': condition }"></div>
```

{% endhint %}

### Multiple Classes: Comma-Separated Syntax

When you need to toggle multiple related classes together, you can use comma-separated class names:

```html
<div alert,alert-danger,fade-in,shake.class="hasError">
  Error message content
</div>
```

```typescript
export class ErrorComponent {
  hasError = false;

  triggerError() {
    this.hasError = true; // All four classes get added at once!
  }

  clearError() {
    this.hasError = false; // All four classes get removed together
  }
}
```

**Important**: No spaces around the commas! The parser expects `class1,class2,class3`, not `class1, class2, class3`.

## Style Binding

Aurelia provides multiple approaches for binding CSS styles, from individual properties to complex style objects.

### Single Style Properties

To bind individual CSS properties dynamically, use the `.style` syntax:

```html
<div background-color.style="themeColor">Themed content</div>
<progress width.style="progressPercentage + '%'">Loading...</progress>
<aside opacity.style="sidebarVisible ? '1' : '0.3'">Sidebar</aside>
```

```typescript
export class ThemedComponent {
  themeColor = '#3498db';
  progressPercentage = 75;
  sidebarVisible = true;
}
```

### Alternative Style Syntax

Aurelia supports two equivalent syntaxes for style binding:

```html
<!-- These do exactly the same thing! -->
<div background-color.style="myColor"></div>
<div style.background-color="myColor"></div>

<!-- Works with any CSS property -->
<div font-size.style="textSize"></div>
<div style.font-size="textSize"></div>
```

Use whichever feels more natural to you. Some developers prefer the first syntax because it reads like "set the background-color style to myColor", while others prefer the second because it's more similar to traditional CSS.

### CSS Custom Properties

Aurelia fully supports CSS custom properties (CSS variables), enabling powerful theming capabilities:

```html
<div --primary-color.style="brandColor">
  <p style="color: var(--primary-color)">Branded text!</p>
</div>

<!-- Or with the alternative syntax -->
<div style.--primary-color="brandColor">
  <p style="color: var(--primary-color)">Same result!</p>
</div>
```

```typescript
export class ThemeManager {
  brandColor = '#e74c3c';

  switchToDarkMode() {
    this.brandColor = '#34495e';
  }
}
```

### Vendor Prefixes

Aurelia supports vendor-prefixed CSS properties for cross-browser compatibility:

```html
<div -webkit-user-select.style="userSelectValue">Non-selectable content</div>
<div style.-webkit-user-select="userSelectValue">Alternative syntax</div>
```

### The `!important` Declaration

Aurelia automatically handles the `!important` CSS declaration when included in style values:

```typescript
export class ImportantComponent {
  criticalColor = 'red!important';

  // Aurelia automatically:
  // 1. Strips the !important from the value
  // 2. Sets the CSS property priority correctly
  // 3. Applies the style with proper priority
}
```

## Advanced Class Binding Techniques

Advanced class binding techniques provide greater flexibility for complex styling scenarios.

### String-Based Class Binding

For scenarios requiring more flexibility than boolean toggling, you can bind class strings directly:

```html
<div class.bind="dynamicClasses">Content with dynamic classes</div>
<div class="base-class ${additionalClasses}">Mixed static and dynamic</div>
```

```typescript
export class FlexibleComponent {
  dynamicClasses = 'btn btn-primary active';
  additionalClasses = 'fade-in hover-effect';

  updateClasses() {
    this.dynamicClasses = `btn btn-${this.isSuccess ? 'success' : 'danger'}`;
  }
}
```

**When to use what**:

* `.class` syntax: When you need boolean toggling of specific classes
* `class.bind`: When you need to build class strings dynamically
* Template interpolation: When you want to mix static and dynamic classes

## Advanced Style Binding

Advanced style binding techniques enable sophisticated styling patterns and better code organization.

### Object-Based Style Binding

For complex styling scenarios, bind an entire style object:

```html
<div style.bind="cardStyles">Beautifully styled card</div>
```

```typescript
export class StylishComponent {
  cardStyles = {
    backgroundColor: '#ffffff',
    border: '1px solid #e1e1e1',
    borderRadius: '8px',
    padding: '16px',
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
  };

  switchToNightMode() {
    this.cardStyles = {
      ...this.cardStyles,
      backgroundColor: '#2d3748',
      color: '#ffffff',
      borderColor: '#4a5568'
    };
  }
}
```

### String Interpolation

Combine static and dynamic styles using template interpolation:

```html
<div style="padding: 16px; background: ${bgColor}; transform: scale(${scale})">
  Combined static and dynamic styles
</div>
```

```typescript
export class HybridComponent {
  bgColor = 'linear-gradient(45deg, #3498db, #2ecc71)';
  scale = 1.0;

  animateIn() {
    this.scale = 1.1;
  }
}
```

### Computed Style Properties

Create dynamic styles based on component state:

```typescript
export class ComputedStyleComponent {
  progress = 0.7;
  theme = 'light';

  get progressBarStyles() {
    return {
      width: `${this.progress * 100}%`,
      backgroundColor: this.theme === 'dark' ? '#3498db' : '#2ecc71',
      transition: 'all 0.3s ease'
    };
  }
}
```

```html
<div class="progress-container">
  <div class="progress-bar" style.bind="progressBarStyles"></div>
</div>
```

## Component Styling Strategies

Beyond template bindings, Aurelia provides several approaches for styling components themselves.

### Convention-Based Styling

Aurelia automatically imports stylesheets that match your component names:

```
my-awesome-component.ts    (component logic)
my-awesome-component.html  (template)
my-awesome-component.css   (styles - automatically imported!)
```

This means you can focus on writing CSS without worrying about imports:

```css
/* my-awesome-component.css */
:host {
  display: block;
  padding: 16px;
}

.content {
  background: linear-gradient(45deg, #3498db, #2ecc71);
  border-radius: 8px;
}
```

### Shadow DOM

For complete style isolation, use Shadow DOM:

```typescript
import { useShadowDOM } from 'aurelia';

@useShadowDOM()
export class IsolatedComponent {
  // Styles are completely encapsulated
}
```

**Shadow DOM Configuration Options**:

```typescript
// Open mode (default) - JavaScript can access shadowRoot
@useShadowDOM({ mode: 'open' })
export class OpenComponent { }

// Closed mode - shadowRoot is not accessible
@useShadowDOM({ mode: 'closed' })
export class ClosedComponent { }

// To use Light DOM (no Shadow DOM), simply don't use the decorator
export class LightDomComponent { }
```

### Shadow DOM Special Selectors

Shadow DOM provides special CSS selectors for enhanced styling control:

```css
/* Style the component host element */
:host {
  display: block;
  border: 1px solid #e1e1e1;
}

/* Style the host when it has a specific class */
:host(.active) {
  background-color: #f8f9fa;
}

/* Style the host based on ancestor context */
:host-context(.dark-theme) {
  background-color: #2d3748;
  color: #ffffff;
}

/* Style slotted content */
::slotted(.special-content) {
  font-weight: bold;
  color: #3498db;
}
```

### Global Shared Styles in Shadow DOM

To share styles across Shadow DOM components, configure shared styles in your application:

```typescript
// main.ts
import Aurelia, { StyleConfiguration } from 'aurelia';
import { MyApp } from './my-app';
import bootstrap from 'bootstrap/dist/css/bootstrap.css';
import customTheme from './theme.css';

Aurelia
  .register(StyleConfiguration.shadowDOM({
    sharedStyles: [bootstrap, customTheme]
  }))
  .app(MyApp)
  .start();
```

### CSS Modules

CSS Modules provide scoped styling by transforming class names to unique identifiers at build time. Aurelia provides the `cssModules()` helper to integrate CSS Modules with your components:

```typescript
import { customElement, cssModules } from 'aurelia';

// Import the CSS module (bundler provides the class mapping)
import styles from './my-component.module.css';
// styles = { title: 'title_abc123', button: 'button_def456' }

@customElement({
  name: 'my-component',
  template: `
    <h1 class="title">My Title</h1>
    <button class="button">Click Me</button>
  `,
  dependencies: [cssModules(styles)]
})
export class MyComponent {}
```

The `cssModules()` helper transforms class names in your template at compile time. In the example above, `class="title"` becomes `class="title_abc123"`.

**Key features:**

* Works with static classes, `class.bind`, and interpolation (`class="some ${myClass}"`)
* Supports multi-class binding syntax (`class1,class2.class="condition"`)
* Each component must register its own `cssModules()` - mappings do not inherit to child components

For more details on using CSS Modules with Shadow DOM, see the [Shadow DOM documentation](/components/shadow-dom#using-css-modules-with-shadow-dom).

## Real-World Examples and Patterns

The following examples demonstrate practical applications of class and style binding techniques in common scenarios.

### Responsive Design with Dynamic Classes

```typescript
export class ResponsiveComponent {
  screenSize = 'desktop';

  get responsiveClasses() {
    return {
      'mobile-layout': this.screenSize === 'mobile',
      'tablet-layout': this.screenSize === 'tablet',
      'desktop-layout': this.screenSize === 'desktop'
    };
  }

  @listener('resize', window)
  updateScreenSize() {
    const width = window.innerWidth;
    if (width < 768) {
      this.screenSize = 'mobile';
    } else if (width < 1024) {
      this.screenSize = 'tablet';
    } else {
      this.screenSize = 'desktop';
    }
  }
}
```

```html
<div class.bind="responsiveClasses">
  <header class="header ${screenSize === 'mobile' ? 'mobile-header' : ''}">
    <!-- Responsive header -->
  </header>
</div>
```

### Theme System with CSS Variables

```typescript
export class ThemeManager {
  currentTheme = 'light';

  get themeVariables() {
    const themes = {
      light: {
        '--primary-color': '#3498db',
        '--background-color': '#ffffff',
        '--text-color': '#333333'
      },
      dark: {
        '--primary-color': '#2ecc71',
        '--background-color': '#2d3748',
        '--text-color': '#ffffff'
      }
    };

    return themes[this.currentTheme];
  }

  toggleTheme() {
    this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
  }
}
```

```html
<div style.bind="themeVariables" class="theme-container">
  <button
    style="background: var(--primary-color); color: var(--text-color)"
    click.trigger="toggleTheme()">
    Toggle Theme
  </button>
</div>
```

### Loading States with Animations

```typescript
export class LoadingComponent {
  isLoading = false;
  loadingProgress = 0;

  async loadData() {
    this.isLoading = true;
    this.loadingProgress = 0;

    // Simulate loading with progress
    const interval = setInterval(() => {
      this.loadingProgress += 10;
      if (this.loadingProgress >= 100) {
        clearInterval(interval);
        this.isLoading = false;
      }
    }, 100);
  }

  get progressBarStyle() {
    return {
      width: `${this.loadingProgress}%`,
      transition: 'width 0.1s ease'
    };
  }
}
}
```

```html
<div loading.class="isLoading">
  <div class="progress-container" show.bind="isLoading">
    <div class="progress-bar" style.bind="progressBarStyle"></div>
  </div>

  <div class="content" hide.bind="isLoading">
    <!-- Your actual content -->
  </div>
</div>
```

### Complex Form Validation Styling

```typescript
export class ValidationForm {
  email = '';
  password = '';

  get emailValidation() {
    return {
      isEmpty: !this.email,
      isInvalid: this.email && !this.isValidEmail(this.email),
      isValid: this.email && this.isValidEmail(this.email)
    };
  }

  get passwordValidation() {
    return {
      isEmpty: !this.password,
      isTooShort: this.password && this.password.length < 8,
      isValid: this.password && this.password.length >= 8
    };
  }

  isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}
```

```html
<form>
  <div class="field">
    <input
      type="email"
      value.bind="email"
      empty.class="emailValidation.isEmpty"
      invalid.class="emailValidation.isInvalid"
      valid.class="emailValidation.isValid">

    <span
      class="error-message"
      show.bind="emailValidation.isInvalid">
      Please enter a valid email
    </span>

    <span
      class="success-indicator"
      show.bind="emailValidation.isValid">
      ✓
    </span>
  </div>
</form>
```

## Performance Tips and Best Practices

### Do's and Don'ts

**✅ DO:**

* Use `.class` for simple boolean toggling
* Use CSS custom properties for theming
* Prefer computed getters for complex style calculations
* Use Shadow DOM for true component isolation
* Cache complex style objects when possible

**❌ DON'T:**

* Inline complex style calculations in templates
* Use string concatenation for class names when `.class` will do
* Forget about CSS specificity when using `!important`
* Mix too many styling approaches in one component

### Performance Optimization

```typescript
export class OptimizedComponent {
  private _cachedStyles: any = null;
  private _lastTheme: string = '';

  // Cache expensive style calculations
  get expensiveStyles() {
    if (this._cachedStyles && this._lastTheme === this.currentTheme) {
      return this._cachedStyles;
    }

    this._cachedStyles = this.calculateComplexStyles();
    this._lastTheme = this.currentTheme;
    return this._cachedStyles;
  }

  private calculateComplexStyles() {
    // Your expensive calculations here
    return { /* styles */ };
  }
}
```

## Troubleshooting Common Issues

### "My styles aren't updating!"

**Problem**: Styles don't change when data changes. **Solution**: Make sure you're using proper binding syntax and that your properties are observable.

```typescript
// ❌ This won't trigger updates
export class BadComponent {
  styles = { color: 'red' };

  changeColor() {
    this.styles.color = 'blue'; // Mutation won't be detected
  }
}

// ✅ This will work
export class GoodComponent {
  styles = { color: 'red' };

  changeColor() {
    this.styles = { ...this.styles, color: 'blue' }; // New object
  }
}
```

### "My CSS classes have weird names!"

**Problem**: Using CSS Modules and seeing transformed class names. **Solution**: This is expected behavior! CSS Modules transform class names to ensure uniqueness.

### "Shadow DOM is blocking my global styles!"

**Problem**: Global CSS frameworks aren't working inside Shadow DOM components. **Why**: Shadow DOM isolates styles; open/closed mode does not change CSS encapsulation. **Solutions**:

* Use Light DOM for components that should inherit global framework styles
* Register framework CSS as shared styles with `StyleConfiguration.shadowDOM({ sharedStyles: [...] })`
* Use CSS variables or `::part` to expose safe customization points

**Learn more**: [Shadow DOM guide](/components/shadow-dom)

## Migration and Compatibility

### Coming from Aurelia 1?

The syntax is mostly the same, with some improvements:

```html
<!-- Aurelia 1 & 2 (still works) -->
<div class.bind="myClasses"></div>

<!-- Aurelia 2 (new!) -->
<div loading,spinner,active.class="isLoading"></div>
```

### Browser Support

All binding features work in modern browsers. For older browsers:

* Shadow DOM requires a polyfill for older browsers
* CSS Modules work everywhere (they're processed at build time)

## Summary

This guide has covered the complete range of class and style binding capabilities in Aurelia 2. Key takeaways include:

1. **Basic class binding** - Use `.class` syntax for simple boolean toggling
2. **Multiple class binding** - Leverage comma-separated syntax for related classes
3. **Style property binding** - Apply individual CSS properties with `.style` syntax
4. **Advanced techniques** - Implement complex styling with objects, interpolation, and CSS variables
5. **Component styling** - Choose appropriate encapsulation strategies for your use case

These techniques provide the foundation for building maintainable, dynamic user interfaces that respond effectively to application state changes.

***

**Additional Resources**: For more information on binding syntax, see the [template syntax guide](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/template-syntax/README.md). To understand when styles are applied, refer to the [component lifecycles](/components/component-lifecycles) documentation.


# Shadow DOM

Learn how to use Shadow DOM in Aurelia components for style encapsulation and native web component features.

Shadow DOM provides native browser encapsulation for your components, isolating styles and DOM structure. Aurelia makes it easy to enable Shadow DOM for any custom element.

## Quick decision guide

Start with **Light DOM** unless you have a specific reason to enable Shadow DOM. Shadow DOM isolates styles by design, so global CSS frameworks and app-wide themes **do not** flow into components unless you explicitly share them. You can mix Light DOM and Shadow DOM in the same app—use Shadow DOM only on components that need strict isolation or native web component features.

## Enabling Shadow DOM

### Using the @useShadowDOM Decorator

The simplest way to enable Shadow DOM is with the `@useShadowDOM` decorator:

```typescript
import { customElement, useShadowDOM } from 'aurelia';

@customElement('my-card')
@useShadowDOM()
export class MyCard {
  message = 'Hello from Shadow DOM';
}
```

By default, this creates a shadow root with `mode: 'open'`.

### Configuring Shadow DOM Mode

Shadow DOM supports two modes: `open` and `closed`.

**Open mode** (default) allows external JavaScript to access the shadow root:

```typescript
@customElement('open-element')
@useShadowDOM({ mode: 'open' })
export class OpenElement {
  // External code can access: element.shadowRoot
}
```

**Closed mode** prevents external access to the shadow root:

```typescript
@customElement('closed-element')
@useShadowDOM({ mode: 'closed' })
export class ClosedElement {
  // External code cannot access shadowRoot
  // element.shadowRoot returns null
}
```

Mode only controls JavaScript access to the shadow root. It does **not** change CSS encapsulation—global styles still will not cross the shadow boundary in open mode.

### Using the Configuration Object

You can also configure Shadow DOM using the `@customElement` decorator's configuration object:

```typescript
import { customElement } from 'aurelia';

@customElement({
  name: 'my-element',
  shadowOptions: { mode: 'open' }
})
export class MyElement {}
```

Or using a static property:

```typescript
export class MyElement {
  static shadowOptions = { mode: 'open' };
}
```

## Styling Shadow DOM Components

Shadow DOM provides complete CSS isolation. Styles defined outside the component won't affect elements inside, and styles inside won't leak out.

## Troubleshooting checklist

If styles or slots are not behaving as expected, check these first:

* **Confirm the component actually uses Shadow DOM**: `@useShadowDOM()` or `shadowOptions` must be set.
* **Global CSS won’t cross the boundary**: Use Light DOM for framework styles, or register shared styles with `StyleConfiguration.shadowDOM({ sharedStyles: [...] })`.
* **Co-located CSS is not auto-injected**: Import CSS as a string and pass it to `shadowCSS()` for Shadow DOM components.
* **Use Shadow DOM selectors**: `:host`, `:host-context()`, and `::slotted()` apply inside the shadow root. Use CSS variables or `::part` for safe theming.
* **Slots require Shadow DOM**: Native `<slot>` only works with Shadow DOM; use `<au-slot>` if you stay in Light DOM.
* **Containerless is incompatible**: You cannot use Shadow DOM and `@containerless` together.
* **Debugging needs open mode**: `mode: 'open'` makes it easier to inspect and tweak styles in DevTools.

### Component-Local Styles

Use the `shadowCSS` helper to register styles for your component:

```typescript
import { customElement, useShadowDOM, shadowCSS } from 'aurelia';

@customElement({
  name: 'styled-card',
  template: `
    <div class="card">
      <h2 class="title">\${title}</h2>
      <div class="content">
        <slot></slot>
      </div>
    </div>
  `,
  dependencies: [
    shadowCSS(`
      .card {
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 16px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .title {
        margin: 0 0 12px 0;
        color: #333;
      }
      .content {
        color: #666;
      }
    `)
  ]
})
@useShadowDOM()
export class StyledCard {
  title = 'Card Title';
}
```

### Using Constructable Stylesheets

For better performance and reusability, you can pass `CSSStyleSheet` instances:

```typescript
// Create a reusable stylesheet
const cardStyles = new CSSStyleSheet();
cardStyles.replaceSync(`
  .card {
    border: 1px solid #ddd;
    padding: 16px;
  }
`);

@customElement({
  name: 'optimized-card',
  template: '<div class="card"><slot></slot></div>',
  dependencies: [shadowCSS(cardStyles)]
})
@useShadowDOM()
export class OptimizedCard {}
```

### Global Shared Styles

Configure styles that apply to all Shadow DOM components in your application:

```typescript
import Aurelia from 'aurelia';
import { StyleConfiguration } from '@aurelia/runtime-html';

Aurelia
  .register(
    StyleConfiguration.shadowDOM({
      sharedStyles: [
        `
          * {
            box-sizing: border-box;
          }
          :host {
            display: block;
          }
        `
      ]
    })
  )
  .app(component)
  .start();
```

Global styles are applied first, followed by component-local styles.

Shared styles only apply to components that actually use Shadow DOM. They do not affect Light DOM components, and selectors like `html` or `body` still cannot reach into a shadow root. If you want a global CSS framework to style Shadow DOM components, import that stylesheet and include it in `sharedStyles`, or keep those components in Light DOM.

### Shadow DOM CSS Selectors

Shadow DOM provides special CSS selectors for enhanced styling control:

#### The `:host` Selector

Style the component's host element from within the shadow root:

```css
/* Inside your component's shadow DOM styles */
:host {
  display: block;
  border: 1px solid #e1e1e1;
  padding: 16px;
}

/* Style the host when it has a specific class */
:host(.highlighted) {
  background-color: #fff3cd;
  border-color: #ffc107;
}

/* Style the host when it has a specific attribute */
:host([disabled]) {
  opacity: 0.5;
  pointer-events: none;
}
```

#### The `:host-context()` Selector

Style the host based on an ancestor's context:

```css
/* When the component is inside a dark theme container */
:host-context(.dark-theme) {
  background-color: #2d3748;
  color: #ffffff;
}

/* When the component is inside a specific page */
:host-context(.admin-page) {
  border-left: 4px solid #dc3545;
}
```

#### The `::slotted()` Selector

Style content that has been projected into a slot:

```css
/* Style all slotted elements */
::slotted(*) {
  margin: 8px 0;
}

/* Style specific slotted elements */
::slotted(p) {
  font-size: 14px;
  line-height: 1.6;
}

/* Style slotted elements with a specific class */
::slotted(.highlight) {
  background-color: yellow;
}
```

{% hint style="warning" %}
**Important**: The `::slotted()` selector only works on direct children of the slot. It cannot select nested descendants within slotted content.
{% endhint %}

#### The `::part()` Selector

Expose specific elements for external styling using `part` attributes:

{% tabs %}
{% tab title="my-card.ts" %}

```typescript
import { customElement, useShadowDOM, shadowCSS } from 'aurelia';

@customElement({
  name: 'my-card',
  template: `
    <div part="container" class="card">
      <header part="header" class="card-header">
        <slot name="header"></slot>
      </header>
      <div part="body" class="card-body">
        <slot></slot>
      </div>
      <footer part="footer" class="card-footer">
        <slot name="footer"></slot>
      </footer>
    </div>
  `,
  dependencies: [
    shadowCSS(`
      .card { border: 1px solid #ddd; border-radius: 8px; }
      .card-header { padding: 16px; background: #f8f9fa; }
      .card-body { padding: 16px; }
      .card-footer { padding: 12px 16px; background: #f8f9fa; }
    `)
  ]
})
@useShadowDOM()
export class MyCard {}
```

{% endtab %}

{% tab title="app.css" %}

```css
/* Style the exposed parts from outside the component */
my-card::part(header) {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}

my-card::part(body) {
  min-height: 100px;
}

my-card::part(footer) {
  border-top: 1px solid #ddd;
}

/* Combine with pseudo-classes */
my-card::part(header):hover {
  background: linear-gradient(135deg, #764ba2, #667eea);
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `::part()` selector is the recommended way to create styling hooks for consumers of your components. It provides explicit control over which internal elements can be styled externally.
{% endhint %}

### Styling from Outside: CSS Custom Properties

The most flexible way to style Shadow DOM components from outside is using CSS custom properties (CSS variables):

{% tabs %}
{% tab title="my-button.ts" %}

```typescript
import { customElement, useShadowDOM, shadowCSS } from 'aurelia';

@customElement({
  name: 'my-button',
  template: '<button><slot></slot></button>',
  dependencies: [
    shadowCSS(`
      button {
        background: var(--button-bg, #007bff);
        color: var(--button-color, white);
        border: none;
        padding: 8px 16px;
        border-radius: var(--button-radius, 4px);
        cursor: pointer;
      }
      button:hover {
        background: var(--button-hover-bg, #0056b3);
      }
    `)
  ]
})
@useShadowDOM()
export class MyButton {}
```

{% endtab %}

{% tab title="app.html" %}

```html
<style>
  /* Theme the button from outside */
  .danger {
    --button-bg: #dc3545;
    --button-hover-bg: #c82333;
    --button-radius: 8px;
  }
</style>

<my-button>Default Button</my-button>
<my-button class="danger">Danger Button</my-button>
```

{% endtab %}
{% endtabs %}

### Using CSS Modules with Shadow DOM

CSS Modules provide class name transformation for avoiding naming conflicts. You can combine `cssModules()` with Shadow DOM for both style encapsulation and class name scoping:

```typescript
import { customElement, useShadowDOM, shadowCSS, cssModules } from 'aurelia';

// CSS Module mapping (typically imported from a .module.css file via your bundler)
const styles = {
  card: 'card_abc123',
  header: 'header_def456',
  body: 'body_ghi789'
};

@customElement({
  name: 'module-card',
  template: `
    <div class="card">
      <header class="header"><slot name="header"></slot></header>
      <div class="body"><slot></slot></div>
    </div>
  `,
  dependencies: [
    cssModules(styles),
    shadowCSS(`
      .card_abc123 { border: 1px solid #ddd; }
      .header_def456 { background: #f5f5f5; padding: 12px; }
      .body_ghi789 { padding: 16px; }
    `)
  ]
})
@useShadowDOM()
export class ModuleCard {}
```

{% hint style="info" %}
**How it works**: `cssModules()` transforms class names in your template at compile time, while `shadowCSS()` injects the actual CSS into the shadow root. When using CSS Modules with Shadow DOM, ensure your CSS rules use the transformed class names.
{% endhint %}

{% hint style="warning" %}
**Note**: CSS Modules mappings do not inherit to child components. Each component must register its own `cssModules()` dependency.
{% endhint %}

## Shadow DOM and Slots

Native `<slot>` elements **require** Shadow DOM. Attempting to use `<slot>` without Shadow DOM will throw a compilation error.

### Basic Slot Usage

```typescript
import { customElement, useShadowDOM } from 'aurelia';

@customElement({
  name: 'modal-dialog',
  template: `
    <div class="modal-overlay">
      <div class="modal-content">
        <slot></slot>
      </div>
    </div>
  `
})
@useShadowDOM()
export class ModalDialog {}
```

Usage:

```html
<modal-dialog>
  <h2>Modal Title</h2>
  <p>Modal content goes here</p>
</modal-dialog>
```

### Named Slots

```typescript
@customElement({
  name: 'card-layout',
  template: `
    <div class="card">
      <header class="card-header">
        <slot name="header"></slot>
      </header>
      <div class="card-body">
        <slot></slot>
      </div>
      <footer class="card-footer">
        <slot name="footer"></slot>
      </footer>
    </div>
  `
})
@useShadowDOM()
export class CardLayout {}
```

Usage:

```html
<card-layout>
  <span slot="header">Card Header</span>
  <p>Main content goes in the default slot</p>
  <div slot="footer">
    <button>Action</button>
  </div>
</card-layout>
```

### Fallback Content

Slots can have default content when nothing is projected:

```typescript
@customElement({
  name: 'greeting-card',
  template: `
    <div class="greeting">
      <slot>Hello, Guest!</slot>
    </div>
  `
})
@useShadowDOM()
export class GreetingCard {}
```

```html
<!-- Uses fallback -->
<greeting-card></greeting-card>
<!-- Output: Hello, Guest! -->

<!-- Overrides fallback -->
<greeting-card>Hello, John!</greeting-card>
<!-- Output: Hello, John! -->
```

### Listening to Slot Changes

React to changes in slotted content:

{% tabs %}
{% tab title="my-list.html" %}

```html
<div class="list">
  <slot slotchange.trigger="handleSlotChange($event)"></slot>
</div>
```

{% endtab %}

{% tab title="my-list.ts" %}

```typescript
import { customElement, useShadowDOM } from 'aurelia';

@customElement('my-list')
@useShadowDOM()
export class MyList {
  handleSlotChange(event: Event) {
    const slot = event.target as HTMLSlotElement;
    const assignedNodes = slot.assignedNodes();
    console.log('Slot changed, node count:', assignedNodes.length);
  }
}
```

{% endtab %}
{% endtabs %}

For more advanced slot usage, including the `@children` decorator and component view model retrieval, see the [Slotted Content documentation](/components/shadow-dom-and-slots).

## Constraints and Limitations

### Cannot Combine with @containerless

Shadow DOM requires a host element to attach to. You cannot use both `@useShadowDOM` and `@containerless` on the same component:

```typescript
// ❌ This will throw an error at runtime
@customElement('invalid-component')
@useShadowDOM()
@containerless()
export class InvalidComponent {}
```

**Error**: `Invalid combination: cannot combine the containerless custom element option with Shadow DOM.`

### Native Slots Require Shadow DOM

Using `<slot>` elements without enabling Shadow DOM will cause a compilation error:

```typescript
// ❌ This will throw a compilation error
@customElement({
  name: 'broken-component',
  template: '<div><slot></slot></div>'
  // Missing shadowOptions!
})
export class BrokenComponent {}
```

**Error**: `Template compilation error: detected a usage of "<slot>" element without specifying shadow DOM options in element: broken-component`

**Solution**: Either enable Shadow DOM or use `<au-slot>` instead:

```typescript
// ✅ Option 1: Enable Shadow DOM
@customElement({
  name: 'fixed-component',
  template: '<div><slot></slot></div>'
})
@useShadowDOM()
export class FixedComponent {}

// ✅ Option 2: Use <au-slot> without Shadow DOM
@customElement({
  name: 'alternative-component',
  template: '<div><au-slot></au-slot></div>'
})
export class AlternativeComponent {}
```

## Choosing Between Shadow DOM and Light DOM

### Use Shadow DOM When:

* **Style isolation is critical**: You need to prevent external styles from affecting your component
* **Building reusable components**: Your component will be used in different contexts and needs predictable styling
* **Using native web component features**: You need features like `<slot>`, CSS `:host` selector, or `::part`
* **Creating a design system**: Components should maintain consistent appearance regardless of environment

### Use Light DOM (no Shadow DOM) When:

* **Easy styling is important**: Parent components or application styles should easily affect the component
* **Working with global styles**: You rely on application-wide styles or CSS frameworks (Bulma/Bootstrap/Tailwind) to flow into components
* **SEO is a concern**: Search engines can more easily index light DOM content
* **Using `<au-slot>`**: You need Aurelia's slot features like `$host` scope access

## Practical Examples

### Themed Button Component

{% tabs %}
{% tab title="theme-button.ts" %}

```typescript
import { customElement, useShadowDOM, shadowCSS, bindable } from 'aurelia';

@customElement({
  name: 'theme-button',
  template: `
    <button class="btn \${variant}">
      <slot></slot>
    </button>
  `,
  dependencies: [
    shadowCSS(`
      .btn {
        padding: var(--btn-padding, 10px 20px);
        border: none;
        border-radius: var(--btn-radius, 4px);
        font-size: var(--btn-font-size, 16px);
        cursor: pointer;
        transition: all 0.2s;
      }
      .btn.primary {
        background: var(--primary-bg, #007bff);
        color: var(--primary-color, white);
      }
      .btn.primary:hover {
        background: var(--primary-hover, #0056b3);
      }
      .btn.secondary {
        background: var(--secondary-bg, #6c757d);
        color: var(--secondary-color, white);
      }
      .btn.secondary:hover {
        background: var(--secondary-hover, #545b62);
      }
    `)
  ]
})
@useShadowDOM()
export class ThemeButton {
  @bindable variant: 'primary' | 'secondary' = 'primary';
}
```

{% endtab %}

{% tab title="usage.html" %}

```html
<style>
  .custom-theme {
    --primary-bg: #28a745;
    --primary-hover: #218838;
    --btn-radius: 20px;
  }
</style>

<theme-button variant="primary">Default Primary</theme-button>
<theme-button variant="secondary">Default Secondary</theme-button>

<div class="custom-theme">
  <theme-button variant="primary">Custom Themed</theme-button>
</div>
```

{% endtab %}
{% endtabs %}

### Card with Multiple Slots

{% tabs %}
{% tab title="info-card.ts" %}

```typescript
import { customElement, useShadowDOM, shadowCSS, bindable } from 'aurelia';

@customElement({
  name: 'info-card',
  template: `
    <div class="card \${expanded ? 'expanded' : ''}">
      <header class="card-header" click.trigger="toggle()">
        <slot name="header">Untitled Card</slot>
        <span class="toggle">\${expanded ? '−' : '+'}</span>
      </header>
      <div class="card-body" if.bind="expanded">
        <slot></slot>
      </div>
      <footer class="card-footer" if.bind="expanded">
        <slot name="footer"></slot>
      </footer>
    </div>
  `,
  dependencies: [
    shadowCSS(`
      .card {
        border: 1px solid #ddd;
        border-radius: 8px;
        overflow: hidden;
        margin-bottom: 16px;
      }
      .card-header {
        background: #f8f9fa;
        padding: 16px;
        cursor: pointer;
        display: flex;
        justify-content: space-between;
        align-items: center;
        user-select: none;
      }
      .card-header:hover {
        background: #e9ecef;
      }
      .toggle {
        font-size: 24px;
        font-weight: bold;
      }
      .card-body {
        padding: 16px;
      }
      .card-footer {
        background: #f8f9fa;
        padding: 12px 16px;
        border-top: 1px solid #ddd;
      }
    `)
  ]
})
@useShadowDOM()
export class InfoCard {
  @bindable expanded = false;

  toggle() {
    this.expanded = !this.expanded;
  }
}
```

{% endtab %}

{% tab title="usage.html" %}

```html
<info-card expanded.bind="true">
  <strong slot="header">User Information</strong>

  <div>
    <p><strong>Name:</strong> John Doe</p>
    <p><strong>Email:</strong> john@example.com</p>
    <p><strong>Role:</strong> Developer</p>
  </div>

  <div slot="footer">
    <button>Edit</button>
    <button>Delete</button>
  </div>
</info-card>

<info-card>
  <span slot="header">System Status</span>
  <p>All systems operational</p>
</info-card>
```

{% endtab %}
{% endtabs %}

### Component with Dynamic Styles

{% tabs %}
{% tab title="progress-bar.ts" %}

```typescript
import { customElement, useShadowDOM, shadowCSS, bindable, resolve } from 'aurelia';
import { INode } from '@aurelia/runtime-html';

@customElement({
  name: 'progress-bar',
  template: `
    <div class="progress-container">
      <div class="progress-bar" css="width: \${percentage}%"></div>
      <span class="progress-text">\${percentage}%</span>
    </div>
  `,
  dependencies: [
    shadowCSS(`
      .progress-container {
        position: relative;
        width: 100%;
        height: 30px;
        background: #e9ecef;
        border-radius: 15px;
        overflow: hidden;
      }
      .progress-bar {
        height: 100%;
        background: var(--progress-color, #007bff);
        transition: width 0.3s ease;
      }
      .progress-text {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-weight: bold;
        color: #333;
      }
    `)
  ]
})
@useShadowDOM()
export class ProgressBar {
  @bindable percentage = 0;

  private host = resolve(INode);

  percentageChanged(newValue: number) {
    // Change color based on progress
    const color = newValue < 30 ? '#dc3545' :
                  newValue < 70 ? '#ffc107' :
                  '#28a745';
    this.host.style.setProperty('--progress-color', color);
  }
}
```

{% endtab %}

{% tab title="usage.html" %}

```html
<progress-bar percentage.bind="25"></progress-bar>
<progress-bar percentage.bind="50"></progress-bar>
<progress-bar percentage.bind="90"></progress-bar>
```

{% endtab %}
{% endtabs %}

## Best Practices

### 1. Use CSS Custom Properties for Theming

Allow users to customize your components through CSS variables with sensible defaults:

```typescript
shadowCSS(`
  .component {
    color: var(--component-color, #333);
    background: var(--component-bg, white);
    padding: var(--component-padding, 16px);
  }
`)
```

### 2. Provide Fallback Content for Slots

Give users a good default experience even when they don't provide slot content:

```html
<slot name="header">
  <h2>Default Header</h2>
</slot>
```

### 3. Namespace Your CSS Variables

Prevent naming conflicts by prefixing your component's CSS variables:

```typescript
shadowCSS(`
  .card {
    background: var(--my-card-bg, white);
    border: 1px solid var(--my-card-border, #ddd);
  }
`)
```

### 4. Consider Performance with Constructable Stylesheets

For optimal performance, Aurelia uses [Constructable Stylesheets](https://developers.google.com/web/updates/2019/02/constructable-stylesheets) when supported by the browser, falling back to `<style>` elements otherwise.

**Automatic caching**: When you pass CSS strings to `shadowCSS()`, Aurelia automatically caches the compiled `CSSStyleSheet` instances. This means multiple instances of the same component share the same stylesheet object in memory.

For maximum control, you can create `CSSStyleSheet` objects directly:

```typescript
// Create once at module level, reuse across all component instances
const cardStyles = new CSSStyleSheet();
cardStyles.replaceSync(`
  .card { border: 1px solid #ddd; padding: 16px; }
  .card-header { font-weight: bold; }
`);

@customElement({
  name: 'my-card',
  template: '<div class="card"><slot></slot></div>',
  dependencies: [shadowCSS(cardStyles)] // Same CSSStyleSheet instance is reused
})
@useShadowDOM()
export class MyCard {}
```

{% hint style="info" %}
Using pre-created `CSSStyleSheet` objects is slightly more efficient than CSS strings because it skips the string-to-stylesheet conversion step, though Aurelia's caching makes this difference minimal for most applications.
{% endhint %}

### 5. Use Open Mode Unless You Have a Reason Not To

Closed mode prevents useful debugging and testing. Use open mode by default:

```typescript
@useShadowDOM() // defaults to open mode
```

### 6. Document Your CSS Custom Properties

If your component supports theming, document the available CSS variables:

```typescript
/**
 * CSS Variables:
 * --card-bg: Background color (default: white)
 * --card-border: Border color (default: #ddd)
 * --card-padding: Internal padding (default: 16px)
 */
@customElement('themable-card')
@useShadowDOM()
export class ThemableCard {}
```

### 7. Convention-Based CSS Does Not Auto-Inject into Shadow DOM

Aurelia's convention-based CSS loading (where `my-component.css` is auto-imported alongside `my-component.ts`) does **not** automatically inject styles into Shadow DOM. For Shadow DOM components, you must explicitly use `shadowCSS()`:

```typescript
// my-card.ts
import { customElement, useShadowDOM, shadowCSS } from 'aurelia';
import styles from './my-card.css?inline'; // Import CSS as string (bundler-specific)

@customElement({
  name: 'my-card',
  template: '<div class="card"><slot></slot></div>',
  dependencies: [shadowCSS(styles)] // Explicitly inject into shadow root
})
@useShadowDOM()
export class MyCard {}
```

{% hint style="info" %}
Convention-based CSS loading works well for Light DOM components where styles are added to the document. For Shadow DOM components, always use `shadowCSS()` to ensure styles are properly scoped within the shadow root.
{% endhint %}

## Additional Resources

* [Slotted Content Documentation](/components/shadow-dom-and-slots) - Deep dive into slots, `@children`, and `@slotted` decorators
* [Web Components Documentation](/developer-guides/web-components) - Using Aurelia components as web components
* [CustomElement API Reference](/components/customelement-api) - Complete API documentation including Shadow DOM options


# Slotted content

Learn how to project content into custom elements using native slots and au-slot, and how to observe and react to slot changes.

Aurelia provides two ways to project content into custom elements:

* **`<slot>`** - Native Web Components slot that requires Shadow DOM
* **`<au-slot>`** - Aurelia's slot implementation that works without Shadow DOM

This guide focuses on slot usage, observation, and advanced patterns. For detailed information about configuring Shadow DOM, styling, and constraints, see the [Shadow DOM guide](/components/shadow-dom).

## Native Slots (`<slot>`)

The `<slot>` element is the browser-native way to project content into Shadow DOM components.

{% hint style="warning" %}
Native `<slot>` elements **require Shadow DOM**. Attempting to use `<slot>` without enabling Shadow DOM will throw a compilation error: `Template compilation error: detected a usage of "<slot>" element without specifying shadow DOM options`.

For content projection without Shadow DOM, use [`<au-slot>`](#au-slot) instead.
{% endhint %}

### Enabling Shadow DOM for Slots

Before using `<slot>`, enable Shadow DOM on your component:

{% tabs %}
{% tab title="modal.ts" %}

```typescript
import { customElement, useShadowDOM } from 'aurelia';

@customElement('au-modal')
@useShadowDOM()
export class AuModal {}
```

{% endtab %}

{% tab title="modal.html" %}

```html
<div class="modal">
    <div class="modal-inner">
        <slot></slot>
    </div>
</div>
```

{% endtab %}
{% endtabs %}

Because we named our component `au-modal` we will then use it like this:

```html
<au-modal>
    <div>
        <p>Modal content inside of the modal</p>
    </div>
</au-modal>
```

Notice how we add `slot="content"` to the projected node? This tells Aurelia to send that markup to the `<slot name="content">` target. When you target the default slot you omit the `slot` attribute entirely. Custom elements can have multiple slots, so how do we tell Aurelia where to project our content?

### Named slots

A named slot is no different to a conventional slot. The only difference is the slot has a name we can reference. A slot without a name gets the name `default` by default.

```html
<div class="modal">
    <div class="modal-inner">
        <slot name="content"></slot>
    </div>
</div>
```

Now, to use our element with a named slot, you can do this:

```html
<au-modal>
    <div slot="content">
        <p>Modal content inside of the modal</p>
    </div>
</au-modal>
```

### Fallback content

A slot can display default content when nothing is explicitly projected into it. Fallback content works for default and named slot elements.

```html
<div class="modal">
    <button type="button" data-action="close" class="close" aria-label="Close" click.trigger="close()" ><span aria-hidden="true">&times;</span></button>
    <div class="modal-inner">
        <slot>This is the default content shown if the user does not supply anything.</slot>
    </div>
</div>
```

### Listening to projection change

#### At the projection target (`<slot>` element), with the [`slotchange` event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event)

The `<slot>` element comes with an event based way to listen to its changes. This can be done via listening to `slotchange` even on the `<slot>` element, like the following example:

{% code title="my-app.html" overflow="wrap" lineNumbers="true" %}

```html
<slot slotchange.trigger="handleSlotChange($event.target.assignedNodes())"></slot>
```

{% endcode %}

{% code title="my-app.ts overflow=" %}

```typescript
class MyApp {
  handleSlotChange(nodes: Node[]) {
    console.log('new nodes are:', nodes);
  }
}
```

{% endcode %}

#### At the projection source (custom element host), with the `@children` decorator

In case where it's not desirable to go to listen to projection change at the targets (`<slot>` elements), it's also possible to listen to projection at the source with `@children` decorator. Decorating a property on a custom element class with `@children` decorator will setup mutation observer to notify of any changes, like the following example:

{% code title="my-details.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { children } from 'aurelia';

export class MyDetails {
  @children('div') divs: HTMLElement[];
}
```

{% endcode %}

{% code title="my-app.html" overflow="wrap" lineNumbers="true" %}

```html
<my-details>
  <div>@children decorator is a good way to listen to node changes without having to deal with boilerplate yourself</div>
</my-details>
```

{% endcode %}

After the initial rendering, `myDetails.divs` will be an array of 1 `<div>` element, and any future addition of any `<div>` elements to the `<my-details>` element will update the `divs` property on `myDetails` instance, with corresponding array.

Additionally, the `@children` decorator will also call a callback as a reactive change handler. The name of the callback, if omitted in the decorator, will be derived based on the property being decorated, example: `divs` -> `divsChanged`

#### `@children` decorator usage

| Usage                                                                           | Meaning                                                                                                                           |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `@children() prop`                                                              | Use default options, observe mutation, and select all elements                                                                    |
| `@children('div') prop`                                                         | Observe mutation, and select only `div` elements                                                                                  |
| `@children({ query: 'my-child' })`                                              | Observe mutation, and select only `my-child` elements, get the component instance if available and fallback to the element itself |
| `@children({ query: 'my-child', map: (node, viewModel) => viewModel ?? node })` | Observe mutation, and select only `my-child` elements, get the component instance if available and fallback to the element itself |

{% hint style="info" %}
Note: the `@children` decorator wont update if the children of a slotted node change — only if you change (e.g. add or delete) the actual nodes themselves.
{% endhint %}

### Retrieving component view models

When using `@children` to target projected element components, it's often desirable to get the underlying component instances rather than the host elements of those. The `@children` decorator by default automatically retrieves those instances, like the following examples:

{% code title="my-item.ts" overflow="wrap" lineNumbers="true" %}

```typescript
export class MyItem {
  ...
}
```

{% endcode %}

{% code title="my-list.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { children } from 'aurelia';
import { MyItem } from './my-item';

export class MyList {
  @children('my-item') items: MyItem[];
}
```

{% endcode %}

{% code title="my-app.html" overflow="wrap" lineNumbers="true" %}

```html
<import from="my-list">

<my-list>
  <my-item repeat.for="option of options" value.bind="option.value"></my-item>
</my-list>
```

{% endcode %}

As `items` property is decorated with `@children('my-item')`, its values is always a list of `MyItem` instances instead of `<my-item>` elements. You can alter this behavior by specifying a map option, like the following example:

{% code title="my-list.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { children } from 'aurelia';
import { MyItem } from './my-item';

export class MyList {
  @children({
    query: 'my-item',
    map: (node) => node
  })
  items: HTMLElement[];
}
```

{% endcode %}

In the above example, we give `map` option a function to decide that we want to take the host element instead of the component instance.

## Au-slot

Aurelia provides another way of content projection with `au-slot`. This is similar to the native `slot` when working with content projection. However, it does not use Shadow DOM. `au-slot` is useful where you want externally defined styles to penetrate the component boundary, facilitating easy styling of components.

Suppose you create your own set of custom elements solely used in your application. In that case, you might want to avoid the native slots in the custom elements, as it might be difficult to style them from your application.

However, if you still want slot-like behavior, then you can use `au-slot`, as that makes styling those custom elements/components easier. Instead of using shadow DOM, the resulting view is composed purely by the Aurelia compilation pipeline.

There are other aspects of `au-slot` as well which will be explored in this section with examples.

{% hint style="info" %}
An obvious question might be, "Why not simply 'turn off' shadow DOM, and use the `slot` itself"? We feel that goes opposite to Aurelia's promise of keeping things as close to native behavior as possible. Moreover, using a different name like `au-slot` makes it clear that the native slot is not used in this case. However, still brings slotting behavior to use.
{% endhint %}

{% hint style="info" %}
If you have used the `replaceable` and `replace part` before or with Aurelia1, it is replaced with `au-slot`.
{% endhint %}

## Basic templating usage

Like `slot`, a "projection target"/"slot" can be defined using a `<au-slot>` element, and a projection to that slot can be provided using a `[au-slot]` attribute. Consider the following example.

{% code title="my-element.html" %}

```html
static content
<au-slot>fallback content for default slot.</au-slot>
<au-slot name="s1">fallback content for s1 slot.</au-slot>
```

{% endcode %}

{% code title="my-app.html" %}

```html
<!-- Usage without projection -->
<my-element></my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    static content
    fallback content for default slot.
    fallback content for s1 slot.
  </my-element>
-->

<!-- Usage with projection -->
<my-element>
  <div>d</div>        <!-- using `au-slot="default"` explicitly also works. -->
  <div au-slot="s1">p1</div>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    static content
    <div>d</div>
    <div>p1</div>
  </my-element>
-->

<my-element>
  <template au-slot="s1">p1</template>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    static content
    fallback content for default slot.
    p1
  </my-element>
-->
```

{% endcode %}

In the example above, the `my-element` custom element defines two slots: one default and one named. The slots can optionally have fallback content; i.e. when no projection is provided for the slot, the fallback content will be displayed. Projecting to a slot is, therefore, also optional. However, when a projection is provided for a slot, that overrides the fallback content of that slot.

Similar to native shadow DOM and `<slot/>`/`[slot]` pair, `[au-slot]` attribute is not mandatory if you are targeting the default slot. All content without explicit `[au-slot]` is treated as targeting the default slot. Having no `[au-slot]` is also equal to having explicit `au-slot` on the content:

{% code title="my-app.html" %}

```html
<template as-custom-element="my-element">
  <au-slot>dfb</au-slot>
</template>


<my-element><div au-slot>projection</div></my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <div>projection</div>
  </my-element>
-->
```

{% endcode %}

Another important point to note is that the usage of `[au-slot]` attribute is supported only on the direct children elements of a custom element. This means that the following examples do not work.

{% code title="my-app.html" %}

```html
<!-- Do NOT work. -->

<div au-slot></div>

<template><div au-slot></div></template>

<my-element>
  <div>
    <div au-slot></div>
  </div>
</my-element>
```

{% endcode %}

### Using `[au-slot]` with template controllers

You can combine `[au-slot]` with Aurelia template controllers such as `if.bind`, `repeat.for`, or your own custom controllers. During compilation Aurelia moves the slotted content into projection templates before it runs those controllers, so the control-flow logic still executes exactly where you expect it to.

{% code title="my-card.html" %}

```html
<au-slot name="actions"></au-slot>
<ul>
  <au-slot name="items"></au-slot>
</ul>
```

{% endcode %}

{% code title="app.html" %}

```html
<my-card>
  <div au-slot="actions" if.bind="showActions">
    <button click.trigger="dismiss()">Dismiss</button>
  </div>

  <template au-slot="items" repeat.for="item of items">
    <li>${item}</li>
  </template>
</my-card>
```

{% endcode %}

The compiler handles both `if.bind` and `repeat.for` on the slotted projections, so you do not need to wrap them inside extra `<template>` tags unless you prefer that style.

**Inject the projected slot information**

It is possible to inject an instance of `IAuSlotsInfo` in a element component view model. This provides information related to the slots inside a custom element. The information includes only the slot names for which content has been projected. Let's consider the following example.

{% tabs %}
{% tab title="my-element.html" %}

```html
<au-slot>dfb</au-slot>
<au-slot name="s1">s1fb</au-slot>
<au-slot name="s2">s2fb</au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
import { resolve } from 'aurelia';
import { IAuSlotsInfo } from '@aurelia/runtime-html';

class MyElement {
  private readonly slotInfo = resolve(IAuSlotsInfo);

  binding() {
    console.log(this.slotInfo.projectedSlots);
  }
}
```

{% endtab %}

{% tab title="my-app.html" %}

```html
<!-- my_element_instance_1 -->
<my-element>
  <div au-slot="default">dp</div>
  <div au-slot="s1">s1p</div>
</my-element>
<!-- my_element_instance_2 -->
<my-element></my-element>
```

{% endtab %}
{% endtabs %}

The following would be logged to the console for the instances of `my-element`.

```html
// my_element_instance_1
['default', 's1']

// my_element_instance_2
[]
```

## Binding scope

It is also possible to use data-binding, interpolation etc., while projecting. While doing so, the scope accessing rule can be described by the following thumb rule:

1. When the projection is provided, the scope of the custom element providing the projection is used.
2. When the projection is not provided, the scope of the inner custom element is used.
3. The outer custom element can still access the inner scope using the `$host` keyword while projecting.

### Examples

To further explain how these rules apply, these rules are explained with the following examples.

#### Projection uses the outer scope by defaultthe

Let's consider the following example with interpolation.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element>
  <div au-slot="s1">${message}</div>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <div>outer</div>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1">${message}</au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}
{% endtabs %}

Although the `my-element` has a `message` property, but as `my-app` projects to `s1` slot, scope of `my-app` is used to evaluate the interpolation expression. Similar behavior can also be observed when binding properties of custom elements, as shown in the following example.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element>
  <foo-bar au-slot="s1" foo.bind="message"></foo-bar>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <foo-bar>outer</foo-bar>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1">${message}</au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}

{% tab title="foo-bar.html" %}

```html
${foo}
```

{% endtab %}

{% tab title="foo-bar.ts" %}

```typescript
export class FooBar {
  @bindable public foo: string;
}
```

{% endtab %}
{% endtabs %}

#### Fallback uses the inner scope by default

Let's consider the following example with interpolation. This is the same example as before, but this time without projection.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element></my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    inner
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1">${message}</au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}
{% endtabs %}

Note that in the absence of projection, the fallback content uses the scope of `my-element`. For completeness, the following example shows that it also holds while binding values to the `@bindable`s in custom elements.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element></my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <foo-bar>inner</foo-bar>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1">
  <foo-bar foo.bind="message"></foo-bar>
</au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}

{% tab title="foo-bar.html" %}

```html
${foo}
```

{% endtab %}

{% tab title="foo-bar.ts" %}

```typescript
export class FooBar {
  @bindable public foo: string;
}
```

{% endtab %}
{% endtabs %}

#### Access the inner scope with `$host`

The outer custom element can access the inner custom element's scope using the `$host` keyword, as shown in the following example.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element>
  <div au-slot="s1">${$host.message}</div>
  <div au-slot="s2">${message}</div>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <div>inner</div>
    <div>outer</div>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1"></au-slot>
<au-slot name="s2"></au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}
{% endtabs %}

Note that using the `$host.message` expression, `MyApp` can access the `MyElement#message`. The following example demonstrates the same behavior for binding values to custom elements.

{% tabs %}
{% tab title="my-app.html" %}

```html
<my-element>
  <foo-bar au-slot="s1" foo.bind="$host.message"></foo-bar>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <foo-bar>inner</foo-bar>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly message: string = 'outer';
}
```

{% endtab %}

{% tab title="my-element.html" %}

```html
<au-slot name="s1"></au-slot>
```

{% endtab %}

{% tab title="my-element.ts" %}

```typescript
export class MyElement {
  public readonly message: string = 'inner';
}
```

{% endtab %}

{% tab title="foo-bar.html" %}

```html
${foo}
```

{% endtab %}

{% tab title="foo-bar.ts" %}

```typescript
export class FooBar {
  @bindable public foo: string;
}
```

{% endtab %}
{% endtabs %}

Let's consider another example of `$host` which highlights the communication between the inside and outside of a custom element that employs `<au-slot>`

{% tabs %}
{% tab title="my-app.html" %}

```html
<template as-custom-element="my-element">
  <bindable name="people"></bindable>
  <au-slot name="grid">
    <au-slot name="header">
      <h4>First Name</h4>
      <h4>Last Name</h4>
    </au-slot>
    <template repeat.for="person of people">
      <au-slot name="content" expose.bind="{ person, $event, $odd, $index }">
        <div>${person.firstName}</div>
        <div>${person.lastName}</div>
      </au-slot>
    </template>
  </au-slot>
</template>

<my-element people.bind="people">
  <template au-slot="header">
    <h4>Meta</h4>
    <h4>Surname</h4>
    <h4>Given name</h4>
  </template>
  <template au-slot="content">
    <div>${$host.$index}-${$host.$even}-${$host.$odd}</div>
    <div>${$host.person.lastName}</div>
    <div>${$host.person.firstName}</div>
  </template>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <h4>Meta</h4>           <h4>Surname</h4>      <h4>Given name</h4>

    <div>0-true-false</div> <div>Doe</div>        <div>John</div>
    <div>1-false-true</div> <div>Mustermann</div> <div>Max</div>
  </my-element>
-->
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  public readonly people: Person[] = [
    new Person('John', 'Doe'),
    new Person('Max', 'Mustermann'),
  ];
}

class Person {
  public constructor(
    public firstName: string,
    public lastName: string,
  ) { }
}
```

{% endtab %}
{% endtabs %}

In the example above, we replace the 'content' template of the grid, defined in `my-element`, from `my-app`. While doing so, we can grab the scope of the `<au-slot name="content" />` and use the properties made available by the binding `expose.bind="{ person, $even, $odd, $index }"`, and use those in the projection template.

Note that `$host` allows us to access whatever the `<au-slot/>` element exposes, and this value can be changed to enable powerful scenarios. Without the `$host` it might not have been easy to provide a template for the repeater from the outside.

{% hint style="info" %}
`expose.bind` is reactive. Updating the bound object (for example, when an inner input changes) immediately updates the `$host` object that all projected templates see.
{% endhint %}

{% hint style="info" %}
The last example is also interesting from another aspect. It shows that many parts of the grid can be replaced with projection while working with a grid. This includes the header of the grid (`au-slot="header"`), the template column of the grid (`au-slot="content"`), or even the whole grid itself (`au-slot="grid"`).
{% endhint %}

{% hint style="warning" %}
The `$host` keyword can only be used in the context of projection. Using it in any other context is not supported and will throw errors with high probability.
{% endhint %}

## Multiple projections for a single slot

It is possible to provide multiple projections to a single slot.

{% code title="my-element.html" %}

```html
<au-slot name="s1">s1</au-slot>
<au-slot name="s2">s2</au-slot>
```

{% endcode %}

{% code title="my-app.html" %}

```html
<my-element>
  <div au-slot="s2">p20</div>
  <div au-slot="s1">p11</div>
  <div au-slot="s2">p21</div>
  <div au-slot="s1">p12</div>
</my-element>
<!-- Rendered (simplified): -->
<!--
  <my-element>
    <div>p11</div>
    <div>p12</div>

    <div>p20</div>
    <div>p21</div>
  </my-element>
-->
```

{% endcode %}

This is useful for many cases. One evident example would a 'tabs' custom element.

{% code title="my-element.html" %}

```html
<au-slot name="header"></au-slot>
<au-slot name="content"></au-slot>
```

{% endcode %}

{% code title="my-app.html" %}

```html
<my-tabs>
  <h3 au-slot="header">Tab1</h3>
  <div au-slot="content">Tab1 content</div>

  <h3 au-slot="header">Tab2</h3>
  <div au-slot="content">Tab2 content</div>

  <!--...-->
</my-tabs>
```

{% endcode %}

This helps keep things closer that belong together. For example, keeping the tab-header and tab-content next to each other provides better readability and understanding of the code to the developer. On other hand, it still places the projected contents in the right slot.

## Duplicate slots

Having more than one `<au-slot>` with the same name is also supported. This lets us project the same content to multiple slots declaratively, as can be seen in the following example.

{% code title="person-card.html" %}

```html
<let details-shown.bind="false"></let>
<au-slot name="name"></au-slot>
<button click.trigger="detailsShown=!detailsShown">Toggle details</button>
<div if.bind="detailsShown">
  <au-slot name="name"></au-slot>
  <au-slot name="role"></au-slot>
  <au-slot name="details"></au-slot>
</div>
```

{% endcode %}

{% code title="my-app.html" %}

```html
<person-card>
  <span au-slot="name"> John Doe </span>
  <span au-slot="role"> Role1 </span>
  <span au-slot="details"> Lorem ipsum </span>
</person-card>
```

{% endcode %}

Note that projection for the name is provided once, but it gets duplicated in 2 slots. You can also see this example in action [here](https://stackblitz.com/edit/au-slot-duplicate-slots?file=my-app.html).

## Listening to `<au-slot>` change

Similar like the standard `<slot>` element allows the ability to listen to changes in the content projected, `<au-slot>` also provides the capability to listen & react to changes.

### With `@slotted` decorator

One way to subscribe to `au-slot` changes is via the `@slotted` decorator, like the following example:

{% code title="app.html" overflow="wrap" lineNumbers="true" %}

```html
<my-summary>
  <p>This is a demo of the @slotted decorator</p>
  <p>It can get all the "p" elements with a simple decorator</p>
</my-summary>
```

{% endcode %}

{% code title="my-summary.html" overflow="wrap" lineNumbers="true" %}

```html
<p>Heading text</p>
<div>
  <au-slot></au-slot>
</div>
```

{% endcode %}

{% code title="my-summary.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { slotted } from 'aurelia';

export class MySummaryElement {
  @slotted('p') paragraphs // assert paragraphs.length === 2
}
```

{% endcode %}

After rendering, the `MySummaryElement` instance will have paragraphs value as an array of 2 `<p>` element as seen in the `app.html`.

The `@slotted` decorator will invoke change handler upon initial rendering, and whenever there's a mutation after wards, while the owning custom element is still active. By default, the callback will be selected based on the name of the decorated property. For example: `paragraphs` -> `paragraphsChanged`, like the following example:

{% code title="my-summary.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { slotted } from 'aurelia';

export class MySummaryElement {
  @slotted('p') paragraphs // assert paragraphs.length === 2

  paragraphsChanged(ps: HTMLParagraphElement[]) {
    // do things
  }
}
```

{% endcode %}

{% code title="my-summary.html" overflow="wrap" lineNumbers="true" %}

```html
<p>Heading text</p>
<div>
  <au-slot></au-slot>
</div>
```

{% endcode %}

{% code title="app.html" %}

```html
<my-summary>
  <p>This is a demo of the @slotted decorator</p>
  <p>It can get all the "p" elements with a simple decorator</p>
</my-summary>
```

{% endcode %}

\### Change handler callback reminders - The change handler will be called upon the initial rendering, and after every mutation afterwards while the custom element is still active {% %}

#### `@slotted` usage

The `@slotted` decorator can be used in multiple forms:

| Usage                                   | Meaning                                                                                                           |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `@slotted() prop`                       | Use default options, observe projection on the `default` slot, and select all elements                            |
| `@slotted('div') prop`                  | Observe projection on the `default` slot, and select only `div` elements                                          |
| `@slotted('div', 'footer') prop`        | Observe projection on the `footer` slot and select only `div` elements                                            |
| `@slotted('$all')`                      | Observe projection on the `default` slot, and select all nodes, including text                                    |
| `@slotted('*')`                         | Observe projection on the `default` slot, and select all elements                                                 |
| `@slotted('div', '*')`                  | Observe projection on all slots, and select only `div` elements                                                   |
| `@slotted('*', '*')`                    | Observe projection on all slots, and select all elements                                                          |
| `@slotted({ query: 'div' })`            | Observe projection on the `default` slot, and select only `div` elements                                          |
| `@slotted({ slotName: 'footer' })`      | Observe projection on `footer` slot, and select all elements                                                      |
| `@slotted({ callback: 'nodeChanged' })` | Observe projection on `default` slot, and select all elements, and call `nodeChanged` method on projection change |

Note: the \`@slotted\` decorator won't be notified if the children of a slotted node change — only if you change (e.g. add or delete) the actual nodes themselves. {% %}

### With `slotchange` binding

The standard `<slot>` element dispatches `slotchange` events for application to react to changes in the projection. This behavior is also supported with `<au-slot>`. The different are with `<slot>`, it's an event while for `<au-slot>`, it's a callback as there's no host to dispatch an event, for `<au-slot>` is a `containerless` element.

The callback will be passed 2 parameters:

| name  | type    | description                                                                       |
| ----- | ------- | --------------------------------------------------------------------------------- |
| name  | string  | the name of the slot calling the change callback                                  |
| nodes | Node\[] | the list of the latest nodes that belongs to the slot calling the change callback |

An example of using `slotchange` behavior may look like the following:

{% code title="app.html" overflow="wrap" lineNumbers="true" %}

```html
<my-summary>
  <p>This is a demo of the @slotted decorator</p>
  <p if.bind="describeMore">It can get all the "p" elements with a simple decorator</p>
</my-summary>
```

{% endcode %}

{% code title="my-summary.html" overflow="wrap" lineNumbers="true" %}

```html
<p>Heading text</p>
<div>
  <au-slot slotchange.bind="onContentChange"></au-slot>
  <au-slot slotchange.bind="(name, nodes) => doSomething(name, nodes)"></au-slot>
</div>
```

{% endcode %}

{% code title="my-summary.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { slotted } from 'aurelia';

export class MySummaryElement {
  @slotted('p') paragraphs // assert paragraphs.length === 1

  onContentChange = (name: string, nodes: Node[]) => {
    // handle the new set of nodes here
    console.assert(this === undefined);
  }

  doSomething(name: string, nodes: Node[]) {
    console.assert(this instanceof MySummaryElement);
  }
}
```

{% endcode %}

{% hint style="info" %}

#### `slotchange` callback reminders

* The callback will not be called upon the initial rendering, it's only called when there's a mutation after the initial rendering.
* The callback pass to slotchange of `<au-slot>` will be call with an `undefined` this, so you should either give it a lambda expression, or a function like the example above.
* The nodes passed to the 2nd parameter of the `slotchange` callback will always be the latest list of nodes.
* the `slotchange` callback doesn't fire if the children of a slotted node change — only if you change (e.g. add or delete) the actual nodes themselves.
* `slotchange` is independent of `@slotted`; you can bind it even if you do not observe the slot contents elsewhere.
  {% endhint %}


# Scope and context

Master the art of scope and binding context - the secret sauce behind Aurelia's powerful data binding magic.

Ever wondered how Aurelia knows where to find your data when you write `${message}` in a template? Or why `$parent.something` works like magic in nested components? Welcome to the world of **scope** and **binding context** – the invisible machinery that makes Aurelia's data binding so delightfully intuitive.

Think of scope as Aurelia's GPS system for finding your data. Just like GPS needs to know your current location to give you directions, Aurelia's binding expressions need to know their current context to find the right data.

{% hint style="success" %}
**What you'll learn in this guide:**

* How scope and binding context work under the hood
* The difference between binding context and override context (and why you should care)
* How to navigate between parent and child scopes like a pro
* When and why component boundaries matter
* The magic behind `$parent`, `$host`, and other special keywords
* How to debug those tricky scope-related issues
  {% endhint %}

## The Big Picture: What is Scope?

Before diving into the details, let's start with a simple analogy. Imagine you're at a family reunion, and someone shouts "Hey, John!" Three different people named John might turn around. To know which John they meant, you need **context** – are they talking to Uncle John, Cousin John, or Little Johnny?

Aurelia faces the same challenge. When it sees `${name}` in your template, it needs to know:

* Which object contains the `name` property?
* Should it look in the current component's data?
* What about parent components?
* Are there any special contextual values (like `$index` from a `repeat.for`)?

This is where **scope** comes in. A scope is like a GPS coordinate that tells Aurelia exactly where to look for data.

### The JavaScript Analogy

If you're familiar with JavaScript's function binding, you'll find this concept familiar:

```typescript
function greet() {
  return `Hello, ${this.name}!`;
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

console.log(greet.call(person1)); // "Hello, Alice!"
console.log(greet.call(person2)); // "Hello, Bob!"
```

Just like JavaScript's `this` binding, Aurelia expressions need a context object to work with. The difference is that Aurelia's scope system is more sophisticated – it can look through multiple layers of context to find what it needs.

## Anatomy of a Scope

Every scope in Aurelia has three main parts:

```
┌─────────────────────────┐
│       Scope             │
│                         │
│  ┌─────────────────┐    │
│  │ bindingContext  │    │ ← Your component's data
│  └─────────────────┘    │
│                         │
│  ┌─────────────────┐    │
│  │ overrideContext │    │ ← Special contextual values
│  └─────────────────┘    │
│                         │
│  parent ────────────────┼─→ Points to parent scope
│  isBoundary: boolean    │ ← Component boundary marker
└─────────────────────────┘
```

### 1. Binding Context: Your Data Home

The **binding context** is usually your component's instance – the object containing all your properties and methods:

```typescript
export class MyComponent {
  message = 'Hello, World!';
  count = 42;

  greet() {
    return `The count is ${this.count}`;
  }
}
```

When Aurelia evaluates `${message}` in the template, it looks at the binding context (your component instance) and finds the `message` property.

### 2. Override Context: Special Contextual Values

The **override context** holds special values that aren't part of your component but are available in templates. The most common example is loop variables:

```html
<div repeat.for="item of items">
  ${$index}: ${item.name}  <!-- $index comes from override context -->
</div>
```

In this example:

* `item` is added to a child scope's binding context
* `$index`, `$first`, `$last`, etc. are added to the override context

### 3. Scope Hierarchy and Parent Access

Scopes form a hierarchy. Each component creates its own scope, and child components can access parent data using `$parent`:

```html
<!-- parent-component.html -->
<div>
  <child-component></child-component>
</div>

<!-- child-component.html -->
<p>Parent's title: ${$parent.title}</p>
```

### 4. Component Boundaries

Component boundaries are important to understand. When you create a custom element, Aurelia marks that scope as a **boundary**. This affects how property resolution works:

* Within a component, Aurelia searches up the scope chain automatically
* At component boundaries, you must use `$parent` explicitly to access parent component data

```html
<!-- This works within the same component -->
<div with.bind="user">
  ${name}  <!-- Finds user.name, then falls back to component properties -->
</div>

<!-- To access parent component data, use $parent -->
<child-component>
  <!-- Inside child, to get parent's data: -->
  ${$parent.parentProperty}
</child-component>
```

### 5. The `$host` Keyword

In slot projections, `$host` gives you access to the component that defines the slot:

```html
<!-- my-card.html (defines slots) -->
<div class="card">
  <slot></slot>
</div>

<!-- Usage with projected content -->
<my-card>
  <p>Card title: ${$host.title}</p>  <!-- Accesses my-card's title -->
</my-card>
```

## Debugging Scopes

When you need to debug scope issues, you can access the scope in the browser console:

```typescript
// In your component, expose the scope for debugging
export class DebugComponent {
  created() {
    (window as any).debugScope = this.$controller.scope;
  }
}
```

Then in the browser console:

```javascript
// Explore the scope structure
debugScope.bindingContext      // Your component instance
debugScope.overrideContext     // Special contextual values
debugScope.parent              // Parent scope (if any)

// Check if it's a component boundary
debugScope.isBoundary
```

## Best Practices

### 1. Keep Component Boundaries in Mind

Always remember that component boundaries exist. If you need parent data, be explicit:

```html
<!-- Good: Explicit about crossing boundaries -->
<div>${$parent.parentProperty}</div>

<!-- Confusing: Relies on scope traversal -->
<div>${parentProperty}</div>
```

### 2. Use Override Context Sparingly

Override context is powerful but can be confusing. Use it for:

* Template controller values (`$index`, `$first`, etc.)
* Temporary view-only values (`let` bindings)
* Slot projection context (`$host`)

Avoid it for regular component data.

### 3. Set Up Override Context Early

If you need override context values, set them up during `binding` or earlier:

```typescript
public binding(): void {
  // Good: Set before binding establishment
  this.$controller.scope.overrideContext.customValue = 'something';
}
```

### 4. Debug Scope Issues Systematically

When debugging scope issues:

1. Check the current scope structure
2. Verify component boundaries
3. Trace the property resolution path
4. Test with explicit `$parent` usage

## Summary

Scope and binding context are the foundation of Aurelia's data binding system. Understanding them helps you:

* Write more predictable binding expressions
* Debug data binding issues effectively
* Use advanced features like slot projections
* Create more maintainable component hierarchies

Remember the key concepts:

* **Binding context** = your component's data
* **Override context** = special contextual values
* **Component boundaries** = where automatic scope traversal stops
* **`$parent`** = explicit parent access
* **`$host`** = slot projection context

With this knowledge, you're well-equipped to master Aurelia's data binding system and build sophisticated, data-driven applications!

{% hint style="info" %}
**Want to learn more?** Check out our guides on [Custom Attributes](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/components/custom-attributes.md), [Template Controllers](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/templates/template-controllers.md), and [Shadow DOM and Slots](/components/shadow-dom-and-slots) to see scope and binding context in action.
{% endhint %}


# CustomElement API

The `CustomElement` resource is a fundamental concept in Aurelia 2, providing the core functionality for creating encapsulated and reusable components. This comprehensive guide covers all aspects of the `CustomElement` API, including methods, decorators, and configuration options.

## Table of Contents

* [Core Methods](#core-methods)
  * [CustomElement.for](#customelement-for)
  * [CustomElement.define](#customelement-define)
  * [CustomElement.getDefinition](#customelement-getdefinition)
  * [CustomElement.find](#customelement-find)
  * [CustomElement.isType](#customelement-istype)
* [Metadata Methods](#metadata-methods)
  * [CustomElement.annotate](#customelement-annotate)
  * [CustomElement.getAnnotation](#customelement-getannotation)
* [Utility Methods](#utility-methods)
  * [CustomElement.generateName](#customelement-generatename)
  * [CustomElement.generateType](#customelement-generatetype)
  * [CustomElement.createInjectable](#customelement-createinjectable)
  * [CustomElement.keyFrom](#customelement-keyfrom)
* [Decorators](#decorators)
  * [@customElement](#customelement-decorator)
  * [@useShadowDOM](#useshadowdom-decorator)
  * [@containerless](#containerless-decorator)
  * [@capture](#capture-decorator)
  * [@processContent](#processcontent-decorator)
* [Definition Objects](#definition-objects)
  * [PartialCustomElementDefinition](#partialcustomelementdefinition)
  * [CustomElementDefinition](#customelementdefinition)

## Core Methods

### CustomElement.for

Retrieves the Aurelia controller associated with a DOM node. The controller provides access to the element's view model, lifecycle methods, and other properties.

#### Method Signatures

```typescript
// Get controller for the current node
CustomElement.for<T>(node: Node): ICustomElementController<T>

// Get controller with optional flag (returns null if not found)
CustomElement.for<T>(node: Node, opts: { optional: true }): ICustomElementController<T> | null

// Search parent nodes for a controller
CustomElement.for<T>(node: Node, opts: { searchParents: true }): ICustomElementController<T>

// Get controller for a named custom element
CustomElement.for<T>(node: Node, opts: { name: string }): ICustomElementController<T> | undefined

// Get controller for a named custom element, searching parents
CustomElement.for<T>(node: Node, opts: { name: string; searchParents: true }): ICustomElementController<T> | undefined
```

#### Parameters

* `node: Node` - The DOM node for which to retrieve the controller
* `opts?: object` - Optional configuration object with the following properties:
  * `optional?: boolean` - If `true`, returns `null` instead of throwing when no controller is found
  * `searchParents?: boolean` - If `true`, searches parent nodes (including containerless elements) for a controller
  * `name?: string` - If provided, only returns controllers for custom elements with this specific name

#### Examples

```typescript
import { CustomElement, ILogger } from 'aurelia';

// Basic usage - get controller for current node
const myElement = document.querySelector('.my-custom-element');
try {
  const controller = CustomElement.for(myElement);
  // You can inject ILogger in your classes for proper logging
  this.logger?.info('View model:', controller.viewModel);
  this.logger?.info('Element state:', controller.state);
} catch (error) {
  this.logger?.error('The provided node does not host a custom element.', error);
}

// Safe retrieval without throwing errors
const optionalController = CustomElement.for(myElement, { optional: true });
if (optionalController) {
  // Controller found and available for use
  optionalController.viewModel.someMethod();
} else {
  // No controller found, handle gracefully
  this.logger?.info('Node is not a custom element');
}

// Search parent hierarchy for any custom element controller
const someInnerElement = document.querySelector('.some-inner-element');
const parentController = CustomElement.for(someInnerElement, { searchParents: true });
// parentController is the closest controller up the DOM tree

// Get controller for a specific named custom element
const namedController = CustomElement.for(myElement, { name: 'my-custom-element' });
if (namedController) {
  // Found a controller for the specific element type
} else {
  // The node is not hosting the named custom element type
}

// Search parents for a specific named custom element
const namedParentController = CustomElement.for(someInnerElement, {
  name: 'my-custom-element',
  searchParents: true
});

// Access view model properties and methods
const controller = CustomElement.for(myElement);
const viewModel = controller.viewModel;
viewModel.myProperty = 'new value';
viewModel.myMethod();

// Access lifecycle state
this.logger?.info('Current state:', controller.state);
this.logger?.info('Is activated:', controller.isActive);
```

### CustomElement.define

Registers a class as a custom element in Aurelia. This method can be called directly or is used internally by the `@customElement` decorator.

#### Method Signatures

```typescript
// Define with name and class
CustomElement.define<T>(name: string, Type: Constructable<T>): CustomElementType<T>

// Define with definition object and class
CustomElement.define<T>(def: PartialCustomElementDefinition, Type: Constructable<T>): CustomElementType<T>

// Define with definition object only (generates type)
CustomElement.define<T>(def: PartialCustomElementDefinition): CustomElementType<T>
```

#### Parameters

* `nameOrDef: string | PartialCustomElementDefinition` - Either the element name or a complete definition object
* `Type?: Constructable` - The class containing the element's logic (optional when using definition object)

#### Examples

```typescript
import { CustomElement } from 'aurelia';

// Basic definition with name and class
class MyCustomElement {
  public message = 'Hello, World!';

  public greet() {
    alert(this.message);
  }
}

CustomElement.define('my-custom-element', MyCustomElement);

// Definition with complete configuration object
const definition = {
  name: 'advanced-element',
  template: '<h1>${title}</h1><div class="content"><au-slot></au-slot></div>',
  bindables: ['title', 'size'],
  shadowOptions: { mode: 'open' },
  containerless: false,
  capture: true,
  dependencies: []
};

class AdvancedElement {
  public title = '';
  public size = 'medium';
}

CustomElement.define(definition, AdvancedElement);

// Definition without explicit type (generates anonymous class)
const simpleDefinition = {
  name: 'simple-element',
  template: '<p>${text}</p>',
  bindables: ['text']
};

const SimpleElementType = CustomElement.define(simpleDefinition);

// Note: Using @customElement decorator is preferred over calling define directly
@customElement('my-element')
class MyElement {
  // This is equivalent to calling CustomElement.define('my-element', MyElement)
}
```

### CustomElement.getDefinition

Retrieves the `CustomElementDefinition` for a custom element class, providing access to all metadata about the element.

#### Method Signature

```typescript
CustomElement.getDefinition<T>(Type: Constructable<T>): CustomElementDefinition<T>
```

#### Parameters

* `Type: Constructable` - The custom element class

#### Return Value

Returns a `CustomElementDefinition` object containing all metadata about the custom element.

#### Example

```typescript
import { CustomElement, ILogger } from 'aurelia';

@customElement({
  name: 'my-element',
  template: '${message}',
  bindables: ['message']
})
class MyElement {
  public message = '';

  constructor(private logger: ILogger) {}

  public logDefinitionInfo() {
    const definition = CustomElement.getDefinition(MyElement);

    this.logger.info('Element name:', definition.name); // 'my-element'
    this.logger.info('Template:', definition.template);
    this.logger.info('Bindables:', definition.bindables);
    this.logger.info('Is containerless:', definition.containerless);
    this.logger.info('Shadow options:', definition.shadowOptions);
    this.logger.info('Dependencies:', definition.dependencies);
    this.logger.info('Aliases:', definition.aliases);
    this.logger.info('Capture mode:', definition.capture);
  }
}
```

### CustomElement.find

Searches for a custom element definition by name within a specific container's registry.

#### Method Signature

```typescript
CustomElement.find(container: IContainer, name: string): CustomElementDefinition | null
```

#### Parameters

* `container: IContainer` - The dependency injection container to search in
* `name: string` - The name of the custom element to find

#### Return Value

Returns the `CustomElementDefinition` if found, or `null` if no element with the specified name is registered.

#### Example

```typescript
import { CustomElement, IContainer, ILogger } from 'aurelia';

// In a custom service or component
class MyService {
  constructor(private container: IContainer, private logger: ILogger) {}

  public checkElementExists(elementName: string): boolean {
    const definition = CustomElement.find(this.container, elementName);
    return definition !== null;
  }

  public getElementTemplate(elementName: string): string | null {
    const definition = CustomElement.find(this.container, elementName);
    return definition?.template as string || null;
  }
}

// Usage in template compiler or dynamic composition
class SomeComponent {
  constructor(private logger: ILogger) {}

  public checkDynamicElement(container: IContainer) {
    const definition = CustomElement.find(container, 'my-dynamic-element');
    if (definition) {
      // Element is registered and available for use
      this.logger.info('Found element:', definition.name);
    } else {
      // Element not found in current container
      this.logger.warn('Element not registered');
    }
  }
}
```

### CustomElement.isType

Checks whether a given value is a custom element type (class decorated with `@customElement` or defined via `CustomElement.define`).

#### Method Signature

```typescript
CustomElement.isType<T>(value: T): value is CustomElementType<T>
```

#### Parameters

* `value: any` - The value to check

#### Return Value

Returns `true` if the value is a custom element type, `false` otherwise.

#### Example

```typescript
import { CustomElement, customElement, ILogger } from 'aurelia';

@customElement('my-element')
class MyElement {}

class RegularClass {}

// Service class that performs type checking
class TypeCheckingService {
  constructor(private logger: ILogger) {}

  public demonstrateTypeChecking() {
    // Type checking
    this.logger.info('MyElement is custom element type:', CustomElement.isType(MyElement)); // true
    this.logger.info('RegularClass is custom element type:', CustomElement.isType(RegularClass)); // false
    this.logger.info('String is custom element type:', CustomElement.isType('string')); // false
    this.logger.info('Number is custom element type:', CustomElement.isType(42)); // false
  }

  // Usage in dynamic scenarios
  public processComponent(component: unknown) {
    if (CustomElement.isType(component)) {
      // Safe to use as custom element
      const definition = CustomElement.getDefinition(component);
      this.logger.info('Processing element:', definition.name);
    } else {
      this.logger.info('Not a custom element type');
    }
  }
}
```

## Metadata Methods

### CustomElement.annotate

Attaches metadata to a custom element class. This is typically used internally by decorators and the framework.

#### Method Signature

```typescript
CustomElement.annotate<K extends keyof PartialCustomElementDefinition>(
  Type: Constructable,
  prop: K,
  value: PartialCustomElementDefinition[K]
): void
```

#### Parameters

* `Type: Constructable` - The custom element class to annotate
* `prop: string` - The property key for the annotation
* `value: any` - The value to associate with the property

#### Example

```typescript
import { CustomElement } from 'aurelia';

class MyElement {}

// Manually annotate the class (decorators do this automatically)
CustomElement.annotate(MyElement, 'template', '${message}');
CustomElement.annotate(MyElement, 'bindables', ['message']);
CustomElement.annotate(MyElement, 'containerless', true);
```

### CustomElement.getAnnotation

Retrieves metadata that was previously attached to a custom element class.

#### Method Signature

```typescript
CustomElement.getAnnotation<K extends keyof PartialCustomElementDefinition>(
  Type: Constructable,
  prop: K
): PartialCustomElementDefinition[K] | undefined
```

#### Parameters

* `Type: Constructable` - The custom element class
* `prop: string` - The property key to retrieve

#### Return Value

Returns the annotation value, or `undefined` if not found.

#### Example

```typescript
import { CustomElement, customElement, ILogger } from 'aurelia';

@customElement({
  name: 'annotated-element',
  template: '${content}'
})
class AnnotatedElement {
  constructor(private logger: ILogger) {}

  public logAnnotations() {
    // Retrieve annotations
    const template = CustomElement.getAnnotation(AnnotatedElement, 'template');
    const bindables = CustomElement.getAnnotation(AnnotatedElement, 'bindables');

    this.logger.info('Template:', template);
    this.logger.info('Bindables:', bindables);
  }
}
```

## Utility Methods

### CustomElement.generateName

Generates a unique name for a custom element, useful for anonymous or dynamically created elements.

#### Method Signature

```typescript
CustomElement.generateName(): string
```

#### Return Value

A string representing a unique name (typically in the format `unnamed-{number}`).

#### Example

```typescript
import { CustomElement } from 'aurelia';

// Generate unique names for dynamic elements
const uniqueName1 = CustomElement.generateName(); // 'unnamed-1'
const uniqueName2 = CustomElement.generateName(); // 'unnamed-2'

// Use with dynamic element creation
class DynamicElement {
  public data = '';
}

const DynamicElementType = CustomElement.define(uniqueName1, DynamicElement);
```

### CustomElement.generateType

Dynamically generates a `CustomElementType` with a given name and prototype properties.

#### Method Signature

```typescript
CustomElement.generateType<P extends object = object>(
  name: string,
  proto?: P
): CustomElementType<Constructable<P>>
```

#### Parameters

* `name: string` - The name for the generated type
* `proto?: object` - An optional object containing properties and methods to add to the prototype

#### Return Value

A `CustomElementType` that can be used with `CustomElement.define`.

#### Example

```typescript
import { CustomElement } from 'aurelia';

// Generate a type with custom properties and methods
const DynamicElement = CustomElement.generateType('dynamic-element', {
  message: 'Hello from Dynamic Element!',
  count: 0,

  increment() {
    this.count++;
  },

  showMessage() {
    alert(`${this.message} Count: ${this.count}`);
  }
});

// Define the generated type
CustomElement.define('dynamic-element', DynamicElement);

// Usage in templates: <dynamic-element></dynamic-element>
```

### CustomElement.createInjectable

Creates an `InjectableToken` for dependency injection scenarios.

#### Method Signature

```typescript
CustomElement.createInjectable<T = any>(): InterfaceSymbol<T>
```

#### Return Value

An `InterfaceSymbol` that can be used as a dependency injection token.

#### Example

```typescript
import { CustomElement, resolve } from 'aurelia';

// Create injectable tokens for custom scenarios
const MyServiceToken = CustomElement.createInjectable<MyService>();

// Use in dependency injection
class MyElement {
  private service = resolve(MyServiceToken);
}
```

### CustomElement.keyFrom

Generates the registry key used internally to store and retrieve custom element definitions.

#### Method Signature

```typescript
CustomElement.keyFrom(name: string): string
```

#### Parameters

* `name: string` - The custom element name

#### Return Value

A string representing the internal registry key.

#### Example

```typescript
import { CustomElement, ILogger } from 'aurelia';

class KeyGeneratorService {
  constructor(private logger: ILogger) {}

  public demonstrateKeyGeneration() {
    const key = CustomElement.keyFrom('my-element');
    this.logger.info(key); // 'au:ce:my-element' (internal format)

    // Used internally for container registration/lookup
    const hasElement = container.has(CustomElement.keyFrom('my-element'));
  }
}
```

## Decorators

### @customElement Decorator

The primary decorator for marking a class as a custom element.

#### Syntax

```typescript
@customElement(name: string)
@customElement(definition: PartialCustomElementDefinition)
```

#### Examples

```typescript
import { customElement } from 'aurelia';

// Simple name-based definition
@customElement('hello-world')
class HelloWorld {
  public message = 'Hello, World!';
}

// Full definition object
@customElement({
  name: 'advanced-component',
  template: `
    <h1>\${title}</h1>
    <div class="content">
      <au-slot></au-slot>
    </div>
  `,
  bindables: ['title', 'theme'],
  shadowOptions: { mode: 'open' },
  dependencies: []
})
class AdvancedComponent {
  public title = '';
  public theme = 'light';
}
```

### @useShadowDOM Decorator

Enables Shadow DOM for the custom element.

#### Syntax

```typescript
@useShadowDOM(options?: { mode: 'open' | 'closed' })
```

#### Example

```typescript
import { customElement, useShadowDOM } from 'aurelia';

@customElement('shadow-element')
@useShadowDOM({ mode: 'open' })
class ShadowElement {
  // This element will render in Shadow DOM
}

// Or with default mode (open)
@customElement('shadow-element-simple')
@useShadowDOM()
class ShadowElementSimple {}
```

### @containerless Decorator

Renders the custom element without its element container.

#### Syntax

```typescript
@containerless()
@containerless(target: Constructable, context: ClassDecoratorContext)
```

#### Example

```typescript
import { customElement, containerless } from 'aurelia';

@customElement('invisible-wrapper')
@containerless()
class InvisibleWrapper {
  // This element won't create its own DOM node
  // Only its content will be rendered
}

// Usage: <invisible-wrapper>content</invisible-wrapper>
// Renders: content (without the wrapper element)
```

### @capture Decorator

Enables capturing of all attributes and bindings that are not explicitly defined as bindables or template controllers.

#### Syntax

```typescript
@capture()
@capture(filter: (attr: string) => boolean)
```

#### Example

```typescript
import { customElement, capture } from 'aurelia';

@customElement('flexible-element')
@capture() // Capture all unrecognized attributes
class FlexibleElement {
  // Any attribute not defined as bindable will be captured
}

@customElement('filtered-element')
@capture((attrName) => attrName.startsWith('data-'))
class FilteredElement {
  // Only capture attributes that start with 'data-'
}
```

### @processContent Decorator

Defines a hook that processes the element's content before compilation.

#### Syntax

```typescript
@processContent(hook: ProcessContentHook)
@processContent(methodName: string | symbol)
@processContent() // Decorator for static method
```

#### Example

```typescript
import { customElement, processContent, IPlatform } from 'aurelia';

@customElement('content-processor')
class ContentProcessor {
  @processContent()
  static processContent(node: HTMLElement, platform: IPlatform, data: Record<PropertyKey, unknown>): boolean | void {
    // Modify the element's content before compilation
    const children = Array.from(node.children);
    children.forEach(child => {
      if (child.tagName === 'SPECIAL') {
        child.setAttribute('processed', 'true');
      }
    });
    return true; // Continue with normal compilation
  }
}

// Or reference a method by name
@customElement('named-processor')
@processContent('customProcessor')
class NamedProcessor {
  static customProcessor(node: HTMLElement, platform: IPlatform, data: Record<PropertyKey, unknown>): boolean | void {
    // Process content
    return true;
  }
}
```

## Definition Objects

### PartialCustomElementDefinition

An object that describes a custom element's configuration. All properties are optional.

#### Properties

```typescript
interface PartialCustomElementDefinition {
  name?: string;                    // Element name (kebab-case)
  template?: string | Node | null;  // HTML template
  bindables?: string[] | object;    // Bindable properties
  dependencies?: any[];             // Required dependencies
  aliases?: string[];               // Alternative names
  containerless?: boolean;          // Render without container
  shadowOptions?: { mode: 'open' | 'closed' } | null; // Shadow DOM options
  hasSlots?: boolean;              // Has <au-slot> elements
  capture?: boolean | ((attr: string) => boolean); // Capture unbound attributes
  enhance?: boolean;               // Enhance existing DOM
  instructions?: any[][];          // Template instructions
  surrogates?: any[];             // Surrogate instructions
  needsCompile?: boolean;         // Requires compilation
  injectable?: any;               // DI token
  watches?: any[];               // Property watchers
  strict?: boolean;              // Strict binding mode
  processContent?: Function;     // Content processing hook
}
```

#### Example

```typescript
const elementDefinition: PartialCustomElementDefinition = {
  name: 'my-component',
  template: `
    <h1>\${title}</h1>
    <p class="description">\${description}</p>
    <div class="actions">
      <au-slot name="actions"></au-slot>
    </div>
  `,
  bindables: ['title', 'description'],
  shadowOptions: { mode: 'open' },
  containerless: false,
  hasSlots: true,
  capture: false,
  dependencies: [],
  aliases: ['my-comp']
};
```

### CustomElementDefinition

The complete, resolved definition of a custom element (read-only).

#### Key Properties

```typescript
interface CustomElementDefinition {
  readonly Type: CustomElementType;      // The element class
  readonly name: string;                 // Element name
  readonly template: string | Node | null; // Compiled template
  readonly bindables: Record<string, BindableDefinition>; // Resolved bindables
  readonly aliases: string[];            // Alternative names
  readonly key: string;                  // Registry key
  readonly containerless: boolean;       // Container rendering mode
  readonly shadowOptions: { mode: 'open' | 'closed' } | null; // Shadow DOM
  readonly hasSlots: boolean;           // Contains slots
  readonly capture: boolean | Function; // Attribute capturing
  readonly enhance: boolean;            // DOM enhancement mode
  readonly dependencies: any[];         // Required dependencies
  readonly instructions: any[][];       // Template instructions
  readonly surrogates: any[];          // Surrogate instructions
  readonly needsCompile: boolean;      // Compilation requirement
  readonly watches: any[];             // Property watchers
  readonly strict: boolean | undefined; // Strict binding mode
  readonly processContent: Function | null; // Content processor
}
```

## Programmatic Resource Aliases

`PartialCustomElementDefinition.aliases` is only one way to expose alternative names. For reusable libraries or bridge packages you often need to add aliases outside of the definition itself. The runtime provides two helpers to make that ergonomic.

### `alias(...aliases)` decorator

Apply the decorator directly to any custom element, custom attribute, value converter, or binding behavior to append aliases to the resource metadata.

```typescript
import { alias, customElement } from '@aurelia/runtime-html';

@alias('counter-panel', 'stats-card')
@customElement({
  name: 'au-counter',
  template: `
    <section class="counter">
      <h2>\${title}</h2>
      <slot></slot>
    </section>
  `
})
export class CounterPanel {
  title = 'Visitors';
}
```

The decorator merges with aliases declared via the definition object, so you can sprinkle default aliases in a base class and extend them in derived implementations without clobbering earlier metadata.

### `registerAliases(...)`

When you need to attach aliases to an existing resource (for example, to keep backwards compatibility after a rename), call `registerAliases` during app startup.

```typescript
import { AppTask, CustomElement, registerAliases } from '@aurelia/runtime-html';
import { IContainer } from '@aurelia/kernel';

export const LegacyCounterAliases = AppTask.creating(IContainer, container => {
  const definition = CustomElement.getDefinition(CounterPanel);
  registerAliases(['legacy-counter', 'legacy-panel'], CustomElement, definition.key, container);
});
```

The `resource` argument identifies which registry to update. Pass `CustomElement`, `CustomAttribute`, `ValueConverter`, or `BindingBehavior` depending on the resource you are aliasing. Because aliases are registered against the supplied container you can scope them to individual feature modules or make them global by running the task in your root configuration.

## Best Practices

### 1. Use Decorators Over Direct API Calls

```typescript
// Preferred
@customElement('my-element')
class MyElement {}

// Avoid unless in dynamic scenarios
CustomElement.define('my-element', MyElement);
```

### 2. Type Your Controllers

```typescript
interface MyElementViewModel {
  title: string;
  count: number;
  increment(): void;
}

const controller = CustomElement.for<MyElementViewModel>(element);
controller.viewModel.increment(); // Fully typed
```

### 3. Handle Errors Gracefully

```typescript
// Use optional flag when controller might not exist
const controller = CustomElement.for(element, { optional: true });
if (controller) {
  // Safe to use
} else {
  // Handle missing controller
}
```

### 4. Leverage Definition Objects for Complex Elements

```typescript
@customElement({
  name: 'complex-element',
  template: complexTemplate,
  shadowOptions: { mode: 'open' },
  bindables: ['data', 'config'],
  dependencies: [SomeService, AnotherDependency]
})
class ComplexElement {}
```


# Component Recipes


# Recipes Overview

Practical component recipes for building common UI elements in Aurelia

This section contains practical, real-world examples of building reusable UI components in Aurelia. Each recipe shows you how to create a complete, production-ready component from scratch.

## What You'll Find Here

These recipes demonstrate:

* **Best practices** for component architecture
* **Accessibility** considerations
* **TypeScript** with proper typing
* **Testing** patterns for each component
* **Real-world** features and edge cases

## Available Recipes

### UI Components

* [**Dropdown Menu**](/components/component-recipes/dropdown-menu): A fully-featured dropdown with keyboard navigation and accessibility
* [**Modal Dialog**](/components/component-recipes/modal-dialog): A flexible modal system with backdrop, animations, and focus management
* [**Tabs Component**](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/components/recipes/tabs-component.md): An accessible tab interface with dynamic content
* [**Tooltip**](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/components/recipes/tooltip.md): Position-aware tooltips with smart placement
* [**Accordion**](/components/component-recipes/accordion): Collapsible content panels with smooth animations

## How to Use These Recipes

Each recipe includes:

1. **Overview**: What the component does and when to use it
2. **Complete Code**: TypeScript and HTML for the component
3. **Usage Examples**: How to consume the component
4. **Styling**: Base CSS to get you started
5. **Testing**: How to test the component
6. **Enhancements**: Ideas for extending the component

## Prerequisites

These recipes assume you're familiar with:

* [Aurelia components](/components/components)
* [Bindable properties](/components/bindable-properties)
* [Template syntax](/templates/overview)
* [Dependency injection](/introduction/essentials/dependency-injection)

## Code Standards

All recipes follow Aurelia 2 best practices:

* Use `resolve()` for dependency injection (not decorators)
* No `<template>` wrappers in HTML files
* Named exports for reusable components
* Proper cleanup in `detaching()` lifecycle hooks
* Accessible markup with ARIA attributes

## Contributing

Have a component recipe you'd like to share? Contributions are welcome! Make sure your recipe includes:

* Complete, working code
* Accessibility considerations
* Usage examples
* Tests
* Clear explanations

***

Ready to build something? Pick a recipe and start coding!


# Dropdown Menu

Build a fully-featured dropdown menu component with keyboard navigation and accessibility

Learn to build a production-ready dropdown menu with keyboard navigation, accessibility, and click-outside detection. This component is perfect for navigation menus, context menus, and action lists.

## What We're Building

A dropdown menu that supports:

* Click to toggle open/close
* Keyboard navigation (Arrow keys, Enter, Escape)
* Click outside to close
* Accessible with ARIA attributes
* Customizable trigger and content
* Positioning options

## Component Code

### dropdown-menu.ts

```typescript
import { bindable, IEventAggregator } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { queueTask } from '@aurelia/runtime';
import { IPlatform } from '@aurelia/runtime-html';

export class DropdownMenu {
  @bindable open = false;
  @bindable position: 'left' | 'right' = 'left';
  @bindable disabled = false;

  private platform = resolve(IPlatform);
  private element?: HTMLElement;
  private triggerButton?: HTMLButtonElement;
  private menuElement?: HTMLElement;
  private clickOutsideHandler?: (e: MouseEvent) => void;

  binding() {
    this.setupClickOutsideHandler();
  }

  attaching(initiator: HTMLElement) {
    this.element = initiator;
    this.triggerButton = this.element.querySelector('[data-dropdown-trigger]') as HTMLButtonElement;
    this.menuElement = this.element.querySelector('[data-dropdown-menu]') as HTMLElement;
  }

  detaching() {
    this.removeClickOutsideListener();
  }

  toggle() {
    if (this.disabled) return;

    this.open = !this.open;

    if (this.open) {
      this.addClickOutsideListener();
      this.focusFirstItem();
    } else {
      this.removeClickOutsideListener();
    }
  }

  close() {
    if (this.open) {
      this.open = false;
      this.removeClickOutsideListener();
      this.triggerButton?.focus();
    }
  }

  handleKeyDown(event: KeyboardEvent) {
    if (this.disabled) return;

    const { key } = event;

    // Toggle on Enter or Space when trigger is focused
    if ((key === 'Enter' || key === ' ') && document.activeElement === this.triggerButton) {
      event.preventDefault();
      this.toggle();
      return;
    }

    // Close on Escape
    if (key === 'Escape' && this.open) {
      event.preventDefault();
      this.close();
      return;
    }

    // Arrow navigation when menu is open
    if (this.open && (key === 'ArrowDown' || key === 'ArrowUp')) {
      event.preventDefault();
      this.navigateItems(key === 'ArrowDown' ? 1 : -1);
      return;
    }

    // Activate item on Enter when focused
    if (key === 'Enter' && this.open && document.activeElement?.hasAttribute('role')) {
      event.preventDefault();
      (document.activeElement as HTMLElement).click();
    }
  }

  private navigateItems(direction: 1 | -1) {
    if (!this.menuElement) return;

    const items = Array.from(this.menuElement.querySelectorAll('[role="menuitem"]')) as HTMLElement[];
    if (items.length === 0) return;

    const currentIndex = items.findIndex(item => item === document.activeElement);
    let nextIndex: number;

    if (currentIndex === -1) {
      // No item focused, focus first or last based on direction
      nextIndex = direction === 1 ? 0 : items.length - 1;
    } else {
      // Move to next/previous item, wrapping around
      nextIndex = (currentIndex + direction + items.length) % items.length;
    }

    items[nextIndex]?.focus();
  }

  private focusFirstItem() {
    // Use tasksSettled to ensure DOM is updated
    queueTask(() => {
      const firstItem = this.menuElement?.querySelector('[role="menuitem"]') as HTMLElement;
      firstItem?.focus();
    });
  }

  private setupClickOutsideHandler() {
    this.clickOutsideHandler = (event: MouseEvent) => {
      const target = event.target as Node;
      if (this.element && !this.element.contains(target)) {
        this.close();
      }
    };
  }

  private addClickOutsideListener() {
    if (this.clickOutsideHandler) {
      // Use timeout to avoid immediate close from the same click that opened it
      setTimeout(() => {
        document.addEventListener('click', this.clickOutsideHandler!, true);
      }, 0);
    }
  }

  private removeClickOutsideListener() {
    if (this.clickOutsideHandler) {
      document.removeEventListener('click', this.clickOutsideHandler, true);
    }
  }

  /**
   * Call this when an item is selected to close the menu
   */
  handleItemClick() {
    this.close();
  }
}
```

### dropdown-menu.html

```html
<div
  class="dropdown \${open ? 'dropdown--open' : ''} dropdown--\${position}"
  keydown.trigger="handleKeyDown($event)"
  ref="dropdownElement">

  <!-- Trigger slot -->
  <button
    type="button"
    class="dropdown__trigger"
    click.trigger="toggle()"
    aria-haspopup="true"
    aria-expanded.bind="open"
    disabled.bind="disabled"
    data-dropdown-trigger>
    <au-slot name="trigger">
      <span>Menu</span>
      <svg class="dropdown__icon" width="12" height="12" viewBox="0 0 12 12">
        <path d="M6 9L1 4h10z" fill="currentColor"/>
      </svg>
    </au-slot>
  </button>

  <!-- Menu content -->
  <div
    class="dropdown__menu"
    role="menu"
    aria-hidden.bind="!open"
    data-dropdown-menu
    if.bind="open">
    <au-slot>
      <div role="menuitem" tabindex="0" click.trigger="handleItemClick()">Menu Item 1</div>
      <div role="menuitem" tabindex="0" click.trigger="handleItemClick()">Menu Item 2</div>
      <div role="menuitem" tabindex="0" click.trigger="handleItemClick()">Menu Item 3</div>
    </au-slot>
  </div>
</div>
```

### dropdown-menu.css

```css
.dropdown {
  position: relative;
  display: inline-block;
}

.dropdown__trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: white;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
}

.dropdown__trigger:hover:not(:disabled) {
  background: #f9fafb;
  border-color: #9ca3af;
}

.dropdown__trigger:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

.dropdown__trigger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.dropdown__icon {
  transition: transform 0.2s;
}

.dropdown--open .dropdown__icon {
  transform: rotate(180deg);
}

.dropdown__menu {
  position: absolute;
  top: calc(100% + 4px);
  min-width: 200px;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
              0 4px 6px -2px rgba(0, 0, 0, 0.05);
  padding: 4px;
  z-index: 1000;
  animation: dropdown-slide-in 0.15s ease-out;
}

.dropdown--left .dropdown__menu {
  left: 0;
}

.dropdown--right .dropdown__menu {
  right: 0;
}

@keyframes dropdown-slide-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.dropdown__menu [role="menuitem"] {
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.15s;
  outline: none;
}

.dropdown__menu [role="menuitem"]:hover,
.dropdown__menu [role="menuitem"]:focus {
  background: #f3f4f6;
}

.dropdown__menu [role="menuitem"]:active {
  background: #e5e7eb;
}

/* Divider */
.dropdown__divider {
  height: 1px;
  background: #e5e7eb;
  margin: 4px 0;
}
```

## Usage Examples

### Basic Dropdown

```html
<dropdown-menu>
  <div au-slot="trigger">
    Actions
  </div>

  <div role="menuitem" tabindex="0">Edit</div>
  <div role="menuitem" tabindex="0">Duplicate</div>
  <div class="dropdown__divider"></div>
  <div role="menuitem" tabindex="0">Delete</div>
</dropdown-menu>
```

### With Custom Trigger

```html
<dropdown-menu position="right">
  <button au-slot="trigger" class="icon-button">
    <svg><!-- Settings icon --></svg>
  </button>

  <div role="menuitem" tabindex="0" click.trigger="openSettings()">
    Settings
  </div>
  <div role="menuitem" tabindex="0" click.trigger="viewProfile()">
    Profile
  </div>
  <div role="menuitem" tabindex="0" click.trigger="logout()">
    Logout
  </div>
</dropdown-menu>
```

### Programmatic Control

```typescript
// your-component.ts
import { DropdownMenu } from './dropdown-menu';

export class YourComponent {
  dropdownOpen = false;

  openDropdown() {
    this.dropdownOpen = true;
  }

  closeDropdown() {
    this.dropdownOpen = false;
  }
}
```

```html
<!-- your-component.html -->
<dropdown-menu open.bind="dropdownOpen">
  <div role="menuitem" tabindex="0" click.trigger="performAction()">
    Action
  </div>
</dropdown-menu>

<button click.trigger="openDropdown()">Open Menu</button>
```

### Disabled State

```html
<dropdown-menu disabled.bind="isProcessing">
  <div au-slot="trigger">
    Actions \${isProcessing ? '(Processing...)' : ''}
  </div>

  <div role="menuitem" tabindex="0">Action 1</div>
  <div role="menuitem" tabindex="0">Action 2</div>
</dropdown-menu>
```

## Testing

Test your dropdown component:

```typescript
import { createFixture } from '@aurelia/testing';
import { DropdownMenu } from './dropdown-menu';

describe('DropdownMenu', () => {
  it('toggles open/close on trigger click', async () => {
    const { component, trigger, queryBy, stop } = await createFixture
      .html`<dropdown-menu></dropdown-menu>`
      .deps(DropdownMenu)
      .build()
      .started;

    expect(component.open).toBe(false);
    expect(queryBy('[data-dropdown-menu]')).toBeNull();

    // Click trigger to open
    trigger.click('[data-dropdown-trigger]');
    expect(component.open).toBe(true);

    // Click trigger to close
    trigger.click('[data-dropdown-trigger]');
    expect(component.open).toBe(false);

    await stop(true);
  });

  it('closes when clicking outside', async () => {
    const { component, trigger, stop } = await createFixture
      .html`
        <div>
          <dropdown-menu></dropdown-menu>
          <button id="outside">Outside</button>
        </div>
      `
      .deps(DropdownMenu)
      .build()
      .started;

    // Open the dropdown
    trigger.click('[data-dropdown-trigger]');
    expect(component.open).toBe(true);

    // Click outside
    trigger.click('#outside');

    // Wait for click handler
    await new Promise(resolve => setTimeout(resolve, 10));

    expect(component.open).toBe(false);

    await stop(true);
  });

  it('closes on Escape key', async () => {
    const { component, trigger, getBy, stop } = await createFixture
      .html`<dropdown-menu></dropdown-menu>`
      .deps(DropdownMenu)
      .build()
      .started;

    // Open the dropdown
    trigger.click('[data-dropdown-trigger]');
    expect(component.open).toBe(true);

    // Press Escape
    trigger.keydown(getBy('.dropdown'), { key: 'Escape' });
    expect(component.open).toBe(false);

    await stop(true);
  });

  it('navigates items with arrow keys', async () => {
    const { trigger, getBy, getAllBy, stop } = await createFixture
      .html`
        <dropdown-menu>
          <div role="menuitem" tabindex="0">Item 1</div>
          <div role="menuitem" tabindex="0">Item 2</div>
          <div role="menuitem" tabindex="0">Item 3</div>
        </dropdown-menu>
      `
      .deps(DropdownMenu)
      .build()
      .started;

    // Open the dropdown
    trigger.click('[data-dropdown-trigger]');

    const dropdown = getBy('.dropdown');
    const items = getAllBy('[role="menuitem"]');

    // First item should be focused
    await new Promise(resolve => setTimeout(resolve, 10));
    expect(document.activeElement).toBe(items[0]);

    // Arrow down to second item
    trigger.keydown(dropdown, { key: 'ArrowDown' });
    expect(document.activeElement).toBe(items[1]);

    // Arrow up back to first
    trigger.keydown(dropdown, { key: 'ArrowUp' });
    expect(document.activeElement).toBe(items[0]);

    await stop(true);
  });

  it('does not open when disabled', async () => {
    const { component, trigger, stop } = await createFixture
      .html`<dropdown-menu disabled.bind="true"></dropdown-menu>`
      .deps(DropdownMenu)
      .build()
      .started;

    trigger.click('[data-dropdown-trigger]');
    expect(component.open).toBe(false);

    await stop(true);
  });
});
```

## Accessibility Features

This dropdown implements WCAG 2.1 guidelines:

* ✅ **Keyboard Navigation**: Full keyboard support with arrow keys
* ✅ **ARIA Attributes**: Proper `role`, `aria-haspopup`, `aria-expanded`, `aria-hidden`
* ✅ **Focus Management**: Focuses first item when opened, returns focus to trigger when closed
* ✅ **Escape to Close**: Standard Escape key behavior
* ✅ **Screen Reader Support**: Announces menu state and items

## Enhancements

### 1. Add Icons to Menu Items

```html
<div role="menuitem" tabindex="0" class="menu-item">
  <svg class="menu-item__icon"><!-- Icon --></svg>
  <span>Edit</span>
</div>
```

### 2. Add Submenus

Nest another `dropdown-menu` inside:

```html
<dropdown-menu>
  <div role="menuitem" tabindex="0">Item 1</div>

  <dropdown-menu position="right">
    <div au-slot="trigger" role="menuitem" tabindex="0">
      More Actions →
    </div>
    <div role="menuitem" tabindex="0">Sub Item 1</div>
    <div role="menuitem" tabindex="0">Sub Item 2</div>
  </dropdown-menu>
</dropdown-menu>
```

### 3. Add Search/Filter

```typescript
export class SearchableDropdown {
  @bindable items: any[] = [];
  searchTerm = '';

  get filteredItems() {
    return this.items.filter(item =>
      item.label.toLowerCase().includes(this.searchTerm.toLowerCase())
    );
  }
}
```

### 4. Add Positioning Intelligence

Use a library like Floating UI to automatically position the menu to avoid viewport overflow:

```typescript
import { computePosition, flip, shift } from '@floating-ui/dom';

async positionMenu() {
  const { x, y } = await computePosition(this.triggerButton!, this.menuElement!, {
    middleware: [flip(), shift({ padding: 8 })]
  });

  Object.assign(this.menuElement!.style, {
    left: `${x}px`,
    top: `${y}px`
  });
}
```

## Best Practices

1. **Always Clean Up**: Remove event listeners in `detaching()` to prevent memory leaks
2. **Focus Management**: Return focus to trigger when closing for better UX
3. **Debounce**: For search/filter, debounce input to avoid excessive filtering
4. **Accessibility**: Test with keyboard only and screen readers
5. **Portal Rendering**: For complex layouts, render menu in a portal to avoid z-index issues

## Summary

You've built a fully-featured dropdown menu with:

* ✅ Click and keyboard interactions
* ✅ Accessibility built-in
* ✅ Click-outside detection
* ✅ Customizable trigger and content
* ✅ Comprehensive tests

This dropdown is production-ready and can be extended with search, submenus, and intelligent positioning!


# Modal Dialog

Build a flexible modal dialog component with backdrop, animations, and focus management

Learn to build a production-ready modal dialog with proper focus management, backdrop click handling, animations, and accessibility. Perfect for confirmations, forms, and detailed content displays.

## What We're Building

A modal dialog that supports:

* Open/close with smooth animations
* Backdrop click to close (optional)
* Escape key to close
* Focus trap (keyboard focus stays within modal)
* Return focus to trigger when closed
* Accessible with ARIA attributes
* Portal rendering (renders outside parent context)
* Scrollable content

## Component Code

### modal-dialog.ts

```typescript
import { bindable, IEventAggregator } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { queueTask } from '@aurelia/runtime';
import { IPlatform } from '@aurelia/runtime-html';

export class ModalDialog {
  @bindable open = false;
  @bindable closeOnBackdropClick = true;
  @bindable closeOnEscape = true;
  @bindable size: 'small' | 'medium' | 'large' | 'full' = 'medium';

  private platform = resolve(IPlatform);
  private element?: HTMLElement;
  private modalElement?: HTMLElement;
  private previousActiveElement?: HTMLElement;
  private focusableElements: HTMLElement[] = [];

  openChanged(newValue: boolean) {
    if (newValue) {
      this.onOpen();
    } else {
      this.onClose();
    }
  }

  attaching(initiator: HTMLElement) {
    this.element = initiator;
    this.modalElement = this.element.querySelector('[data-modal]') as HTMLElement;
  }

  detaching() {
    // Clean up if modal is still open
    if (this.open) {
      this.cleanupModal();
    }
  }

  closeModal() {
    this.open = false;
  }

  handleBackdropClick(event: MouseEvent) {
    // Only close if clicking the backdrop itself, not content inside
    if (this.closeOnBackdropClick && event.target === event.currentTarget) {
      this.closeModal();
    }
  }

  handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Escape' && this.closeOnEscape) {
      event.preventDefault();
      this.closeModal();
      return;
    }

    // Tab key focus trap
    if (event.key === 'Tab') {
      this.handleTabKey(event);
    }
  }

  private onOpen() {
    // Store currently focused element to return focus later
    this.previousActiveElement = document.activeElement as HTMLElement;

    // Prevent body scroll
    document.body.style.overflow = 'hidden';

    // Wait for DOM to render, then focus first element
    queueTask(() => {
      this.updateFocusableElements();
      this.focusFirstElement();
    });
  }

  private onClose() {
    this.cleanupModal();
  }

  private cleanupModal() {
    // Restore body scroll
    document.body.style.overflow = '';

    // Return focus to element that opened the modal
    if (this.previousActiveElement) {
      this.previousActiveElement.focus();
      this.previousActiveElement = undefined;
    }
  }

  private updateFocusableElements() {
    if (!this.modalElement) return;

    const focusableSelectors = [
      'a[href]',
      'button:not([disabled])',
      'textarea:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      '[tabindex]:not([tabindex="-1"])'
    ].join(', ');

    this.focusableElements = Array.from(
      this.modalElement.querySelectorAll(focusableSelectors)
    ) as HTMLElement[];
  }

  private focusFirstElement() {
    const firstFocusable = this.focusableElements[0];
    if (firstFocusable) {
      firstFocusable.focus();
    }
  }

  private handleTabKey(event: KeyboardEvent) {
    if (this.focusableElements.length === 0) return;

    const firstElement = this.focusableElements[0];
    const lastElement = this.focusableElements[this.focusableElements.length - 1];
    const activeElement = document.activeElement as HTMLElement;

    if (event.shiftKey) {
      // Shift + Tab: Move backwards
      if (activeElement === firstElement) {
        event.preventDefault();
        lastElement.focus();
      }
    } else {
      // Tab: Move forwards
      if (activeElement === lastElement) {
        event.preventDefault();
        firstElement.focus();
      }
    }
  }
}
```

### modal-dialog.html

```html
<div
  if.bind="open"
  class="modal modal--\${size}"
  role="dialog"
  aria-modal="true"
  keydown.trigger="handleKeyDown($event)"
  data-modal>

  <!-- Backdrop -->
  <div
    class="modal__backdrop"
    click.trigger="handleBackdropClick($event)">

    <!-- Content container -->
    <div class="modal__content" role="document">

      <!-- Header slot -->
      <div class="modal__header" if.bind="$slots.header">
        <au-slot name="header"></au-slot>
        <button
          type="button"
          class="modal__close"
          click.trigger="closeModal()"
          aria-label="Close modal">
          ×
        </button>
      </div>

      <!-- Body slot -->
      <div class="modal__body">
        <au-slot>
          <p>Modal content goes here</p>
        </au-slot>
      </div>

      <!-- Footer slot -->
      <div class="modal__footer" if.bind="$slots.footer">
        <au-slot name="footer"></au-slot>
      </div>

    </div>
  </div>
</div>
```

### modal-dialog.css

```css
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: modal-fade-in 0.2s ease-out;
}

@keyframes modal-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.modal__backdrop {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow-y: auto;
}

.modal__content {
  position: relative;
  background: white;
  border-radius: 12px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
              0 10px 10px -5px rgba(0, 0, 0, 0.04);
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  animation: modal-slide-up 0.2s ease-out;
  margin: auto;
}

@keyframes modal-slide-up {
  from {
    opacity: 0;
    transform: translateY(20px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Size variants */
.modal--small .modal__content {
  width: 100%;
  max-width: 400px;
}

.modal--medium .modal__content {
  width: 100%;
  max-width: 600px;
}

.modal--large .modal__content {
  width: 100%;
  max-width: 900px;
}

.modal--full .modal__content {
  width: 100%;
  max-width: 95vw;
  max-height: 95vh;
}

.modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 24px 24px 16px;
  border-bottom: 1px solid #e5e7eb;
}

.modal__header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  color: #111827;
}

.modal__close {
  background: none;
  border: none;
  font-size: 28px;
  line-height: 1;
  color: #6b7280;
  cursor: pointer;
  padding: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  transition: all 0.15s;
}

.modal__close:hover {
  background: #f3f4f6;
  color: #111827;
}

.modal__close:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

.modal__body {
  padding: 24px;
  overflow-y: auto;
  flex: 1;
}

.modal__footer {
  padding: 16px 24px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

.modal__footer button {
  padding: 8px 16px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s;
}

.modal__footer button.btn-primary {
  background: #3b82f6;
  color: white;
  border: none;
}

.modal__footer button.btn-primary:hover {
  background: #2563eb;
}

.modal__footer button.btn-secondary {
  background: white;
  color: #374151;
  border: 1px solid #d1d5db;
}

.modal__footer button.btn-secondary:hover {
  background: #f9fafb;
}
```

## Usage Examples

### Basic Modal

```typescript
// your-component.ts
export class YourComponent {
  showModal = false;

  openModal() {
    this.showModal = true;
  }

  closeModal() {
    this.showModal = false;
  }
}
```

```html
<!-- your-component.html -->
<button click.trigger="openModal()">Open Modal</button>

<modal-dialog open.bind="showModal">
  <h2 au-slot="header">Welcome!</h2>

  <p>This is the modal content. You can put anything here.</p>

  <div au-slot="footer">
    <button class="btn-secondary" click.trigger="closeModal()">Cancel</button>
    <button class="btn-primary" click.trigger="closeModal()">OK</button>
  </div>
</modal-dialog>
```

### Confirmation Dialog

```typescript
export class ConfirmDialog {
  showConfirm = false;
  confirmMessage = '';

  confirm(message: string): Promise<boolean> {
    this.confirmMessage = message;
    this.showConfirm = true;

    return new Promise(resolve => {
      this.resolveConfirm = resolve;
    });
  }

  handleConfirm(result: boolean) {
    this.showConfirm = false;
    if (this.resolveConfirm) {
      this.resolveConfirm(result);
    }
  }

  async deleteItem() {
    const confirmed = await this.confirm('Are you sure you want to delete this item?');
    if (confirmed) {
      // Delete the item
    }
  }

  private resolveConfirm?: (value: boolean) => void;
}
```

```html
<modal-dialog open.bind="showConfirm" size="small">
  <h2 au-slot="header">Confirm Action</h2>

  <p>\${confirmMessage}</p>

  <div au-slot="footer">
    <button class="btn-secondary" click.trigger="handleConfirm(false)">
      Cancel
    </button>
    <button class="btn-primary" click.trigger="handleConfirm(true)">
      Confirm
    </button>
  </div>
</modal-dialog>
```

### Form Modal

```typescript
export class FormModal {
  showForm = false;
  formData = {
    name: '',
    email: '',
    message: ''
  };

  openForm() {
    this.showForm = true;
    this.resetForm();
  }

  closeForm() {
    this.showForm = false;
  }

  async submitForm() {
    // Validate and submit
    console.log('Submitting:', this.formData);
    this.closeForm();
  }

  resetForm() {
    this.formData = { name: '', email: '', message: '' };
  }
}
```

```html
<modal-dialog open.bind="showForm" size="medium" close-on-backdrop-click.bind="false">
  <h2 au-slot="header">Contact Us</h2>

  <form>
    <div class="form-group">
      <label for="name">Name</label>
      <input id="name" type="text" value.bind="formData.name">
    </div>

    <div class="form-group">
      <label for="email">Email</label>
      <input id="email" type="email" value.bind="formData.email">
    </div>

    <div class="form-group">
      <label for="message">Message</label>
      <textarea id="message" rows="4" value.bind="formData.message"></textarea>
    </div>
  </form>

  <div au-slot="footer">
    <button class="btn-secondary" click.trigger="closeForm()">Cancel</button>
    <button class="btn-primary" click.trigger="submitForm()">Send Message</button>
  </div>
</modal-dialog>
```

### Full-Screen Modal

```html
<modal-dialog open.bind="showDetails" size="full">
  <h2 au-slot="header">Full Details</h2>

  <div class="content-grid">
    <!-- Large amount of content -->
  </div>

  <div au-slot="footer">
    <button class="btn-primary" click.trigger="showDetails = false">Close</button>
  </div>
</modal-dialog>
```

## Testing

```typescript
import { createFixture } from '@aurelia/testing';
import { ModalDialog } from './modal-dialog';

describe('ModalDialog', () => {
  it('opens and closes', async () => {
    const { component, queryBy, stop } = await createFixture
      .html`<modal-dialog open.bind="isOpen"></modal-dialog>`
      .component(class { isOpen = false; })
      .deps(ModalDialog)
      .build()
      .started;

    expect(queryBy('[data-modal]')).toBeNull();

    component.isOpen = true;
    await new Promise(resolve => setTimeout(resolve, 10));

    expect(queryBy('[data-modal]')).toBeTruthy();

    component.isOpen = false;
    await new Promise(resolve => setTimeout(resolve, 10));

    expect(queryBy('[data-modal]')).toBeNull();

    await stop(true);
  });

  it('closes on Escape key', async () => {
    const { component, trigger, getBy, stop } = await createFixture
      .html`<modal-dialog open.bind="isOpen"></modal-dialog>`
      .component(class { isOpen = true; })
      .deps(ModalDialog)
      .build()
      .started;

    await new Promise(resolve => setTimeout(resolve, 10));

    trigger.keydown(getBy('[data-modal]'), { key: 'Escape' });

    expect(component.isOpen).toBe(false);

    await stop(true);
  });

  it('closes on backdrop click when enabled', async () => {
    const { component, trigger, getBy, stop } = await createFixture
      .html`<modal-dialog open.bind="isOpen" close-on-backdrop-click.bind="true"></modal-dialog>`
      .component(class { isOpen = true; })
      .deps(ModalDialog)
      .build()
      .started;

    await new Promise(resolve => setTimeout(resolve, 10));

    trigger.click(getBy('.modal__backdrop'));

    expect(component.isOpen).toBe(false);

    await stop(true);
  });

  it('does not close on content click', async () => {
    const { component, trigger, getBy, stop } = await createFixture
      .html`<modal-dialog open.bind="isOpen"></modal-dialog>`
      .component(class { isOpen = true; })
      .deps(ModalDialog)
      .build()
      .started;

    await new Promise(resolve => setTimeout(resolve, 10));

    trigger.click(getBy('.modal__content'));

    expect(component.isOpen).toBe(true);

    await stop(true);
  });

  it('prevents body scroll when open', async () => {
    const { component, stop } = await createFixture
      .html`<modal-dialog open.bind="isOpen"></modal-dialog>`
      .component(class { isOpen = false; })
      .deps(ModalDialog)
      .build()
      .started;

    expect(document.body.style.overflow).toBe('');

    component.isOpen = true;
    await new Promise(resolve => setTimeout(resolve, 10));

    expect(document.body.style.overflow).toBe('hidden');

    component.isOpen = false;
    await new Promise(resolve => setTimeout(resolve, 10));

    expect(document.body.style.overflow).toBe('');

    await stop(true);
  });
});
```

## Accessibility Features

This modal follows WCAG 2.1 guidelines:

* ✅ **Focus Trap**: Tab key cycles through focusable elements within modal
* ✅ **Focus Management**: Focuses first element when opened, returns focus when closed
* ✅ **Keyboard Support**: Escape key closes modal
* ✅ **ARIA Attributes**: `role="dialog"`, `aria-modal="true"` for screen readers
* ✅ **Body Scroll Lock**: Prevents scrolling background content

## Enhancements

### 1. Add Transition Animations

Use Aurelia's animation system for smoother transitions:

```typescript
import { animator } from '@aurelia/runtime-html';

export class AnimatedModal {
  private animator = resolve(animator);

  async openModal() {
    this.open = true;
    await tasksSettled();
    await this.animator.enter(this.modalElement!);
  }

  async closeModal() {
    await this.animator.leave(this.modalElement!);
    this.open = false;
  }
}
```

### 2. Add Confirmation Before Close

```typescript
export class UnsavedChangesModal {
  @bindable hasUnsavedChanges = false;

  async closeModal() {
    if (this.hasUnsavedChanges) {
      const confirmed = confirm('You have unsaved changes. Close anyway?');
      if (!confirmed) return;
    }

    this.open = false;
  }
}
```

### 3. Add Modal Service

Create a global modal service for programmatic modals:

```typescript
// modal-service.ts
import { IEventAggregator } from 'aurelia';
import { resolve } from '@aurelia/kernel';

export interface ModalConfig {
  title: string;
  message: string;
  buttons?: Array<{ label: string; action: () => void; primary?: boolean }>;
}

export class ModalService {
  private ea = resolve(IEventAggregator);

  alert(title: string, message: string) {
    return this.open({
      title,
      message,
      buttons: [{ label: 'OK', action: () => {}, primary: true }]
    });
  }

  confirm(title: string, message: string): Promise<boolean> {
    return new Promise(resolve => {
      this.open({
        title,
        message,
        buttons: [
          { label: 'Cancel', action: () => resolve(false) },
          { label: 'Confirm', action: () => resolve(true), primary: true }
        ]
      });
    });
  }

  private open(config: ModalConfig) {
    this.ea.publish('modal:open', config);
  }
}
```

## Best Practices

1. **Focus Management**: Always return focus to the trigger element
2. **Body Scroll**: Lock body scroll to prevent confusion
3. **Escape Key**: Always allow Escape to close (unless critical action)
4. **Backdrop Click**: Make it configurable, disable for forms with unsaved changes
5. **Portal Rendering**: For complex apps, render modals in a portal at document root
6. **Stacking**: Support multiple modals with z-index management

## Summary

You've built a fully-featured modal dialog with:

* ✅ Smooth animations
* ✅ Focus trap and management
* ✅ Keyboard support
* ✅ Accessible markup
* ✅ Multiple sizes
* ✅ Customizable behavior

This modal is production-ready and handles all common use cases!


# Accordion

Build an accessible accordion component with smooth animations and keyboard support

Learn to build a simple yet powerful accordion component for collapsible content panels. Perfect for FAQs, settings panels, and content organization.

## What We're Building

An accordion that supports:

* Expand/collapse panels
* Single or multiple panels open
* Smooth animations
* Keyboard navigation
* Accessible with ARIA attributes
* Customizable styling

## Component Code

### accordion.ts

```typescript
import { bindable } from 'aurelia';

export class Accordion {
  @bindable allowMultiple = false;
  @bindable openPanels: number[] = [];

  togglePanel(index: number) {
    if (this.allowMultiple) {
      // Multiple panels can be open
      const panelIndex = this.openPanels.indexOf(index);
      if (panelIndex > -1) {
        this.openPanels.splice(panelIndex, 1);
      } else {
        this.openPanels.push(index);
      }
    } else {
      // Only one panel can be open
      if (this.isPanelOpen(index)) {
        this.openPanels = [];
      } else {
        this.openPanels = [index];
      }
    }
  }

  isPanelOpen(index: number): boolean {
    return this.openPanels.includes(index);
  }
}
```

### accordion.html

```html
<div class="accordion">
  <au-slot></au-slot>
</div>
```

### accordion-panel.ts

```typescript
import { bindable, resolve } from 'aurelia';
import { Accordion } from './accordion';

export class AccordionPanel {
  @bindable title = '';
  @bindable index = 0;

  private accordion = resolve(Accordion);

  get isOpen(): boolean {
    return this.accordion.isPanelOpen(this.index);
  }

  toggle() {
    this.accordion.togglePanel(this.index);
  }

  handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.toggle();
    }
  }
}
```

### accordion-panel.html

```html
<div class="accordion-panel \${isOpen ? 'accordion-panel--open' : ''}">
  <button
    type="button"
    class="accordion-panel__header"
    click.trigger="toggle()"
    keydown.trigger="handleKeyDown($event)"
    aria-expanded.bind="isOpen">

    <span class="accordion-panel__title">\${title}</span>

    <svg class="accordion-panel__icon" width="16" height="16" viewBox="0 0 16 16">
      <path d="M8 12L2 6h12z" fill="currentColor"/>
    </svg>
  </button>

  <div
    class="accordion-panel__content"
    aria-hidden.bind="!isOpen">
    <div class="accordion-panel__body">
      <au-slot></au-slot>
    </div>
  </div>
</div>
```

### accordion.css

```css
.accordion {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
}

.accordion-panel {
  border-bottom: 1px solid #e5e7eb;
}

.accordion-panel:last-child {
  border-bottom: none;
}

.accordion-panel__header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  background: white;
  border: none;
  cursor: pointer;
  transition: background 0.15s;
  text-align: left;
  font-size: 16px;
  font-weight: 500;
}

.accordion-panel__header:hover {
  background: #f9fafb;
}

.accordion-panel__header:focus {
  outline: 2px solid #3b82f6;
  outline-offset: -2px;
  z-index: 1;
}

.accordion-panel__title {
  color: #111827;
}

.accordion-panel__icon {
  color: #6b7280;
  transition: transform 0.2s;
  flex-shrink: 0;
}

.accordion-panel--open .accordion-panel__icon {
  transform: rotate(180deg);
}

.accordion-panel__content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease-out;
}

.accordion-panel--open .accordion-panel__content {
  max-height: 1000px; /* Adjust based on your content */
}

.accordion-panel__body {
  padding: 0 20px 16px;
  color: #374151;
  line-height: 1.6;
}
```

## Usage Examples

### Basic Accordion (Single Panel Open)

```html
<accordion>
  <accordion-panel index="0" title="What is Aurelia?">
    Aurelia is a modern JavaScript framework for building web applications.
  </accordion-panel>

  <accordion-panel index="1" title="How do I install Aurelia?">
    You can install Aurelia using npm: <code>npm install aurelia</code>
  </accordion-panel>

  <accordion-panel index="2" title="Where can I learn more?">
    Check out the official documentation at docs.aurelia.io
  </accordion-panel>
</accordion>
```

### Multiple Panels Open

```html
<accordion allow-multiple.bind="true">
  <accordion-panel index="0" title="Account Settings">
    <p>Manage your account settings here.</p>
  </accordion-panel>

  <accordion-panel index="1" title="Privacy Settings">
    <p>Control your privacy preferences.</p>
  </accordion-panel>

  <accordion-panel index="2" title="Notification Settings">
    <p>Configure your notification preferences.</p>
  </accordion-panel>
</accordion>
```

### Controlled Open Panels

```typescript
// your-component.ts
export class FAQPage {
  openPanels = [0]; // First panel open by default

  openAll() {
    this.openPanels = [0, 1, 2, 3];
  }

  closeAll() {
    this.openPanels = [];
  }
}
```

```html
<!-- your-component.html -->
<div>
  <button click.trigger="openAll()">Expand All</button>
  <button click.trigger="closeAll()">Collapse All</button>
</div>

<accordion allow-multiple.bind="true" open-panels.bind="openPanels">
  <accordion-panel index="0" title="Question 1">Answer 1</accordion-panel>
  <accordion-panel index="1" title="Question 2">Answer 2</accordion-panel>
  <accordion-panel index="2" title="Question 3">Answer 3</accordion-panel>
  <accordion-panel index="3" title="Question 4">Answer 4</accordion-panel>
</accordion>
```

### With Rich Content

```html
<accordion>
  <accordion-panel index="0" title="Product Features">
    <ul>
      <li>Feature 1: Fast performance</li>
      <li>Feature 2: Easy to use</li>
      <li>Feature 3: Highly customizable</li>
    </ul>
  </accordion-panel>

  <accordion-panel index="1" title="Pricing">
    <div class="pricing-grid">
      <div class="plan">
        <h3>Basic</h3>
        <p>$9/month</p>
      </div>
      <div class="plan">
        <h3>Pro</h3>
        <p>$29/month</p>
      </div>
    </div>
  </accordion-panel>
</accordion>
```

## Testing

```typescript
import { createFixture } from '@aurelia/testing';
import { Accordion } from './accordion';
import { AccordionPanel } from './accordion-panel';

describe('Accordion', () => {
  it('toggles panel open/closed', async () => {
    const { getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion>
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    const panel = getAllBy('.accordion-panel')[0];

    // Initially closed
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    // Click to open
    trigger.click('.accordion-panel__header');
    expect(panel.classList.contains('accordion-panel--open')).toBe(true);

    // Click to close
    trigger.click('.accordion-panel__header');
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    await stop(true);
  });

  it('allows only one panel open when allowMultiple=false', async () => {
    const { component, getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion allow-multiple.bind="false">
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
          <accordion-panel index="1" title="Panel 2">Content 2</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    // Open first panel
    trigger.click('.accordion-panel:first-child .accordion-panel__header');
    expect(component.openPanels).toEqual([0]);

    // Open second panel
    trigger.click('.accordion-panel:nth-child(2) .accordion-panel__header');
    expect(component.openPanels).toEqual([1]); // First closed, second open

    await stop(true);
  });

  it('allows multiple panels open when allowMultiple=true', async () => {
    const { component, getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion allow-multiple.bind="true">
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
          <accordion-panel index="1" title="Panel 2">Content 2</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    // Open first panel
    trigger.click('.accordion-panel:first-child .accordion-panel__header');
    expect(component.openPanels).toEqual([0]);

    // Open second panel
    trigger.click('.accordion-panel:nth-child(2) .accordion-panel__header');
    expect(component.openPanels).toEqual([0, 1]); // Both open

    await stop(true);
  });

  it('supports keyboard navigation', async () => {
    const { getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion>
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    const button = getAllBy('.accordion-panel__header')[0];
    const panel = getAllBy('.accordion-panel')[0];

    // Press Enter to open
    trigger.keydown(button, { key: 'Enter' });
    expect(panel.classList.contains('accordion-panel--open')).toBe(true);

    // Press Space to close
    trigger.keydown(button, { key: ' ' });
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    await stop(true);
  });
});
```

## Accessibility Features

This accordion implements WCAG 2.1 guidelines:

* ✅ **ARIA Attributes**: `aria-expanded` indicates panel state
* ✅ **Keyboard Support**: Enter and Space keys toggle panels
* ✅ **Focus Management**: Buttons are focusable with visible focus indicators
* ✅ **Semantic HTML**: Uses `<button>` for interactive headers

## Enhancements

### 1. Add Animation Callbacks

```typescript
export class AnimatedAccordion extends Accordion {
  @bindable onBeforeOpen?: (index: number) => void;
  @bindable onAfterOpen?: (index: number) => void;

  togglePanel(index: number) {
    const wasOpen = this.isPanelOpen(index);

    if (!wasOpen && this.onBeforeOpen) {
      this.onBeforeOpen(index);
    }

    super.togglePanel(index);

    if (!wasOpen && this.onAfterOpen) {
      setTimeout(() => this.onAfterOpen!(index), 300); // After animation
    }
  }
}
```

### 2. Add Icons

```html
<accordion-panel index="0" title="Custom Icon">
  <svg au-slot="icon" width="20" height="20">
    <!-- Custom icon -->
  </svg>

  Panel content here
</accordion-panel>
```

### 3. Add Lazy Loading

```typescript
export class LazyAccordionPanel extends AccordionPanel {
  @bindable loadContent?: () => Promise<any>;
  content: any = null;
  loaded = false;

  async toggle() {
    super.toggle();

    if (this.isOpen && !this.loaded && this.loadContent) {
      this.content = await this.loadContent();
      this.loaded = true;
    }
  }
}
```

## Best Practices

1. **Animation Performance**: Use `max-height` instead of `height: auto` for smooth transitions
2. **Content Height**: Set reasonable `max-height` values or calculate dynamically
3. **Accessibility**: Always include `aria-expanded` for screen readers
4. **Focus Visible**: Ensure keyboard focus is clearly visible
5. **Mobile**: Test touch interactions and ensure adequate tap targets

## Summary

You've built a fully-functional accordion with:

* ✅ Single and multiple panel modes
* ✅ Smooth animations
* ✅ Keyboard support
* ✅ Accessible markup
* ✅ Easy customization

This accordion is ready for FAQs, settings panels, and any collapsible content!


# Template compilation


# processContent

Learn how to manipulate the DOM from the usage-side of a custom element using the processContent hook.

There are scenarios where we would like to transform the template provided by the usage-side. The 'processContent' hook lets us define a pre-compilation hook to make that transformation.

The signature of the hook function is as follows.

```typescript
// pseudo-code; `typeof TCustomElement` doesn't work in Generics form.
<TCustomElement>(this: typeof TCustomElement, node: INode, platform: IPlatform) => boolean | void;
```

There are two important things to note here.

First is the `node` argument. It is the DOM tree on the usage-side for the custom element. For example, if there is a custom element named `my-element`, on which a 'processContent' hook is defined, and it is used somewhere as shown in the following markup, then when the hook is invoked, the `node` argument will provide the DOM tree that represents the following markup.

```html
<my-element>
 <foo></foo>
 <bar></bar>
</my-element>
```

Then inside the hook this DOM tree can be transformed/mutated into a different DOM tree. The mutation can be addition/removal of attributes or element nodes.

Second is the return type `boolean | void`. Returning from this function is optional. Only an explicit `false` return value results in skipping the compilation (and thereby enhancing) of the child nodes in the DOM tree. The implication of skipping the compilation of the child nodes is that Aurelia will not touch those DOM fragments and will be kept as it is. In other words, if the mutated node contains custom elements, custom attributes, or template controllers, those will not be hydrated.

The `platform` argument is just the helper to have platform-agnostic operations as it abstracts the platform. Lastly the `this` argument signifies that the hook function always gets bound to the custom element class function for which the hook is defined.

The most straight forward way to define the hook is to use the `processContent` property while defining the custom-element.

```typescript
import { customElement, INode, IPlatform } from '@aurelia/runtime-html';

// Use a standalone function
function processContent(node: INode, platform: IPlatform) { }
@customElement({ name: 'my-element', processContent })
export class MyElement { }

// ... or use a static method named 'processContent' (convention)
@customElement({ name: 'my-element' })
export class MyElement {
  static processContent(node: INode, platform: IPlatform) { }
}
```

Apart from this, there is also the `@processContent` decorator which can used class-level or method-level.

```typescript
import { customElement, INode, IPlatform, processContent } from '@aurelia/runtime-html';

// ...or a standalone method
function processContent(this: typeof MyElement, node: INode, platform: IPlatform) { }
@processContent(processContent)
export class MyElement {
}

// ...or the method-level decorator
export class MyElement {
  @processContent()
  static processContent(node: INode, platform: IPlatform) { }
}
```

That's the API. Now let us say consider an example. Let us say that we want to create a custom elements that behaves as a tabs control. That is this custom element shows different sets of information grouped under a set of headers, and when the header is clicked the associated content is shown. To this end, we can conceptualize the markup for this custom element as follows.

```html
<!--tabs.html-->
<div class="header">
  <au-slot name="header"></au-slot>
</div>
<div class="content">
  <au-slot name="content"></au-slot>
</div>
```

The markup has 2 slots for the header and content projection. While using the `tabs` custom element we want to have the following markup.

{% hint style="info" %}
If you are unfamiliar with the `au-slot` then visit the [documentation](#au-slot). 'processContent' can be very potent with `au-slot`.
{% endhint %}

```html
<!--app.html-->
<tabs>
  <tab header="Tab one">
    <span>content for first tab.</span>
  </tab>
  <tab header="Tab two">
    <span>content for second tab.</span>
  </tab>
  <tab header="Tab three">
    <span>content for third tab.</span>
  </tab>
</tabs>
```

Now note that there is no custom element named `tab`. The idea is to keep the usage-markup as much dev-friendly as possible, so that it is easy to maintain, and the semantics are quite clear. Also it is easy to refactor as now we know which parts belong together. To support this usage-syntax we will use the 'processContent' hook to rearrange the DOM tree, so that the nodes are correctly projected at the end. A prototype implementation is shown below.

{% hint style="info" %}
Note the use of `$host` scope reference that is used when generating markup that will be projected into the slot. Host is required to access the `activeTabId` property and the `showTab` function of the `Tabs` custom element that is *hosting* the projected markup. More details available at [Slotted content](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/developer-guides/scenarios/components/shadow-dom-and-slots.md).
{% endhint %}

```typescript
// tabs.ts
import { INode, IPlatform, processContent } from '@aurelia/runtime-html';

class Tabs {

  @processContent()
  public static processTabs(node: INode, p: IPlatform): boolean {
    const el = node as Element;

    // At first we prepare two templates that will provide the projections to the `header` and `content` slot respectively.
    const headerTemplate = p.document.createElement('template');
    headerTemplate.setAttribute('au-slot', 'header');
    const contentTemplate = p.document.createElement('template');
    contentTemplate.setAttribute('au-slot', 'content');

    // Query the `<tab>` elements present in the `node`.
    const tabs = toArray(el.querySelectorAll('tab'));
    for (let i = 0; i < tabs.length; i++) {
      const tab = tabs[i];

      // Add header.
      const header = p.document.createElement('button');
      // Add a class binding to mark the active tab.
      header.setAttribute('class.bind', `$host.activeTabId=='${i}'?'active':''`);
      // Add a click delegate to activate a tab.
      header.setAttribute('click.trigger', `$host.showTab('${i}')`);
      header.appendChild(p.document.createTextNode(tab.getAttribute('header')));
      headerTemplate.content.appendChild(header);

      // Add content.
      const content = p.document.createElement('div');
      // Show the content if the tab is activated.
      content.setAttribute('if.bind', `$host.activeTabId=='${i}'`);
      content.append(...toArray(tab.childNodes));
      contentTemplate.content.appendChild(content);

      el.removeChild(tab);
    }
    // Set the first tab as the initial active tab.
    el.setAttribute('active-tab-id', '0');

    el.append(headerTemplate, contentTemplate);
  }

  @bindable public activeTabId: string;
  public showTab(tabId: string) {
    this.activeTabId = tabId;
  }
}
```

**Example transformation function for default `[au-slot]`**

If you have used [`au-slot`](#au-slot), you might have noticed that in order to provide a projection the usage of `[au-slot]` attribute is mandatory, even if the projections are targeted to the default `au-slot`. With the help of the 'processContent' hook we can workaround this minor inconvenience. The following is a sample transformation function that loops over the direct children under `node` and demotes the nodes without any `[au-slot]` attribute to a synthetic `template[au-slot]` node.

```typescript
processContent(node: INode, p: IPlatform) {
  const projection = p.document.createElement('template');
  projection.setAttribute('au-slot', '');
  const content = projection.content;
  for (const child of toArray(node.childNodes)) {
    if (!(child as Element).hasAttribute('au-slot')) {
      content.append(child);
    }
  }
  if (content.childElementCount > 0) {
    node.appendChild(projection);
  }
}
```


# Extending templating syntax

The Aurelia template compiler is powerful and developer-friendly, allowing you extend its syntax with great ease.

## Context

Sometimes you will see the following template in an Aurelia application:

```html
<input value.bind="message">
```

Aurelia understands that `value.bind="message"` means `value.two-way="message"`, and later creates a two way binding between view model `message` property, and input `value` property. How does Aurelia know this?

By default, Aurelia is taught how to interpret a `bind` binding command on a property of an element via a Attribute Syntax Mapper. Application can also tap into this class to teach Aurelia some extra knowledge so that it understands more than just `value.bind` on an `<input/>` element.

## Examples

You may sometimes come across some custom input element in a component library, some examples are:

* Microsoft FAST `text-field` element: <https://explore.fast.design/components/fast-text-field>
* Ionic `ion-input` element: <https://ionicframework.com/docs/api/input>
* Polymer `paper-input` element: <https://www.webcomponents.org/element/@polymer/paper-input>
* and many more...

Regardless of the lib choice an application takes, what is needed in common is the ability to have a concise syntax to describe the two way binding intention with those custom elements. Some examples for the above custom input elements:

```html
<fast-text-field value.bind="message">
<ion-input value.bind="message">
<paper-input value.bind="message">
```

should be treated as:

```html
<fast-text-field value.two-way="message">
<ion-input value.two-way="message">
<paper-input value.two-way="message">
```

In the next section we will look into how to teach Aurelia such knowledge. Before diving in, keep the following mental model in mind:

1. **Attribute patterns** (`@attributePattern`) split attribute names into `target` + `command` pairs. Use them when you want to teach the compiler new syntaxes such as `[(value)]`. See [Attribute Patterns](/components/template-compilation/attributepattern) for a full walkthrough.
2. **Attribute syntax mapper** (`IAttrMapper`) decides whether `value.bind` really means `value.two-way`, and how attribute names map onto DOM properties.
3. **Node observer locator** (`INodeObserverLocator`) teaches the runtime how to observe those DOM properties (which events fire, whether values are readonly, etc.).

All three steps are optional, but more advanced templating extensions usually need at least 2 and 3.

## Using the Attribute Syntax Mapper

The Attribute Syntax Mapper decides which binding command Aurelia should use when you write `.bind`. Built-in rules already cover native elements (`value.bind` on `<input>` becomes `.two-way`, `checked.bind` on `checkbox` becomes `.two-way`, etc.). When you integrate with design systems or Web Components, you nearly always need to extend the mapper so that your terse syntax keeps working.

Every Aurelia application uses a single mapper instance. Grab it with `resolve(IAttrMapper)` wherever you configure your app (typically via `AppTask`).

```typescript
import { IAttrMapper, resolve } from 'aurelia';

export class MyCustomElement {
  private attrMapper = resolve(IAttrMapper);

  constructor() {
    // do something with this.attrMapper
  }
}
```

`IAttrMapper` exposes:

* `useMapping(config)` — map attributes (by tag name) to DOM properties.
* `useGlobalMapping(config)` — same mapping, but applied to every tag.
* `useTwoWay(predicate)` — force `.bind` to behave like `.two-way` for certain `(element, attrName)` pairs. `attrName` is the kebab-case attribute name; return `true` to enable two-way.

Example: teach Aurelia that `<fast-text-field value.bind="...">` should become `value.two-way`.

```typescript
attrMapper.useTwoWay((element, attrName) => {
  switch (element.tagName) {
    case 'FAST-TEXT-FIELD':
    case 'ION-INPUT':
    case 'PAPER-INPUT':
      return attrName === 'value';
    default:
      return false;
  }
});
```

## Combining the attribute syntax mapper with the node observer locator

Teaching Aurelia to map `value.bind` to `value.two-way` is the first half of the story. The second half ensures the runtime knows **how** to observe that DOM property. Do this via the Node Observer Locator. Retrieve it with `resolve(INodeObserverLocator)` from `@aurelia/runtime`:

```typescript
import { resolve } from 'aurelia';
import { INodeObserverLocator } from '@aurelia/runtime';

export class MyCustomElement {
  private nodeObserverLocator = resolve(INodeObserverLocator);

  constructor() {
    // do something with this.nodeObserverLocator
  }
}
```

After grabbing the locator, call `useConfig()` (per-tag) or `useConfigGlobal()` (all tags). Each config object describes:

* `events: string[]` — events to subscribe to.
* `readonly?: boolean` — if `true`, Aurelia never writes to the property (useful for `files`).
* `default?: unknown` — fallback value when a binding sets `null`/`undefined`.
* `type?: INodeObserverConstructor` — provide a custom observer implementation.

Example: watch `<fast-text-field value>` via the `change` event.

```typescript
nodeObserverLocator.useConfig('FAST-TEXT-FIELD', 'value', { events: ['change' ] });
```

Similarly, examples for `<ion-input>` and `<paper-input>`:

```typescript
nodeObserverLocator.useConfig('ION-INPUT', 'value', { events: ['change' ] });
nodeObserverLocator.useConfig('PAPER-INPUT', 'value', { events: ['change' ] });
```

{% hint style="success" %}
If an object is passed to the `.useConfig` API of the Node Observer Locator, it will be used as a multi-registration call, as per following example, where we register `<fast-text-field>`, `<ion-input>`, `<paper-input>` all in a single call:

```typescript
nodeObserverLocator.useConfig({
  'FAST-TEXT-FIELD': {
    value: { events: ['change'] }
  },
  'ION-INPUT': {
    value: { events: ['change'] }
  },
  'PAPER-INPUT': {
    value: { events: ['change'] }
  }
})
```

{% endhint %}

## Putting it together

Combine both extensions inside `AppTask.creating` so they run before Aurelia instantiates your root component. The example below integrates a subset of [Microsoft FAST](https://explore.fast.design/components/fast-text-field) controls:

```typescript
import Aurelia, { AppTask, IAttrMapper } from 'aurelia';
import { INodeObserverLocator } from '@aurelia/runtime';

Aurelia
  .register(
    AppTask.creating(IAttrMapper, attrMapper => {
      attrMapper.useTwoWay((el, attrName) => {
        switch (el.tagName) {
          case 'FAST-TEXT-FIELD':
          case 'FAST-TEXT-AREA':
          case 'FAST-SLIDER':
            return attrName === 'value';
          default:
            return false;
        }
      });
    }),
    AppTask.creating(INodeObserverLocator, nodeObserverLocator => {
      nodeObserverLocator.useConfig({
        'FAST-TEXT-FIELD': {
          value: { events: ['change'] }
        },
        'FAST-TEXT-AREA': {
          value: { events: ['change'] }
        },
        'FAST-SLIDER': {
          value: { events: ['change'] }
        }
      });
    })
  )
  .app(class MyApp {})
  .start();
```

With the above, your Aurelia application now understands the concise `value.bind` syntax and listens to the correct events:

```html
<fast-text-field value.bind="message"></fast-text-field>
<fast-text-area value.bind="description"></fast-text-area>
<fast-slider value.bind="fontSize"></fast-slider>
```

## Troubleshooting and best practices

* **Duplicate mapping errors** – `IAttrMapper` throws if you register the same tag/attribute twice. Remove or consolidate the previous registration before adding new rules.
* **Verify DOM property names** – `useMapping` expects actual property names (`valueAsNumber`, `formNoValidate`, etc.). Typos silently fall back to camelCase conversion.
* **Mind attribute casing** – The mapper receives attributes in kebab-case. If your component exposes camelCase properties (common for Web Components), map `'my-prop'` → `'myProp'`.
* **Use `'new'` containers sparingly** – When augmenting `INodeObserverLocator`, you rarely need custom observers. Prefer event-only configs before writing a new observer type.
* **Test with devtools** – Toggle your custom elements in the browser and inspect `element.value`. If the value updates but Aurelia bindings do not, double-check the observer config. If bindings update but DOM does not, revisit `useMapping`.

Once you understand the flow—pattern → mapper → observer—you can make nearly any third-party component feel native inside Aurelia templates.


# Modifying template parsing with AttributePattern

Aurelia's attribute pattern system allows you to create custom template syntax extensions that can emulate other framework syntaxes like Angular or Vue, or define entirely new patterns for your specific needs. This powerful extensibility feature integrates directly with Aurelia's template compiler and binding engine.

## Architecture Overview

The attribute pattern system consists of several core components:

* **AttributePatternDefinition**: Defines pattern structure with `pattern` and `symbols`
* **AttrSyntax**: The parsed result containing binding information
* **SyntaxInterpreter**: A finite state machine that efficiently parses attribute names
* **AttributeParser**: Manages pattern registration and result caching
* **Pattern Priority System**: Resolves conflicts when multiple patterns match

## When to reach for attribute patterns

Create an attribute pattern when **the attribute name itself** needs to convey extra meaning. Typical use cases include:

* Porting syntaxes from other frameworks (`[(value)]`, `@click`, `:value`, `#ref`).
* Building DSLs where symbols separate intent (for example, `data-track.click.once`).
* Collapsing multiple instructions into one attribute, such as `emit:save` or `listen:customer.updated`.

If you simply need `value.bind` to default to two-way binding, prefer the attribute mapper. If you want to change how an attribute behaves after it has been parsed, reach for a binding command instead. Attribute patterns run **before** the mapper and binding commands, so they are ideal for inventing new syntaxes.

## Basic Pattern Definition

### AttributePatternDefinition Interface

```typescript
export interface AttributePatternDefinition {
  pattern: string;   // Pattern with PART placeholders
  symbols: string;   // Characters treated as separators/delimiters
}
```

### The PART Keyword

`PART` in patterns represents dynamic segments that can match any characters except those defined in `symbols`. Think of `PART` as a flexible placeholder equivalent to the regex `([^symbols]+)`.

### Symbols Behavior

The `symbols` property defines characters that:

* Act as separators between pattern segments
* Are excluded from `PART` matching
* Can be used for readability and structure

**Example:**

```typescript
{ pattern: 'foo@PART', symbols: '@' }
```

* `foo@bar` → parts: `['foo', 'bar']` (with symbols)
* Without symbols → parts: `['foo@', 'bar']` (without symbols)

## Pattern Class Structure

### Basic Pattern Class

```typescript
import { attributePattern, AttrSyntax } from '@aurelia/template-compiler';

@attributePattern({ pattern: '[(PART)]', symbols: '[()]' })
export class AngularTwoWayBindingAttributePattern {
  public ['[(PART)]'](rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'two-way');
  }
}
```

> **Note**: `AttrSyntax` must be imported from `@aurelia/template-compiler`, not from the main `aurelia` package, as it's not currently re-exported there.

### Pattern Method Signature

Each pattern method must:

1. Have the exact same name as the pattern string
2. Accept three required parameters:
   * `rawName: string` - Original attribute name (e.g., "\[(value)]")
   * `rawValue: string` - Attribute value (e.g., "message")
   * `parts: readonly string[]` - Extracted PART values (e.g., \["value"])
3. Return an `AttrSyntax` instance

## AttrSyntax Constructor

The `AttrSyntax` class has the following constructor signature:

```typescript
export class AttrSyntax {
  public constructor(
    public rawName: string,      // Original attribute name
    public rawValue: string,     // Original attribute value
    public target: string,       // Target property/element
    public command: string | null, // Binding command
    public parts: readonly string[] | null = null // Additional parts for complex patterns
  ) {}
}
```

### AttrSyntax Parameters Explained

| Parameter  | Description                                 | Example                                     |
| ---------- | ------------------------------------------- | ------------------------------------------- |
| `rawName`  | Original attribute name from template       | `"[(value)]"`                               |
| `rawValue` | Original attribute value                    | `"message"`                                 |
| `target`   | The target property, element, or identifier | `"value"`                                   |
| `command`  | Binding command type                        | `"two-way"`, `"bind"`, `"trigger"`, `"ref"` |
| `parts`    | Additional parts for complex patterns       | For event modifiers, extended syntax        |

### Common Binding Commands

* `'bind'` - One-way to view binding
* `'to-view'` - Explicit one-way to view
* `'from-view'` - One-way from view
* `'two-way'` - Two-way data binding
* `'trigger'` - Event binding
* `'capture'` - Event capture
* `'ref'` - Element/component reference
* `null` - Custom or no specific command

## Pattern Registration

### Global Registration

Register patterns globally at application startup:

```typescript
import { Aurelia } from 'aurelia';
import { AngularTwoWayBindingAttributePattern } from './patterns/angular-patterns';

Aurelia
  .register(AngularTwoWayBindingAttributePattern)
  .app(MyApp)
  .start();
```

### Local Registration

Register patterns for specific components:

```typescript
import { customElement } from '@aurelia/runtime-html';
import { AngularTwoWayBindingAttributePattern } from './patterns/angular-patterns';

@customElement({
  name: 'my-component',
  template: '<input [(value)]="message">',
  dependencies: [AngularTwoWayBindingAttributePattern]
})
export class MyComponent {
  public message = 'Hello World';
}
```

### Inline Pattern Definition

For simple patterns, you can define them inline:

```typescript
import { AttributePattern } from '@aurelia/template-compiler';

@customElement({
  name: 'my-component',
  template: '<input !value="message">',
  dependencies: [
    // Define pattern inline and register directly
    (() => {
      @attributePattern({ pattern: '!PART', symbols: '!' })
      class InlineExclamationPattern {
        '!PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
          return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
        }
      }
      return InlineExclamationPattern;
    })()
  ]
})
```

## Multiple Patterns per Class

A single class can handle multiple related patterns:

```typescript
@attributePattern(
  { pattern: 'PART#PART', symbols: '#' }, // view-model#uploadVM
  { pattern: '#PART', symbols: '#' }      // #uploadInput
)
export class AngularSharpRefAttributePattern {
  public ['PART#PART'](rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, parts[1], parts[0], 'ref');
  }

  public ['#PART'](rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, parts[0], 'element', 'ref');
  }
}
```

## Pattern Priority System

When multiple patterns could match the same attribute name, Aurelia uses a priority system:

1. **Static segments** (exact text matches) have highest priority
2. **Dynamic segments** (PART) have medium priority
3. **Symbol segments** have lower priority

**Example Priority Resolution:**

```typescript
// Given patterns: 'PART.PART', 'value.PART', 'PART.bind'
// For attribute 'value.bind':
// - 'value.PART' matches with 1 static + 1 dynamic = higher priority
// - 'PART.bind' matches with 1 dynamic + 1 static = same priority
// - 'PART.PART' matches with 2 dynamic = lower priority
// Result: First pattern with highest static count wins
```

## Advanced Pattern Examples

### Event Modifiers

```typescript
@attributePattern(
  { pattern: 'PART.trigger:PART', symbols: '.:' },
  { pattern: 'PART.capture:PART', symbols: '.:' }
)
export class EventModifierPattern {
  public 'PART.trigger:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger', parts);
  }

  public 'PART.capture:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'capture', parts);
  }
}
```

### Static Patterns (No PART)

```typescript
@attributePattern(
  { pattern: 'promise.resolve', symbols: '' },
  { pattern: 'promise.catch', symbols: '' },
  { pattern: 'ref', symbols: '' }
)
export class StaticPatterns {
  public 'promise.resolve'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, rawValue, 'promise-resolve');
  }

  public 'promise.catch'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, rawValue, 'promise-catch');
  }

  public 'ref'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'element', 'ref');
  }
}
```

### Complex Multi-PART Patterns

```typescript
@attributePattern({ pattern: 'PART.PART.PART', symbols: '.' })
export class ThreePartPattern {
  public 'PART.PART.PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    // For something like 'user.address.street.bind'
    // parts = ['user', 'address', 'street', 'bind']
    const target = `${parts[0]}.${parts[1]}.${parts[2]}`;
    return new AttrSyntax(rawName, rawValue, target, parts[3]);
  }
}
```

## Built-in Pattern Examples

Aurelia includes several built-in patterns you can reference:

### Dot-Separated Patterns

```typescript
// Built-in: handles 'value.bind', 'checked.two-way', etc.
@attributePattern(
  { pattern: 'PART.PART', symbols: '.' },
  { pattern: 'PART.PART.PART', symbols: '.' }
)
export class DotSeparatedAttributePattern {
  public 'PART.PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], parts[1]);
  }

  public 'PART.PART.PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, `${parts[0]}.${parts[1]}`, parts[2]);
  }
}
```

### Shorthand Binding Patterns

```typescript
// Built-in: handles ':value', '@click', etc.
@attributePattern({ pattern: ':PART', symbols: ':' })
export class ColonPrefixedBindAttributePattern {
  public ':PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
  }
}

@attributePattern(
  { pattern: '@PART', symbols: '@' },
  { pattern: '@PART:PART', symbols: '@:' }
)
export class AtPrefixedTriggerAttributePattern {
  public '@PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger');
  }

  public '@PART:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger', [parts[0], 'trigger', ...parts.slice(1)]);
  }
}
```

## Framework Syntax Examples

### Angular-Style Patterns

```typescript
// Angular ref syntax: #myInput
@attributePattern({ pattern: '#PART', symbols: '#' })
export class AngularRefPattern {
  public '#PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, parts[0], 'element', 'ref');
  }
}

// Angular property binding: [value]
@attributePattern({ pattern: '[PART]', symbols: '[]' })
export class AngularPropertyBinding {
  public '[PART]'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
  }
}

// Angular event binding: (click)
@attributePattern({ pattern: '(PART)', symbols: '()' })
export class AngularEventBinding {
  public '(PART)'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger');
  }
}

// Angular two-way binding: [(ngModel)]
@attributePattern({ pattern: '[(PART)]', symbols: '[()]' })
export class AngularTwoWayBinding {
  public '[(PART)]'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'two-way');
  }
}
```

### Vue-Style Patterns

```typescript
// Vue property binding: :value
@attributePattern({ pattern: ':PART', symbols: ':' })
export class VuePropertyBinding {
  public ':PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
  }
}

// Vue event binding: @click
@attributePattern({ pattern: '@PART', symbols: '@' })
export class VueEventBinding {
  public '@PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger');
  }
}

// Vue v-model directive
@attributePattern({ pattern: 'v-model', symbols: '' })
export class VueModelDirective {
  public 'v-model'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'value', 'two-way');
  }
}
```

## Performance Considerations

### Caching System

The attribute parser maintains an internal cache of parsed interpretations. Once an attribute name is parsed, the result is cached for subsequent uses, improving template compilation performance.

### Pattern Optimization

* **Order Matters**: More specific patterns should be defined first when possible
* **Symbol Selection**: Choose symbols that don't conflict with common attribute patterns
* **Minimal Patterns**: Avoid overly complex patterns that could match unintended attributes

### Registration Timing

Patterns must be registered before template compilation begins. Late registration after the application starts may not take effect for already-compiled templates.

## Debugging and Error Handling

### Common Pattern Errors

1. **Missing Method**: Pattern method name doesn't match pattern string exactly
2. **Wrong Signature**: Method signature doesn't match required parameters
3. **Symbol Conflicts**: Pattern symbols conflict with other registered patterns
4. **Registration Timing**: Patterns registered after compilation begins

### Debugging Tips

```typescript
// Enable debug logging to see pattern matching
import { LoggerConfiguration, LogLevel } from '@aurelia/kernel';

Aurelia
  .register(LoggerConfiguration.create({ level: LogLevel.debug }))
  .register(MyPatternClass)
  .app(MyApp)
  .start();
```

### Pattern Testing

Test your patterns with various attribute combinations:

```typescript
// Testing patterns is typically done through the DI container
import { DI } from '@aurelia/kernel';
import { ISyntaxInterpreter, IAttributePattern } from '@aurelia/template-compiler';

// Create a container and register your pattern
const container = DI.createContainer();
container.register(MyPatternClass);

const interpreter = container.get(ISyntaxInterpreter);
const attrPattern = container.get(IAttributePattern);

// Test pattern interpretation
const result = interpreter.interpret('[(value)]');
if (result.pattern) {
  console.log('Pattern matched:', result.pattern);
  console.log('Parts:', result.parts);
}
```

## Integration with Template Compiler

Attribute patterns integrate seamlessly with Aurelia's template compilation process:

1. **Template Analysis**: The compiler scans for all attributes
2. **Pattern Matching**: Each attribute name is tested against registered patterns
3. **Syntax Creation**: Matching patterns create `AttrSyntax` objects
4. **Binding Generation**: The compiler generates appropriate bindings based on the syntax
5. **Runtime Execution**: Bindings execute during component lifecycle

### Working alongside binding commands and the attribute mapper

* **Attribute patterns** decide the final `target` and `command` for an attribute. They are the only hook that can rewrite `foo.bar.baz` into whatever structure you need.
* **Binding commands** use that parsed information to produce instructions. If your pattern returns `command: 'permission'`, the binding command named `permission` will receive the attribute.
* **Attribute mapper** only runs when `command === 'bind'`. If your pattern emits `'bind'`, the mapper can still remap `value.bind` to `value.two-way` or translate attribute names into DOM properties.

Design patterns so they hand off clear targets and commands to the downstream pipeline. When in doubt, log the resulting `AttrSyntax` objects while authoring your pattern to confirm the values that later hooks will see.

## Best Practices

### Pattern Design

1. **Intuitive Syntax**: Create patterns that feel natural to developers
2. **Consistent Naming**: Follow consistent conventions across related patterns
3. **Clear Symbols**: Use symbols that clearly separate pattern parts
4. **Avoid Conflicts**: Test patterns against existing Aurelia syntax

### Registration Strategy

1. **Global vs Local**: Use global registration for widely-used patterns, local for component-specific ones
2. **Bundle Size**: Consider the impact of registering many patterns globally
3. **Tree Shaking**: Local registration helps with tree shaking unused patterns

### Error Recovery

1. **Graceful Fallback**: Design patterns to fail gracefully when they don't match
2. **Clear Errors**: Provide meaningful error messages in pattern methods
3. **Validation**: Validate pattern inputs and provide helpful feedback

## Complete Examples

### Custom Framework Integration

```typescript
// Complete React-like pattern system
@attributePattern(
  { pattern: 'className', symbols: '' },
  { pattern: 'onClick', symbols: '' },
  { pattern: 'onChange', symbols: '' }
)
export class ReactLikePatterns {
  public 'className'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'class', 'bind');
  }

  public 'onClick'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'click', 'trigger');
  }

  public 'onChange'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'change', 'trigger');
  }
}
```

### Advanced Component System

```typescript
// Advanced pattern for component communication
@attributePattern(
  { pattern: 'emit:PART', symbols: ':' },
  { pattern: 'listen:PART', symbols: ':' },
  { pattern: 'sync:PART', symbols: ':' }
)
export class ComponentCommunicationPatterns {
  public 'emit:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'emit-event');
  }

  public 'listen:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'listen-event');
  }

  public 'sync:PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'sync-prop');
  }
}
```

The attribute pattern system provides unlimited flexibility for creating custom template syntaxes that fit your team's needs or emulate familiar patterns from other frameworks, all while maintaining full integration with Aurelia's binding and compilation systems.

## Quick Reference Cheatsheet

Here's a corrected cheatsheet with working examples:

```typescript
// attr-patterns.ts

import { attributePattern, AttrSyntax } from '@aurelia/template-compiler';

// Angular-style patterns

@attributePattern({ pattern: '#PART', symbols: '#' })
export class AngularSharpRefAttributePattern {
  public '#PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, parts[0], 'element', 'ref');
  }
}

@attributePattern({ pattern: '[PART]', symbols: '[]' })
export class AngularOneWayBindingAttributePattern {
  public '[PART]'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
  }
}

@attributePattern({ pattern: '(PART)', symbols: '()' })
export class AngularEventBindingAttributePattern {
  public '(PART)'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger');
  }
}

@attributePattern({ pattern: '[(PART)]', symbols: '[()]' })
export class AngularTwoWayBindingAttributePattern {
  public '[(PART)]'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'two-way');
  }
}

// Vue-style patterns

@attributePattern({ pattern: ':PART', symbols: ':' })
export class VueOneWayBindingAttributePattern {
  public ':PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'bind');
  }
}

@attributePattern({ pattern: '@PART', symbols: '@' })
export class VueEventBindingAttributePattern {
  public '@PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'trigger');
  }
}

@attributePattern({ pattern: 'v-model', symbols: '' })
export class VueTwoWayBindingAttributePattern {
  public 'v-model'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, 'value', 'two-way');
  }
}

// Custom patterns

@attributePattern({ pattern: '::PART', symbols: '::' })
export class DoubleColonTwoWayBindingAttributePattern {
  public '::PART'(rawName: string, rawValue: string, parts: readonly string[]): AttrSyntax {
    return new AttrSyntax(rawName, rawValue, parts[0], 'two-way');
  }
}
```

```typescript
// main.ts

import { Aurelia } from 'aurelia';
import {
  AngularEventBindingAttributePattern,
  AngularOneWayBindingAttributePattern,
  AngularSharpRefAttributePattern,
  AngularTwoWayBindingAttributePattern,
  DoubleColonTwoWayBindingAttributePattern,
  VueEventBindingAttributePattern,
  VueOneWayBindingAttributePattern,
  VueTwoWayBindingAttributePattern
} from './attr-patterns';

Aurelia
  .register(
    AngularSharpRefAttributePattern,
    AngularOneWayBindingAttributePattern,
    AngularEventBindingAttributePattern,
    AngularTwoWayBindingAttributePattern,
    VueOneWayBindingAttributePattern,
    VueEventBindingAttributePattern,
    VueTwoWayBindingAttributePattern,
    DoubleColonTwoWayBindingAttributePattern
  )
  .app(MyApp)
  .start();
```

## Next steps

* Pair attribute patterns with the [attribute mapper](/components/template-compilation/attributemapper) when you need to translate new syntaxes into existing DOM APIs.
* Continue with [Extending templating syntax](/components/template-compilation/extending-templating-syntax) to see how patterns, mappers, and observers work together end-to-end.
* Explore [custom binding commands](/components/template-compilation/bindingcommand) whenever your pattern should hand off to bespoke runtime behavior instead of the default `bind`/`two-way` commands.


# Extending binding language

The Aurelia template compiler is powerful and developer-friendly, allowing you extend its binding language with great ease.

The Aurelia binding language provides commands like `.bind`, `.one-way`, `.trigger`, `.for`, `.class` etc. These commands are used in the view to express the intent of the binding, or in other words, to build binding instructions.

Although the out-of-box binding language is sufficient for most use cases, Aurelia also provides a way to extend the binding language so that developers can create their own incredible stuff when needed.

In this article, we will build an example to demonstrate how to introduce your own binding commands using the `@bindingCommand` decorator from the template compiler.

## Binding command

Before jumping directly into the example, let's first understand what a binding command is. In a nutshell, a binding command is a piece of code used to register "keywords" in the binding language and provide a way to build binding instructions from that.

To understand it better, we start our discussion with the template compiler. The template compiler is responsible for parsing templates and, among all, creating attribute syntaxes. This is where the [attribute patterns](/components/template-compilation/attributepattern) come into play. Depending on how you define your attribute patterns, the attribute syntaxes will be created with or without a binding command name, such as `bind`, `one-way`, `trigger`, `for`, `class`, etc. The template compiler then instantiates binding commands for the attribute syntaxes with a binding command name. Later, binding instructions are built from these binding commands, which are "rendered" by renderers. Depending on the binding instructions, the " rendering " process can differ. For this article, the rendering process details are unimportant, so we will skip it.

## Creating a custom binding command

To create a binding command, decorate a class with `@bindingCommand` and implement the following interface:

```typescript
interface BindingCommandInstance {
  ignoreAttr: boolean;
  build(info: ICommandBuildInfo, parser: IExpressionParser, mapper: IAttrMapper): IInstruction;
}
```

When the template compiler encounters an attribute, it first lets custom elements or attributes claim it. Only when no bindable handles the attribute does it look up a binding command whose name matches the parsed instruction. Setting `ignoreAttr = true` tells the compiler that your command consumes the attribute as-is and it should not keep probing for other handlers. Built-in commands like `.two-way` keep this value `false`, whereas specialized commands such as `.attr` set it to `true` so they can short-circuit the remaining checks.

The more interesting part of the interface is the `build` method. The template compiler calls this method to build binding instructions. The `info` parameter contains information about the element, the attribute name, the bindable definition (if present), and the custom element/attribute definition (if present). The `parser` parameter is used to parse the attribute value into an expression. The `mapper` parameter of [type `IAttrMapper`](/components/template-compilation/attributemapper) is used to determine the binding mode, the target property name, etc. (for more information, refer to the [documentation](/components/template-compilation/extending-templating-syntax)). In short, here comes your logic to convert the attribute information into a binding instruction.

For our example, we want to create a binding command that can trigger a handler when custom events such as `bs.foo.bar`, `bs.fizz.bizz` etc. are fired, and we want the following syntax:

```html
<div foo.bar.bs="ev => handleCustomEvent(ev)"></div>
```

instead of

```html
<div bs.foo.bar.trigger="ev => handleCustomEvent(ev)"></div>
```

We first create a class that implements the `BindingCommandInstance` interface to do that.

```typescript
import { IExpressionParser } from 'aurelia';
import {
  BindingCommandInstance,
  ICommandBuildInfo,
  ListenerBindingInstruction,
  bindingCommand,
} from '@aurelia/template-compiler';

@bindingCommand('bs')
export class BsBindingCommand implements BindingCommandInstance {
  public ignoreAttr = true; // we fully own attributes that end with .bs

  public build(
    info: ICommandBuildInfo,
    exprParser: IExpressionParser,
  ) {
    return new ListenerBindingInstruction(
      /* from           */ exprParser.parse(info.attr.rawValue, 'IsFunction'),
      /* to             */ `bs.${info.attr.target}`,
      /* preventDefault */ true,
      /* capture        */ false,
    );
  }
}
```

Note that from the `build` method, we are creating a `ListenerBindingInstruction` with `bs.` prefixed to the event name used in the markup. Thus, we are saying that the handler should be invoked when a `bs.*` event is raised.

To register the custom binding command, it needs to be registered with the dependency injection container.

### Registering the custom binding command

```typescript
import Aurelia from 'aurelia';
import { BsBindingCommand } from './bs-binding-command';
import { MyRoot } from './my-root';

Aurelia
  .register(BsBindingCommand)
  .app(MyRoot)
  .start();
```

Because `@bindingCommand` wires up the resource metadata, registering the class is all the compiler needs to find it.

#### Why `ignoreAttr = true`?

Setting `ignoreAttr = true` tells the compiler that this binding command fully manages the attribute in the view. Without this flag, Aurelia might attempt to interpret the same attribute as a custom attribute or a normal bindable property. This can lead to conflicts or warnings if you reuse attribute names already in use by other features.

#### Debugging custom binding commands

If your command doesn't behave as expected:

* Make sure you've registered it before Aurelia starts (see the `main.ts` snippet above).
* Double-check that the **command name** (e.g., `'bs'`) matches in both the `@bindingCommand('bs')` decorator and your view markup (`foo.bar.bs="..."`).
* Use browser dev tools to confirm whether your event is fired and that the method in your view model is triggered.

And that's it! We have created our own binding command and registered it. This means the following syntax will work:

```html
<div foo.bar.bs="ev => handleCustomEvent(ev)"></div>
<!--         ^^
             |_________ custom binding command
-->
```

## Live example

This binding command can be seen in action below.

{% embed url="<https://stackblitz.com/edit/aurelia2-custom-binding-command?ctl=1&embed=1&file=src%2Fmy-app.ts>" %}

> Note that the example defines a custom attribute pattern to support `foo.bar.fizz.bs="ev => handle(ev)"` syntax.


# Using the template compiler

The template compiler is used by Aurelia under the hood to process templates and provides hooks and APIs allowing you intercept and modify how this behavior works in your applications.

## Hooks

There are scenarios where an application wants to control how to preprocess a template before it is compiled. There could be various reasons, such as accessibility validation, adding debugging attributes etc...

Aurelia supports this via template compiler hooks, enabled with the default template compiler. To use these features, declare and then register the desired hooks with either global (at startup) or local container (at dependencies (runtime) or `<import>` with convention).

An example of declaring global hooks that will be called for every template:

### With VanillaJS

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

Aurelia
  .register(TemplateCompilerHooks.define(class {
    compiling(template: HTMLElement) {
      template.querySelector('table')?.setAttribute(someAttribute, someValue);
    }
  }));
```

### With decorator

```typescript
import Aurelia, { templateCompilerHooks } from 'aurelia';

@templateCompilerHooks
class MyTableHook1 {
  compiling(template) {...}
}
// paren ok too
@templateCompilerHooks()
class MyTableHook1 {
  compiling(template) {...}
}

Aurelia.register(MyTableHook1);
```

### Supported hooks

* **compiling**: this hook will be invoked before the template compiler starts compiling a template. Use this hook if there need to be any changes to a template before any compilation.

### Hooks invocation order

All hooks from local and global registrations will be invoked: local first, then global.

## Compilation Behavior

The default compiler will remove all binding expressions while compiling a template. This is to clean the rendered HTML and increase the performance of cloning compiled fragments.

Though this is not always desirable for debugging, it could be hard to figure out what element mapped to the original part of the code. To enable an easier debugging experience, the default compiler has a property `debug` that when set to `true` will keep all expressions intact during the compilation.

This property can be set early in an application lifecycle via `AppTask`, so that all the rendered HTML will keep their original form. An example of doing this is:

```typescript
import Aurelia, { AppTask, ITemplateCompiler } from 'aurelia';
import { MyApp } from './my-app';

Aurelia
  .register(AppTask.creating(ITemplateCompiler, compiler => compiler.debug = true))
  .app(MyApp)
  .start();
```

List of attributes that are considered expressions:

* containerless
* as-element
* ref
* attr with binding expression (`attr.command="..."`)
* attr with interpolation (`attr="${someExpression}"`)
* custom attribute
* custom element bindables

## Scenarios

Now that we understand how the template compiler works let's create fun scenarios showcasing how you might use it in your Aurelia applications.

### Feature Flagging in Templates

If your application uses feature flags to toggle features on and off, you may want to modify templates based on these flags conditionally.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

class FeatureFlagHook {
  compiling(template: HTMLElement) {
    const featureElements = template.querySelectorAll('[data-feature]');
    for (const element of featureElements) {
      const featureName = element.getAttribute('data-feature') ?? '';
      if (!activeFeatureFlags[featureName]) {
        element.remove();
      }
    }
  }
}

const activeFeatureFlags: Record<string, boolean> = {
  'new-ui': true,
  'beta-feature': false
};

Aurelia.register(TemplateCompilerHooks.define(FeatureFlagHook))
  .app(MyApp)
  .start();
```

Here, elements with a `data-feature` attribute will be removed from the template if the corresponding feature flag is set to `false`, allowing for easy management of feature rollouts.

### Auto-Generating Form Field IDs for Label Association

For accessibility purposes, form fields must associate `label` elements with matching `for` and `id` attributes. We can automate this process during template compilation.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

class FormFieldHook {
  private fieldCounter = 0;

  compiling(template: HTMLElement) {
    const formFields = template.querySelectorAll('input, textarea, select');
    for (const field of formFields) {
      if (!field.hasAttribute('id')) {
        const uniqueId = `form-field-${this.fieldCounter++}`;
        field.setAttribute('id', uniqueId);
        
        const label = template.querySelector(`label[for="${field.getAttribute('name')}"]`);
        if (label) {
          label.setAttribute('for', uniqueId);
        }
      }
    }
  }
}

Aurelia.register(TemplateCompilerHooks.define(FormFieldHook))
  .app(MyApp)
  .start();
```

In this use case, the hook generates a unique `id` for each form field that doesn't already have one and updates the corresponding `label`'s `for` attribute to match. This ensures that form fields are properly labelled for screen readers and other assistive technologies.

### Automatic ARIA Role Assignment

To enhance accessibility, you might want to automatically assign ARIA roles to certain elements based on their class or other attributes to make your application more accessible without manually annotating each element.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

class AriaRoleHook {
  compiling(template: HTMLElement) {
    const buttons = template.querySelectorAll('.btn');
    for (const button of buttons) {
      if (!button.hasAttribute('role')) {
        button.setAttribute('role', 'button');
      }
    }
  }
}

Aurelia.register(TemplateCompilerHooks.define(AriaRoleHook))
  .app(MyApp)
  .start();
```

This hook assigns the `role="button"` to all elements that have the `.btn` class and do not already have a role defined. This helps ensure that custom-styled buttons are accessible.

### Content Security Policy (CSP) Compliance

If your application needs to comply with strict Content Security Policies, you should ensure that inline styles are not used within your templates. A template compiler hook can help you enforce this policy.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

class CSPHook {
  compiling(template: HTMLElement) {
    const elementsWithInlineStyles = template.querySelectorAll('[style]');
    for (const element of elementsWithInlineStyles) {
      console.warn(`Inline style removed from element for CSP compliance:`, element);
      element.removeAttribute('style');
    }
  }
}

Aurelia.register(TemplateCompilerHooks.define(CSPHook))
  .app(MyApp)
  .start();
```

This hook scans for any elements with inline `style` attributes and removes them, logging a warning for developers to take notice and refactor the styles into external stylesheets.

### Lazy Loading Image Optimization

For performance optimization, you should implement lazy loading for images. The template compiler can automatically add lazy loading attributes to your image tags.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

class LazyLoadingHook {
  compiling(template: HTMLElement) {
    const images = template.querySelectorAll('img:not([loading])');
    for (const img of images) {
      img.setAttribute('loading', 'lazy');
    }
  }
}

Aurelia.register(TemplateCompilerHooks.define(LazyLoadingHook))
  .app(MyApp)
  .start();
```

This hook finds all `img` elements without a `loading` attribute and sets it to `lazy`, instructing the browser to defer loading the image until it is near the viewport.

### Dynamic Theme Class Injection

If your application supports multiple themes, you can use a template compiler hook to inject the relevant theme class into the root of your templates based on user preferences.

```typescript
import Aurelia, { TemplateCompilerHooks } from 'aurelia';

const userSelectedTheme = 'dark'; // For example, a dark theme

class ThemeClassHook {
  private readonly currentTheme = userSelectedTheme;

  compiling(template: HTMLElement) {
    const rootElement = template.querySelector(':root');
    if (rootElement) {
      rootElement.classList.add(`theme-${this.currentTheme}`);
    }
  }
}

Aurelia.register(TemplateCompilerHooks.define(ThemeClassHook))
  .app(MyApp)
  .start();
```

This hook adds a theme-specific class to the root element of every template, allowing for theme-specific styles to be applied consistently across the application.

## Node APIs used

The default template compiler will turn a template, either in string or already an element, into an element before the compilation. During the compilation, these APIs on the `Node` & `Element` classes are accessed and invoked:

* `Node.prototype.nodeType`
* `Node.prototype.nodeName`
* `Node.prototype.childNodes`
* `Node.prototype.childNode`
* `Node.prototype.firstChild`
* `Node.prototype.textContent`
* `Node.prototype.parentNode`
* `Node.prototype.appendChild`
* `Node.prototype.insertBefore`
* `Element.prototype.attributes`
* `Element.prototype.hasAttribute`
* `Element.prototype.getAttribute`
* `Element.prototype.setAttribute`
* `Element.prototype.classList.add`

If it is desirable to use the default template compiler in any environment other than HTML, ensure the template compiler can hydrate the input string or object into some object with the above APIs.


# Extending the template compiler

The Aurelia template compiler is highly extensible, providing multiple hooks and extension points for advanced customization. This guide covers the advanced features and extension mechanisms available for developers who need to extend template compilation behavior.

## Template Compiler Hooks

### Registering Compilation Hooks

Template compiler hooks allow you to modify templates during the compilation process:

```typescript
import { templateCompilerHooks, ITemplateCompilerHooks } from 'aurelia';

@templateCompilerHooks
class MyCompilerHook implements ITemplateCompilerHooks {
  compiling(template: HTMLElement): void {
    // Modify template before compilation
    this.addDefaultAttributes(template);
    this.injectDevelopmentHelpers(template);
  }

  private addDefaultAttributes(template: HTMLElement): void {
    // Add default attributes to form elements
    template.querySelectorAll('input[type="text"]').forEach(input => {
      if (!input.hasAttribute('autocomplete')) {
        input.setAttribute('autocomplete', 'off');
      }
    });
  }

  private injectDevelopmentHelpers(template: HTMLElement): void {
    if (__DEV__) {
      // Add development-only attributes
      template.querySelectorAll('[data-dev-hint]').forEach(el => {
        el.setAttribute('title', el.getAttribute('data-dev-hint')!);
      });
    }
  }
}
```

### Global vs Component-Level Hooks

Hooks can be registered globally or at the component level:

```typescript
// Global hook registration
container.register(MyCompilerHook);

// Component-level hook
@customElement({
  name: 'my-component',
  template: '<div>...</div>',
  hooks: [MyCompilerHook]
})
export class MyComponent { }
```

## Advanced Attribute Pattern System

### Creating Custom Attribute Syntax

The attribute pattern system allows you to create custom binding syntax:

```typescript
import { attributePattern, AttrSyntax } from 'aurelia';

@attributePattern({ pattern: 'PART.vue:PART', symbols: '.:' })
class VueStyleAttributePattern {
  'PART.vue:PART'(rawName: string, rawValue: string, parts: string[]): AttrSyntax {
    const [target, event] = parts;
    return new AttrSyntax(rawName, rawValue, target, 'trigger', [event]);
  }
}

// Usage: <button click.vue:prevent="handleClick()">
```

### Complex Pattern Matching

Support for multi-part patterns with custom symbols:

```typescript
@attributePattern({ pattern: 'PART.PART.PART', symbols: '.' })
class NestedPropertyPattern {
  'PART.PART.PART'(rawName: string, rawValue: string, parts: string[]): AttrSyntax {
    const [obj, prop, command] = parts;
    return new AttrSyntax(rawName, rawValue, `${obj}.${prop}`, command, parts);
  }
}

// Usage: <input user.profile.bind="userProfile">
```

## Custom Binding Commands

### Advanced Binding Command Features

Binding commands can take full control of attribute processing:

```typescript
import { bindingCommand, BindingCommandInstance, IInstruction } from 'aurelia';

@bindingCommand('throttle')
class ThrottleBindingCommand implements BindingCommandInstance {
  ignoreAttr = true; // Take full control of attribute processing

  build(info: ICommandBuildInfo, parser: IExpressionParser): IInstruction {
    const [delay = '250', event = 'input'] = info.attr.rawValue.split(':');
    
    return new ThrottleInstruction(
      parser.parse(info.attr.rawValue),
      parseInt(delay, 10),
      event
    );
  }
}

// Usage: <input value.throttle="500:input">
```

### Multi-Attribute Processing

Commands can process multiple attributes for complex scenarios:

```typescript
@bindingCommand('form')
class FormBindingCommand implements BindingCommandInstance {
  build(info: ICommandBuildInfo, parser: IExpressionParser): IInstruction {
    const formAttributes = this.collectFormAttributes(info.attr.syntax.target);
    
    return new FormInstruction(
      parser.parse(info.attr.rawValue),
      formAttributes
    );
  }

  private collectFormAttributes(element: Element): Record<string, string> {
    const attrs: Record<string, string> = {};
    for (const attr of element.attributes) {
      if (attr.name.startsWith('form-')) {
        attrs[attr.name.substring(5)] = attr.value;
      }
    }
    return attrs;
  }
}
```

## Template Element Factory Customization

### Custom Template Caching

The template element factory supports custom caching strategies:

```typescript
import { ITemplateElementFactory, IMarkupCache } from 'aurelia';

class CustomTemplateElementFactory implements ITemplateElementFactory {
  private customCache = new Map<string, HTMLTemplateElement>();

  createTemplate(markup: string): HTMLTemplateElement {
    // Custom caching logic
    const cacheKey = this.generateCacheKey(markup);
    
    if (this.customCache.has(cacheKey)) {
      return this.customCache.get(cacheKey)!.cloneNode(true) as HTMLTemplateElement;
    }

    const template = this.createTemplateElement(markup);
    this.customCache.set(cacheKey, template);
    return template;
  }

  private generateCacheKey(markup: string): string {
    // Custom cache key generation
    return `${markup.length}-${this.hashCode(markup)}`;
  }
}
```

### Template Wrapping Detection

Customize how templates are wrapped for proper compilation:

```typescript
class SmartTemplateFactory implements ITemplateElementFactory {
  createTemplate(markup: string): HTMLTemplateElement {
    const wrapped = this.intelligentWrap(markup);
    return this.createTemplateElement(wrapped);
  }

  private intelligentWrap(markup: string): string {
    // Custom wrapping logic based on content
    if (markup.includes('<tr>')) {
      return `<table><tbody>${markup}</tbody></table>`;
    }
    if (markup.includes('<option>')) {
      return `<select>${markup}</select>`;
    }
    return markup;
  }
}
```

## Advanced Resource Resolution

### Custom Resource Discovery

Implement custom resource resolution for dynamic components:

```typescript
import { IResourceResolver, IResourceDescriptions } from 'aurelia';

class DynamicResourceResolver implements IResourceResolver {
  resolve(name: string, context: IContainer): IResourceDescriptions | null {
    // Check if this is a dynamic component request
    if (name.startsWith('dynamic-')) {
      return this.resolveDynamicComponent(name, context);
    }
    
    return null; // Let default resolver handle it
  }

  private resolveDynamicComponent(name: string, context: IContainer): IResourceDescriptions {
    const componentType = this.loadDynamicComponent(name);
    return {
      [name]: {
        type: componentType,
        keyFrom: name,
        definition: componentType.definition
      }
    };
  }
}
```

### Bindables Information Caching

Optimize bindable resolution with custom caching:

```typescript
class OptimizedResourceResolver implements IResourceResolver {
  private bindablesCache = new Map<Function, Record<string, BindableDefinition>>();

  getBindables(Type: Function): Record<string, BindableDefinition> {
    if (this.bindablesCache.has(Type)) {
      return this.bindablesCache.get(Type)!;
    }

    const bindables = this.computeBindables(Type);
    this.bindablesCache.set(Type, bindables);
    return bindables;
  }
}
```

## Local Template System

### Advanced Local Element Definitions

Create complex local element hierarchies:

```typescript
@customElement({
  name: 'dashboard',
  template: `
    <template as-custom-element="widget">
      <bindable property="title"></bindable>
      <bindable property="data"></bindable>
      <div class="widget">
        <h3>\${title}</h3>
        <div class="content" innerhtml.bind="data"></div>
      </div>
    </template>
    
    <template as-custom-element="chart-widget">
      <bindable property="chart-data"></bindable>
      <widget title="Chart" data.bind="renderChart(chartData)"></widget>
    </template>
    
    <div class="dashboard">
      <chart-widget chart-data.bind="metrics"></chart-widget>
    </div>
  `
})
export class Dashboard {
  renderChart(data: any): string {
    return `<canvas data-chart="${JSON.stringify(data)}"></canvas>`;
  }
}
```

### Dynamic Local Template Creation

Create local templates programmatically:

```typescript
@customElement({
  name: 'dynamic-layout',
  template: `<div ref="container"></div>`
})
export class DynamicLayout {
  @ViewSlot() container!: ViewSlot;

  attached(): void {
    this.createLocalTemplate();
  }

  private createLocalTemplate(): void {
    const template = `
      <template as-custom-element="dynamic-item">
        <bindable property="item"></bindable>
        <div class="item">\${item.name}</div>
      </template>
    `;
    
    this.container.add(this.viewFactory.create(template));
  }
}
```

## Compilation Context System

### Hierarchical Resource Resolution

Work with compilation contexts for advanced scenarios:

```typescript
class CustomCompiler {
  compileWithContext(template: string, parentContext?: ICompilationContext): ICompiledTemplate {
    const context = this.createCompilationContext(parentContext);
    
    // Add custom resources to context
    context.addResource('custom-element', MyCustomElement);
    context.addResource('value-converter', MyConverter);
    
    return this.compile(template, context);
  }

  private createCompilationContext(parent?: ICompilationContext): ICompilationContext {
    const context = new CompilationContext(parent);
    
    // Configure context for specific compilation needs
    context.resolveResources = true;
    context.debug = __DEV__;
    
    return context;
  }
}
```

### Custom Dependency Injection

Customize DI container behavior during compilation:

```typescript
class ScopedCompiler {
  compileWithScope(template: string, scope: Record<string, any>): ICompiledTemplate {
    const container = this.createScopedContainer(scope);
    const context = new CompilationContext(container);
    
    return this.compile(template, context);
  }

  private createScopedContainer(scope: Record<string, any>): IContainer {
    const container = DI.createContainer();
    
    // Register scope variables as services
    Object.entries(scope).forEach(([key, value]) => {
      container.register(Registration.instance(key, value));
    });
    
    return container;
  }
}
```

## Performance Optimization

### Template Compilation Caching

Implement aggressive template caching for performance:

```typescript
class CachedTemplateCompiler {
  private compilationCache = new Map<string, ICompiledTemplate>();
  private templateHashCache = new Map<string, string>();

  compile(template: string, context: ICompilationContext): ICompiledTemplate {
    const hash = this.getTemplateHash(template, context);
    
    if (this.compilationCache.has(hash)) {
      return this.compilationCache.get(hash)!;
    }

    const compiled = this.performCompilation(template, context);
    this.compilationCache.set(hash, compiled);
    return compiled;
  }

  private getTemplateHash(template: string, context: ICompilationContext): string {
    const contextHash = this.getContextHash(context);
    return `${template.length}-${contextHash}`;
  }
}
```

### Compilation Mode Optimization

Configure compilation for different environments:

```typescript
interface CompilationOptions {
  resolveResources?: boolean;
  debug?: boolean;
  enhance?: boolean;
  aot?: boolean;
}

class OptimizedCompiler {
  compile(template: string, options: CompilationOptions = {}): ICompiledTemplate {
    const context = this.createOptimizedContext(options);
    
    if (options.aot) {
      return this.compileAOT(template, context);
    }
    
    return this.compileJIT(template, context);
  }

  private createOptimizedContext(options: CompilationOptions): ICompilationContext {
    const context = new CompilationContext();
    
    context.resolveResources = options.resolveResources ?? true;
    context.debug = options.debug ?? __DEV__;
    context.enhance = options.enhance ?? false;
    
    return context;
  }
}
```

## Best Practices

### 1. Hook Registration

* Register global hooks early in application bootstrap
* Use component-level hooks for specific customizations
* Keep hooks lightweight to avoid compilation performance impact

### 2. Pattern and Command Design

* Design patterns to be intuitive and consistent with Aurelia conventions
* Use descriptive names and clear syntax
* Provide good error messages for invalid usage

### 3. Resource Resolution

* Cache expensive resource lookups
* Implement fallback mechanisms for missing resources
* Use lazy loading for dynamic components

### 4. Performance Considerations

* Profile template compilation in development
* Use AOT compilation for production builds
* Implement smart caching strategies
* Monitor memory usage with large template caches

### 5. Testing Extensions

* Create unit tests for custom hooks and commands
* Test compilation output for correctness
* Verify performance impact of extensions
* Test edge cases and error handling

The template compiler's extensibility allows for powerful customizations while maintaining framework performance and consistency. Use these extension points judiciously to enhance your application's template processing capabilities.


# Attribute mapping

Learn about binding values to attributes of DOM elements and how to extend the attribute mapping with great ease.

Attribute mapping is Aurelia's way of keeping template syntax concise. After an attribute pattern parses the attribute name but before a binding command emits instructions, the mapper answers two questions:

1. Which DOM property does this attribute target?
2. Should `.bind` implicitly behave like `.two-way` for this attribute?

This is the mechanism that lets you write `<input value.bind="message">` and automatically get a two-way binding. By teaching the mapper about your own elements, you can bring the same ergonomics to Web Components, design systems, or DSLs.

## When to extend `IAttrMapper`

Reach for the mapper when:

* **Bridging custom elements** – Third-party components often expose camelCase properties such as `valueAsDate` or `formNoValidate`.
* **Designing DSLs** – Attributes like `data-track` or `foo-bar` need to land on specific DOM properties regardless of casing.
* **Improving authoring ergonomics** – Upgrading `progress.bind` to two-way on slider-like controls keeps templates readable.

If you need to invent new attribute syntaxes (`[(value)]`, `@click`, etc.), start with [attribute patterns](/components/template-compilation/attributepattern). If you need to observe DOM properties, follow up with the [node observer locator](/components/template-compilation/extending-templating-syntax#combining-the-attribute-syntax-mapper-with-the-node-observer-locator).

## How the mapper decides

When Aurelia encounters an attribute that does not belong to a custom element bindable, it walks through the mapper logic:

1. Check tag-specific mappings registered via `useMapping`.
2. Fall back to global mappings from `useGlobalMapping`.
3. If no mapping exists, camelCase the attribute name.
4. If the binding command is `bind`, ask each predicate registered via `useTwoWay` whether the attribute should become two-way.

Your extensions only run for attributes that are not already handled by custom element bindables, so you can layer mappings without unintentionally overriding component contracts.

## Registering mappings during startup

Use `AppTask.creating` to register mappings before Aurelia instantiates the root component:

```typescript
import Aurelia, { AppTask, IAttrMapper } from 'aurelia';

Aurelia.register(
  AppTask.creating(IAttrMapper, attrMapper => {
    attrMapper.useMapping({
      'MY-CE': { 'fizz-buzz': 'FizzBuzz' },
      INPUT: { 'fizz-buzz': 'fizzbuzz' },
    });
    attrMapper.useGlobalMapping({
      'foo-bar': 'FooBar',
    });
  })
);
```

Keys inside `useMapping` must match the element's `tagName` (uppercase). The destination values must match the actual DOM property names exactly (`formNoValidate`, not `formnovalidate`).

With the mapping above in place, templates stay clean:

```html
<input fizz-buzz.bind="userLimit" foo-bar.bind="hint" ref="input">
<my-ce fizz-buzz.bind="42" foo-bar.bind="43" ref="myCe"></my-ce>
```

```typescript
export class App {
  private input!: HTMLInputElement;
  private myCe!: HTMLElement & { FizzBuzz?: number; FooBar?: number };

  public attached() {
    console.log(this.input.fizzbuzz); //  userLimit
    console.log(this.myCe.FizzBuzz);  //  42
  }
}
```

## Enabling implicit two-way bindings

Some controls should default to two-way binding even when authors write `.bind`. Use `useTwoWay` to register a predicate `(element, attrName) => boolean`:

```typescript
import Aurelia, { AppTask, IAttrMapper } from 'aurelia';

Aurelia.register(
  AppTask.creating(IAttrMapper, attrMapper => {
    attrMapper.useTwoWay((element, attrName) =>
      element.tagName === 'MY-CE' && attrName === 'fizz-buzz');
  })
);
```

Predicates receive the live element, so you can inspect classes, attributes, or even dataset values before opting into two-way. Keep the logic lightweight—these predicates run for every `*.bind` attribute Aurelia encounters.

## Troubleshooting and best practices

* **Uppercase tag names** – Browsers expose `element.tagName` in uppercase; use the same casing in `useMapping`.
* **Avoid duplicates** – Registering the same tag/attribute combination twice throws. Remove or consolidate old mappings before adding new ones.
* **Destination accuracy** – Mistyped destination properties silently fall back to camelCase conversion. Inspect the element in devtools and read `Object.keys(element)` if unsure.
* **Predicate order matters** – `useTwoWay` predicates run in registration order. Put the most specific check first.
* **Verify manually** – Toggle the DOM property in devtools. If the UI updates but Aurelia does not, revisit the observer configuration. If neither updates, revisit the mapping.
* **Pair with observers** – Mapping alone does not teach Aurelia how to observe custom properties. Follow up with `INodeObserverLocator.useConfig` so bindings know which events to listen to.

With the mapper tailored to your components, you can keep templates expressive while relying on the full power of Aurelia's binding system.


# Overview

A guided tour of Aurelia fundamentals; start here before diving into the deeper topic guides.

Use this section as your orientation to Aurelia. Each topic builds on the last, moving from first render through composition patterns, state management, and the services that power real applications.

## How to Navigate

* **Start with the introductions** to see Aurelia's templating flavor and ergonomics in action.
* **Pick the boot path** (full app vs. enhancement) that matches your project.
* **Add capabilities incrementally**: routing, composition, observation, without waiting for a rewrite.
* **Dip into advanced topics last**, once the fundamentals are comfortable.

## Topic Map

| Theme                    | Read this first                                                                                                                         | Follow with                                                                                                                                                                                                                                                                                                                |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Templates & syntax       | [Built-in template features](/getting-to-know-aurelia/introduction/built-in-template-features)                                          | [Class & style binding](/getting-to-know-aurelia/introduction/class-and-style-binding), [Attribute transferring](/getting-to-know-aurelia/introduction/attribute-transferring)                                                                                                                                             |
| Bootstrapping            | [App configuration & startup](/getting-to-know-aurelia/startup-and-enhancement/app-configuration-and-startup)                           | [Enhance](/getting-to-know-aurelia/startup-and-enhancement/enhance)                                                                                                                                                                                                                                                        |
| Navigation               | [Choosing a router](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-to-know-aurelia/routing/choosing-a-router.md) | Router fundamentals, navigation, lifecycle, and advanced guides for each router package                                                                                                                                                                                                                                    |
| Composition patterns     | [Template controllers](/getting-to-know-aurelia/composition-patterns/template-controllers)                                              | [Dynamic composition](/getting-to-know-aurelia/composition-patterns/dynamic-composition), [Portalling elements](/getting-to-know-aurelia/composition-patterns/portalling-elements)                                                                                                                                         |
| State & observation      | [Understanding the binding system](/getting-to-know-aurelia/state-and-observation/synchronous-binding-system)                           | [Observation overview](/getting-to-know-aurelia/state-and-observation/observation), [Watching data](/getting-to-know-aurelia/state-and-observation/watching-data)                                                                                                                                                          |
| Services & runtime hooks | [Dependency injection primer](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection)                                 | [App tasks](/getting-to-know-aurelia/services-and-runtime-hooks/app-tasks), [Task queue](/getting-to-know-aurelia/services-and-runtime-hooks/task-queue), [Event aggregator](/getting-to-know-aurelia/services-and-runtime-hooks/event-aggregator), [Logging](/getting-to-know-aurelia/services-and-runtime-hooks/logging) |
| Deep dives               | None                                                                                                                                    | [Framework internals](/getting-to-know-aurelia/advanced/framework-internals)                                                                                                                                                                                                                                               |

## Suggested Learning Path

1. Skim the **templating introductions** to get comfortable with Aurelia's binding language.
2. Choose your **startup approach**: full SPA bootstrap or incremental enhancement.
3. Layer on **routing** once you have more than one screen.
4. Explore **composition and state tools** to keep components small and expressive.
5. Wire in **services and lifecycle hooks** as the app grows in complexity.
6. Only then crack open the **framework internals** for architecture digging or advanced debugging.

Each article calls out prerequisites and links forward so you can keep learning without losing the big picture.


# Introduction


# Built-in template features

Use Aurelia's built-in template commands such as if, show, repeat, and switch to control markup dynamically.

This topic demonstrates how to work with Aurelia's built-in template commands which allow you to conditionally show and hide content, loop over collections of content, conditional rendering using switch/case syntax in your views and a trove of other built-in template features.

## Adding or removing an element using if.bind

You can add or remove an element by specifying an `if.bind` on an element and passing in a true or false value.

When `if.bind` is passed `false` Aurelia will remove the element all of its children from the view. When an element is removed, if it is a custom element or has any events associated with it, they will be cleaned up, thus freeing up memory and other resources they were using.

In the following example, we are passing a value called `isLoading` which is populated whenever something is loading from the server. We will use it to show a loading message in our view.

```html
<div if.bind="isLoading">Loading...</div>
```

When `isLoading` is a truthy value, the element will be displayed and added to the DOM. When `isLoading` is falsy, the element will be removed from the DOM, disposing of any events or child components inside of it.

## Showing or hiding an element using show\.bind

You can conditionally show or hide an element by specifying a `show.bind` and passing in a true or false value.

When `show.bind` is passed `false` the element will be hidden, but unlike `if.bind` it will not be removed from the DOM. Any resources, events or bindings will remain. It's the equivalent of `display: none;` in CSS, the element is hidden, but not removed.

In the following example, we are passing a value called `isLoading` which is populated whenever something is loading from the server. We will use it to show a loading message in our view.

```html
<div show.bind="isLoading">Loading...</div>
```

When `isLoading` is a truthy value, the element will be visible. When `isLoading` is falsy, the element will be hidden, but remain in the view.

## Conditionally add or remove elements using switch.bind

In Javascript we have the ability to use `switch/case` statements which act as neater `if` statements. We can use `switch.bind` to achieve the same thing within our templates.

```html
<p switch.bind="selectedAction">
  <span case="mask">You are more protected from aerosol particles, and others are protected from you.</span>
  <span case="sanitizer">You are making sure viruses won't be spreaded easily.</span>
  <span case="wash">You are helping eliminate the virus.</span>
  <span case="all">You are protecting yourself and people around you. You rock!</span>
  <span default-case>Unknown.</span>
</p>
```

The `switch.bind` controller will watch the bound value, which in our case is `selectedAction` and when it changes, match it against our case values. It is important to note that this will add and remove elements from the DOM like the `if.bind` does.

## Using promises in templates with promise.bind

When working with promises in Aurelia, previously in version 1 you had to resolve them in your view model and then pass the values to your view templates. It worked, but it meant you had to write code to handle those promise requests. In Aurelia 2 we can work with promises directly inside of our templates.

The `promise.bind` template controller allows you to use `then`, `pending` and `catch` in your views removing unnecessary boilerplate.

In the following example, notice how we have a parent `div` with the `promise.bind` binding and then a method called `fetchAdvice`? Followed by other attributes inside `then` and `catch` which handle both the resolved value as well as any errors.

Ignore the `i` variable being incremented, this is only there to make Aurelia fire off a call to our `fetchAdvice` method as it sees the parameter value has changed.

{% tabs %}
{% tab title="my-app.html" %}

```html
<let i.bind="0"></let>

<div promise.bind="fetchAdvice(i)">
  <span pending>Fetching advice...</span>
  <span then="data">
    Advice id: ${data.slip.id}<br>
    ${data.slip.advice}
    <button click.trigger="i = i+1">try again</button>
  </span>
  <span catch="err">
    Cannot get an addvice, error: ${err}
    <button click.trigger="i = i+1">try again</button>
  </span>
</div>
```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
export class MyApp {
  fetchAdvice() {
    return fetch("https://api.adviceslip.com/advice")
      .then(r => r.ok ? r.json() : (() => { throw new Error('Unable to fetch NASA APOD data') }))
  }
}

```

{% endtab %}
{% endtabs %}

## Looping over collections with repeat.for

{% hint style="success" %}
To see live examples of `repeat.for` being used, visit the [`repeat.for` examples](/examples/examples/binding-and-templating#looping-with-repeatfor).
{% endhint %}

You can use the `repeat.for` binding to iterate over collections of data in your templates. Think of `repeat.for` as a for loop, it can iterate arrays, maps and sets.

```html
<ul>
    <li repeat.for="item of items">${item.name}</li>
</ul>
```

Breaking this intuitive syntax down, it works like this:

* Loop over every item in the items array
* Store each iterative value in the local variable `item` on the left hand side
* For each iteration, make the current item available

If you were to write the above in Javascript form, it would look like this:

```
for (let item of items) {
    console.log(item.name);
}
```

### Creating ranges with repeat.for

The `repeat.for` functionality doesn't just allow you to work with collections, it can be used to generate ranges.

In the following example, we generate a range of numbers to 10. We subtract the value from the index inside to create a reverse countdown.

```html
<p repeat.for="i of 10">${10-i}</p>
<p>Blast Off!<p>
```

### Getting the index (and other contextual properties) inside of repeat.for

Aurelia's binding engine makes several special properties available to you in your binding expressions. Some properties are available everywhere, while others are only available in a particular context. Below is a brief summary of the available contextual properties within repeats.

* `$index` - In a repeat template, the index of the item in the collection.
* `$first` - In a repeat template, is `true` if the item is the first item in the array.
* `$last` - In a repeat template, is `true` if the item is the last item in the array.
* `$middle` - In a repeat template, is `true` if the item is neither first nor last.
* `$even` - In a repeat template, is `true` if the item has an even numbered index.
* `$odd` - In a repeat template, is `true` if the item has an odd numbered index.
* `$length` - In a repeat template, this indicates the length of the collection.
* `$previous` - In a repeat template, returns the previous item in the collection (or `undefined` for the first item).
* `$parent` - Explicitly accesses the outer scope from within a `repeat` template. You may need this when a property on the current scope masks a property on the outer scope. Note that this property is chainable, e.g. `$parent.$parent.foo` is supported.

Inside of the `repeat.for` these can be accessed. In the following example we display the current index value.

```html
<ul>
    <li repeat.for="item of items">${$index}</li>
</ul>
```


# Class & style binding

Bind CSS classes and inline styles in Aurelia templates using expressive syntax.

Class and style bindings in Aurelia allow you to bind to CSS properties and add one or more classes to your HTML elements inside of your views.

## Binding to the class attribute

The class binding allows you to bind one or more classes to an element and its native `class` attribute.

### Binding to a single class

Adding or removing a single class value from an element can be done using the `.class` binding. By prefixing the `.class` binding with the name of the class you want to conditionally display, for example, `selected.class="myBool"` you can add a selected class to an element. The value you pass into this binding is a boolean value (either true or false), if it is `true` the class will be added, otherwise, it will be removed.

```
<p selected.class="isSelected">I am selected (I think)</p>
```

Inside of your view model, you would specify `isSelected` as a property and depending on the value, the class would be added or removed.

Here is a working example of a boolean value being toggled using `.class` bindings.

{% embed url="<https://stackblitz.com/edit/aurelia-conditional-class?embed=1&file=my-app.html&hideExplorer=1&hideNavigation=1&view=preview>" %}

### Binding to multiple classes

Unlike singular class binding, you cannot use the `.class` binding syntax to conditionally bind multiple CSS classes. However, there is a multitude of different ways in which this can be achieved.

| Syntax                    | Input Type | Example                    |
| ------------------------- | ---------- | -------------------------- |
| `class.bind="someString"` | `string`   | `'col-md-4 bg-${bgColor}'` |
| `class="${someString}"`   | `string`   | `col-md-4 ${someString}`   |

## Binding to the style attribute

Dynamically set CSS styles on elements in your view templates.

### Binding to a single style

You can dynamically add a CSS style value to an element using the `.style` binding in Aurelia.

```
<p background.style="bg">My background is blue</p>
```

Inside of your view model, you would specify `bg` as a string value on your class.

Here is a working example of a style binding setting the background color to blue:

{% embed url="<https://stackblitz.com/edit/aurelia-conditional-style?embed=1&file=my-app.html&hideExplorer=1&hideNavigation=1&view=preview>" %}

### Binding to multiple styles

To bind to one or more CSS style properties you can either use a string containing your style values (including dynamic values) or an object containing styles.

#### Style binding using strings

This is what a style string looks like, notice the interpolation here? It almost resembles just a plain native style attribute, with exception of the interpolation for certain values. Notice how you can also mix normal styles with interpolation as well?

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  private backgroundColor = 'black';
  private textColor = '#FFF';
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<p style="color: ${textColor}; font-weight: bold; background: ${backgroundColor};">Hello there</p>

```

{% endcode %}

You can also bind a string from your view model to the `style` property instead of inline string assignment by using `style.bind="myString"` where `myString` is a string of styles inside of your view model.

#### Style binding using objects

Styles can be passed into an element by binding to the styles property and using `.bind` to pass in an object of style properties. We can rewrite the above example to use style objects.

{% code title="my-app.ts" %}

```typescript
export class MyApp {
  private styleObject = {
    background: 'black',
    color: '#FFF'
  };
}
```

{% endcode %}

{% code title="my-app.html" %}

```html
<p style.bind="styleObject">Hello there</p>

```

{% endcode %}

From a styling perspective, both examples above do the same thing. However, we are passing in an object and binding it to the `style` property instead of a string.


# Attribute transferring

Forward bindings from a custom element to its inner template using Aurelia's spread operators.

Attribute transferring lets a custom element pass bindings it receives down to elements inside its own template. It keeps component APIs small while still exposing the flexibility callers need.

As an application grows, the components inside it also grow. Something that starts simple like the following component

```typescript
export class FormInput {
  @bindable label
  @bindable value
}
```

with the template

```html
<label>${label}
  <input value.bind="value">
</label>
```

can quickly grow out of hand with a number of needs for configuration: aria, type, min, max, pattern, tooltip, validation etc...

After a while, the `FormInput` component above will become more and more like a relayer that simply passes bindings through. The number of `@bindable` properties increases and maintenance becomes tedious:

```typescript
export class FormInput {
  @bindable label
  @bindable value
  @bindable type
  @bindable tooltip
  @bindable arias
  @bindable etc
}
```

And the usage of such element may look like this

```html
<form-input
  label.bind="label"
  value.bind="message"
  tooltip.bind="Did you know Aurelia syntax comes from an idea of an Angular community member? We greatly appreciate Angular and its community for this."
  validation.bind="...">
```

to be repeated like this inside:

```html
<label>${label}
  <input value.bind tooltip.bind validation.bind min.bind max.bind>
</label>
```

Coordinating all of those bindings isn't difficult, just repetitive. Attribute transferring, conceptually similar to JavaScript spread syntax, reduces the template to:

```html
<label>${label}
  <input ...$attrs>
</label>
```

This moves the bindings declared on `<form-input>` onto the `<input>` element inside the component.

## Aurelia Spread Operators Overview

Aurelia provides several spread operators for different use cases:

| Operator        | Purpose                                         | Example                               |
| --------------- | ----------------------------------------------- | ------------------------------------- |
| `...$attrs`     | Spread captured attributes from parent element  | `<input ...$attrs>`                   |
| `...$bindables` | Spread object properties to bindable properties | `<my-component ...$bindables="user">` |
| `...expression` | Shorthand for bindable spreading                | `<my-component ...user>`              |

Each operator serves a specific purpose in component composition and data flow.

## Usage

To transfer attributes and bindings from a custom element, there are two steps:

* Set `capture` to `true` on a custom element via `@customElement` decorator:

```typescript
@customElement({
  ...,
  capture: true
})
```

This tells the template compiler to capture bindings and attributes (with some exceptions) for later reuse.

* Spread the captured attributes onto an element:

```html
<input ...$attrs>
```

{% hint style="warning" %}
Avoid chaining attribute transfer across many component layers. Deep “prop drilling” is difficult to follow and can become a maintenance burden. Keep transfers to at most one or two levels.
{% endhint %}

## How it works

### What attributes are captured

Everything except template controller and custom element bindables are captured. For the following example:

View model:

```typescript
export class FormInput {
  @bindable label
}
```

Usage:

```html
<form-input if.bind="needsComment" label.bind="label" value.bind="extraComment" class="form-control" style="background: var(--theme-purple)" tooltip="Hello, ${tooltip}">
```

What are captured:

* `value.bind="extraComment"`
* `class="form-control"`
* `style="background: var(--theme-purple)"`
* `tooltip="Hello, ${tooltip}"` What are not captured:
* `if.bind="needsComment"` (`if` is a template controller)
* `label.bind="label"` (`label` is a bindable property)

### How will attributes be applied in ...$attrs

Attributes that are spread onto an element will be compiled as if it was declared on that element.

This means `.bind` command will work as expected when it's transferred from some element onto some element that uses `.two-way` for `.bind`.

It also means that spreading onto a custom element will also work: if a captured attribute is targeting a bindable property of the applied custom element. An example:

```html
app.html
<input-field value.bind="message">

input-field.html
<my-input ...$attrs>
```

if `value` is a bindable property of `my-input`, the end result will be a binding that connects the `message` property of the corresponding `app.html` view model with `<my-input>` view model `value` property. Binding mode is also preserved like normal attributes.

## Advanced Spread Patterns

### Mixed Binding Patterns

You can combine multiple spread operators and explicit bindings on the same element:

```html
<!-- Spread bindables, then attributes, then explicit bindings -->
<input-field ...user ...$attrs id.bind="fieldId" class="form-control">
```

**Binding Priority (last wins):**

1. `...$bindables` / `...expression` (first)
2. `...$attrs` (second)
3. Explicit bindings (last, highest priority)

Note: According to the existing documentation, `...$attrs` will always result in bindings after `...$bindables`/`$bindables.spread`/`...expression`, regardless of their order in the template.

```html
<!-- The explicit value.bind will override any value from spreading -->
<input ...$attrs value.bind="explicitValue">
```

### Complex Member Access

Spread operators support complex expressions:

```html
<!-- Deep property access -->
<user-card ...user.profile.details>
<user-card ...user.addresses[0]>

<!-- Method calls and computed properties -->
<user-card ...user.getDetails()>
<user-card ...user.details | processUser>

<!-- For complex expressions, use the full syntax -->
<user-card ...$bindables="user.addresses.find(addr => addr.primary)">
```

### Conditional Spreading

You can conditionally spread attributes based on expressions:

```html
<!-- Only spread if user exists -->
<user-card ...$bindables="user || {}">

<!-- Spread different objects based on condition -->
<user-card ...$bindables="isAdmin ? adminUser : regularUser">

<!-- Combine with template controllers -->
<user-card if.bind="user" ...user>
```

## Automatic Expression Inference

Aurelia can automatically infer property names in certain binding scenarios:

### Shorthand Binding Syntax

```html
<!-- These are equivalent -->
<input value.bind="value">
<input value.bind>  <!-- Auto-infers 'value' property -->

<!-- Works with different binding commands -->
<input value.two-way="value">
<input value.two-way>  <!-- Auto-infers 'value' property -->

<!-- Attribute binding -->
<div textcontent.bind="textcontent">
<div textcontent.bind>  <!-- Auto-infers 'textcontent' property -->

<!-- Custom attributes -->
<div tooltip.bind="tooltip">
<div tooltip.bind>  <!-- Auto-infers 'tooltip' property -->
```

### Inference Rules

* Property name must match the attribute name exactly
* Only works with simple property access (no expressions)
* Works with all binding commands (`.bind`, `.two-way`, `.one-way`, etc.)
* Case-sensitive: `firstName.bind` infers `firstName`, not `firstname`

## Performance Considerations

### Binding Creation Optimization

Spread operators include several performance optimizations:

```typescript
// Aurelia optimizes repeated spread operations
class UserCard {
  @bindable user = { name: 'John', age: 30 };
  
  updateUser() {
    // If the same object reference is returned, bindings aren't recreated
    this.user = this.user; // No rebinding
    
    // New object reference triggers binding recreation
    this.user = { ...this.user, age: 31 }; // Rebinding occurs
  }
}
```

### One-time Change Detection

Spread operations are optimized to prevent unnecessary binding updates:

```html
<!-- Bindings are created once and reused when possible -->
<user-card ...user>
```

### Memory Usage Guidelines

* Spread operators create bindings for each property accessed
* Large objects with many properties create many bindings
* Consider using specific bindable properties for frequently changing data
* Use spreading primarily for configuration and setup data

## Error Handling & Edge Cases

### Null and Undefined Handling

Spread operators handle null and undefined values gracefully:

```html
<!-- Safe spreading - handles null/undefined gracefully -->
<user-card ...user>           <!-- Safe even if user is null/undefined -->
<user-card ...$bindables="user || {}">  <!-- Explicit fallback -->

<!-- Member access on null/undefined -->
<user-card ...user?.profile>  <!-- Safe with optional chaining -->
```

### Invalid Expressions

```html
<!-- These will be handled gracefully -->
<user-card ...undefined>      <!-- No bindings created -->
<user-card ...nonExistentVar> <!-- No bindings created -->
<user-card ...user.invalid>   <!-- No bindings created -->
```

### Type Safety with TypeScript

TypeScript provides compile-time validation for spread operations:

```typescript
interface User {
  name: string;
  email: string;
  age: number;
}

export class UserCard {
  @bindable name: string;
  @bindable email: string;
  // age is not a bindable, so it won't be bound even if present in the object
}

const user: User = { name: 'John', email: 'john@example.com', age: 30 };
```

```html
<!-- Only name and email will be bound based on component's @bindable properties -->
<user-card ...user>
```

## Advanced Capture Patterns

### Capture Filtering

Filter which attributes are captured using a function:

```typescript
@customElement({
  name: 'secure-input',
  template: '<input ...$attrs>',
  capture: attr => !attr.startsWith('on') // Exclude event handlers
})
export class SecureInput {
  @bindable value: string;
}
```

```typescript
@customElement({
  name: 'styled-input',
  template: '<input ...$attrs>',
  capture: attr => ['class', 'style', 'disabled'].includes(attr) // Only style-related
})
export class StyledInput {
  @bindable value: string;
}
```

### Multi-level Capture Guidelines

```html
<!-- Level 1: App uses form-group -->
<form-group title="User Info" ...validation>
  <!-- Level 2: form-group uses input-field -->
  <input-field label="Email" ...validation>
    <!-- Level 3: input-field uses input -->
    <input ...$attrs>
  </input-field>
</form-group>
```

{% hint style="warning" %}
**Best Practice**: Limit capture levels to 2-3 maximum to maintain code clarity and avoid prop-drilling anti-patterns.
{% endhint %}

### Template Controller Compatibility

Spread operators work with template controllers:

```html
<!-- Template controllers are not captured -->
<input-field if.bind="showField" ...fieldProps>

<!-- Multiple template controllers -->
<input-field if.bind="showField" repeat.for="field of fields" ...field>
```

## Integration Examples

### Component Composition

```typescript
// Base input component
export class BaseInput {
  @bindable value: string;
  @bindable placeholder: string;
  @bindable disabled: boolean;
}

// Specialized email input
@customElement({
  name: 'email-input',
  template: '<base-input type="email" ...$attrs>',
  capture: true
})
export class EmailInput {}

// Form field wrapper
@customElement({
  name: 'form-field',
  template: `
    <div class="form-field">
      <label if.bind="label">\${label}</label>
      <div class="input-wrapper">
        <div class="content-replaceable" replaceable part="input">
          <input ...$attrs>
        </div>
      </div>
      <div class="error" if.bind="error">\${error}</div>
    </div>
  `,
  capture: true
})
export class FormField {
  @bindable label: string;
  @bindable error: string;
}
```

Usage:

```html
<!-- Complex composition -->
<form-field label="Email Address" error.bind="emailError">
  <email-input au-slot="input" value.bind="email" placeholder="Enter email">
</form-field>
```

### Working with Third-party Components

```typescript
// Wrapper for third-party component
@customElement({
  name: 'material-input',
  template: '<mat-input ...$attrs>',
  capture: attr => !attr.startsWith('au-') // Exclude Aurelia-specific attributes
})
export class MaterialInput {
  @bindable value: string;
}
```

### Dynamic Component Creation

```typescript
export class DynamicForm {
  @bindable fieldConfigs: FieldConfig[];
  
  createField(config: FieldConfig) {
    return {
      component: config.component,
      props: config.props
    };
  }
}
```

```html
<div repeat.for="config of fieldConfigs">
  <compose 
    view-model.bind="config.component"
    ...$bindables="config.props">
  </compose>
</div>
```

## Common Patterns and Best Practices

### 1. Configuration Objects

```typescript
// Good: Use spreading for configuration
interface ButtonConfig {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  icon?: string;
}

const submitConfig: ButtonConfig = {
  variant: 'primary',
  size: 'medium',
  icon: 'save'
};
```

```html
<custom-button ...submitConfig>Submit</custom-button>
```

### 2. Conditional Properties

```typescript
// Good: Build objects conditionally
const inputProps = {
  value: userInput,
  ...(isRequired && { required: true }),
  ...(hasError && { 'aria-invalid': true }),
  ...(isDisabled && { disabled: true })
};
```

```html
<input ...$bindables="inputProps">
```

### 3. Proxy Objects for Transformation

```typescript
// Good: Transform data before spreading
const transformedUser = {
  displayName: user.fullName,
  email: user.contactInfo.email,
  isActive: user.status === 'active'
};
```

```html
<user-card ...transformedUser>
```

### 4. Default Values with Spreading

```typescript
// Good: Provide defaults
const defaultFieldProps = {
  size: 'medium',
  variant: 'outline'
};

const fieldProps = {
  ...defaultFieldProps,
  ...customProps
};
```

```html
<form-field ...$bindables="fieldProps">
```

This comprehensive documentation now covers all the advanced patterns and edge cases developers might encounter when working with Aurelia's spread operators.


# Startup & enhancement


# App configuration and startup

Configure Aurelia applications, register global resources, and choose the startup pattern that fits your project.

## Application Startup

Aurelia provides two main approaches for application startup: a quick setup using static methods with sensible defaults, and a verbose setup that gives you complete control over configuration.

> **Before you start:** If you have not already chosen a project scaffold, walk through the [section overview](/getting-to-know-aurelia/getting-to-know-aurelia) for context on how this guide fits with enhancement, routing, and composition topics.

### Quick startup

The quick startup approach uses static methods on the Aurelia class and is the most common choice for new applications.

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';

import { MyRootComponent } from './my-root-component';

// Simplest startup - hosts to <my-root-component> element, or <body> if not found
await Aurelia.app(MyRootComponent).start();

// Register additional features before startup
await Aurelia
  .register(
    RouterConfiguration.customize({ useUrlFragmentHash: false })
  )
  .app(MyRootComponent)
  .start();

// Specify a custom host element
await Aurelia
  .register(
    RouterConfiguration.customize({ useUrlFragmentHash: false })
  )
  .app({
    component: MyRootComponent,
    host: document.querySelector('my-start-tag')
  })
  .start();

// Keep the Aurelia instance for later lifecycle calls
const app = Aurelia
  .register(
    RouterConfiguration.customize({ useUrlFragmentHash: false })
  )
  .app(MyRootComponent);

await app.start();
```

### Verbose Startup

The verbose approach gives you complete control over the DI container and configuration. Use this when integrating Aurelia into existing applications or when you need fine-grained control.

```typescript
import { Aurelia, StandardConfiguration } from '@aurelia/runtime-html';
import { RouterConfiguration } from '@aurelia/router';
import { LoggerConfiguration, LogLevel } from '@aurelia/kernel';
import { ShellComponent } from './shell';

// Create Aurelia instance with explicit configuration
const au = new Aurelia();

au.register(
  StandardConfiguration,  // Essential runtime configuration
  RouterConfiguration.customize({ useUrlFragmentHash: false }),
  LoggerConfiguration.create({ level: LogLevel.debug })
);

au.app({
  host: document.querySelector('body'),
  component: ShellComponent
});

// Await startup before using the running application
await au.start();
```

## Awaiting startup and shutdown

`start()` performs its work synchronously when it can. It returns `void` for fully synchronous startup and `Promise<void>` when startup includes asynchronous work. `await` handles both forms and reports startup errors at the call site:

```typescript
const au = new Aurelia()
  .register(StandardConfiguration)
  .app({ host: document.body, component: ShellComponent });

await au.start();
```

If startup throws or rejects, that Aurelia instance enters a terminal state. Aurelia preserves the original failure. When the value is an `Error`, its stack points to the failing app task or component hook. Fix the cause before creating and starting a new application instance.

After a successful start, `stop()` waits for the root component to deactivate. It also waits for application tasks and queued framework work. The default `stop()` retains the root for another start. `stop(true)` also disposes it. A stop requested during asynchronous startup waits for activation to finish and then deactivates the application. The `start()` result completes at `au-started`; the queued `stop()` result completes separately at `au-stopped`.

```typescript
await au.stop(true);
```

A stop failure leaves that Aurelia instance in a terminal state and preserves the original application error.

The `au-started` and `au-stopped` events identify successful transitions. Both events bubble from the application host and expose the Aurelia instance through `event.detail`.

**When to use verbose startup:**

* Integrating Aurelia into existing applications
* Custom DI container configuration needed
* Multiple Aurelia apps in one page
* Advanced debugging or testing scenarios

**StandardConfiguration** includes essential services like:

* Template compiler and renderer
* Binding engine and observers
* Custom element/attribute support
* Built-in value converters and binding behaviors
* DOM event handling and delegation
* Shadow DOM and CSS module support

## Registering Global Resources

### Registering a single custom element

To make a custom element globally available throughout your application, register it before calling `app()`.

```typescript
import Aurelia from 'aurelia';
import { CardCustomElement } from './components/card';

// Quick startup
await Aurelia
  .register(CardCustomElement)  // No type casting needed
  .app(MyRootComponent)
  .start();

// Verbose startup
const au = new Aurelia();
au.register(
  StandardConfiguration,
  CardCustomElement
);
au.app({ host: document.body, component: MyRootComponent });
await au.start();
```

### Registering multiple resources

Group related components into resource modules for better organization.

**src/components/index.ts:**

```typescript
export { CardCustomElement } from './card';
export { CollapseCustomElement } from './collapse';
export { ModalCustomElement } from './modal';
```

**src/main.ts:**

```typescript
import Aurelia from 'aurelia';
import * as GlobalComponents from './components';

// Register all exported components at once
await Aurelia
  .register(GlobalComponents)
  .app(MyRootComponent)
  .start();
```

### Registering other resource types

```typescript
import Aurelia from 'aurelia';
import { MyValueConverter } from './converters/my-value-converter';
import { MyBindingBehavior } from './behaviors/my-binding-behavior';
import { MyCustomAttribute } from './attributes/my-custom-attribute';

await Aurelia
  .register(
    MyValueConverter,
    MyBindingBehavior,
    MyCustomAttribute
  )
  .app(MyRootComponent)
  .start();
```

## Advanced Configuration

### Custom DI registrations

```typescript
import { Registration } from '@aurelia/kernel';
import { MyService, IMyService } from './services/my-service';

await Aurelia
  .register(
    Registration.singleton(IMyService, MyService)
  )
  .app(MyRootComponent)
  .start();
```

### Environment-specific configuration

```typescript
import Aurelia, { LoggerConfiguration, LogLevel } from 'aurelia';

const isProduction = process.env.NODE_ENV === 'production';

await Aurelia
  .register(
    LoggerConfiguration.create({
      level: isProduction ? LogLevel.warn : LogLevel.debug
    })
  )
  .app(MyRootComponent)
  .start();
```

## Enhancement Mode

Sometimes you need Aurelia to light up markup that already exists in the DOM. Instead of calling `app()`, reach for `Aurelia.enhance`:

```typescript
const enhanceRoot = await Aurelia.enhance({
  host: document.querySelector('#existing-content'),
  component: { message: 'Hello from enhanced content!' }
});
```

Enhancement is ideal for progressive hydration, CMS integrations, or widgets embedded in non-Aurelia pages. You can register resources before enhancing and provide a custom DI container. For permanent cleanup, await `enhanceRoot.deactivate()` and then call `enhanceRoot.dispose()`.

For a full guide, including cleanup patterns, lifecycle hooks, and advanced recipes, see the dedicated [Enhance](/getting-to-know-aurelia/startup-and-enhancement/enhance) article.

## Next steps

* Continue with [Enhance](/getting-to-know-aurelia/startup-and-enhancement/enhance) for progressive integration scenarios.
* Wire services using [dependency injection](/getting-to-know-aurelia/services-and-runtime-hooks/dependency-injection) once your shell is running.
* Explore [choosing a router](https://github.com/aurelia/aurelia/tree/master/docs/user-docs/getting-to-know-aurelia/routing/choosing-a-router.md) to add navigation after the app is bootstrapped.


# Enhance

Learn how to use Aurelia's enhance feature to add interactivity to existing HTML, integrate with other frameworks, hydrate server-rendered content, and create multiple Aurelia instances in your applic

## What is Enhancement?

Enhancement adds Aurelia's data binding, templating, and component features directly to HTML already in the page. The existing DOM stays in place while Aurelia connects it to your data and behavior.

This is perfect for:

* **Progressive enhancement** of server-rendered pages
* **Integration** with existing applications or other frameworks
* **Widget development** where you control specific sections of a page
* **Content Management Systems** where you want to add interactivity to generated content
* **Legacy application modernization** done incrementally

Standard startup renders an Aurelia application into a host. Enhancement applies Aurelia behavior to markup already present in that host.

> **Before you start:** Review [App configuration and startup](/getting-to-know-aurelia/startup-and-enhancement/app-configuration-and-startup) to understand the standard bootstrap flow; enhancement builds on those concepts.

## Understanding What Gets Enhanced

When you enhance an element, Aurelia treats that element as if it were the template of a custom element. The existing HTML becomes the "template" and your component object becomes the "view model" that provides data and behavior.

### Basic Enhancement Syntax

```typescript
// Using the convenience method (recommended)
const enhanceRoot = await Aurelia.enhance({
  host: document.querySelector('#my-content'),
  component: { message: 'Hello World' }
});

// Using instance method  
const au = new Aurelia();
const enhanceRoot = await au.enhance({
  host: document.querySelector('#my-content'),
  component: { message: 'Hello World' }
});
```

### Component Types: Classes, Instances, or Objects

You can enhance with three different component types:

```typescript
// 1. Plain object (most common for simple cases)
const enhanceRoot = await Aurelia.enhance({
  host: element,
  component: {
    message: 'Hello',
    items: [1, 2, 3],
    handleClick() { 
      console.log('Clicked!'); 
    }
  }
});

// 2. Class instance (when you need constructor logic)
class MyViewModel {
  message = 'Hello';
  constructor() {
    // initialization logic
  }
}
const enhanceRoot = await Aurelia.enhance({
  host: element,
  component: new MyViewModel()
});

// 3. Custom element class (for reusable components)
@customElement({ name: 'my-widget' })
class MyWidget {
  @bindable message: string;
}
const enhanceRoot = await Aurelia.enhance({
  host: element,
  component: MyWidget
});
```

### Key Enhancement Concepts

1. **Existing DOM is preserved**: Enhancement connects Aurelia behavior to your current HTML.
2. **Existing event handlers remain**: Any JavaScript event listeners you've already attached stay functional
3. **Manual lifecycle management**: Deactivate and dispose the enhancement root during permanent cleanup.
4. **Template compilation**: Aurelia compiles the existing HTML for bindings and directives

### Proper Cleanup

During permanent cleanup, deactivate the enhanced content and dispose its root:

```typescript
const enhanceRoot = await Aurelia.enhance({ host, component });

// Later, during permanent cleanup:
await enhanceRoot.deactivate();
enhanceRoot.dispose();
```

## Practical Enhancement Examples

### Server-Rendered Content Enhancement

Suppose your server renders this HTML:

```html
<!-- Server-rendered content -->
<div id="user-profile">
  <h2>Welcome back!</h2>
  <div class="stats">
    <span>Loading user data...</span>
  </div>
  <button id="refresh-btn">Refresh</button>
</div>
```

You can enhance it to make it interactive:

```typescript
import Aurelia from 'aurelia';

// Your existing server-rendered element
const profileElement = document.querySelector('#user-profile');

// Enhance with Aurelia interactivity
const enhanceRoot = await Aurelia.enhance({
  host: profileElement,
  component: {
    username: 'Loading...',
    loginCount: 0,
    
    async created() {
      // Load user data when component initializes
      const userData = await fetch('/api/user/profile').then(r => r.json());
      this.username = userData.username;
      this.loginCount = userData.loginCount;
    },
    
    refreshData() {
      this.created(); // Reload data
    }
  }
});

// Update your HTML to use bindings:
// <h2>Welcome back, ${username}!</h2>
// <div class="stats">
//   <span>Login count: ${loginCount}</span>
// </div>
// <button click.trigger="refreshData()">Refresh</button>
```

### Widget Integration Example

Create interactive widgets within existing pages:

```html
<!-- Existing page content -->
<div class="article">
  <h1>My Blog Post</h1>
  <p>Some content...</p>
  
  <!-- Widget placeholder -->
  <div id="comment-widget">
    <h3>Comments</h3>
    <div class="loading">Loading comments...</div>
  </div>
</div>
```

```typescript
// Enhance just the comment widget
const commentWidget = document.querySelector('#comment-widget');

const enhanceRoot = await Aurelia.enhance({
  host: commentWidget,
  component: {
    comments: [],
    newComment: '',
    
    async created() {
      this.comments = await this.loadComments();
    },
    
    async loadComments() {
      return fetch('/api/comments/123').then(r => r.json());
    },
    
    async addComment() {
      if (!this.newComment.trim()) return;
      
      await fetch('/api/comments', {
        method: 'POST',
        body: JSON.stringify({ text: this.newComment }),
        headers: { 'Content-Type': 'application/json' }
      });
      
      this.newComment = '';
      this.comments = await this.loadComments();
    }
  }
});

// Update HTML to:
// <div id="comment-widget">
//   <h3>Comments (${comments.length})</h3>
//   <div repeat.for="comment of comments">
//     <p>${comment.text}</p>
//   </div>
//   <div>
//     <input value.bind="newComment" placeholder="Add comment...">
//     <button click.trigger="addComment()">Post</button>
//   </div>
// </div>
```

## Dynamic Content Enhancement

Enhancement is perfect for content that gets added to the page after initial load.

### Enhancing Dynamically Loaded Content

```typescript
import { Aurelia, resolve } from 'aurelia';

export class DynamicContentComponent {
  private enhancedRoots: Array<any> = [];
  
  constructor(private au = resolve(Aurelia)) {}

  async loadMoreContent() {
    // Load HTML from server
    const response = await fetch('/api/content/next-page');
    const htmlContent = await response.text();
    
    // Create container for new content
    const container = document.createElement('div');
    container.innerHTML = htmlContent;
    document.querySelector('#content-area').appendChild(container);
    
    // Enhance the new content
    const enhanceRoot = await this.au.enhance({
      host: container,
      component: {
        currentUser: this.currentUser,
        likePost: (postId) => this.likePost(postId),
        sharePost: (postId) => this.sharePost(postId)
      }
    });
    
    // Keep track for cleanup
    this.enhancedRoots.push(enhanceRoot);
  }
  
  // Clean up when component is destroyed
  async unbinding() {
    for (const root of this.enhancedRoots) {
      await root.deactivate();
      root.dispose();
    }
    this.enhancedRoots = [];
  }
}
```

### Enhancing Modal or Dialog Content

```typescript
export class ModalService {
  private currentModal: any = null;
  
  async showModal(contentHtml: string, viewModel: any) {
    // Create modal element
    const modal = document.createElement('div');
    modal.className = 'modal';
    modal.innerHTML = `
      <div class="modal-content">
        <button class="close" click.trigger="closeModal()">&times;</button>
        ${contentHtml}
      </div>
    `;
    
    document.body.appendChild(modal);

    // Enhance the modal content
    this.currentModal = await Aurelia.enhance({
      host: modal,
      component: {
        ...viewModel,
        closeModal: () => this.closeModal()
      }
    });
  }
  
  async closeModal() {
    if (this.currentModal) {
      await this.currentModal.deactivate();
      this.currentModal.dispose();
      document.querySelector('.modal')?.remove();
      this.currentModal = null;
    }
  }
}
```

## Advanced Enhancement Patterns

### Using Custom Containers

When you need specific services or configurations for enhanced content:

```typescript
import { DI, Registration } from '@aurelia/kernel';
import { LoggerConfiguration, LogLevel } from 'aurelia';

// Create custom container for widget
const widgetContainer = DI.createContainer()
  .register(
    Registration.singleton('ApiService', MyApiService),
    LoggerConfiguration.create({ level: LogLevel.debug })
  );

const enhanceRoot = await Aurelia.enhance({
  host: document.querySelector('#my-widget'),
  component: MyWidget,
  container: widgetContainer  // Use custom container
});
```

### Lifecycle Hooks in Enhanced Components

Enhanced components support all standard Aurelia lifecycle hooks:

```typescript
const enhanceRoot = await Aurelia.enhance({
  host: element,
  component: {
    data: null,
    
    // Called when component is being set up
    created() {
      console.log('Component created');
    },
    
    // Called before data binding starts
    binding() {
      console.log('Starting data binding');
    },
    
    // Called after data binding completes
    bound() {
      console.log('Data binding complete');
    },
    
    // Called when component is being attached to DOM
    attaching() {
      console.log('Attaching to DOM');
    },
    
    // Called after component is attached to DOM
    attached() {
      console.log('Attached to DOM - ready for user interaction');
      // Good place for focus, animations, etc.
    },
    
    // Called when component is being removed
    detaching() {
      console.log('Detaching from DOM');
    },
    
    // Called when data bindings are being torn down
    unbinding() {
      console.log('Unbinding data');
      // Cleanup subscriptions, timers, etc.
    }
  }
});
```

## Common Enhancement Patterns

### Progressive Enhancement Checklist

1. **Identify enhancement targets**: Elements that need interactivity
2. **Preserve existing functionality**: Keep existing event handlers working
3. **Plan your data flow**: How will data get to enhanced components?
4. **Handle permanent cleanup**: Deactivate and dispose each enhancement root
5. **Test without JavaScript**: Ensure basic functionality works without enhancement

### Best Practices

* **Start small**: Enhance specific widgets before entire sections
* **Use meaningful component objects**: Include methods and properties that make sense
* **Handle errors gracefully**: Enhancement might fail if DOM structure changes
* **Document what gets enhanced**: Make it clear to other developers
* **Consider performance**: Don't enhance too many elements at once

### When NOT to Use Enhancement

* **New applications**: Use regular `Aurelia.app()` for greenfield projects
* **Full page control**: When you control the entire page, standard app startup is simpler
* **Simple static content**: If content doesn't need interactivity
* **Performance critical sections**: Enhancement has overhead compared to pre-compiled templates

Enhancement shines when you need to add Aurelia's power to existing content without rebuilding everything from scratch.

## Next steps

* Use [dynamic composition](/getting-to-know-aurelia/composition-patterns/dynamic-composition) when you need to render different components inside an enhanced region.
* Combine enhancement with [watching data](/getting-to-know-aurelia/state-and-observation/watching-data) to react to model changes in legacy markup.
* Explore [portalling elements](/getting-to-know-aurelia/composition-patterns/portalling-elements) for UI that needs to escape its original DOM location.


# Routing

Understand the @aurelia/router package, its core concepts, and how to navigate the rest of the routing documentation.

Aurelia's primary router gives you a declarative, component-first navigation system with strong type safety, multi-viewport layouts, and deep integration with dependency injection. If you have used Angular's router or the classic Aurelia 1 router, the mental model will feel familiar: define a route table, map URLs to components, nest layouts, guard navigation, lazy-load feature areas, and respond to lifecycle events. The Aurelia router stays HTML-friendly and convention-driven, letting you compose navigation without wrapper modules or excessive configuration.

> Still deciding between routers? Start with [Choosing the right Aurelia router](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/getting-to-know-aurelia/routing/choosing-a-router.md).

## Highlights

* **Structured route tree** lets you describe nested layouts, auxiliary viewports, and fallback routes in one place while still co-locating child routes with their components via `@route`.
* **Viewport-first layouts** allow flexible page composition: declare multiple `<au-viewport>` elements, name them, and target them from route definitions. This makes responsive shells and split views straightforward.
* **Lifecycle hooks and events** mirror the intent of Angular guards (`canLoad`, `canActivate`, etc.) while using Aurelia conventions (`canLoad`, `loading`, `canUnload`, `unloading`). Hooks execute in well-defined order and support async work.
* **Navigation state management** gives you centralized insight into route activation, title building, and analytics hooks, ideal for larger apps.
* **Progressive enhancement** via the `load` and `href` attributes keeps markup readable and usable even before hydration.

Refer to the package README for release notes and API exports: [`packages/router/README.md`](https://github.com/aurelia/aurelia/blob/master/packages/router/README.md).

## Choose the right guide

Work through the topics in this order when you are new to the router:

1. **Fundamentals**
   * [Getting started](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started)
   * [Router configuration](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration)
   * [Defining routes and viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes) · [Viewports in depth](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)
2. **Navigation patterns**
   * [Imperative navigation](/getting-to-know-aurelia/aurelia-router/navigation/navigating)
   * [Build menus with the navigation model](/getting-to-know-aurelia/aurelia-router/navigation/navigation-model)
   * [Accessing the active route](/getting-to-know-aurelia/aurelia-router/navigation/current-route)
3. **Lifecycle, hooks, and events**
   * [Routing lifecycle](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)
   * [Router hooks](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks)
   * [Router events](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-events)
4. **Advanced scenarios**
   * [Transition plans](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans)
   * [Router state management](/getting-to-know-aurelia/aurelia-router/advanced/router-state-management)
   * [Error handling](/getting-to-know-aurelia/aurelia-router/advanced/error-handling)
   * [Advanced API reference](/getting-to-know-aurelia/aurelia-router/advanced/advanced-api-reference)
5. **Support resources**
   * [Testing guide](/getting-to-know-aurelia/aurelia-router/support/testing-guide)
   * [Troubleshooting](/getting-to-know-aurelia/aurelia-router/support/troubleshooting)

Keep the [live StackBlitz examples](https://stackblitz.com/@Sayan751/collections/router-lite) handy while you read; most topics embed a runnable demo.

## Feature map

| Capability                                             | How to use it                                                    | Related doc                                                                                         |
| ------------------------------------------------------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Configure base path, hash vs pushState, title building | `RouterConfiguration.customize` and `RouterOptions`              | [Router configuration](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration)   |
| Map URLs to components with strong typing              | `@route` decorator inside your component                         | [Defining routes](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)          |
| Compose multiple viewports or named layouts            | `<au-viewport>` and named viewports                              | [Viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)                         |
| Control navigation flow                                | Lifecycle hooks (`canLoad`, `loading`, `canUnload`, `unloading`) | [Routing lifecycle](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle) |
| Listen for navigation changes                          | `Router.addEventListener(...)` or DI inject `IRouterEvents`      | [Router events](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-events)         |
| Persist and observe route state                        | Inject `ICurrentRoute` / `IRouter`                               | [Router state management](/getting-to-know-aurelia/aurelia-router/advanced/router-state-management) |
| Customize transitions                                  | Provide a `transitionPlan` or set per-route strategies           | [Transition plans](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans)               |

## Where to go next

* Explore targeted recipes in the [developer guides](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/developer-guides/routing/configured-routing.md).
* Pair routing with state management via [the store plugin](/aurelia-packages/store) or your preferred data layer.
* Review the [router package CHANGELOG](https://github.com/aurelia/aurelia/blob/master/packages/router/CHANGELOG.md) when upgrading between versions.


# Quick Reference ("How Do I...")

Navigate your Aurelia 2 application with confidence using this task-focused quick reference.

## Table of Contents

* [Getting Started](#getting-started)
* [Navigation](#navigation)
* [Route Parameters](#route-parameters)
* [Route Protection](#route-protection)
* [Lifecycle Hooks](#lifecycle-hooks)
* [Advanced Topics](#advanced-topics)
* [Troubleshooting](#troubleshooting)

***

## Getting Started

### How do I install and configure the router?

```typescript
// Install
npm i @aurelia/router

// Configure in main.ts
import { RouterConfiguration } from '@aurelia/router';

Aurelia
  .register(RouterConfiguration.customize({
    useUrlFragmentHash: false,  // Clean URLs (default)
    historyStrategy: 'push',     // Browser history
  }))
  .app(MyApp)
  .start();
```

[Full configuration options →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration)

### How do I define routes?

```typescript
import { route } from '@aurelia/router';

@route({
  routes: [
    { path: '', component: Home, title: 'Home' },
    { path: 'about', component: About, title: 'About' },
    { path: 'users/:id', component: UserDetail }
  ]
})
export class MyApp {}
```

[Configuring routes →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)

### How do I set up a viewport?

```html
<!-- In your root component template -->
<nav>
  <a href="home">Home</a>
  <a href="about">About</a>
</nav>

<au-viewport></au-viewport>
```

[Viewports documentation →](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)

### How do I use hash-based routing instead of clean URLs?

```typescript
RouterConfiguration.customize({
  useUrlFragmentHash: true  // URLs like /#/about
})
```

[Hash vs PushState routing →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#choose-between-hash-and-pushstate-routing-using-useurlfragmenthash)

***

## Navigation

### How do I create navigation links?

```html
<!-- Using href (simple) -->
<a href="about">About</a>
<a href="users/42">User 42</a>

<!-- Using load (structured) -->
<a load="route: users; params.bind: {id: userId}">User Profile</a>
```

[Navigation methods →](/getting-to-know-aurelia/aurelia-router/navigation/navigating)

### How do I navigate programmatically?

```typescript
import { IRouter } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class MyComponent {
  private readonly router = resolve(IRouter);

  navigateToUser(id: number) {
    this.router.load(`users/${id}`);

    // Or with options
    this.router.load('users', {
      params: { id },
      queryParams: { tab: 'profile' }
    });
  }
}
```

[Using the Router API →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-the-router-api)

### How do I highlight the active link?

```typescript
// Configure active class globally
RouterConfiguration.customize({
  activeClass: 'active'
})
```

```html
<!-- Use with load attribute -->
<a load="home" active.bind="isHomeActive">Home</a>

<!-- Or use the configured active class -->
<a load="home">Home</a>  <!-- Gets 'active' class automatically -->
```

[Active CSS class →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#configure-active-class)

### How do I navigate to parent routes from nested components?

```html
<!-- Using href with ../ prefix -->
<a href="../sibling">Go to sibling route</a>

<!-- Using load with context -->
<a load="route: sibling; context.bind: parentContext">Sibling</a>
```

[Ancestor navigation →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#navigate-in-current-and-ancestor-routing-context)

### How do I pass query parameters?

```typescript
// Programmatically
router.load('search', {
  queryParams: { q: 'aurelia', page: 1 }
});
// Result: /search?q=aurelia&page=1
```

[Query parameters →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-navigation-options)

### How do I handle external links?

**Good news**: External links work automatically! The router automatically ignores:

```html
<!-- These automatically bypass the router (no special attributes needed!) -->
<a href="https://example.com">External site</a>
<a href="mailto:test@example.com">Email</a>
<a href="tel:+1234567890">Phone</a>
<a href="//cdn.example.com/file.pdf">Protocol-relative</a>
<a href="ftp://files.example.com">FTP</a>

<!-- Also bypassed: -->
<a href="/internal" target="_blank">New tab</a>
<a href="/internal" target="other">Named target</a>
```

**Only use `external` attribute for edge cases:**

```html
<!-- When URL looks internal but should bypass router -->
<a href="/api/download" external>API endpoint</a>
<a href="/old-page.html" external>Legacy HTML page</a>
```

**How it works**: The router uses the `URL` constructor to check if a link is external. Any URL that can be parsed without a base (like `https://`, `mailto:`, etc.) is automatically treated as external.

[Bypassing the router →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#bypassing-the-href-custom-attribute)

***

## Route Parameters

### How do I define route parameters?

```typescript
@route({
  routes: [
    { path: 'users/:id', component: UserDetail },           // Required
    { path: 'posts/:id?', component: PostDetail },          // Optional
    { path: 'files/*path', component: FileViewer },         // Wildcard
    { path: 'items/:id{{^\\d+$}}', component: ItemDetail }, // Constrained
  ]
})
```

[Path and parameters →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#path-and-parameters)

### How do I access route parameters in my component?

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';

export class UserDetail implements IRouteViewModel {
  userId: string;

  canLoad(params: Params) {
    this.userId = params.id;
    return true;
  }
}
```

[Lifecycle hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)

### How do I get all parameters including from parent routes?

```typescript
import { IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class NestedComponent {
  private readonly routeContext = resolve(IRouteContext);

  attached() {
    const allParams = this.routeContext.getRouteParameters<{
      companyId: string;
      projectId: string;
      userId: string;
    }>({ includeQueryParams: true });
  }
}
```

[Aggregate parameters →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#aggregate-parameters-with-getrouteparameters) [Route parameters guide →](/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters)

### How do I constrain parameters with regex?

```typescript
{
  path: 'users/:id{{^\\d+$}}',  // Only numbers
  component: UserDetail
}
```

[Constrained parameters →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#constrained-parameters)

***

## Route Protection

### How do I protect routes (authentication)?

```typescript
import { lifecycleHooks } from '@aurelia/runtime-html';
import { IRouteViewModel, Params, RouteNode } from '@aurelia/router';

@lifecycleHooks()
export class AuthHook {
  canLoad(viewModel: IRouteViewModel, params: Params, next: RouteNode) {
    const isLoggedIn = !!localStorage.getItem('authToken');

    if (!isLoggedIn) {
      return 'login';  // Redirect to login
    }

    return true;  // Allow navigation
  }
}
```

[Router hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks)

### How do I implement authorization (role-based access)?

```typescript
@lifecycleHooks()
export class AuthorizationHook {
  canLoad(viewModel: IRouteViewModel, params: Params, next: RouteNode) {
    const requiredPermission = next.data?.permission;

    if (requiredPermission && !this.hasPermission(requiredPermission)) {
      return 'forbidden';
    }

    return true;
  }

  private hasPermission(permission: string): boolean {
    // Check user permissions
    return true;
  }
}
```

```typescript
// In route configuration
{
  path: 'admin',
  component: AdminPanel,
  data: { permission: 'admin' }
}
```

[Router hooks example →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks#example-authentication-and-authorization)

### How do I prevent navigation away from unsaved forms?

```typescript
import { IRouteViewModel, RouteNode } from '@aurelia/router';

export class EditForm implements IRouteViewModel {
  private isDirty = false;

  canUnload(next: RouteNode | null, current: RouteNode) {
    if (this.isDirty) {
      return confirm('You have unsaved changes. Leave anyway?');
    }
    return true;
  }
}
```

[canUnload hook →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle#canunload)

### How do I redirect based on conditions?

```typescript
export class Dashboard implements IRouteViewModel {
  canLoad(params: Params) {
    const userRole = this.authService.getRole();

    if (userRole === 'admin') {
      return 'admin/dashboard';
    } else if (userRole === 'user') {
      return 'user/dashboard';
    }

    return 'login';
  }
}
```

[Redirect from canLoad →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle#redirect-to-another-view-from-canload)

***

## Lifecycle Hooks

### How do I load data before showing a component?

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';

export class UserDetail implements IRouteViewModel {
  user: User | null = null;

  async loading(params: Params) {
    this.user = await fetch(`/api/users/${params.id}`)
      .then(r => r.json());
  }
}
```

[loading hook →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle#loading)

### How do I run code after a component is fully loaded?

```typescript
export class Dashboard implements IRouteViewModel {
  loaded(params: Params) {
    // Track page view
    analytics.track('page_view', { page: 'dashboard' });

    // Scroll to top
    window.scrollTo(0, 0);
  }
}
```

[loaded hook →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle#loaded)

### When do lifecycle hooks run?

| Hook        | When                          | Use For                                |
| ----------- | ----------------------------- | -------------------------------------- |
| `canLoad`   | Before activation             | Guards, redirects, param validation    |
| `loading`   | After approval, before render | Data fetching, state setup             |
| `loaded`    | After render                  | Analytics, scroll, post-render effects |
| `canUnload` | Before deactivation           | Unsaved changes warnings               |
| `unloading` | Before removal                | Cleanup, save drafts                   |

[Hook summary →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle#hook-summary)

### What's the difference between component hooks and router hooks?

* **Component hooks** (`IRouteViewModel`): Implemented on the component itself
* **Router hooks** (`@lifecycleHooks()`): Shared across multiple components

```typescript
// Component hook
export class MyComponent implements IRouteViewModel {
  canLoad(params: Params) {
    // Runs only for this component
  }
}

// Router hook (shared)
@lifecycleHooks()
export class AuthHook {
  canLoad(viewModel: IRouteViewModel, params: Params) {
    // Runs for all components where this is registered
  }
}
```

[Router hooks vs component hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks#anatomy-of-a-lifecycle-hook)

***

## Advanced Topics

### How do I handle 404 / unknown routes?

```typescript
@route({
  routes: [
    { path: 'home', component: Home },
    { path: 'about', component: About },
    { path: 'not-found', component: NotFound }
  ],
  fallback: 'not-found'  // Redirect unknown routes here
})
export class MyApp {}
```

[Fallback configuration →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#fallback-redirecting-the-unknown-path)

### How do I create route aliases / redirects?

```typescript
@route({
  routes: [
    { path: '', redirectTo: 'home' },
    { path: 'about-us', redirectTo: 'about' },
    { path: 'home', component: Home },
    { path: 'about', component: About }
  ]
})
```

[Redirects →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#redirect-to-another-path)

### How do I work with multiple viewports (sibling routes)?

```html
<au-viewport name="left"></au-viewport>
<au-viewport name="right"></au-viewport>
```

```html
<!-- Load components into both viewports -->
<a href="products@left+details/42@right">Products + Details</a>
```

```typescript
// Programmatically
router.load([
  { component: Products, viewport: 'left' },
  { component: Details, params: { id: 42 }, viewport: 'right' }
]);
```

[Sibling viewports →](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#sibling-viewports)

### How do I implement nested/child routes?

```typescript
@route({
  routes: [
    {
      path: 'users/:id',
      component: UserLayout,
      // Child routes defined in UserLayout
    }
  ]
})
export class MyApp {}

// In UserLayout
@route({
  routes: [
    { path: '', component: UserProfile },
    { path: 'posts', component: UserPosts },
    { path: 'settings', component: UserSettings }
  ]
})
export class UserLayout {}
```

```html
<!-- UserLayout template -->
<h2>User: ${userId}</h2>
<nav>
  <a href="posts">Posts</a>
  <a href="settings">Settings</a>
</nav>
<au-viewport></au-viewport>
```

[Hierarchical routing →](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#hierarchical-routing) [Child routing playbook →](/getting-to-know-aurelia/aurelia-router/fundamentals/child-routing)

### How do I lazy load routes?

```typescript
@route({
  routes: [
    { path: 'home', component: Home },
    // Dynamic import for lazy loading
    { path: 'admin', component: () => import('./admin/admin-panel') }
  ]
})
```

[Using inline import() →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#using-inline-import)

### How do I set/change the page title?

```typescript
// Set a route node title part in route configuration
{
  path: 'about',
  component: About,
  title: 'About Us'
}

// Set a document title override for one navigation
router.load('about', { title: 'Custom Title' });

// Set a route node title part after loading data
loading(params, next) {
  next.title = `About ${params.id}`;
}

// Custom title building
RouterConfiguration.customize({
  buildTitle(transition) {
    const title = transition.routeTree.root.getTitle(' - ');
    return title ? `${title} | My App` : 'My App';
  }
})
```

[Setting titles →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#setting-the-title) | [Customizing titles →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#customizing-title)

### How do I generate URLs without navigating?

```typescript
import { IRouter } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

const router = resolve(IRouter);

// Generate path
const userPath = await router.generatePath({
  component: 'users',
  params: { id: 42 }
});
// Result: "/users/42"

// Use in template
<a href.bind="userPath">View User</a>
```

[Path generation →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#path-generation)

### How do I work with base paths (multi-tenant apps)?

```typescript
RouterConfiguration.customize({
  basePath: '/tenant1/app'  // All routes will be prefixed
})
```

```html
<base href="/tenant1/app">
```

[Base path configuration →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#configuring-basepath)

### How do I handle browser back/forward buttons?

```typescript
// The router handles this automatically with historyStrategy

// To control history behavior per navigation:
router.load('page', {
  historyStrategy: 'replace'  // Don't create history entry
});

router.load('page', {
  historyStrategy: 'push'  // Create history entry (default)
});
```

[History strategy →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#configure-browser-history-strategy)

### How do I access the current route information?

```typescript
import { ICurrentRoute } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class MyComponent {
  private readonly currentRoute = resolve(ICurrentRoute);

  // Use in bindings or read it after navigation (for example in event handlers).
  logCurrentRoute() {
    console.log('Current path:', this.currentRoute.path);
    console.log('Parameters:', this.currentRoute.parameterInformation);
  }
}
```

[Current route →](/getting-to-know-aurelia/aurelia-router/navigation/current-route)

***

## Troubleshooting

### My routes don't work with clean URLs (no hash)

**Problem**: Getting 404 errors when refreshing or accessing routes directly

**Solution**:

1. Ensure `<base href="/">` is in your HTML
2. Configure server for SPA routing (return index.html for all routes)
3. Or use hash routing: `useUrlFragmentHash: true`

[PushState configuration →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#choose-between-hash-and-pushstate-routing-using-useurlfragmenthash)

### External links are triggering the router (rare)

**Problem**: External links somehow being handled by router

**This should NOT happen** - the router automatically ignores external links like `https://`, `mailto:`, `tel:`, etc.

**If it's happening:**

1. Check your link format - is it truly external?
2. You probably don't need the `external` attribute anymore
3. Links with protocol (`https://`, `mailto:`) are automatically bypassed

**Only needed for edge cases:**

```html
<!-- Internal-looking URLs that should bypass router -->
<a href="/api/download" external>API endpoint</a>
<a href="/static/old-page.html" external>Legacy page</a>
```

[Bypassing href →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#bypassing-the-href-custom-attribute)

### Navigation isn't working from nested components

**Problem**: Links to sibling routes not working

**Solution**: Use `../` prefix for parent context

```html
<a href="../sibling">Sibling Route</a>
```

[Ancestor navigation →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#navigate-in-current-and-ancestor-routing-context)

### My lifecycle hooks aren't being called

**Problem**: `canLoad`, `loading`, etc. not executing

**Solution**: Implement the `IRouteViewModel` interface

```typescript
import { IRouteViewModel } from '@aurelia/router';

export class MyComponent implements IRouteViewModel {
  canLoad(params: Params) { /* ... */ }
}
```

[Lifecycle hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)

### Route parameters aren't updating when navigating between same routes

**Problem**: Navigating from `/users/1` to `/users/2` doesn't update component

**Solution**: Configure transition plan

```typescript
{
  path: 'users/:id',
  component: UserDetail,
  transitionPlan: 'invoke-lifecycles'  // Re-invoke hooks
}
```

[Transition plans →](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans)

### How do I debug routing issues?

```typescript
import { IRouterEvents } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class MyApp {
  constructor() {
    const events = resolve(IRouterEvents);

    events.subscribe('au:router:navigation-start', (evt) => {
      console.log('Navigation started:', evt);
    });

    events.subscribe('au:router:navigation-end', (evt) => {
      console.log('Navigation ended:', evt);
    });

    events.subscribe('au:router:navigation-error', (evt) => {
      console.error('Navigation error:', evt);
    });
  }
}
```

[Router events →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-events)

***

## Complete Documentation

### Getting Started

* [Getting Started](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started)
* [Router Configuration](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration)

### Routes and Navigation

* [Configuring Routes](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)
* [Route Parameters Guide](/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters)
* [Route Expression Syntax](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/router/route-expression-syntax.md)
* [Navigation](/getting-to-know-aurelia/aurelia-router/navigation/navigating)
* [Viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)
* [Child Routing Playbook](/getting-to-know-aurelia/aurelia-router/fundamentals/child-routing)

### Lifecycle and Guards

* [Lifecycle Hooks](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)
* [Router Hooks](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks)
* [Transition Plans](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans)

### State and Events

* [Current Route](/getting-to-know-aurelia/aurelia-router/navigation/current-route)
* [Navigation Model](/getting-to-know-aurelia/aurelia-router/navigation/navigation-model)
* [Router Events](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-events)

### Advanced Topics

* [Error Handling](/getting-to-know-aurelia/aurelia-router/advanced/error-handling)
* [Testing Guide](/getting-to-know-aurelia/aurelia-router/support/testing-guide)

### Reference

* [API Reference](https://github.com/aurelia/aurelia/blob/master/docs/user-docs/router/api-reference.md)
* [Troubleshooting](/getting-to-know-aurelia/aurelia-router/support/troubleshooting)


# Visual Diagrams

Visual explanations of Aurelia 2's router architecture and concepts.

## Table of Contents

1. [Route Matching Pipeline](#1-route-matching-pipeline)
2. [Navigation Flow](#2-navigation-flow)
3. [Lifecycle Hook Execution Order](#3-lifecycle-hook-execution-order)
4. [Component vs Router Hooks](#4-component-vs-router-hooks)
5. [Viewport Hierarchy](#5-viewport-hierarchy)
6. [History Strategy](#6-history-strategy)
7. [Transition Plans](#7-transition-plans)
8. [Route Parameter Flow](#8-route-parameter-flow)

***

## 1. Route Matching Pipeline

How the router resolves a URL to components:

```
┌──────────────────────────────────────────────────────────┐
│ User navigates to: /products/42/reviews                  │
└──────────────────┬───────────────────────────────────────┘
                   ↓
        ┌──────────────────────┐
        │ 1. Parse URL         │
        │ Path: /products/42/  │
        │       reviews        │
        │ Fragment: #section2  │
        │ Query: ?sort=date    │
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │ 2. Match Routes      │
        │ - Check path pattern │
        │ - Extract params     │
        │ - Apply constraints  │
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │ 3. Build Route Tree  │
        │ Root                 │
        │  └─ Products (:id)   │
        │      └─ Reviews      │
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │ 4. Execute Hooks     │
        │ - canLoad (guard)    │
        │ - loading (data)     │
        │ - canUnload (prev)   │
        └──────────┬───────────┘
                   ↓
        ┌──────────────────────┐
        │ 5. Render Components │
        │ - Swap viewports     │
        │ - loaded hooks       │
        │ - Update title       │
        └──────────────────────┘

Route Configuration Match Example:

routes: [
  {
    path: 'products/:id',           ✓ Matches /products/42
    component: ProductDetail,
    routes: [
      { path: 'reviews', ... }      ✓ Matches /reviews
    ]
  },
  {
    path: 'products/:id{{^\\d+$}}', ✓ Only if :id is numeric
    component: ProductDetail
  }
]
```

**Key Points**:

* Routes are matched top-to-bottom in configuration order
* First matching route wins
* Parameters are extracted during matching
* Constraints (`{{regex}}`) are validated
* Hierarchical routes build a route tree

[Route matching documentation →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)

***

## 2. Navigation Flow

How different navigation methods work:

```
┌─────────────────────────────────────────────────────────────┐
│                     NAVIGATION METHODS                      │
└─────────────────────────────────────────────────────────────┘

METHOD 1: href attribute (Declarative)
─────────────────────────────────────────
<a href="products/42">             ┌──────────────┐
    ─────────────────────────────>│ href handler │
                                   └──────┬───────┘
                                          ↓
                               ┌──────────────────┐
                               │ Parse URL string │
                               └──────────┬───────┘
                                          ↓
                                   Navigate to URL

Context: Current route context by default
Use ../  to navigate to parent context


METHOD 2: load attribute (Structured)
──────────────────────────────────────────
<a load="route: products;          ┌──────────────┐
         params.bind: {id: 42}">───>│ load handler │
                                   └──────┬───────┘
                                          ↓
                               ┌──────────────────────┐
                               │ Build instruction    │
                               │ from structured data │
                               └──────────┬───────────┘
                                          ↓
                                   Navigate to route

Context: Current by default, can bind custom context
Active: Supports .active bindable for styling


METHOD 3: IRouter.load() (Programmatic)
────────────────────────────────────────────
router.load('products/42', {       ┌──────────────┐
  queryParams: { ... },            │ IRouter.load │
  context: this                    └──────┬───────┘
});                                       ↓
                               ┌──────────────────────┐
                               │ Full JavaScript API  │
                               │ - Error handling     │
                               │ - Async/await        │
                               │ - Options object     │
                               └──────────┬───────────┘
                                          ↓
                                   Navigate to route

Context: Root by default (different from href/load!)
Returns: Promise<boolean> for success/failure


ALL METHODS CONVERGE
────────────────────────────────────────────
                    ↓
         ┌────────────────────┐
         │ Router Core Engine │
         └──────────┬─────────┘
                    ↓
         ┌────────────────────┐
         │ Route Matching     │
         │ Hook Execution     │
         │ Component Loading  │
         └────────────────────┘
```

**Decision Guide**:

* Use `href` for simple, static links
* Use `load` when you need parameter binding or active state
* Use `IRouter.load()` for conditional/programmatic navigation

[Navigation documentation →](/getting-to-know-aurelia/aurelia-router/navigation/navigating)

***

## 3. Lifecycle Hook Execution Order

Complete sequence when navigating from ComponentA to ComponentB:

```
┌────────────────────────────────────────────────────────┐
│ Navigation: /page-a  →  /page-b                        │
└────────────────────────────────────────────────────────┘

PHASE 1: CAN UNLOAD (Current Component)
════════════════════════════════════════
ComponentA (current)
  ↓
┌─────────────────────────────────┐
│ 1. canUnload()                  │ → Return false to cancel navigation
│    - Check unsaved changes      │   Return true to allow
│    - User confirmation          │
└─────────────────┬───────────────┘
                  ↓
         [Navigation Cancelled?] ─── No ──→ Continue
                  │
                 Yes
                  ↓
            Stay on page A


PHASE 2: CAN LOAD (Next Component)
══════════════════════════════════════
ComponentB (next)
  ↓
┌─────────────────────────────────┐
│ 2. Router hooks: canLoad()      │ → Return false to block
│    - Authentication checks       │   Return NavigationInstruction to redirect
│    - Authorization               │   Return true to allow
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 3. Component: canLoad()         │ → Component-level validation
│    - Parameter validation        │
│    - Conditional logic           │
└─────────────────┬───────────────┘
                  ↓
         [Navigation Allowed?] ─── No ──→ Show fallback or redirect
                  │
                 Yes
                  ↓
            Continue to load


PHASE 3: UNLOADING (Current Component)
═══════════════════════════════════════
ComponentA (current)
  ↓
┌─────────────────────────────────┐
│ 4. Router hooks: unloading()    │
│    - Global cleanup             │
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 5. Component: unloading()       │
│    - Save drafts                │
│    - Cleanup subscriptions      │
│    - Log analytics              │
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 6. Component detached           │ ← Standard Aurelia lifecycle
│    - DOM removal                │
└─────────────────────────────────┘


PHASE 4: LOADING (Next Component)
══════════════════════════════════════
ComponentB (next)
  ↓
┌─────────────────────────────────┐
│ 7. Router hooks: loading()      │
│    - Shared data loading        │
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 8. Component: loading()         │
│    - Fetch component data       │
│    - Initialize state           │
│    - Show loading UI            │
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 9. Component attached           │ ← Standard Aurelia lifecycle
│    - DOM insertion              │
└─────────────────┬───────────────┘
                  ↓
         Swap viewport content
         (ComponentA → ComponentB)
                  ↓
┌─────────────────────────────────┐
│ 10. Component: loaded()         │
│     - Post-render effects       │
│     - Scroll to top             │
│     - Track page view           │
└─────────────────┬───────────────┘
                  ↓
┌─────────────────────────────────┐
│ 11. Update browser history      │
│     Update document title       │
└─────────────────────────────────┘
                  ↓
           Navigation Complete


TIMING DIAGRAM (with async operations)
═══════════════════════════════════════════

Time  ComponentA              ComponentB
────  ──────────              ──────────
  0ms canUnload() ────────┐
                          │
100ms                     └─> [approved]
                              canLoad() ──────┐
                                              │
200ms                                         └─> [approved]
      unloading() ───────┐
                         │
250ms                    └─> [cleanup done]
                             loading() ──────┐
                                             │ ← async data fetch
400ms                                        └─> [data loaded]
      [detached]
                             [attached]
                             loaded() ───────┐
                                             │
410ms                                        └─> [done]
      ████████ (visible)    ░░░░░░░░ (hidden)
      ░░░░░░░░ (hidden)     ████████ (visible)
```

**Important Notes**:

1. All hooks can be async (return Promise)
2. Router waits for each hook to complete before proceeding
3. Returning `false` from guard hooks stops navigation
4. Router hooks run before component hooks
5. `unloading` and `loading` happen in parallel for performance

[Lifecycle hooks documentation →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)

***

## 4. Component vs Router Hooks

Two ways to implement lifecycle logic:

```
┌─────────────────────────────────────────────────────────────┐
│               COMPONENT HOOKS (Local)                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  export class ProductDetail implements IRouteViewModel {    │
│    canLoad(params: Params): boolean {                       │
│      // 'this' refers to component instance                 │
│      return this.validateProduct(params.id);                │
│    }                                                         │
│  }                                                           │
│                                                              │
│  ✓ Use for component-specific logic                         │
│  ✓ Direct access to component state via 'this'              │
│  ✓ Runs only for this component                             │
│  ✗ Cannot share logic across components                     │
└─────────────────────────────────────────────────────────────┘

                              ↓↑

┌─────────────────────────────────────────────────────────────┐
│              ROUTER HOOKS (Shared/Global)                   │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  @lifecycleHooks()                                           │
│  export class AuthHook {                                     │
│    canLoad(                                                  │
│      viewModel: IRouteViewModel,  ← component instance      │
│      params: Params,                                         │
│      next: RouteNode                                         │
│    ): boolean {                                              │
│      // 'this' is the hook instance, not the component      │
│      return this.authService.isAuthenticated();             │
│    }                                                         │
│  }                                                           │
│                                                              │
│  // Register globally                                        │
│  Aurelia.register(AuthHook);                                 │
│                                                              │
│  ✓ Share logic across all components                        │
│  ✓ Centralized cross-cutting concerns                       │
│  ✓ Access component via viewModel parameter                 │
│  ✗ Extra indirection to access component state              │
└─────────────────────────────────────────────────────────────┘


EXECUTION ORDER (both registered)
══════════════════════════════════════════════════════════════

Navigation triggered
        ↓
┌─────────────────────┐
│ 1. Router Hooks     │ ← Runs first (global checks)
│    canLoad()        │
└──────────┬──────────┘
           ↓
    [return false?] ─── Yes ──→ Navigation blocked
           │
          No
           ↓
┌─────────────────────┐
│ 2. Component Hook   │ ← Runs second (local checks)
│    canLoad()        │
└──────────┬──────────┘
           ↓
    [return false?] ─── Yes ──→ Navigation blocked
           │
          No
           ↓
   Navigation continues


COMMON PATTERNS
═══════════════════════════════════════════════════════════

Pattern 1: Authentication (Router Hook)
────────────────────────────────────────
@lifecycleHooks()
class AuthHook {
  canLoad(...) {
    if (!isLoggedIn) return 'login';
    return true;
  }
}
→ Applies to all routes
→ Centralized auth logic


Pattern 2: Data Loading (Component Hook)
─────────────────────────────────────────
class ProductDetail implements IRouteViewModel {
  async loading(params: Params) {
    this.product = await fetchProduct(params.id);
  }
}
→ Component-specific data
→ Direct state access


Pattern 3: Mixed Approach (Both)
─────────────────────────────────────────
@lifecycleHooks()
class PermissionHook {
  canLoad(vm, params, next) {
    const requiredPermission = next.data?.permission;
    return this.hasPermission(requiredPermission);
  }
}

class AdminPanel implements IRouteViewModel {
  canLoad(params) {
    // Additional component-specific checks
    return this.validateContext(params);
  }
}
→ Global permission check first
→ Then component-specific validation
```

**Decision Guide**:

* **Router hooks** for: Authentication, authorization, logging, analytics
* **Component hooks** for: Data fetching, validation, component state
* **Both** when you need layered checks (global + local)

[Router hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks) | [Component hooks →](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)

***

## 5. Viewport Hierarchy

How viewports nest and relate to each other:

```
SIMPLE (SINGLE VIEWPORT)
════════════════════════════════════

<my-app>
  <nav>...</nav>
  <au-viewport></au-viewport>  ← Single viewport
</my-app>

Route: /products
         ↓
┌────────────────┐
│ <my-app>       │
│   <nav>        │
│   ┌──────────┐ │
│   │ Products │ │ ← Loaded into viewport
│   └──────────┘ │
│ </my-app>      │
└────────────────┘


HIERARCHICAL (NESTED VIEWPORTS)
═══════════════════════════════════════════════

<my-app>
  <au-viewport></au-viewport>        ← Root viewport
    ↓
    <products-page>
      <au-viewport></au-viewport>    ← Child viewport
        ↓
        <product-detail>
        </product-detail>
    </products-page>
</my-app>

Route: /products/42/reviews
         ↓
┌─────────────────────────────────────┐
│ Root Component (my-app)             │
│ ┌─────────────────────────────────┐ │
│ │ Products (viewport: default)    │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Product 42 (viewport: deflt)│ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ Reviews (viewport: def) │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘

Route Tree:
Root
 └─ products
     └─ 42 (product-detail)
         └─ reviews


SIBLING VIEWPORTS (MULTIPLE VIEWPORTS)
═══════════════════════════════════════════════

<my-app>
  <div class="layout">
    <au-viewport name="left"></au-viewport>
    <au-viewport name="right"></au-viewport>
  </div>
</my-app>

Route: products@left+details/42@right
         ↓
┌───────────────────────────────────────┐
│ Root Component                        │
│ ┌───────────────┬─────────────────┐   │
│ │ Products      │ Product Details │   │
│ │ (left)        │ (right)         │   │
│ │               │ ID: 42          │   │
│ │ - Item 1      │                 │   │
│ │ - Item 2      │ Description...  │   │
│ │ - Item 3      │                 │   │
│ └───────────────┴─────────────────┘   │
└───────────────────────────────────────┘

Route Configuration:
routes: [
  { path: 'products', component: ProductList },
  { path: 'details/:id', component: ProductDetail }
]

Navigation:
<a href="products@left+details/42@right">Load both</a>
router.load([
  { component: ProductList, viewport: 'left' },
  { component: ProductDetail, params: { id: 42 }, viewport: 'right' }
]);


COMPLEX (NESTED + SIBLING)
═══════════════════════════════════════════════

<my-app>
  <au-viewport></au-viewport>           ← Root
    ↓
    <dashboard>
      <au-viewport name="main"></au-viewport>
      <au-viewport name="sidebar"></au-viewport>
        ↓                    ↓
        <content>       <sidebar-content>
          <au-viewport></au-viewport>  ← Nested in main
        </content>
    </dashboard>
</my-app>

Route: /dashboard/content@main+sidebar@sidebar/nested
         ↓
┌──────────────────────────────────────────┐
│ Root (my-app)                            │
│ ┌──────────────────────────────────────┐ │
│ │ Dashboard                            │ │
│ │ ┌─────────────────┬────────────────┐ │ │
│ │ │ Main            │ Sidebar        │ │ │
│ │ │ ┌─────────────┐ │                │ │ │
│ │ │ │ Nested Comp │ │ Sidebar Content│ │ │
│ │ │ └─────────────┘ │                │ │ │
│ │ └─────────────────┴────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────┘
```

**Key Concepts**:

* **Default viewport**: `<au-viewport></au-viewport>` (no name)
* **Named viewport**: `<au-viewport name="aside"></au-viewport>`
* **Target viewport**: Use `@viewportName` in navigation
* **Hierarchical**: Nested components each have their own viewport
* **Sibling**: Multiple viewports at the same level

[Viewports documentation →](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)

***

## 6. History Strategy

How router interacts with browser history:

```
STRATEGY: 'push' (default)
══════════════════════════════════════════

User Journey:
  /home  →  /about  →  /contact

Browser History Stack:
┌─────────────┐
│  /contact   │ ← Current (length: 3)
├─────────────┤
│  /about     │   [Back button goes here]
├─────────────┤
│  /home      │
└─────────────┘

Code:
router.load('contact', { historyStrategy: 'push' });

✓ Each navigation adds new entry
✓ Back button works as expected
✓ Forward button available after going back
✗ History grows unbounded


STRATEGY: 'replace'
══════════════════════════════════════════

User Journey:
  /home  →  /about  →  /contact (replace)

Browser History Stack:
┌─────────────┐
│  /contact   │ ← Current (length: 2)
├─────────────┤
│  /home      │   [Back button goes here]
└─────────────┘
     ↑
 /about was replaced by /contact

Code:
router.load('contact', { historyStrategy: 'replace' });

✓ No history pollution
✓ Good for redirects/corrections
✓ Prevents "back" to intermediate states
✗ Can't navigate back to replaced pages


STRATEGY: 'none'
══════════════════════════════════════════

User Journey:
  /home  →  /about  →  /contact (none)

Browser History Stack:
┌─────────────┐
│  /home      │ ← Current (length: 1)
└─────────────┘

URL bar shows: /contact
But history still has: /home

Code:
router.load('contact', { historyStrategy: 'none' });

✓ No history interaction at all
✓ Good for modal-style navigation
✗ Back button goes to previous app page, not /about
✗ URL and history out of sync


COMPARISON
══════════════════════════════════════════════════════════

Use Case                           | Strategy
─────────────────────────────────────────────────────────
Normal navigation                  | 'push'
Login redirect                     | 'replace'
Fixing invalid route               | 'replace'
Multi-step form (same logical page)| 'replace'
Modal / overlay content            | 'none'
Wizard steps (want back to work)   | 'push'
Correcting user typos in URL       | 'replace'


REAL-WORLD EXAMPLE: Login Flow
═══════════════════════════════════

// User tries to access protected route
canLoad() {
  if (!isLoggedIn) {
    // Redirect to login WITH replace
    // So after login, "back" doesn't go to login page
    router.load('login', { historyStrategy: 'replace' });
    return false;
  }
}

// After successful login
login() {
  authenticate();
  // Navigate to dashboard WITH replace
  // So "back" from dashboard doesn't go to login
  router.load('dashboard', { historyStrategy: 'replace' });
}

History progression:
1. User at /home
2. Tries /admin → redirected to /login (replace)
   History: [/home, /login]
3. After login → /admin (replace)
   History: [/home, /admin]
4. Back button → goes to /home (skips /login)


REAL-WORLD EXAMPLE: Wizard
═══════════════════════════════════

// Multi-step form
wizard.nextStep() {
  currentStep++;
  // Use push so back button works
  router.load(`wizard/step${currentStep}`, {
    historyStrategy: 'push'
  });
}

History: /wizard/step1 → /wizard/step2 → /wizard/step3
Back button goes through steps correctly


REAL-WORLD EXAMPLE: Search Filters
══════════════════════════════════════

// User adjusts filters
applyFilters() {
  // Use replace to update URL without history spam
  router.load('search', {
    queryParams: { ...filters },
    historyStrategy: 'replace'
  });
}

Without replace:
/search → /search?cat=A → /search?cat=A&sort=price
         → /search?cat=A&sort=price&page=2
         → /search?cat=A&sort=price&page=3
[User hits back 4 times to go back!]

With replace:
/search → /search?cat=A&sort=price&page=3
[User hits back once to go back!]
```

**Decision Guide**:

* **push**: Normal navigation, want history
* **replace**: Redirects, corrections, interim states
* **none**: Modals, overlays, no history needed

[History strategy documentation →](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#configure-browser-history-strategy)

***

## 7. Transition Plans

What happens when navigating to the same component with different parameters:

```
Scenario: Navigate from /users/1 to /users/2
(Same component, different parameter)


TRANSITION PLAN: 'replace' (default)
════════════════════════════════════════

/users/1 (ComponentA, id=1)
    ↓
router.load('/users/2')
    ↓
┌──────────────────────────────┐
│ 1. Unload current instance   │
│    - unloading() called      │
│    - detaching() called      │
│    - Component destroyed     │
└────────────┬─────────────────┘
             ↓
┌──────────────────────────────┐
│ 2. Create new instance       │
│    - New component instance  │
│    - canLoad() called        │
│    - loading() called        │
│    - attached() called       │
│    - loaded() called         │
└────────────┬─────────────────┘
             ↓
/users/2 (ComponentA, id=2) ← Different instance

Timeline:
ComponentA(id=1)  ComponentA(id=2)
  unloading()
  detaching()
  [destroyed]
                  canLoad()
                  loading()
                  attached()
                  loaded()

✓ Clean slate, no stale state
✓ Simple mental model
✗ Slower (full recreation)
✗ Loses component state
✗ Re-runs constructor, bound, etc.


TRANSITION PLAN: 'invoke-lifecycles'
════════════════════════════════════════

/users/1 (ComponentA, id=1)
    ↓
router.load('/users/2')
    ↓
┌──────────────────────────────┐
│ 1. Keep existing instance    │
│    - Same component object   │
│    - No destruction          │
└────────────┬─────────────────┘
             ↓
┌──────────────────────────────┐
│ 2. Re-invoke hooks           │
│    - canLoad() called        │
│    - loading() called        │
│    - loaded() called         │
│    (NO attach/detach)        │
└────────────┬─────────────────┘
             ↓
/users/2 (ComponentA, id=2) ← Same instance!

Timeline:
ComponentA(id=1)
  canLoad(id=2)
  loading(id=2)
  loaded(id=2)
ComponentA(id=2)

✓ Faster (reuses instance)
✓ Can preserve component state
✓ Smoother transitions/animations
✗ Must handle state updates correctly
✗ Potential for stale data bugs


COMPARISON
══════════════════════════════════════════════════════════

Aspect                  | replace          | invoke-lifecycles
────────────────────────────────────────────────────────────────
Instance                | New              | Reused
Speed                   | Slower           | Faster
State                   | Fresh            | Preserved*
Lifecycle hooks         | All              | Subset
DOM                     | Removed/readded  | Stays
Use for                 | Default behavior | Param-only changes

* Preserved state can be a pro or con depending on use case


CONFIGURATION
═══════════════════════════════════════════════════════════

Global configuration:
@route({
  transitionPlan: 'invoke-lifecycles',  ← All routes
  routes: [...]
})

Per-route configuration:
{
  path: 'users/:id',
  component: UserDetail,
  transitionPlan: 'invoke-lifecycles'   ← Just this route
}

Per-navigation override:
router.load('users/2', {
  transitionPlan: 'invoke-lifecycles'   ← Just this navigation
});


REAL-WORLD EXAMPLE: User Profile Tabs
═══════════════════════════════════════════════════════════

Component:
class UserProfile implements IRouteViewModel {
  userId: string;
  userData: User;
  selectedTab = 'overview';  ← Component state

  loading(params: Params) {
    if (this.userId !== params.id) {
      // Different user - fetch new data
      this.userId = params.id;
      this.userData = await fetchUser(params.id);
    }
    // Update tab from URL
    this.selectedTab = params.tab || 'overview';
  }
}

Routes:
{
  path: 'users/:id/:tab?',
  component: UserProfile,
  transitionPlan: 'invoke-lifecycles'  ← Preserve state
}

Navigation:
/users/123/overview → /users/123/posts
└─ Same user, keep loaded data, just update tab

/users/123/posts → /users/456/posts
└─ Different user, fetch new data in loading()


WHEN TO USE EACH
═══════════════════════════════════════════════════════════

Use 'replace' when:
✓ You want clean state each time
✓ Component has complex initialization
✓ Different params mean completely different data
✓ You don't trust yourself to handle reuse correctly

Use 'invoke-lifecycles' when:
✓ Only parameters change (same logical entity)
✓ You want to preserve UI state (scroll, selections)
✓ Performance matters (frequent navigation)
✓ You have good loading() logic that handles updates


COMMON PITFALL
═══════════════════════════════════════════════════════════

// ✗ BAD: Doesn't update when params change
class ProductDetail implements IRouteViewModel {
  product: Product;

  constructor() {
    this.product = fetchProduct(params.id);  ← params not available!
  }
}

// ✓ GOOD: Updates on every navigation
class ProductDetail implements IRouteViewModel {
  product: Product;

  loading(params: Params) {
    this.product = await fetchProduct(params.id);  ← Correct!
  }
}
```

**Rule of Thumb**:

* Default (`replace`): Safe, always works
* `invoke-lifecycles`: Optimize when parameters drive content, not fundamentally different pages

[Transition plans documentation →](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans)

***

## 8. Route Parameter Flow

How parameters flow from URL to component:

```
URL: /products/42/reviews?sort=date&page=2#reviews-section
     \_______/\__/\______/\__________________/\_____________/
         │     │     │            │                 │
      path   param  path      query            fragment


PARSING
═══════════════════════════════════════════════════════════

Router processes URL:
┌────────────────────────────────┐
│ Path segments: [products, 42,  │
│                 reviews]        │
│ Path params:   {id: '42'}      │
│ Query params:  {sort: 'date',  │
│                 page: '2'}     │
│ Fragment:      'reviews-section'│
└────────────────────────────────┘


ROUTE MATCHING
═══════════════════════════════════════════════════════════

Configuration:
{
  path: 'products/:id',
  component: ProductDetail,
  routes: [
    { path: 'reviews', component: Reviews }
  ]
}

Match result:
┌─────────────────────────────────────────┐
│ Route Tree:                             │
│   products (:id = '42')                 │
│     └─ reviews                          │
│                                         │
│ Params object:                          │
│   { id: '42' }                          │
│                                         │
│ Query object:                           │
│   { sort: 'date', page: '2' }          │
└─────────────────────────────────────────┘


ACCESS IN COMPONENT
═══════════════════════════════════════════════════════════

Method 1: Lifecycle hooks
──────────────────────────────────────────
class ProductDetail implements IRouteViewModel {
  productId: string;

  canLoad(params: Params, next: RouteNode) {
    // Path parameters
    this.productId = params.id;  // '42'

    // Query parameters
    const sort = next.queryParams.get('sort');  // 'date'
    const page = next.queryParams.get('page');  // '2'

    // Fragment
    const fragment = next.fragment;  // 'reviews-section'

    return true;
  }
}


Method 2: ICurrentRoute
──────────────────────────────────────────
import { ICurrentRoute } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

class ProductDetail {
  private readonly currentRoute = resolve(ICurrentRoute);

  attached() {
    // Current path
    console.log(this.currentRoute.path);  // 'products/42/reviews'

    // Parameters (includes all from parent routes)
    const params = this.currentRoute.parameterInformation[0].params;
    console.log(params.get('id'));  // '42'

    // Query string (need to parse)
    const url = this.currentRoute.url;
    const queryString = url.split('?')[1];  // 'sort=date&page=2'
  }
}


Method 3: getRouteParameters (aggregates hierarchy)
────────────────────────────────────────────────────
import { IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

class NestedComponent {
  private readonly context = resolve(IRouteContext);

  attached() {
    // Get all params from entire route hierarchy
    const allParams = this.context.getRouteParameters<{
      companyId: string;    // From /companies/:companyId
      projectId: string;    // From /projects/:projectId
      userId: string;       // From /users/:userId
    }>({
      includeQueryParams: true  // Also include ?foo=bar
    });

    console.log(allParams.companyId);  // Nearest definition wins
  }
}


PARAMETER TYPES
═══════════════════════════════════════════════════════════

All parameters are strings!
─────────────────────────────────────────
URL: /products/42?count=10&active=true

params.id      // '42' (string, not number!)
params.count   // '10' (string, not number!)
params.active  // 'true' (string, not boolean!)

Always convert:
const id = Number(params.id);
const count = parseInt(params.count, 10);
const active = params.active === 'true';


PARAMETER BINDING WITH load
═══════════════════════════════════════════════════════════

Template:
<a load="route: products; params.bind: {id: productId}">
  View Product
</a>

Component:
productId = 42;

Generated URL:
/products/42

With multiple params:
<a load="route: items;
         params.bind: {
           id: itemId,
           category: itemCategory,
           extra: 'value'
         }">
  View Item
</a>

Route: /items/:id/:category?
Generated: /items/42/electronics?extra=value
                 │        │           └─ query (not in path)
                 │        └─ matches :category
                 └─ matches :id


PROGRAMMATIC WITH OPTIONS
═══════════════════════════════════════════════════════════

router.load('products/42', {
  queryParams: {
    sort: 'price',
    page: 1
  },
  fragment: 'reviews'
});

Generated URL:
/products/42?sort=price&page=1#reviews


Or with structured instruction:
router.load({
  component: 'products',
  params: { id: 42 },
  children: [
    { component: 'reviews' }
  ]
}, {
  queryParams: { sort: 'date' }
});

Generated URL:
/products/42/reviews?sort=date


PARAMETER CONSTRAINTS
═══════════════════════════════════════════════════════════

Validate during routing:
{
  path: 'products/:id{{^\\d+$}}',  // Only digits
  component: ProductDetail
}

URL: /products/42      ✓ Matches
URL: /products/abc     ✗ Doesn't match, goes to fallback

Custom validation in component:
canLoad(params: Params) {
  const id = Number(params.id);

  if (!Number.isInteger(id) || id <= 0) {
    return 'not-found';  // Redirect to 404
  }

  return true;
}
```

**Key Points**:

* All parameters are strings
* Path params come from URL segments
* Query params come from `?key=value`
* Access via lifecycle hooks or ICurrentRoute
* Always validate and convert types

[Path parameters →](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#path-and-parameters) | [Query parameters →](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-navigation-options)

***

## Summary

These diagrams cover the core architectural concepts of Aurelia 2's router:

1. **Route Matching** - How URLs become components
2. **Navigation** - Three ways to navigate and their differences
3. **Lifecycle** - Complete hook execution sequence
4. **Hooks** - Component vs Router hooks
5. **Viewports** - Nested and sibling viewport patterns
6. **History** - Push vs Replace vs None strategies
7. **Transitions** - Replace vs invoke-lifecycles behavior
8. **Parameters** - How data flows from URL to component

For more details, see the complete [Router Documentation](/getting-to-know-aurelia/aurelia-router/router).


# Fundamentals


# Getting started

{% hint style="info" %}
**Bundler note:** These examples import '.html' files as raw strings (showing '?raw' for Vite/esbuild). Configure your bundler as described in [Importing external HTML templates with bundlers](/components/components#importing-external-html-templates-with-bundlers) so the imports resolve to strings on Webpack, Parcel, etc.
{% endhint %}

Routing with Aurelia feels like a natural part of the framework. It can easily be implemented into your applications in a way that feels familiar if you have worked with other frameworks and library routers. Here is a basic example of routing in an Aurelia application using `router`.

The following getting started guide assumes you have an Aurelia application already created. If not, [consult our Quick Start](/getting-started/quick-install-guide) to get Aurelia installed in minutes.

## Installation

```bash
npm i @aurelia/router
```

## Configure the `router`

To use the `router`, we have to register it with Aurelia. We do this at the bootstrapping phase.

{% code title="main.ts" %}

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { MyApp } from './my-app';

Aurelia
  .register(RouterConfiguration.customize({
      useUrlFragmentHash: true, // <-- enables the routing using the URL `hash`
    }))
  .app(MyApp)
  .start();
```

{% endcode %}

{% hint style="info" %}
To know more about the different configuration options for router, please refer the [documentation](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration) on that topic.
{% endhint %}

## Create the routable components

For this example, we have a root component which is `MyApp`. And then we are going to define two routes for the root component, namely `home` and `about`.

Let us define these components first.

{% tabs %}
{% tab title="home.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import template from './home.html?raw';

@customElement({ name: 'ho-me', template })
export class Home {
  private readonly message: string = 'Welcome to Aurelia2 router!';
}
```

{% endtab %}

{% tab title="home.html" %}

```html
<h1>${message}</h1>
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="about.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import template from './about.html?raw';

@customElement({ name: 'ab-out', template })
export class About {
  private readonly message = 'Aurelia2 router is simple';
}
```

{% endtab %}

{% tab title="about.html" %}

```html
<h1>${message}</h1>
```

{% endtab %}
{% endtabs %}

## Configure the routes

With the routable components in place, let's define the routes. To this end, we need to add the route configurations to our root component `MyApp`.

{% tabs %}
{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import template from './my-app.html?raw';
import { Home } from './home';
import { About } from './about';

@route({
  routes: [
    {
      path: ['', 'home'],
      component: Home,
      title: 'Home',
    },
    {
      path: 'about',
      component: About,
      title: 'About',
    },
  ],
})
@customElement({ name: 'my-app', template })
export class MyApp {}

```

{% endtab %}

{% tab title="my-app.html" %}

```html
<nav>
  <a href="home">Home</a>
  <a href="about">About</a>
</nav>

<au-viewport></au-viewport>
```

{% endtab %}
{% endtabs %}

There are couple of stuffs to note here. We start by looking at the configurations defined using the `@route` decorator where we list out the routes under the `routes` property in the configuration object in the `@route` decorator. The most important things in every route configurations are the `path` and the `component` properties. This instructs the router to use the defined `component` in the viewport when it sees the associated `path`.

{% hint style="info" %}
To know more about configuring routes, please refer to the respective [documentation](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes).
{% endhint %}

The viewport is specified in the view (see `my-app.html`) by using the `<au-viewport>` custom element. For example, the router will use this element to display the `Home` component when it sees the `/` (the empty path) or the `/home` paths.

The `nav>a` elements are added to navigate from one view to another.

See this in action:

{% embed url="<https://stackblitz.com/edit/router-lite-getting-started?ctl=1&embed=1&file=src/main.ts>" %}

## Using pushstate

If you have opened the demo then you can notice that the URL in the address bar or the URLs in the `nav>a` elements contains a `#` (example: `/#home`, `/#about` etc.). Depending on your project need and esthetics you may want to get rid of the `#`-character. To this end, you need set the [`useUrlFragmentHash` to `false`](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#choose-between-hash-and-pushstate-routing-using-useurlfragmenthash), which is also the default.

## Live examples

For the documentation of router, many live examples are prepared. It is recommended to use the live examples as you read along the documentation. However, if you are feeling adventurous enough to explore the features by yourself, here is the complete [collection of the live examples](https://stackblitz.com/@Sayan751/collections/router-lite) at your disposal.

{% hint style="info" %}
The examples are created using StackBlitz. Sometimes, you need to open the examples in a new tab to see changes in the URL, title etc. To this end, copy the URL appearing on the address bar on the right pane and open that in a new tab.
{% endhint %}


# Router configuration

Learn about configuring the Router.

{% hint style="info" %}
**Bundler note:** These examples import '.html' files as raw strings (showing '?raw' for Vite/esbuild). Configure your bundler as described in [Importing external HTML templates with bundlers](/components/components#importing-external-html-templates-with-bundlers) so the imports resolve to strings on Webpack, Parcel, etc.
{% endhint %}

The router allows you to configure how it interprets and handles routing in your Aurelia applications. The `customize` method on the `RouterConfiguration` object can be used to configure router settings.

## Complete Configuration Reference

The router accepts the following configuration options through `RouterConfiguration.customize()` (all map directly to `RouterOptions` except for `basePath`):

| Option                            | Type                                                                 | Default              | Description                                                                                                               |
| --------------------------------- | -------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `useUrlFragmentHash`              | boolean                                                              | `false`              | When `true`, uses hash (`#/path`) URLs instead of pushState. Leave `false` for clean URLs.                                |
| `useHref`                         | boolean                                                              | `true`               | Enables the router to intercept standard `href` links. Set to `false` if you only want to route via the `load` attribute. |
| `historyStrategy`                 | `'push' \| 'replace' \| 'none' \| (instructions) => HistoryStrategy` | `'push'`             | Controls how each navigation interacts with `history`. Provide a function to choose per navigation.                       |
| `basePath`                        | `string \| null`                                                     | `null`               | Overrides the base segment used to resolve relative routes. Defaults to `document.baseURI`.                               |
| `activeClass`                     | `string \| null`                                                     | `null`               | CSS class applied by the `load` attribute when a link is active.                                                          |
| `useNavigationModel`              | boolean                                                              | `true`               | Generates the navigation model so you can build menus from `IRouteContext.routeConfigContext.navigationModel`.            |
| `buildTitle`                      | `(transition: Transition) => string \| null`                         | `null`               | Customises final document title generation. Return `null` to skip document title updates.                                 |
| `restorePreviousRouteTreeOnError` | boolean                                                              | `true`               | Restores the previous route tree if a navigation throws, preventing partial states.                                       |
| `treatQueryAsParameters`          | boolean                                                              | `false` (deprecated) | Treats query parameters as route parameters. Avoid new usage; scheduled for removal in the next major release.            |
| `useEagerLoading`                 | boolean                                                              | `false`              | When `true`, eagerly loads all route configurations upfront when the application starts.                                  |

> Pass a partial options object—the router merges your values with the defaults so you only specify what changes. Configure options before the router starts (for example, via `AppTask`) so navigations consistently use the same settings.

## Choose between hash and pushState routing using `useUrlFragmentHash`

If you do not provide any configuration value, the default is pushState routing. If you prefer hash-based routing to be used, you can enable this like so:

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { MyApp } from './my-app';

Aurelia
  .register(RouterConfiguration.customize({ useUrlFragmentHash: true }))
  .app(MyApp)
  .start();
```

By calling the `customize` method, you can supply a configuration object containing the property `useUrlFragmentHash` and supplying a boolean value. If you supply `true` this will enable hash mode. The default is `false`.

If you are working with pushState routing, you will need a `<base>` element with `href` attribute (for more information, refer [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)) in the head of your document. The scaffolded application from the CLI includes this in the `index.html` file, but if you're starting from scratch or building within an existing application you need to be aware of this.

```html
<head>
  <base href="/">
</head>
```

{% hint style="info" %}
PushState requires server-side support. This configuration is different depending on your server setup. For example, if you are using Webpack DevServer, you'll want to set the `devServer.historyApiFallback` option to `true`. If you are using ASP.NET Core, you'll want to call `routes.MapSpaFallbackRoute` in your startup code. See your preferred server technology's documentation for more information on how to allow 404s to be handled on the client with push state.
{% endhint %}

## Configuring `basePath`

Configuring a base path is useful in many real-life scenarios. One such example is when you are hosting multiple smaller application under a single hosting service. In this case, you probably want the URLs to look like `https://example.com/app1/view42` or `https://example.com/app2/view21`. In such cases, it is useful to specify a different [`base#href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) value for every app.

```html
<!-- app1/index.html -->
<head>
  <base href="/app1">
</head>

<!-- app2/index.html -->
<head>
  <base href="/app2">
</head>
```

Run the following example to understand how the value defined in `base#href` is affecting the URLs.

{% embed url="<https://stackblitz.com/edit/router-lite-base-href?embed=1&file=src/my-app.html>" %}

When you open the example in a new browser tab, you can note that the URL in the address bar looks the `HOSTING_PREFIX/app/home` or `HOSTING_PREFIX/app/about`. This is also true for the `href` values in the `a` tags. This happens because `<base href="/app">` is used in the `index.ejs` (producing the index.html). In this case, the `router` is picking up the `baseURI` information and performing the routing accordingly.

This needs bit more work when you are supporting multi-tenancy for your app. In this case, you might want the URLs look like `https://example.com/tenant-foo/app1/view42` or `https://example.com/tenant-bar/app2/view21`. You cannot set the `document.baseURI` every time you start the app for a different tenant, as that value is static and readonly, read from the `base#href` value.

With `router` you can support this by setting the `basePath` value differently for each tenant, while customizing the router configuration, at bootstrapping phase. Following is an example that implements the aforementioned URL convention. To better understand, open the the example in a new tab and check the URL in address bar when you switch tenants as well as the links in the `a` tags.

{% embed url="<https://stackblitz.com/edit/router-lite-base-path?ctl=1&embed=1&file=src/main.ts>" %}

The actual configuration takes place in the `main.ts` while customizing the router configuration in the following lines of code.

```typescript
  // this can either be '/', '/app[/+]', or '/TENANT_NAME/app[/+]'
  let basePath = location.pathname;
  const tenant =
    (!basePath.startsWith('/app') && basePath != '/'
      ? basePath.split('/')[1]
      : null) ?? 'none';
  if (tenant === 'none') {
    basePath = '/app';
  }
  const host = document.querySelector<HTMLElement>('app');
  const au = new Aurelia();
  au.register(
    StandardConfiguration,
    RouterConfiguration.customize({
      basePath,
    }),
    Registration.instance(ITenant, tenant) // <-- this is just to inject the tenant name in the `my-app.ts`
  );
```

There are also the following links, included in the `my-app.html`, to simulate tenant switch/selection.

{% tabs %}
{% tab title="my-app.html" %}

```html
tenant: ${tenant}
<nav>
  <a href="${baseUrl}/foo/app" external>Switch to tenant foo</a>
  <a href="${baseUrl}/bar/app" external>Switch to tenant bar</a>
</nav>
<nav>
  <a load="home">Home</a>
  <a load="about">About</a>
</nav>

<au-viewport></au-viewport>

```

{% endtab %}

{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import template from './my-app.html?raw';
import { Home } from './home';
import { About } from './about';
import { DI } from '@aurelia/kernel';
import { resolve } from '@aurelia/kernel';

export const ITenant = DI.createInterface<string>('tenant');

@route({
  routes: [
    {
      path: ['', 'home'],
      component: Home,
      title: 'Home',
    },
    {
      path: 'about',
      component: About,
      title: 'About',
    },
  ],
})
@customElement({ name: 'my-app', template })
export class MyApp {
  private baseUrl = location.origin;
  private readonly tenant: string = resolve(ITenant);
}
```

{% endtab %}
{% endtabs %}

Note the `a` tags with [`external` attribute](/getting-to-know-aurelia/aurelia-router/navigation/navigating#bypassing-the-href-custom-attribute). Note that when you switch to a tenant, the links in the `a` tags also now includes the tenant name; for example when we switch to tenant 'foo' the 'Home' link is changed to `/foo/app/home` from `/app/home`.

## Provide a custom location manager

If your host does not behave like a normal browser history stack (for example, a native WebView, an Electron shell, or a sandbox that proxies URLs), override the router’s location manager. The router always resolves `ILocationManager` from DI and ships with a browser-based implementation. Register your own class that implements the same public surface (`startListening`, `stopListening`, `handleEvent`, `pushState`, `replaceState`, `getPath`, `addBaseHref`, `removeBaseHref`) before the router starts:

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration, ILocationManager } from '@aurelia/router';
import { Registration } from '@aurelia/kernel';

class WebViewLocationManager implements ILocationManager {
  constructor(private readonly host = window) {}

  startListening() {/* connect to native host */}
  stopListening() {/* disconnect */}
  handleEvent() {/* publish au:router:location-change */}
  pushState(state: unknown, title: string, url: string) {
    this.host.history.pushState(state, title, `/app/${url}`);
  }
  replaceState(state: unknown, title: string, url: string) {
    this.host.history.replaceState(state, title, `/app/${url}`);
  }
  getPath() {
    return this.host.location.pathname.replace('/app/', '');
  }
  addBaseHref(path: string) { return `/app/${path}`; }
  removeBaseHref(path: string) { return path.replace('/app/', ''); }
}

Aurelia.register(
  RouterConfiguration.customize(),
  Registration.singleton(ILocationManager, WebViewLocationManager),
);
```

Because `RouterConfiguration` registers `BrowserLocationManager` as a singleton, registering your custom implementation afterward replaces it everywhere. Match the method contracts from the linked file so the router keeps receiving normalized URLs and can keep raising `au:router:location-change`.

## Swap the URL parser

`RouterOptions` stores an `_urlParser` instance that is derived from `useUrlFragmentHash`. Advanced apps can replace that parser before any navigation happens. The `_urlParser` field is marked `readonly`, so use `Writable<T>` from `@aurelia/kernel` when mutating:

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration, RouterOptions, IRouterOptions, type IUrlParser } from '@aurelia/router';
import { AppTask } from '@aurelia/runtime-html';
import type { Writable } from '@aurelia/kernel';

const createSignedParser = (baseParser: IUrlParser): IUrlParser => ({
  parse(value) {
    const raw = value.replace(/;sig=.*$/, '');
    return baseParser.parse(raw);
  },
  stringify(path, query, fragment, isRooted) {
    const base = baseParser.stringify(path, query, fragment, isRooted);
    return `${base};sig=${sessionStorage.getItem('signature') ?? ''}`;
  },
});

Aurelia.register(
  RouterConfiguration.customize(),
  AppTask.creating(IRouterOptions, (options: RouterOptions) => {
    (options as Writable<RouterOptions>)._urlParser = createSignedParser(options._urlParser);
  }),
);
```

Every call to `ViewportInstruction.toUrl`, `router.load`, or `router.generatePath` now runs through your parser while still using the same API surface as the built-in implementation.

## Customizing title

A `buildTitle` function can be used to customize the [default behavior of building the title](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#setting-the-title). When configured, `buildTitle` owns document title generation for every navigation. Route configuration titles, assigned route-node titles, and navigation options are still available on the `Transition`, but the builder decides how to use them. For this example, we assume that we have the configured the routes as follows:

```typescript
import { route, IRouteViewModel } from '@aurelia/router';
@route({
    title: 'Aurelia', // <-- this is the base title
    routes: [
      {
        path: ['', 'home'],
        component: import('./components/home-page'),
        title: 'Home',
      }
    ]
})
export class MyApp implements IRouteViewModel {}
```

With this route configuration in place, when we navigate to `/home`, the default-built title will be `Home | Aurelia`. We can use the following `buildTitle` function to use `-` as the separator when users navigate to `/` or `/home` route.

```typescript
// main.ts
import { RouterConfiguration, Transition } from '@aurelia/router';
import { Aurelia } from '@aurelia/runtime-html';
const au = new Aurelia();
au.register(
  RouterConfiguration.customize({
    buildTitle(tr: Transition) {
      return tr.routeTree.root.getTitle(' - ') ?? 'Aurelia';
    },
  }),
);
```

Check out the following live example. You might need to open the demo in a new tab to observe the title changes.

{% embed url="<https://stackblitz.com/edit/router-lite-buildtitle?ctl=1&embed=1&file=src/main.ts>" %}

**Translating the title**

When localizing your app, you would also like to translate the title. Note that the router does not facilitate the translation by itself. However, there are enough hooks that can be leveraged to translate the title. To this end, we would use the [`data` property](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#advanced-route-configuration-options) in the route configuration to store the i18n key.

```typescript
import type { IRouteViewModel, Routeable } from '@aurelia/router';
export class MyApp implements IRouteViewModel {
  static title: string = 'Aurelia';
  static routes: Routeable[] = [
    {
      path: ['', 'home'],
      component: import('./components/home-page'),
      title: 'Home',
      data: {
        i18n: 'routes.home'
      }
    }
  ];
}
```

As `data` is an object of type `Record<string, unknown>`, you are free to chose the property names inside the `data` object. Here we are using the `i18n` property to store the i18n key for individual routes.

In the next step we make use of the `buildTitle` customization as well as a `AppTask` hook to subscribe to the locale change event.

```typescript
import { I18N, Signals } from '@aurelia/i18n';
import { IEventAggregator } from '@aurelia/kernel';
import { IRouter, RouterConfiguration, Transition } from '@aurelia/router';
import { AppTask, Aurelia } from '@aurelia/runtime-html';
(async function () {
  const host = document.querySelector<HTMLElement>('app');
  const au = new Aurelia();
  const container = au.container;
  let i18n: I18N | null = null;
  let router: IRouter | null = null;
  au.register(
    // other registrations such as the StandardRegistration, I18NRegistrations come here
    RouterConfiguration.customize({
      buildTitle(tr: Transition) {
        // Use the I18N to translate the titles using the keys from data.i18n.
        i18n ??= container.get(I18N);
        const root = tr.routeTree.root;
        const baseTitle = root.context.routeConfigContext.config.title;
        const child = tr.routeTree.root.children[0];
        return `${baseTitle} - ${i18n.tr(child.data.i18n as string)}`;
      },
    }),
    AppTask.afterActivate(IEventAggregator, ea => {
      // Ensure that the title changes whenever the locale is changed.
      ea.subscribe(Signals.I18N_EA_CHANNEL, () => {
        (router ??= container.get(IRouter)).updateTitle();
      });
    }),
  );
  // start aurelia here
})().catch(console.error);
```

This customization in conjunction with the previously shown routing configuration will cause the title to be `Aurelia - Startseite` when user is navigated to `/` or `/home` route and the current locale is `de`. Here we are assuming that the i18n resource for the `de` locale contains the following.

```json
{
  "routes": {
    "home": "Startseite"
  }
}
```

The following example demonstrate the title translation.

{% embed url="<https://stackblitz.com/edit/router-lite-translate-title?ctl=1&embed=1&file=src/main.ts>" %}

## Enable or disable the usage of the `href` custom attribute using `useHref`

By default, the router will allow you to use both `href` as well as `load` for specifying routes. Where this can get you into trouble is external links, `mailto:` links and other types of links that do not route. A simple example looks like this:

```html
<a href="mailto:myemail@gmail.com">Email Me</a>
```

This seemingly innocent and common scenario by default will trigger the router and will cause an error.

You have two options when it comes to working with external links. You can specify the link as external using the [`external` attribute](/getting-to-know-aurelia/aurelia-router/navigation/navigating#bypassing-the-href-custom-attribute).

```html
<a href="mailto:myemail@gmail.com" external>Email Me</a>
```

Or, you can set `useHref` to `false` (default is `true`) and only ever use the `load` attribute for routes.

```typescript
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';

Aurelia
  .register(RouterConfiguration.customize({
    useHref: false,
  }))
  .app(MyApp)
  .start();
```

## Configure browser history strategy

Using the `historyStrategy` configuration option it can be instructed, how the router should interact with the browser history object. This configuration option can take the following values: `push`, `replace`, and `none`.

### `push`

This is the default strategy. In this mode, the router will interact with Browser history to `push` a new navigation state each time a new navigation is performed. This enables the end users to use the back and forward buttons of the browser to navigate back and forth in an application using the router.

Check out the following example to see this in action.

{% embed url="<https://stackblitz.com/edit/router-lite-historystrategy-push?ctl=1&embed=1&file=src/main.ts>" %}

The main configuration can be found in the `main.ts`.

```typescript
import { RouterConfiguration } from '@aurelia/router';
import { Aurelia, StandardConfiguration } from '@aurelia/runtime-html';
import { MyApp as component } from './my-app';

(async function () {
  const host = document.querySelector<HTMLElement>('app');
  const au = new Aurelia();
  au.register(
    StandardConfiguration,
    RouterConfiguration.customize({
      historyStrategy: 'push', // default value can can be omitted
    })
  );
  au.app({ host, component });
  await au.start();
})().catch(console.error);
```

To demonstrate the `push` behavior, there is a small piece of code in the `my-app.ts` that listens to router events to create informative text (the `history` property in the class) from the browser history object that is used in the view to display the information.

```typescript
import { resolve } from '@aurelia/kernel';
import { IHistory } from '@aurelia/runtime-html';
import { IRouterEvents } from '@aurelia/router';

export class MyApp {
  private history: string;
  public constructor() {
    let i = 0;
    const history = resolve(IHistory);
    resolve(IRouterEvents).subscribe('au:router:navigation-end', () => {
      this.history = `#${++i} - len: ${history.length} - state: ${JSON.stringify(history.state)}`;
    });
  }
}
```

As you click the `Home` and `About` links in the example, you can see that the new states are being pushed to the history, and thereby increasing the length of the history.

### `replace`

This can be used to replace the current state in the history. Check out the following example to see this in action. Note that the following example is identical with the previous example, with the difference of using the `replace`-value as the history strategy.

{% embed url="<https://stackblitz.com/edit/router-lite-historystrategy-replace?ctl=1&embed=1&file=src/main.ts>" %}

As you interact with this example, you can see that new states are replacing old states, and therefore, unlike the previous example, you don't observe any change in the length of the history.

### `none`

Use this if you don't want the router to interact with the history at all. Check out the following example to see this in action. Note that the following example is identical with the previous example, with the difference of using the `none`-value as the history strategy.

{% embed url="<https://stackblitz.com/edit/router-lite-historystrategy-none?ctl=1&embed=1&file=src/main.ts>" %}

As you interact with this example, you can see that there is absolutely no change in the history information, indicating non-interaction with the history object.

### Override configured history strategy

You can use the [navigation options](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-navigation-options) to override the configured history strategy for individual routing instructions.

### Return a dynamic history strategy

`RouterOptions.historyStrategy` is declared as `ValueOrFunc<HistoryStrategy>`, so you can supply a function whenever you call `RouterConfiguration.customize`. That callback receives the `ViewportInstructionTree` for the pending transition, allowing you to branch on route metadata:

```typescript
import {
  RouterConfiguration,
  type HistoryStrategy,
  type ViewportInstructionTree,
  type ViewportInstruction,
} from '@aurelia/router';

const touchesViewport = (instruction: ViewportInstruction, name: string): boolean => {
  if (instruction.viewport === name) {
    return true;
  }

  return instruction.children.some(child => touchesViewport(child, name));
};

RouterConfiguration.customize({
  historyStrategy(instructions: ViewportInstructionTree): HistoryStrategy {
    const updatesSettingsPanel = instructions.children.some(child => touchesViewport(child, 'settings'));
    return updatesSettingsPanel ? 'replace' : 'push';
  },
});
```

The router invokes your function right before it pushes or replaces browser history inside `router.load`, so every navigation—declarative or programmatic—follows the same rule.

## Configure active class

Using the `activeClass` option you can add a class name to the router configuration. This class name is used by the [`load` custom attribute](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-the-load-custom-attribute) when the associated instruction is active. The default value for this option is `null`, which also means that the `load` custom attribute won't add any class proactively. Note that the router does not define any CSS class out-of-the-box. If you want to use this feature, make sure that you defines the class as well in your stylesheet.

```typescript
// main.ts
RouterConfiguration.customize({
  activeClass: 'active-route'
})
```

```css
/* styles.css */
.active-route {
  font-weight: bold;
  color: #007acc;
  text-decoration: underline;
}
```

```html
<!-- These links will get the 'active-route' class when their routes are active -->
<a load="home">Home</a>
<a load="about">About</a>
```

## Disable navigation model generation

If you're not using the navigation model feature for building menus, you can disable it to improve performance:

```typescript
RouterConfiguration.customize({
  useNavigationModel: false
})
```

This prevents the router from generating navigation model data, which can be useful in applications with many routes where you don't need the navigation model functionality.

## Error recovery configuration

The `restorePreviousRouteTreeOnError` option controls what happens when navigation fails:

```typescript
// Default behavior - restore previous route on error (recommended)
RouterConfiguration.customize({
  restorePreviousRouteTreeOnError: true
})

// Strict mode - leave application in error state
RouterConfiguration.customize({
  restorePreviousRouteTreeOnError: false
})
```

With the default `true` setting, if navigation fails (due to guards returning false, component loading errors, etc.), the router will restore the previous working route. Setting this to `false` provides stricter error handling but requires your application to handle error states properly.

## Observing navigation state while configuring the router

Beyond setting up routes, hash/push mode, or titles, you can optionally observe the active route and track query parameters. One way is to inject `ICurrentRoute` in any of your components. Another is to watch router events:

```typescript
import { RouterConfiguration, IRouter, IRouterEvents, NavigationEndEvent, ICurrentRoute } from '@aurelia/router';
import { DI } from '@aurelia/kernel';

const container = DI.createContainer();
container.register(
  RouterConfiguration.customize({ useHref: false }) // for example
);

const routerEvents = container.get(IRouterEvents);
const currentRoute = container.get(ICurrentRoute);
const router = container.get(IRouter);

routerEvents.subscribe('au:router:navigation-end', (evt: NavigationEndEvent) => {
  console.log('Navigation ended on:', evt.finalInstructions.toUrl(true, router.options._urlParser, true));
  console.log('Active route object:', currentRoute.path);
});
```

This can help debug or log your router's runtime state. See the [ICurrentRoute docs](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes#retrieving-the-current-route-and-query-parameters) for an example usage.

## Treat query parameters as path parameters

When the `treatQueryAsParameters` property in the router configuration is set to `true`, the router will treat query parameters as path parameters. The default value is `false`.

{% hint style="warning" %}
`treatQueryAsParameters` is deprecated and will be removed in the next major version.
{% endhint %}

## Use eager loading for route configurations

When the `useEagerLoading` property in the router configuration is set to `true`, the router will eagerly load all route configurations upfront when the application starts. The default value is `false`.

Consider the following scenario. A parent route with paths `[ 'parent', 'parent/:id' ]` configures a child route with path `['child']`. Given this scenario, if when a user tries to navigate to the path `/parent/child`, the router might 'recognize' the `child` segment as a value for the `:id` parameter of the parent route, instead of recognizing it as the child route. This problem is the artifact of how the route-recognizer works under the lazy-loading scenario. The recognizer tries to match the path hungrily, without having any information about the child routes.

To avoid this problem, you can set the `useEagerLoading` property to `true` in the router configuration. Under this configuration, the router will make all the route information available to the route-recognizer when the application starts, thereby avoiding the aforementioned problem.

```typescript
RouterConfiguration.customize({
  useEagerLoading: true,
})
```

For the above mentioned paths-constellation, under eager-loading the router will essentially create the following routing table.

| Path               | Components                                                             |
| ------------------ | ---------------------------------------------------------------------- |
| `parent`           | ParentComponent                                                        |
| `parent/:id`       | ParentComponent with the (required) `:id` parameter                    |
| `parent/child`     | \[ParentComponent, ChildComponent]                                     |
| `parent/:id/child` | \[ParentComponent with the (required) `:id` parameter, ChildComponent] |

As all the routing paths contribute to create a single routing table, the usage of empty paths are discouraged under eager-loading. For example, if instead of `child`, the child route was configured with an empty path `''`, then the routing table would have contained two identical paths `parent` and `parent/:id`, which is not allowed. To this end, apply the following pattern, when using eager-loading.

```diff
  @route({
    routes: [
-     { path: ['', 'child'], component: ChildComponent },
+     { path: ['child'], component: ChildComponent },
    ]
  })
- @customElement({ name: 'routed-component', template: `<au-viewport></au-viewport>` })
+ @customElement({ name: 'routed-component', template: `<au-viewport default="child"></au-viewport>` })
  export class RoutedComponent {}
```

## Advanced Configuration Scenarios

### Combining Multiple Options

Most real-world applications will need to combine multiple configuration options:

```typescript
// Production-ready configuration
RouterConfiguration.customize({
  useUrlFragmentHash: false,           // Use clean URLs
  historyStrategy: 'push',             // Standard browser navigation
  activeClass: 'active',               // Highlight active nav items
  useNavigationModel: true,            // Enable navigation model for menus
  restorePreviousRouteTreeOnError: true, // Graceful error recovery
  buildTitle: (transition) => {
    // Custom title building with SEO considerations
    const routeTitle = transition.routeTree.root.getTitle(' - ');
    return routeTitle ? `${routeTitle} | My App` : 'My App';
  }
})
```

### Environment-Specific Configuration

You might want different configurations for different environments:

```typescript
// environment-based configuration
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';

RouterConfiguration.customize({
  useUrlFragmentHash: isDevelopment,            // Hash routing in dev for simplicity
  historyStrategy: isDevelopment ? 'replace' : 'push', // Keep history noise low in dev, full history in prod
  restorePreviousRouteTreeOnError: !isDevelopment, // Let errors surface in dev, recover in prod
  buildTitle: isProduction
    ? (tr) => buildSEOTitle(tr)                 // SEO-optimised titles in production
    : (tr) => `[DEV] ${tr.routeTree.root.getTitle(' / ') ?? 'Unknown route'}`,
});
```

### Micro-frontend Configuration

When building micro-frontends, you might need specific base path configurations:

```typescript
// Determine base path from current location
const currentPath = window.location.pathname;
const microFrontendName = currentPath.split('/')[1]; // e.g., 'admin', 'customer', 'reports'

RouterConfiguration.customize({
  basePath: `/${microFrontendName}`,
  useUrlFragmentHash: false,
  historyStrategy: 'push',
  buildTitle: (transition) => {
    const appName = microFrontendName.charAt(0).toUpperCase() + microFrontendName.slice(1);
    const routeTitle = transition.routeTree.root.getTitle(' - ');
    return routeTitle ? `${routeTitle} - ${appName}` : appName;
  }
})
```

### Single-Page Application Embedded in Existing Site

When your Aurelia app is embedded within a larger traditional website:

```typescript
RouterConfiguration.customize({
  basePath: '/spa',                     // App lives under /spa path
  useUrlFragmentHash: true,             // Hash routing to avoid conflicts
  historyStrategy: 'replace',           // Don't interfere with main site navigation
  useHref: false,                       // Only use load attribute to avoid conflicts
  activeClass: 'spa-active',            // Namespaced CSS class
})
```

## Common Configuration Patterns

### Mobile-Optimized Configuration

```typescript
RouterConfiguration.customize({
  historyStrategy: 'replace',           // Reduce memory usage on mobile
  useNavigationModel: false,            // Disable if using custom mobile navigation
  restorePreviousRouteTreeOnError: true, // Important for unreliable mobile networks
})
```

### Debug-Friendly Development Configuration

```typescript
RouterConfiguration.customize({
  useUrlFragmentHash: true,             // Easier to debug without server setup
  restorePreviousRouteTreeOnError: false, // See errors clearly in development
  buildTitle: (transition) => {
    // Detailed debugging information in title
    const route = transition.routeTree.root.children[0];
    return `[${route?.component?.name || 'Unknown'}] ${route?.getTitle(' / ') || 'No Title'}`;
  }
})
```

## Router lifecycle (advanced)

The router exposes `start(performInitialNavigation: boolean)` and `stop()`, but when you register `RouterConfiguration` the router is started and stopped automatically via `AppTask`.

* Use `router.stop()` if you temporarily need to suspend routing (for example, while showing a modal flow that should ignore browser back/forward).
* If you call `router.stop()`, call `router.start(false)` to resume listening without triggering another initial navigation.
* Delaying the *very first* navigation requires a custom router configuration, because `RouterConfiguration` always starts the router during app activation.

```typescript
import { resolve } from '@aurelia/kernel';
import { IRouter } from '@aurelia/router';

export class DebugPanel {
  private readonly router = resolve(IRouter);

  pauseRouting() {
    this.router.stop();
  }

  resumeRouting() {
    this.router.start(false);
  }
}
```

## Troubleshooting Configuration Issues

### Common Problems and Solutions

**Problem**: Routes not working with `useUrlFragmentHash: false`

```typescript
// Solution: Ensure base tag is set correctly
// In your index.html:
<base href="/">

// And configure your server for SPA routing
RouterConfiguration.customize({
  useUrlFragmentHash: false
})
```

**Problem**: External links being processed by router

```typescript
// Solution 1: Disable href processing
RouterConfiguration.customize({
  useHref: false  // Only use load attribute for routing
})

// Solution 2: Mark external links explicitly
// <a href="mailto:test@example.com" external>Contact</a>
```

**Problem**: Navigation not updating browser history

```typescript
// Check your history strategy
RouterConfiguration.customize({
  historyStrategy: 'push'  // Ensure this is not 'none'
})
```


# Defining routes

Learn about configuring routes in Router.

{% hint style="info" %}
**Bundler note:** These examples import '.html' files as raw strings (showing '?raw' for Vite/esbuild). Configure your bundler as described in [Importing external HTML templates with bundlers](/components/components#importing-external-html-templates-with-bundlers) so the imports resolve to strings on Webpack, Parcel, etc.
{% endhint %}

The router takes your routing instructions and matches the URL to one of the configured Routes to determine which components to render. To register routes you can either use the `@route` decorator or you can use the `static routes` property to register one or more routes in your application. This section describes the route configuration options in details.

## Route configuration basics

The routing configuration syntax for router is similar to that of other routers you might have worked with before. If you have worked with Express.js routing, then the syntax will be very familiar to you.

A route is an object containing a few required properties that tell the router what component to render, what URL it should match on and other route-specific configuration options.

The most usual case of defining a route configuration is by specifying the `path` and the `component` properties. The idea is to use the `path` property to define a pattern, which when seen in the URL path, the view model defined using the `component` property is activated by the router. Simply put, a routing configuration is a mapping between one or more path patterns to components. Below is the simple example (from the [getting started](/getting-to-know-aurelia/aurelia-router/fundamentals/getting-started) section) of this.

```typescript
import { route } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

@route({
  title: 'Aurelia',
  routes: [
    {
      path: ['', 'home'],
      component: Home,
    },
    {
      path: 'about',
      component: About,
    },
  ],
})
export class MyApp {}
```

For the example above, when the router sees either the path `/` or `/home`, it loads the `Home` component and if it sees the `/about` path it loads the `About` component.

{% hint style="info" %}
Note that you can map multiple paths to a single component. Although these paths can be thought of as aliases, multiple paths, in combination with [path parameters](#path-and-parameters) gets interesting. Another way of creating aliases is to use the [`redirectTo`](#redirect-to-another-path) configuration option.
{% endhint %}

Note that the example above uses the `@route` decorator. In case you cannot use the decorator, you can use the static properties instead. The example shown above can be rewritten as follows.

```typescript
import { Routeable } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

export class MyApp {
  // corresponds to the `title` property in the options object used in the @route decorator.
  static title: string = 'Aurelia';

  // corresponds to the `routes` property in the options object used in the @route decorator.
  static routes: Routeable[] = [
    {
      path: ['', 'home'],
      component: Home,
    },
    {
      path: 'about',
      component: About,
    },
  ];
}
```

As the re-written example shows, you can convert the properties in the options object used for the `@route` decorator into `static` properties in the view model class.

Apart from the static API including the `@route` decorator, there is also an instance-level hook named `getRouteConfig` that you can use to configure your routes. This is shown in the example below.

```typescript
import { IRouteConfig, RouteNode } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

export class MyApp {
  public getRouteConfig(_parentConfig: IRouteConfig | null, _routeNode: RouteNode | null): IRouteConfig {
    return {
      routes: [
        {
          path: ['', 'home'],
          component: Home,
          title: 'Home',
        },
        {
          path: 'about',
          component: About,
          title: 'About',
        },
      ],
    };
  }
}
```

See this in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-getrouteconfig-hook?ctl=1&embed=1&file=src/my-app.ts>" %}

Note that the hook is also supplied with a parent route configuration, and the new route node. These values can be nullable; for example, for root node there is no parent route configuration.

The `getRouteConfig` can also be `async`. This is shown in the example below.

```typescript
import { IRouteConfig, RouteNode } from '@aurelia/router';

export class MyApp {
  public getRouteConfig(_parentConfig: IRouteConfig | null, _routeNode: RouteNode | null): IRouteConfig {
    return {
      routes: [
        {
          path: ['', 'home'],
          component: await import('./home').then((x) => x.Home),
          title: 'Home',
        },
        {
          path: 'about',
          component: await import('./about').then((x) => x.About),
          title: 'About',
        },
      ],
    };
  }
}
```

See this in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-async-getrouteconfig-hook?ctl=1&embed=1&file=src/my-app.ts>" %}

## `path` and parameters

The path defines one or more patterns, which are used by the router to evaluate whether or not an URL matches a route or not. A path can be either a static string (empty string is also allowed, and is considered as the default route) without any additional dynamic parts in it, or it can contain parameters. The paths defined on every routing hierarchy (note that routing configurations can be hierarchical) must be unique.

### Required parameters

Required parameters are prefixed with a colon. The following example shows how to use a required parameter in the `path`.

```typescript
import { route } from '@aurelia/router';
import { Product } from './product';

@route({
  routes: [
    {
      path: 'products/:id',
      component: Product,
    },
  ],
})
export class MyApp {}
```

When a given URL matches one such route, the parameter value is made available in the `canLoad`, and `load` [routing hooks](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle).

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';
import template from './product.html?raw';

@customElement({ name: 'pro-duct', template })
export class Product implements IRouteViewModel {
  public canLoad(params: Params): boolean {
    console.log(params.id);
    return true;
  }
}
```

Note that the value of the `id` parameter as defined in the route configuration (`:id`) is available via the `params.id`. Check out the live example to see this in action.

{% embed url="<https://stackblitz.com/edit/router-lite-required-param?ctl=1&embed=1&file=src/my-app.ts>" %}

### Optional parameters

Optional parameters start with a colon and end with a question mark. The following example shows how to use an optional parameter in the `path`.

```typescript
import { route } from '@aurelia/router';
import { Product } from './product';

@route({
  routes: [
    {
      path: 'product/:id?',
      component: Product,
    },
  ],
})
export class MyApp {}
```

In the example, shown above, the `Product` component is loaded when the router sees paths like `/product` or `/product/some-id`, that is irrespective of a value for the `id` parameter. You can see the live example below.

{% embed url="<https://stackblitz.com/edit/router-lite-optional-param?ctl=1&embed=1&file=src/my-app.ts>" %}

Note that there is an additional link added to the `products.html` to fetch a random product.

```html
<li>
  <a href="../product">Random product</a>
</li>
```

As the `id` parameter is optional, even without a value for the `id` parameter, clicking the link loads the `Product` component. Depending on whether or not there is a value present for the `id` parameter, the `Product` component generates a random id and loads that.

```typescript
public canLoad(params: Params): boolean {
  let id = Number(params.id);
  if (Number.isNaN(id)) {
    id = Math.ceil(Math.random() * 30);
  }

  this.promise = this.productService.get(id);
  return true;
}
```

### Wildcard parameters

The wildcard parameters, start with an asterisk instead of a colon, act as a catch-all, capturing everything provided after it. The following example shows how to use a wildcard parameter in the `path`.

```typescript
import { route } from '@aurelia/router';
import { Product } from './product';

@route({
  routes: [
    {
      id: 'foo',
      path: ['product/:id', 'product/:id/*rest'],
      component: Product,
    },
  ],
})
export class MyApp {}
```

In the example, shown above, the `Product` component is loaded when the router sees paths like `/product/some-id` or `/product/some-id/foo/bar`. You can see the live example below.

{% embed url="<https://stackblitz.com/edit/router-lite-wildcard-param?ctl=1&embed=1&file=src/my-app.ts>" %}

The example utilizes a wildcard parameter named `rest`, and when the value of `rest` is `'image'`, an image for the product is shown. To this end, the `canLoad` hook of the `Product` view-model reads the `rest` parameter.

```typescript
public canLoad(params: Params): boolean {
  const id = Number(params.id);
  this.promise = this.productService.get(id);
  this.showImage = params.rest == 'image';
  return true;
}
```

### Constrained parameters

Any required and optional parameters can be constrained by using a regular expression.

The following example shows how to use a wildcard parameter in the `path`.

```typescript
import { route } from '@aurelia/router';
import { Product } from './product';

@route({
  routes: [
    {
      id: 'foo',
      path: ['product/:id{{^\\d+$}}'],
      component: Product,
    },
  ],
})
export class MyApp {}
```

Note that the syntax to define a parameter constraint is as follows.

```
:PARAM_NAME{{REGEX_CONSTRAINT}}
```

The example above shows that the `Product` component is loaded when the router sees paths like `/product/123`, but not `/product/abc`.

You can see the live example below.

{% embed url="<https://stackblitz.com/edit/router-lite-constrained-param?ctl=1&embed=1&file=src%2Fproducts.html>" %}

{% hint style="info" %}
Note that `^` and `$` implies that the value of complete path segment must match the regular expression. You are however free to choose any regular expression that fits your needs. For example, `'product/:id{{^\\d+}}'` is also a valid constraint and will match paths like `/product/123`, `/product/123abc` etc.
{% endhint %}

## Setting the title

You can configure the title for the routes while you are configuring the routes. The title can be configured in the root level, as well as in the individual route level. This can be seen in the following example using the `@route` decorator.

```typescript
import { route, IRouteViewModel } from '@aurelia/router';
@route({
    title: 'Aurelia', // <-- this is the base title
    routes: [
      {
        path: ['', 'home'],
        component: import('./components/home-page'),
        title: 'Home',
      }
    ]
})
export class MyApp implements IRouteViewModel {}
```

If you prefer using the static `routes` property, the title can be set using a static `title` property in the class. The following example has exactly the same effect as of the previous example.

```typescript
import type { IRouteViewModel, Routeable } from '@aurelia/router';
export class MyApp implements IRouteViewModel {
  static title: string = 'Aurelia'; // <-- this is the base title
  static routes: Routeable[] = [
    {
      path: ['', 'home'],
      component: import('./components/home-page'),
      title: 'Home',
    }
  ];
}
```

With this configuration in place, the default-built title will be `Home | Aurelia` when user is navigated to `/` or `/home` route. That is, the titles of the child routes precedes the base title. You can customize this default behavior by using a [custom `buildTitle` function](/getting-to-know-aurelia/aurelia-router/fundamentals/router-configuration#customizing-title) when customizing the router configuration.

Note that, instead of a string, a function can also be used for `title` to lazily set the title.

There are a few supported title entry points, and they apply at different levels:

* Route configuration titles (`@route`, static `title`, and child `routes[].title`) set title parts for matching route nodes.
* A route title can be a function: `(node: RouteNode) => string | null`. The router evaluates it when composing the title.
* Lifecycle hooks and router hooks that receive the next `RouteNode` can assign `next.title` to replace that node's title part during the current navigation.
* `router.load(instruction, { title })` provides a document title override for that navigation when no custom `buildTitle` is configured.
* `buildTitle` takes over document title generation globally. Use `router.updateTitle()` to recompute the current title when external state used by `buildTitle` changes, such as the active locale.

Directly assigning `document.title` is outside the router pipeline. The router may overwrite it on the next navigation, and `ICurrentRoute.title` will not be updated from that direct assignment.

## Redirect to another path

By specifying the `redirectTo` property on our route, we can create route aliases. These allow us to redirect to other routes. In the following example, we redirect our default route to the `home` page and the `about-us` to `about` page.

```typescript
@route({
  routes: [
    { path: '', redirectTo: 'home' },
    { path: 'about-us', redirectTo: 'about' },
    {
      path: 'home',
      component: Home,
    },
    {
      path: 'about',
      component: About,
    },
  ],
})
export class MyApp {}
```

You can see this action below.

{% embed url="<https://stackblitz.com/edit/router-lite-redirect?ctl=1&embed=1&file=src/my-app.ts>" %}

Note that redirection also works when there are multiple paths/aliases defined for the same component.

```typescript
@route({
  routes: [
    { path: 'foo', redirectTo: 'home' },
    { path: 'bar', redirectTo: 'about' },
    { path: 'fizz', redirectTo: 'about-us' },
    {
      path: ['', 'home'],
      component: Home,
      title: 'Home',
    },
    {
      path: ['about', 'about-us'],
      component: About,
    },
  ],
})
export class MyApp {}
```

You can see this action below.

{% embed url="<https://stackblitz.com/edit/router-lite-redirect-multiple-paths?ctl=1&embed=1&file=src/my-app.ts>" %}

You can use route parameters for `redirectTo`. The following example shows that the parameters from the `about-us` path is rearranged to the `about` path.

```typescript
import { route } from '@aurelia/router';
import { About } from './about';

@route({
  routes: [
    { path: 'about-us/:foo/:bar', redirectTo: 'about/:bar/:foo' },
    {
      path: 'about/:p1?/:p2?',
      component: About,
      title: 'About',
    },
  ],
})
export class MyApp {}
```

You can see this action below.

{% embed url="<https://stackblitz.com/edit/router-lite-redirect-parameterized?ctl=1&embed=1&file=src/main.ts>" %}

## Fallback: redirecting the unknown path

We can instruct the router to redirect the users to a different configured path, whenever it sees any unknown/un-configured paths. To this end, we can use the `fallback` configuration option. Following example shows how to use this configuration option.

{% embed url="<https://stackblitz.com/edit/router-lite-redirect-multiple-paths-ecrbxz?ctl=1&embed=1&file=src/my-app.ts>" %}

As the example shows, the `fallback` is configured as follows.

```typescript
import { route } from '@aurelia/router';
import template from './my-app.html?raw';
import { Home } from './home';
import { About } from './about';
import { NotFound } from './not-found';

@route({
  routes: [
    {
      path: ['', 'home'],
      component: Home,
      title: 'Home',
    },
    {
      path: 'about',
      component: About,
      title: 'About',
    },
    {
      path: 'notfound',
      component: NotFound,
      title: 'Not found',
    },
  ],
  fallback: 'notfound', // <-- fallback configuration
})
export class MyApp {}
```

There is a custom element, named `NotFound`, which is meant to be loaded when any unknown/un-configured route is encountered. As you can see in the above example, clicking the "Foo" link that is with un-configured `href`, leads to the `NotFound` view.

{% hint style="info" %}
It is recommended that you configure a `fallback` at the root to handle the navigation to un-configured routes gracefully.
{% endhint %}

Another way of defining the `fallback` is to use the [route-`id`](#route-configuration-options). The following example demonstrates this behavior, where the `NotFound` view can be reached via multiple aliases, and instead of choosing one of these aliases the route-`id` is used to refer the route.

{% embed url="<https://stackblitz.com/edit/router-lite-fallback-using-routeid?ctl=1&embed=1&file=src/my-app.ts>" %}

The name of the custom element, meant to be displayed for any un-configured route can also be used to define `fallback`. The following example demonstrates this behavior, where `not-found`, the name of custom element `NotFound`, is used to refer the route.

{% embed url="<https://stackblitz.com/edit/router-lite-fallback-using-ce-name?ctl=1&embed=1&file=src/not-found.ts>" %}

An important point to note here is that when you are using the custom element name as fallback, you need to ensure that the custom element is registered to the DI container. Note that in the example above, the `NotFound` component is registered to the DI container in `main.ts`.

A `fallback` defined on parent is inherited by the children (to know more about hierarchical routing configuration, refer the [documentation](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#hierarchical-routing)). However, every child can override the fallback as needed. The following example demonstrate this. The root has two [sibling viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#sibling-viewports) and two children components can be loaded into each of those by clicking the link. Every child defines their own child routing configuration. The root defines a `fallback` and one of the children overrides the `fallback` by defining one of its' own. With this configuration in place, when navigation to a un-configured route ('Foo') is attempted for each children, one loads the overridden version whereas the other loads the fallback inherited from the parent (in this case the root).

{% embed url="<https://stackblitz.com/edit/router-lite-fallback-hierarchical?ctl=1&embed=1&file=src/my-app.ts>" %}

A function can be used for `fallback`. The function takes the following signature.

```typescript
fallback(viewportInstruction: ViewportInstruction, routeNode: RouteNode, context: IRouteContext): string;
```

An example can look like below, where the example redirects the user to `NF1` component if an attempt to load a path `/foo` is made. Every other attempt to load an unknown path is results loading the `NF2` component.

```typescript
import { customElement } from '@aurelia/runtime-html';
import {
  IRouteContext,
  ITypedNavigationInstruction_string,
  route,
  RouteNode,
  ViewportInstruction,
} from '@aurelia/router';

@customElement({ name: 'ce-a', template: 'a' })
class A {}

@customElement({ name: 'n-f-1', template: 'nf1' })
class NF1 {}

@customElement({ name: 'n-f-2', template: 'nf2' })
class NF2 {}

@route({
  routes: [
    { id: 'r1', path: ['', 'a'], component: A },
    { id: 'r2', path: ['nf1'], component: NF1 },
    { id: 'r3', path: ['nf2'], component: NF2 },
  ],
  fallback(vi: ViewportInstruction, _rn: RouteNode, _ctx: IRouteContext): string {
    return (vi.component as ITypedNavigationInstruction_string).value === 'foo' ? 'r2' : 'r3';
  },
})
@customElement({
  name: 'my-app',
  template: `
  <nav>
  <a href="a">A</a>
  <a href="foo">Foo</a>
  <a href="bar">Bar</a>
</nav>

<au-viewport></au-viewport>`
})
export class MyApp {}
```

You can also see this in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-fallback-using-function?ctl=1&embed=1&file=src/my-app.html>" %}

You can also use non-string fallbacks. For example, you can use a class as the value for `fallback`; such as `fallback: NotFound`. Or, if you are using a function, you choose to return a class instead of returning a string. These combinations are also supported by router.

## Case sensitive routes

Routes can be marked as case-sensitive in the configuration, allowing the navigation to the component only when the case matches exactly the configured path. See the example below where the navigation to the "about" page is only successful when the casing matches.

```typescript
import { route } from '@aurelia/router';
import { About } from './about';

@route({
  routes: [
    {
      path: 'AbOuT',
      component: About,
      caseSensitive: true,
    },
  ],
})
export class MyApp {}
```

Thus, only an attempt to the `/AbOuT` path loads the `About` component; any attempt with a different casing is navigated to the `fallback`. See this in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-case-sensitive?ctl=1&embed=1&file=src/my-app.ts>" %}

## Advanced route configuration options

There are few other routing configuration which aren't discussed above. Our assumption is that these options are more involved and might not be used that often. Moreover, to understand the utility of these options fully, knowledge of other parts of the route would be beneficial. Therefore, this section only briefly introduces these options providing links to the sections with detailed examples.

* `id` — The unique ID for this route. The router implicitly generates a `id` for a given route, if an explicit value for this property is missing. Although this is not really an advanced property, due to the fact that a route can be uniquely identified with `id`, it can be used in many interesting ways. For example, this can be used to generate the `href`s in the view when using the [`load` custom attribute](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-the-load-custom-attribute) or using the [`Router#load` API](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-the-router-api). Using this property is also very convenient when there are multiple aliases for a single route, and we need a unique way to refer to this route.
* `transitionPlan` — How to behave when the currently active component is scheduled to be loaded again in the same viewport. For more details, please refer the [documentation](/getting-to-know-aurelia/aurelia-router/advanced/transition-plans).
* `viewport` — The name of the viewport this component should be loaded into. This demands a full fledged documentation of its own. Refer to the [viewport documentation](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#specifying-a-viewport-name-on-a-route) for more details.
* `data` — Any custom data that should be accessible to matched components or hooks. The value of this configuration property must be an object and the object can take any shape (that is there is no pre-defined interface/class for this object). A typical use-case for the `data` property is to define the permissions, required by the users, when they attempt to navigate to this route. Refer [an example](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/router-hooks#example-authentication-and-authorization) of this.
* `nav` - Set this flag to `false` (default value is `true`), to instruct the router not to add the route to the [navigation model](/getting-to-know-aurelia/aurelia-router/navigation/navigation-model). This is typically useful to [exclude routes](/getting-to-know-aurelia/aurelia-router/navigation/navigation-model#excluding-routes-from-the-navigation-model) from the public navigation menu.

## Specifying component

Before finishing the section on the route configuration, we need to discuss one last topic for completeness, and that is how many different ways you can configure the `component`. Throughout various examples so far we have seen that components are configured by importing and using those in the routing configuration. However, there are many other ways in which the components can be configured. This section discusses those.

### Using inline `import()`

Components can be configured using the [`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) or dynamic import. Instead of statically importing the components, those can be imported using `import()`-syntax, as the example shows below.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: import('./home'),
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: import('./about'),
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

You can see this in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-component-inline-import?ctl=1&embed=1&file=src/my-app.ts>" %}

{% hint style="info" %}
If you are using TypeScript, ensure that the `module` property set to `esnext` in your `tsconfig.json` to support inline import statements.
{% endhint %}

### Using the name

Components can be configured using only the custom-element name of the component.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: 'ho-me', // <-- assuming that Home component has the name 'ho-me'
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: 'ab-out', // <-- assuming that About component has the name 'ab-out'
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

However, when configuring the route this way, you need to register the components to the DI.

```typescript
// main.ts
import { RouterConfiguration } from '@aurelia/router';
import { Aurelia, StandardConfiguration } from '@aurelia/runtime-html';
import { About } from './about';
import { Home } from './home';
import { MyApp as component } from './my-app';

(async function () {
  const host = document.querySelector<HTMLElement>('app');
  const au = new Aurelia();
  au.register(
    StandardConfiguration,
    RouterConfiguration,

    // component registrations
    Home,
    About,
  );
  au.app({ host, component });
  await au.start();
})().catch(console.error);
```

You can see this configuration in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-component-ce-name?ctl=1&embed=1&file=src/my-app.ts>" %}

### Using a function returning the class

Components can be configured using a function that returns a class.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: () => {
+         @customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
+         class Home {
+           private readonly message: string = 'Welcome to Aurelia2 router!';
+         }
+         return Home;
+       },
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: () => {
+         @customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
+         class About {
+           private readonly message = 'Aurelia2 router is simple';
+         }
+         return About;
+       },
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

You can see this configuration in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-component-function?ctl=1&embed=1&file=src/my-app.ts>" %}

### Using custom element definition

Components can be configured using custom element definition.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

+ class Home {
+   private readonly message: string = 'Welcome to Aurelia2 router!';
+ }
+ const homeDefn = CustomElementDefinition.create(
+   { name: 'ho-me', template: '<h1>${message}</h1>' },
+   Home
+ );
+ CustomElement.define(homeDefn, Home);
+
+ class About {
+   private readonly message = 'Aurelia2 router is simple';
+ }
+ const aboutDefn = CustomElementDefinition.create(
+   { name: 'ab-out', template: '<h1>${message}</h1>' },
+   About
+ );
+ CustomElement.define(aboutDefn, About);

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: homeDefn,
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: aboutDefn,
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

You can see this configuration in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-component-ce-defn?ctl=1&embed=1&file=src/my-app.ts>" %}

### Using custom element instance

Components can be configured using custom element instance.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

+ @customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
+ class Home {
+   private readonly message: string = 'Welcome to Aurelia2 router!';
+ }
+
+ @customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
+ class About {
+   private readonly message = 'Aurelia2 router is simple';
+ }

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: new Home(),
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: new About(),
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

You can see this configuration in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-component-ce-instance-jx3kee?ctl=1&embed=1&file=src/my-app.ts>" %}

### Using conventional HTML-only custom element

When using HTML-only custom elements, facilitated via the convention, the custom element can be directly used while configuring routes.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';
+ import * as About from './about.html';
+ import * as Home from './home.html';

  @route({
    routes: [
      {
        path: ['', 'home'],
        component: Home,
        title: 'Home',
      },
      {
        path: 'about',
        component: About,
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

Using `import()` function directly is also supported.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: import('./home.html'),
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: import('./about.html'),
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

When importing the HTML-only custom elements in the HTML file using `<require from="">` syntax, use the custom element name in the route configuration.

```html
  <require from="./about.html"></require>
  <require from="./home.html"></require>

  <au-viewport></au-viewport>
```

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
- import { About } from './about';
- import { Home } from './home';

  @route({
    routes: [
      {
        path: ['', 'home'],
-       component: Home,
+       component: 'home',
        title: 'Home',
      },
      {
        path: 'about',
-       component: About,
+       component: 'about',
        title: 'About',
      },
    ],
  })
@customElement({ name: 'my-app', template })
export class MyApp {}
```

### Using a navigation strategy

Components can be configured using a navigation strategy. A navigation strategy is an instance of `NavigationStrategy` class that takes a factory method to return a routable component. The following example shows how to use a navigation strategy.

```diff
  import { customElement } from '@aurelia/runtime-html';
  import { route } from '@aurelia/router';
  import template from './my-app.html?raw';
  import { About } from './about';
  import { Home } from './home';

+ let dataLoaded = false;

  @route({
    routes: [
      {
        path: ['', 'home'],
        component: Home,
        title: 'Home',
      },
      {
        path: 'about',
        component: About,
        title: 'About',
      },
+     {
+      path: 'foo',
+      component: new NavigationStrategy(() => {
+        if (dataLoaded) return Home;
+
+        dataLoaded = true;
+        return About;
+      })
+     }
    ],
  })
  @customElement({ name: 'my-app', template })
  export class MyApp {}
```

The factory method takes has the following signature.

```typescript
type NavigationStrategyComponent = string | RouteType | Promise<IModule> | CustomElementDefinition;
function getComponent(viewportInstruction: IViewportInstruction, ctx: IRouteContext, node: RouteNode, route: RecognizedRoute<unknown>): string | RouteType | Promise<IModule> | CustomElementDefinition;
```

The parameters can be used further to determine which component to load.

## Using classes as routes

Using router it is also possible to use the routed view model classes directly as routes configuration. While doing so, if no paths have been explicitly configured for the components, the custom element name and aliases can be used as routing instructions. The following example demonstrates that the `C1` and `C2` classes are used directly as the child routes for the `Root`.

```typescript
@customElement({ name: 'c-1', template: 'c1', aliases: ['c-a', 'c-one'] })
class C1 { }

@customElement({ name: 'c-2', template: 'c2', aliases: ['c-b', 'c-two'] })
class C2 { }

@route({
  routes: [C1, C2]
})
@customElement({ name: 'ro-ot', template: '<au-viewport></au-viewport>' })
class Root { }
```

The example above implies that `router.load('c-1')`, or `router.load('c-a')` and `router.load('c-2')`, `router.load('c-two')` will load the `C1` and `C2` respectively.

{% hint style="info" %}
To know more about the router API refer [this section](/getting-to-know-aurelia/aurelia-router/navigation/navigating#using-the-router-api).
{% endhint %}

## Distributed routing configurations

The examples discussed so far demonstrate the classic use-cases of route configurations where the parents define the child routes. Another aspect of these examples are that all the route configurations are centralized on the parent component. This section provides some examples where that configuration is distributed across different components.

We start by noting that every component can define its own path. This is shown in the following example.

{% tabs %}
{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

@route({
  routes: [Home, About],
})
@customElement({
  name: 'my-app',
  template: `
<nav>
  <a href="home">Home</a>
  <a href="about">About</a>
</nav>

<au-viewport></au-viewport>
`
})
export class MyApp {}

```

{% endtab %}

{% tab title="home.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route(['', 'home'])
@customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
export class Home {
  private readonly message: string = 'Welcome to Aurelia2 router!';
}
```

{% endtab %}

{% tab title="about.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route('about')
@customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
export class About {
  private readonly message = 'Aurelia2 router is simple';
}
```

{% endtab %}
{% endtabs %}

The example shows that both `Home` and `About` uses the `@route` decorator to define their own paths. This reduces the child-route configuration for `MyApp` to `@route({ routes: [Home, About] })`. The example can be seen in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-path-on-vm?ctl=1&embed=1&file=src/my-app.ts>" %}

Note that other properties of route configuration can also be used in this way.

{% tabs %}
{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

@route({
  routes: [Home, About],
})
@customElement({
  name: 'my-app',
  template: `
<nav>
  <a href="home">Home</a>
  <a href="about">About</a>
</nav>

<au-viewport></au-viewport>
`
})
export class MyApp {}

```

{% endtab %}

{% tab title="home.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: ['', 'home'], title: 'Home' })
@customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
export class Home {
  private readonly message: string = 'Welcome to Aurelia2 router!';
}
```

{% endtab %}

{% tab title="about.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: 'about', title: 'About' })
@customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
export class About {
  private readonly message = 'Aurelia2 router is simple';
}
```

{% endtab %}
{% endtabs %}

The previous example demonstrates that the `Home` and `About` components define the `title` for themselves. The example can be seen in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-route-config-on-vm?ctl=1&embed=1&file=src/home.ts>" %}

While adapting to distributes routing configuration, the parent can still override the configuration for its children. This makes sense, because even if a component defines its own `path`, `title` etc. the parent may choose to reach (route) the component via a different path or display a different title when the component is loaded. This is shown below, where the `MyApp` overrides the routing configurations defined by `About`.

{% tabs %}
{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import { route } from '@aurelia/router';
import { Home } from './home';
import { About } from './about';

@route({
  routes: [
    Home,
    { path: 'about-us', component: About, title: 'About us' }
  ],
})
@customElement({
  name: 'my-app',
  template: `
<nav>
  <a href="home">Home</a>
  <a href="about-us">About</a>
</nav>

<au-viewport></au-viewport>
`
})
export class MyApp {}

```

{% endtab %}

{% tab title="home.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: ['', 'home'], title: 'Home' })
@customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
export class Home {
  private readonly message: string = 'Welcome to Aurelia2 router!';
}
```

{% endtab %}

{% tab title="about.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: 'about', title: 'About' })
@customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
export class About {
  private readonly message = 'Aurelia2 router is simple';
}
```

{% endtab %}
{% endtabs %}

This can be seen in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-parent-overrides-comp-route-config?ctl=1&embed=1&file=src/my-app.ts>" %}

You can also use the `@route` decorator and the `getRouteConfig` together.

{% tabs %}
{% tab title="my-app.ts" %}

```typescript
import { customElement } from '@aurelia/runtime-html';
import {
  IRouteConfig,
  IRouteViewModel,
  route,
  RouteNode,
} from '@aurelia/router';
import template from './my-app.html?raw';
import { Home } from './home';
import { About } from './about';

@route({ title: 'Aurelia2' })
@customElement({
  name: 'my-app',
  template: `
<nav>
  <a href="home">Home</a>
  <a href="about">About</a>
</nav>

<au-viewport></au-viewport>
`
})
export class MyApp implements IRouteViewModel {
  public getRouteConfig?(
    parentConfig: IRouteConfig | null,
    routeNode: RouteNode | null
  ): IRouteConfig {
    return {
      routes: [Home, About],
    };
  }
}
```

{% endtab %}

{% tab title="home.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: ['', 'home'], title: 'Home' })
@customElement({ name: 'ho-me', template: '<h1>${message}</h1>' })
export class Home {
  private readonly message: string = 'Welcome to Aurelia2 router!';
}
```

{% endtab %}

{% tab title="about.ts" %}

```typescript
import { route } from '@aurelia/router';
import { customElement } from '@aurelia/runtime-html';

@route({ path: 'about', title: 'About' })
@customElement({ name: 'ab-out', template: '<h1>${message}</h1>' })
export class About {
  private readonly message = 'Aurelia2 router is simple';
}
```

{% endtab %}
{% endtabs %}

This can be seen in action below.

{% embed url="<https://stackblitz.com/edit/router-lite-hybrid-config?ctl=1&embed=1&file=src/my-app.ts>" %}

The advantage of this kind of distributed configuration is that the routing configuration of every component is defined by every component itself, thereby encouraging encapsulation, leaving the routing configuration at the parent level to merely listing out its child components. On the other hand, highly distributed route configuration may prevent easy overview of the configured routes. That's the trade-off. Feel free to mix and match as per your need and aesthetics.

## Retrieving the current route and query parameters

Apart from configuring routes, you may also want to read which route is currently active and obtain any query parameters. If you only defined or named a few of them as route parameters, others might appear as query parameters. Here is a short example:

```typescript
import { ICurrentRoute } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class MyComponent {
  private currentRoute = resolve(ICurrentRoute);

  attached() {
    console.log('Current path:', this.currentRoute.path);
    console.log('Query params:', this.currentRoute.url.split('?')[1] ?? 'no query');
    // Or, to see them as a structured object:
    console.log('Parsed query:', Object.fromEntries(this.currentRoute.parameterInformation[0].params ?? {}));
  }
}
```

You can inject `ICurrentRoute` in any routed view-model. For an elaborate approach, see additional references in the [navigation model](/getting-to-know-aurelia/aurelia-router/navigation/navigation-model#using-the-isactive-property) and [navigation docs](/getting-to-know-aurelia/aurelia-router/navigation/navigating).


# Child routing playbook

Build deeply nested navigation trees with Aurelia's router, including layouts, sibling viewports, and relative navigation.

Child routing lets each routed component own its own navigation tree. Use it to build dashboards with nested layouts, multi-step forms, or resource detail pages that include tabs or auxiliary panels. This guide walks through the patterns you will use most often.

## 1. Define parent and child routes

Every routed component can declare a `routes` array inside the `@route` decorator. Parent components stay slim—most of the structure lives in the child components.

```typescript
import { route } from '@aurelia/router';
import { UsersPage } from './users/users-page';
import { ReportsPage } from './reports/reports-page';

@route({
  routes: [
    { path: '', component: UsersPage, title: 'Users' },
    { path: 'reports', component: ReportsPage, title: 'Reports' },
  ]
})
export class AdminLayout {}
```

Each child component can keep nesting:

```typescript
import { route } from '@aurelia/router';
import { UserOverview } from './user-overview';
import { UserSettings } from './user-settings';

@route({
  routes: [
    { path: ':id', component: UserOverview, title: 'Overview' },
    { path: ':id/settings', component: UserSettings, title: 'Settings' },
  ]
})
export class UsersPage {}
```

When the router loads `AdminLayout`, it automatically instantiates the nested layout components and surfaces their routes inside the `<au-viewport>` declared in each template.

## 2. Render child viewports in parent templates

Every component that declares child routes must include at least one `<au-viewport>` in its view:

```html
<!-- admin-layout.html -->
<nav>
  <a load="">Users</a>
  <a load="reports">Reports</a>
</nav>

<au-viewport></au-viewport>
```

```html
<!-- users-page.html -->
<section class="users">
  <aside>
    <user-list></user-list>
  </aside>
  <main>
    <au-viewport></au-viewport>
  </main>
</section>
```

You can name child viewports to run siblings in parallel:

```html
<au-viewport name="main"></au-viewport>
<au-viewport name="details"></au-viewport>
```

Then target them with multi-viewport instructions such as `href="orders@main+profile@details"` or `router.load([{ component: Orders, viewport: 'main' }, { component: Profile, viewport: 'details' }])`. See [Viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports#sibling-viewports) for more combinations.

## 3. Share layout data across child routes

Load shared data once in the parent and expose it through a service that both parent and children resolve from DI.

```typescript
import { singleton } from '@aurelia/kernel';

type AdminSummary = {
  totalUsers: number;
  activeUsers: number;
};

@singleton()
export class AdminStatsStore {
  private readonly fallback: AdminSummary = { totalUsers: 0, activeUsers: 0 };
  private _value: AdminSummary | null = null;

  set(summary: AdminSummary) {
    this._value = summary;
  }

  get value() {
    return this._value ?? this.fallback;
  }
}
```

```typescript
import { IRouteViewModel, Params } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class AdminLayout implements IRouteViewModel {
  private readonly store = resolve(AdminStatsStore);

  async loading(_params: Params) {
    const summary = await fetch('/api/admin/summary').then(res => res.json());
    this.store.set(summary);
  }
}
```

```typescript
import { resolve } from '@aurelia/kernel';

export class UsersPage {
  private readonly store = resolve(AdminStatsStore);

  get stats() {
    return this.store.value;
  }
}
```

Because the store is a singleton, each child route can read the latest summary without manually passing data down the tree.

## 4. Navigate within the current hierarchy

Relative navigation keeps nested layouts decoupled from the app root. Always resolve `IRouteContext` (or pass `context` through `router.load`) when a child needs to target a sibling or parent.

```typescript
import { IRouter, IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';

export class UserTabs {
  private readonly router = resolve(IRouter);
  private readonly context = resolve(IRouteContext);

  openSettings(id: string) {
    return this.router.load(`${id}/settings`, {
      context: this.context,
    });
  }
}
```

You can achieve the same thing in templates:

```html
<a href="../">Back to list</a>
<a href="../${user.id}/settings">Settings</a>
```

The `../` prefix climbs up one routing context before evaluating the rest of the path.

## 5. Combine child routes with parameters

Child routes can declare their own parameters and still reuse parent parameters. The router merges them automatically when you call `IRouteContext.getRouteParameters({ includeQueryParams: true })` or receive the `Params` argument in lifecycle hooks. See the [Route parameters guide](/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters) for a complete walkthrough.

## 6. Lazy-load nested modules

You can reference dynamic imports inside any `component` slot. The router will `await` the module before instantiating the component.

```typescript
@route({
  routes: [
    {
      path: 'reports',
      component: () => import('./reports/reports-index').then(m => m.ReportsIndex),
      title: 'Reports',
    },
  ],
})
export class AdminLayout {}
```

This works at every level of the tree, so you pay the cost only when users actually navigate there.

## 7. Test nested layouts in isolation

Create the parent component via the testing harness, call `router.load` with a path that exercises the child routes, and then assert against the rendered DOM. Because every child route uses a real component (not a string lookup), you get high-confidence integration coverage:

```typescript
import { createFixture } from '@aurelia/testing';
import { RouterConfiguration, IRouter } from '@aurelia/router';

const { appHost, container, startPromise, stop } = createFixture(
  '<au-viewport></au-viewport>',
  AdminLayout,
  [RouterConfiguration],
);
await startPromise;

const router = container.get(IRouter);
await router.load('reports/daily');
expect(appHost.querySelector('reports-daily')).not.toBeNull();

await stop(true);
```

## Scenario recipes

### Tabs inside a detail page

**Outcome:** `/users/:id` loads a layout with tabs (`overview`, `activity`, `settings`) without re-rendering the outer chrome.

1. Parent layout defines `routes` for each tab and keeps the `<au-viewport>` inside the main column.
2. Tabs use `href="tabName"` so navigation stays relative to the current user context.
3. Store the selected tab in a store or read it from `ICurrentRoute.fragment` if you also want anchor links.

Validation checklist:

* Navigating from `overview` to `settings` preserves the `:id` value.
* Browser back button cycles tabs without losing the parent layout.
* A deep link to `/users/42/settings` opens the settings tab immediately.

### Protected admin area with nested guards

**Outcome:** Block access to admin child routes unless the parent layout validates the session, while letting each child enforce its own role.

1. Implement `canLoad` on the parent layout to check authentication. Return `'login'` to redirect unauthorized users.
2. Register additional router hooks (or per-view-model `canLoad`) on children for permissions such as `reports:read`.
3. Use `IRouterEvents` to show a toast whenever a guard cancels navigation.

Validation checklist:

* Visiting `/admin/reports` while logged out redirects to `/login`.
* Visiting `/admin/users` with insufficient role triggers the child guard and surfaces an error message.
* Successful navigation still shows the admin shell.

### Multi-pane dashboards

**Outcome:** A dashboard shows a list in the left viewport and detail in the right viewport, both driven by routing.

1. Parent template declares `<au-viewport name="list">` and `<au-viewport name="detail">`.
2. Route instructions load both panes simultaneously, e.g. `router.load([{ component: ReportsList, viewport: 'list' }, { component: ReportsDetail, params: { id }, viewport: 'detail' }])`.
3. Child components navigate using `context: resolve(IRouteContext)` to avoid resetting the other viewport.

Validation checklist:

* Loading `/dashboard` shows default list + placeholder detail.
* Clicking a row updates only the detail viewport.
* Sharing `/dashboard@detail=report/weekly` opens the same detail for other users.

## Related resources

* [Configuring routes](/getting-to-know-aurelia/aurelia-router/fundamentals/configuring-routes)
* [Viewports](/getting-to-know-aurelia/aurelia-router/fundamentals/viewports)
* [Routing lifecycle](/getting-to-know-aurelia/aurelia-router/lifecycle-and-events/routing-lifecycle)
* [Route parameters](/getting-to-know-aurelia/aurelia-router/fundamentals/route-parameters)
* [Navigating](/getting-to-know-aurelia/aurelia-router/navigation/navigating#navigate-in-current-and-ancestor-routing-context)




---

[Next Page](/llms-full.txt/1)

