This is a pattern for writing sane and predictable Angularjs code coming from someone who had a hard time with JavaScript and Angularjs.
Recently, upon joining Headspin, I have had the chance to work exclusively in Javascript and AngularJS, both of which I had little experience. At Headspin, we are trying to solve a unique problem for app developers – debugging mobile apps over global networks in real-time. The web UI and data dashboard is a very crucial part of what we do.
However, as a part of learning JavaScript and Angularjs, it took me longer than I wanted to to wrap my mind around all the scopes and the states of an Angular application, which were sprinkled everywhere in the code and can be mutated from almost any where. I ended up writing ugly JavaScript which I wasn’t proud of and it was less fun reading it. The vicious cycle kept spiraling down the blackhole for me like its digest
counterpart as I got more involved.
Finally, I felt that it was time to stop what I was doing before I fell deeper into the abyss and reflect on what went wrong.
I started by pinpointing some of the things which got in the way of my learning and understanding of the framework and also the JavaScript Language itself. I came up with a rough laundry list:
- unrestricted mutability surface
- bi-directional data flow
- lack of clear lines between controllers and services
On Complexity
It is natural for humans to simplify in order to understand. We are generally bad at keeping up with complexity, let alone multitasking.
When one is faced with complexity, the right thing to do is to minimize the “surface area” on which one is forced to interface with things at hand. For instance, in the film 300, king Leonidas tactically led his small group of three hundred warriors into a narrow gap between the cliffs and managed to hold back millions(?) of Persian soldiers. Regardless of whether it’s a fact or fiction, this tactic of minimizing the attack surface is a brilliant but obvious one in the face of complexity, or in our case, a number of moving parts in the code trying to change the state of the application.
Javascript, being a haphazard functional language as it is, does not do a great job at restricting mutations. This result is what can and often be seen in an Angularjs or any Javascript code:
class FooService {
constructor() {
this.state = "foo";
}
addBaz() {
this.state = this.state + " baz";
}
addBar() {
this.state = this.state + " bar";
}
_addBaz() {
this.addBaz();
}
// this goes on ...
}
angular.module("Foo").service("FooService", FooService);
Obviously, this is very cumbersome, but shamelessly it’s how I often did just to get things done and dreamed of refactor later, since it is so easy to add another “shortcut” method to achieve what I want.
Things become much worse when you inject a service into a controller and put yourself in an awkward situation of having to decide which is in charge of managing the application state.
function FooController ($scope, FooService) {
$scope.FooService = FooService;
$scope.addBaz = () => {
FooService.addBaz();
// or you can do this
// $scope.FooService.addBaz();
}
}
angular.module("Foo").controller("FooController", FooController);
I learned later that controller should act as a “dispatcher” while service can be seen as a persistent layer. However, this is not reflected or encouraged enough in AngularJS. It is very easy to create a fat service that does the job of controller and inject it into a controller that solely acts as a puppet.
For example, where does one draw a line between a controller and service? When is it appropriate to inject a service into a controller, and use the controller’s functions as the API and when to just directly use the service instance attached to the controller’s scope to call its own inner methods? In another word, what is stopping us from doing:
<div ng-controller="FooController">
<!-- Using controller's service instance as API to state -->
<button ng-click="FooService.addBaz()">Add Baz from Svc</button>
<!-- INSTEAD OF-->
<!-- Using controller's method as API to state -->
<button ng-click="addBaz()">Add Baz from Ctrl</button>
</div>
or this:
<div ng-controller="FooController">
<!-- Using controller as a state container -->
<p>{{state}}</p>
<!-- INSTEAD OF -->
<!-- Using the controller's service instance as container -->
<p>{{FooService.state}}</p>
</div>
Start Using Component Now
From Angularjs 1.5 onward, the framework introduced components and encourage their usage over directives. Components have less functionalities and were designed with an isolate scope and encourage one-way data bindings. A component’s scope is always isolated from the outside world and “inlets” are controlled solely via bindings:
function FreeChildController () {
this.inTheMood = false;
}
let FreeChildComponent = {
controller: FreeChildController,
bindings: {
inlet: "<"
},
template: "<h1>{{$ctrl.inTheMood ? $ctrl.inlet : 'nanana'}}</h1>"
}
With this, the enclosing scope of the ParentController
can only interact, unidirectionally through the FreeChildComponent
’s bound attribute inlet
while the component has no business meddling with the outside scope.
<div ng-controller="ParentController as parent">
<free-child inlet="parent.complaint"></free-child>
</div>
The Elm’s Way
As I've mentioned, before I jumped into AngularJS, I have had the chance to code in Elm, an ML-like reactive language that compiles to Javascript. What was most notable about it is its architecture, which promotes uni-directional data flow and very sane state cycle. This architecture itself has inspired Redux, a state container add-on well known in the React community.
Elm’s architecture consist of three parts – Model, Update, and View.
Model
The model is the single source of truth or the state of the existing application. In Elm, the model is often defined as a record instance (similar to an object in Javascript). Since Elm is a pure functional language, the model never gets mutated in-place. Every update to the model return a new instance of the modified model and pass it to Elm runtime (liken to AngularJS’s digest cycle).
Update
Update is perhaps the most interesting part of an Elm’s application. It is a single function accepting a Msg
type and the model as arguments, pattern-matching the message received to those pre-defined in the Msg
Union type, and return a modified model. This is the only part the model’s state gets modified.
View
In Elm, you don’t write HTML markup. Elm’s views are also just pure functions which accept the model and return an instance of Html
and Msg
, which get rendered to HTML DOM by its runtime. Below is a basic snippet of a simple counter app in Elm.
main =
beginnerProgram { model = 0, view = view, update = update }
view model =
div []
[ button [ onClick Decrement ] [ text “-” ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text “+” ]
]
type Msg = Increment | Decrement
update msg model =
case msg of
Increment -> model + 1
Decrement -> model – 1
It is almost readable without any knowledge of Elm.
There are other approach to achieving similar behavior in JavaScript, but Elm had succeeded most gracefully due to the design of the language itself.
Restructuring AngularJS
Before I go on, I’d like to be clear that this is an opinionated pattern. This pattern is not meant to be a framework, module, or even a rule. This can appear as unconventional to Javascript and Angular programmers, but coming from a fresh mind like mine I have nothing but a strong urge to improve my affair with Angular.
With that being said, here is a few things I would do going forward with AngularJS:
Model
- A service should act as a very thin store or state container, and should be injected into a controller which work as the store manager to provide the API to the state.
- A service should return a closure of a constructor of the store instead of setting its internal state in a implicitly so that the starting state and messages option can be injected from a controller or unit test.
- A service’s state should only be updated via an
update
function in the controller, which send a message string to be matched in the service’s messages object and trigger the appropriate pure function. This means the store controller contains only one function. - The model should be a single object – a source of truth – grouping all the properties and gets updated and returned as a whole.
// ES6 class
class StoreSvc {
constructor () {
return (initState, messageOpts) => {
this.model = initState;
this.messages = MessageOpts;
return this;
}
}
}
app.module("myModule").service("StoreSvc", MyStore);
Apart from being easier in to test the service, I also found this approach to encourage delegating the task of initiating the state to some other entity. The most important thing to note is this pattern makes the service becomes a very generic persistent state layer with zero functionality. What defines each service is the messages object passed in during instantiation, which is decided by the controller in control of the service. This means how an application interacts with the state is up to the controller providing the descriptive messages
map. This hence become the API to the application model, held by the service and controlled by the controller.
This is an example of a controller “attaching” to the store service and providing an API to the model:
function StoreController (StoreSvc) {
// provide a starting model state
let model = {
name: "",
age: 0
};
// provide a messages object aka API to the model
let messages = {
SetName : ((model, name) => Object.assign(model, {name: name})),
SetAge : ((model, age) => Object.assign(model, {age: age}))
};
// initiate a store
this.store = StoreSvc(model, messages);
}
In the messages
object, the keys are capitalized on purpose to distinguish them from other object keys. Here Object.assign
is used to merge the existing model with the object containing the property that needs updating and return the clone, which is a functional approach versus the traditional mutation of the model.
Update
The controller contains only one function, namely
update
(it can be any name), which sends the appropriate message to trigger a pure function in themessageOpts
, an object mapping message keys to functions. Theupdate
function is the only place in the application mutate the service's model.The controller initiates the starting model state and messages mapping (or use another service to fetch the data, possibly via
$http
) by injecting them into the service’s constructor.Ideally, the store controller should take care of updating the store service only and should not worry about managing the DOM/component. That should be the component’s controller’s job.
Here is what a basic update
function may look like:
this.update = (message, model, ...args) => {
if (message in this.store.messages) {
this.store.model = this.store.messages[message](model, ...args);
}
}
View
- Components is strongly preferred over directives.
- In a component, a UI-driven action should always call an appropriate function bound to the store’s controller’s update function with the right message and argument(s).
- A component can interpolate the data in the model from the store controller’s binding.
- Only use one-directional bindings (
<
) to let in data from an enclosing store controller’s scope. A component has no business changing anything outside of itself. - Bi-directional bindings such as
ngModel
should be used with caution. In the example code, it is abandoned in favor of a suite ofngKeydown
,ngKeyup
, and$event.key
.
Here is how a component might look like:
let storeDashboard = {
controller: myStoreController,
bindings: {
title: "<"
},
template: `
<h4>{{$ctrl.title}}</h4>
<ul>
<li>
{{$ctrl.store.model.name}}
<input ng-model="$ctrl.store.model.name">
</li>
<li>
{{$ctrl.store.model.age}}
<button ng-click="$ctrl.update('SetAge', $ctrl.store.model, 0)">Reset</button>
</li>
</ul>
`
}
It is also useful to refactor the update
function to return the controller’s instance.
this.update = (msg, model, ...args) => {
if (msg in this.store.messages) {
let newModel = this.store.messages[msg](model, ...args);
// model mutation happens here
this.store.model = newModel;
}
return this;
}
}
Now it is possible to chain update actions in a single directive call in the DOM:
<button type="button"
ng-click="$ctrl
.update('Decrement', $ctrl.store.model)
.update('Attach', $ctrl.store.model)">
-
</button>
Simplified code = Predictable State
With this pattern, it is much easier to trace how the model gets mutated as a group of state. The controller becomes very lean, since all the local functions are refactored and grouped into the messages object as pure functions and let update act as a single immutability surface, thus super simple to debug. The meat of the application is condensed into the messages
object, a map of message strings and to preferably small, self-contained pure functions that return the new model object.
To recap, here is a simple counter app portraying the three parts as Model-View-Update. I went all the way of avoiding ngModel
for other key events instead, which is lagging but I felt get my point across about avoiding bidirectional bindings).
This one demonstrate a full pattern of a store service with a controller providing the API which enclose a component’s controller’s scope and send in restricted values and functions through the component’s input bindings.
Conclusion
It is worth saying again that this pattern is a just a personal exploration resulting from my own caveats working with JavaScript and Angularjs and an attempt to overcome it.
You can grab code from the github repo (not completed yet though).
Originally published here.
Top comments (0)