This is the first post in a multi-part series covering some of the inversion-of-control and dependency-injection patterns at work in every Aurelia application. If you've never heard of DI or IoC, take a few minutes to read this excellent post by Martin Fowler. The fact is, if you've ever built an Aurelia app you've used these patterns, maybe without even knowing it!
Inversion of Control
Inversion of control (IoC) describes a design in which custom-written portions of a computer program receive the flow of control from a generic, reusable library. A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the reusable code that calls into the custom, or task-specific, code.
Nice Wikipedia quote... how does it apply to Aurelia?
Consider a typical single page application with multiple screens. If you built this application without Aurelia you might find yourself doing a lot of this:
- Load/instantiate a view-model.
- Load/instantiate a view.
- Bind the view to the view-model.
- Append the view to the DOM.
- User clicks a link.
- Parse the url hash, determine which view-model to load/instantiate, check whether the current view can be deactivated, etc
- Rinse and repeat.
Without Aurelia you're implementing the logic that controls the application lifecycle in addition to the features specific to you application.
Contrast this with an application built with Aurelia. You write none of the boiler-plate to control the application life code above. Instead you focus on writing the views, view-models, behaviors and routes that embody your application's custom logic and appearance. Aurelia inverts the control, handling the application lifecycle while allowing you to customize the default behavior at well defined lifecycle hooks.
Lifecycle hooks are optional methods you attach to view-models. Aurelia's router and templating engine will invoke these methods at the appropriate time, allowing you to control specific lifecycle steps.
There's the route configuration and screen activation hooks that allow you to control navigation within your app:
configureRouter(config, router)- Implement this hook if your view-model needs to translating url changes into application state.
canActivate(params, routeConfig, navigationInstruction)- Implement this hook if you want to control whether or not your view-model can be navigated to. Return a boolean value, a promise for a boolean value, or a navigation command.
activate(params, routeConfig, navigationInstruction)- Implement this hook if you want to perform custom logic just before your view-model is displayed. You can optionally return a promise to tell the router to wait to bind and attach the view until after you finish your work.
canDeactivate()- Implement this hook if you want to control whether or not the router can navigate away from your view-model when moving to a new route. Return a boolean value, a promise for a boolean value, or a navigation command.
deactivate()- Implement this hook if you want to perform custom logic when your view-model is being navigated away from. You can optionally return a promise to tell the router to wait until after your finish your work.
There's also the component/behavior lifecycle hooks that allow customizing a view, custom-element or custom-attribute's lifecycle:
created(view)- Invoked after both the view and view-model have been created. Allows your behavior to have direct access to the View instance.
bind(bindingContext)- Invoked when the databinding engine binds the view. The binding context is the instance that the view is databound to.
unbind()- Invoked when the databinding engine unbinds the view.
attached()- Invoked when the view that contains the extension is attached to the DOM.
detached()- Invoked when the view that contains the extension is detached from the DOM.
Last but not least, there are a number of decorators that allow you to further customize your component's behavior within Aurelia's composition lifecycle:
@processContent(false|Function)- Tells the compiler that the element's content requires special processing. If you provide
falseto the decorator, the the compiler will not process the content of your custom element. It is expected that you will do custom processing yourself. But, you can also supply a custom function that lets you process the content during the view's compilation. That function can then return true/false to indicate whether or not the compiler should also process the content. The function takes the following form
function(compiler, resources, node, instruction):boolean
@useView(path)- Specifies a different view to use.
@noView()- Indicates that this custom element does not have a view and that the author intends for the element to handle its own rendering internally.
@inlineView(markup, dependencies?)- Allows the developer to provide a string that will be compiled into the view.
@containerless()- Causes the element's view to be rendered without the custom element container wrapping it. This cannot be used in conjunction with
@useShadowDOM. It also cannot be uses with surrogate behaviors.
@useShadowDOM()- Causes the view to be rendered in the ShadowDOM. When an element is rendered to ShadowDOM, a special
DOMBoundaryinstance can optionally be injected into the constructor. This represents the shadow root.
As you can see Aurelia makes heavy use of the IoC pattern to reduce the work required to build applications. It does this without sacrificing flexibility by using lifecycle hooks, overridable conventions and a pluggable architecture built upon a dependency injection container. In the next post I'll cover some of the inner workings of Aurelia's DI container, followed by a third post covering common uses for the container as well as it's use within the Aurelia framework.