Write AngularJS like it's 2018
Warning: Please consider that this post is over 5 years old and the content may no longer be relevant.
When React became mainstream it changed the way we thought about writing single page JavaScript applications, it promoted readability and maintainability over abstracting complex interactions. These principles resonated with the JavaScript community and influenced the development of Angular, unfortunately many of us are still stuck maintaining AngularJS (Angular 1.x) apps that are growing ever more complex.
This post presents 9 tips for writing better AngularJS based on more modern principles. These tips are the distilled learnings from working with a team of developers managing an AngularJS app over the last 3 years that has grown to over 25,000 lines of Angular.
1. Use AngularJS component syntax
Available from version 1.5+, AngularJS’s new component syntax is an opinionated way to write directives, leveraging principles from Angular 2+ and React.
var TodoItemController = function() {
this.styles = styles;
};
angular
.module('todos.todo-list.todo-item', [])
.component('todoItem', {
templateUrl: template,
bindings: {
todo: '<'
},
controller: TodoListController
});
Components promote the use of Controller functions over linking functions. Controllers are the correct place to put initialisation code as they are executed inside-out, so parents are initialised before children. Post-link functions (commonly used with directives) are executed outside-in so children are linked before parents which can cause confusion.
Components can only be used as elements, not properties. Where you could use a directive as an attribute <div my-component>
, a component must be <my-component>
.
Don’t use self closing elements (e.g. <my-component />
), always include the closing tag <my-component></my-component>
. The self closing tag syntax is valid XML (which is why it’s used in React’s JSX), but is invalid in HTML5, it causes browsers to silently ignore any following elements, which can be a pain to debug.
2. Use one-way bindings & callbacks
Avoid using two-way bindings '='
which promotes multiple components updating a shared model. Changes to the model can be hard to trace and requires watchers to trigger behaviour on changes. Use one-way bindings '<'
to pass data to a component, and callbacks for receiving data from a component.
bindings: {
todo: '<',
onItemCompleted: '<',
onItemRemoved: '<',
onItemTitleChanged: '<'
},
Use the one-way binding modifier even for callbacks, do not use expression bindings '&'
.
Expression bindings allow you to execute ad hoc code in the binding, e.g.
<my-component on-update="$ctrl.value = newValue"/>
But they come at the cost of readability, where did newValue
come from? And you need to remember to use the special syntax when invoking them from the component.
this.onUpdate({ newValue: theNewValue });
Make your life simpler by always passing a function reference using one-way bindings. e.g.
<!-- handleUpdate is a function defined on your controller -->
<my-component on-update="$ctrl.handleUpdate"/>
Callback naming conventions
The component that receives a callback should be named onVerb
, this aligns with standard HTML event handlers like onclick
.
The callback function that is passed to the component should be named handleVerb
.
3. Prefer small, stateless components
Make your components as small as is feasible, small components are easier to reuse, read and to test. Try to follow to the single responsibility principle for a component, one component should do or draw one thing.
Aim to create stateless components, a stateless component’s output (rendered HTML) is only dependant on it’s inputs (bindings). Avoid injecting services and making API calls inside your component, which makes testing and reasoning about your component more difficult.
4. Hoist state management up
Obviously, somewhere in the app needs to manage state, try to identify the lowest common component in the component tree that uses state and make this the state management / ‘impure’ component. E.g. a todo-list
component might be the appropriate place to fetch a collection of todos, then each todo-item
would be passed a todo model and callbacks to edit or delete the model. todo-list
is the stateful component, todo-item
is a stateless component.
Using stateful containers
An alternative approach used by Redux is to only ever allow stateless components, and state management like fetching data is done by a container. A container is component that is not allowed to do any rendering, it simply passes the managed data to a component. Extending our first example we could have a containers/todo-list
that fetches the data then passes it to a components/todo-list
which is a stateless component that renders the list and also renders individual components/todo-item
.
5. Structure your project correctly
src/
├─── components/
│ ├─── header/
│ │ ├─── index.js
│ │ ├─── styles.css
│ │ └─── template.html
│ └─── todo-list/
│ ├─── todo-item/
│ │ ├─── index.js
│ │ ├─── styles.css
│ │ └─── template.html
│ ├─── index.js
│ ├─── styles.css
│ └─── template.html
├─── models/
│ └─── todo.js
├─── services/
│ └─── todos-store.js
├─── filters/
│ └─── trim.js
├─── index.html
└─── app.js
All components live in a components
folder. Keep the template, styles and code that relate to a component in one folder. I prefer to name them index.js
, template.html
and styles.css
. Nest any dependant components under the parent component folder.
6. Define a new module for every component.
Each component should import all dependencies within the component, and should be it’s own contained module. This makes the component more portable and avoids ordering issues when bundling. E.g.
'use strict';
var angular = require('angular');
var template = require('./template.html');
var styles = require('./styles.css');
require('components/todo-list/todo-item');
var TodoListController = function() {
this.styles = styles;
};
angular
.module('todos.todo-list', [
'todos.todo-list.todo-item'
])
.component('todoList', {
templateUrl: template,
bindings: {
todos: '<',
onItemAdded: '<'
},
controller: TodoListController
});
7. Use prefixes for component and module names
To avoid clashes with other packages, pick a root module name for your app and prefix all modules in your app with it. Prefix any dependent components with the name of its parent. E.g. 'my-app.parent-component.child-component'
.
Again, to avoid name clashes with other packages, use a short prefix for all your components, try to keep it to 3 characters or less. e.g. <my-foo>
, <my-bar>
.
8. Use CSS Modules
CSS Modules allows you to scope your styles to a single component by obfuscating the class name. You’ll need to use a CSS processor that supports this, like webpack and css-loader. Import the stylesheet and use the style dictionary to access class names, e.g.
/* styles.css */
.todo-item {
font-size: 24px;
}
// index.js
var styles = require('./styles.css');
var TodoListController = function() {
this.styles = styles;
};
<!-- template.html -->
<div class="{{$ctrl.styles.todoItem}}"></div>
When using css-loader I recommend turning on the following options:
modules: true
Makes every style a local (obfuscated) style by default. You can still reference global styles with :global(.row)
.
camelCase: true
Converts spinal-case style names to camelCase so they are JavaScript friendly.
localIdentName: '[name]__[local]__[hash:base64:5]'
Makes the obfuscated style name more readable (e.g. styles__todo-item__2RWzh
) and aids in debugging.
9. Prefer unit tests to e2e tests
Unit (spec) tests are faster to run. Where possible prefer to write unit tests rather than protractor e2e tests. Our project has over 300 protractor tests that take around 15 minutes to run on Travis. There are some circumstances where e2e tests are preferable, for example testing routing, testing the interaction between components or testing style / layouts. For unit tests you can manually compile angular templates then use jQuery operations to query or interact with the generated DOM element.
Read my article on the component driver pattern for AngularJS
Example code
A reference project is available at https://github.com/phdesign/todo-better-angular, using webpack and ES5 JavaScript.