AngularJS and $q: get hard-coded values asynchronously — like they came from server side

Radek Anuszewski
Frontend Weekly
Published in
5 min readFeb 21, 2017
Photo credits: Deviq.com: Open-Closed Principle

One of the features of $q service from Angular is performing synchronous tasks in “asynchronous” way. Important use case is loading hard-coded data in way, which needs less changes in code when requirements change — for example, when hard-codes list of types should be modified and is stored in database.

The worst possible way

And in the same time, still quite popular is putting values directly in HTML:

<select 
ng-model="$ctrl.typeId"
>
<option ng-value="1">Small</option>
<option ng-value="2">Big</option>
</select>

Which is even easier today because from 1.6.0 version Angular supports non-string values with select and ng-value (which is a great thing itself). But forget about this quasi-solution as soon as possible.

Storing hard-coded values directly in controller

Most often this is a result of copy-pasting code:

function Controller () { 
var $ctrl = this;
$ctrl.types = [
{
id: 1,
text: "Small",
},
{
id: 2,
text: "Big",
},
];
}
<select
ng-model="$ctrl.typeId"
>
<option
ng-repeat="type in $ctrl.types"
ng-value="type.id"
>
{{type.text}}
</option>
</select>

(Personally I love trailing commas, so happy they are in function parameter list in ES 2017/JS 7!)
And it is hard to maintain, possible changes have to be made in many places, so many of us found useful to:

Store hard-coded values in separated service or as a constant

And it’s a nice solution, cause hard-coded values are delivered from single place. So separated service:

function TypeService () { 
var types = [
{
id: 1,
text: "Small",
},
{
id: 2,
text: "Big",
},
];

function getTypes () {
return angular.copy(types);
}

return {
getTypes: getTypes,
};
}
// And then in controller
function Controller (TypeService) {
var $ctrl = this;
$ctrl.$onInit = function () {
$ctrl.types = TypeService.getTypes();
};
}

And Angular constant:

app.constant("TypesConstant", [
{
id: 1,
text: "Small",
},
{
id: 2,
text: "Big",
},
];
);
// And then in controller
function Controller (TypesConstant) {
var $ctrl = this;
$ctrl.$onInit = function () {
$ctrl.types = angular.copy(TypesConstant);
};
}

I hope you use components and are familiar with $onInit hook: in some cases $onInit can be really handy. It looks nice, it is delivered from one place so future changes have to be made only here. Moreover, maybe select itself should be a component? Why not, it will help to provide consistency around all application (remember to prefix your directives/components):

function SimpleSelectController () { 
var $ctrl = this;
$ctrl.$onChanges = function (changes) {
if (changes.elements && Array.isArray(changes.elements))
$ctrl.options = angular.copy($ctrl.elements);
}
$ctrl.change = function () {
$ctrl.$onChange({
value: $ctrl.value,
});
}
}
app.component("raSimpleSelect", {
controller: SimpleSelectController,
templateUrl: 'ra-simple-select.component.html',
bindings: {
options: '<',
name: '@',
label: '@',
},
});
//Template
<label for="{{$ctrl.name}}">
{{$ctrl.label}}
</label>
<select name="{{$ctrl.name}}" id="{{$ctrl.name}}"
ng-model="$ctrl.value"
ng-change="$ctrl.change()"
>
<option
ng-repeat="option in options"
ng-value="option.value"
>
{{option.name}}
</option>
</select>

And usage:

function Controller (TypesConstant) { 
var $ctrl = this;

$ctrl.$onInit = function () {
var types = angular.copy(TypesConstant);
$ctrl.types = types.map(_mapToSelectFormat);
}
// Because select component expects object with "name" and "value"
// Every array element has to be mapped
function _mapToSelectFormat (type) {
return {
name: type.text,
value: type.id
};
}

$ctrl.updateModel = function (value) {
$ctrl.typeId = value;
};
}
//In HTML
<ra-simple-select
elements="$ctrl.types"
on-change="$ctrl.updateModel(value)"
></ra-simple-select>

(SimpleSelectController uses $onChanges, not $onInit, because $onChanges is much better choice when component input value is loaded asynchronously)
Now we are sure that every select has label with for attribute, and of course it can go further — we could “encapsulate” styles, additional behavior like remember initial value with button to restore it… But what is this paragraph about?

Back to the merits — is something wrong with service/constant solution for hard-coded values?

Photo credits: Imgflip meme generator

War never changes — but requirements tend to change frequently

And what is hard-coded today, tomorrow can be loaded from server/Firebase/whatever. Also, it is common to do not have everything when project starts (for example, types are “Small” and “Big” — but our customer would like to have additional types like “Extra small”, “Medium” and “Extra big”). Or it is even possible to create separate module for types, cause our customer wants to change it on her/his own. Or it could be a company policy to keep any static values provided by customer in database — and I think it may be the most common use case, but unless customer deliver its values, they will be hard-coded somewhere.

Open Closed Principle

Brain surgery is not necessary when putting a hat. But with service/constant version, when source of types changes, it requires changes in controller’s code:

function Controller (TypeService) { 
var $ctrl = this;
$ctrl.$onInit = function () {
$ctrl.types = TypeService.getTypes();
};
}

Will become:

function Controller (TypeService) { 
var $ctrl = this;
$ctrl.$onInit = function () {
TypeService.getTypes().then(_bindTypes);
};

function _bindTypes (types) {
$ctrl.types = types;
}

}

But why do not treat types as loaded asynchronously just from the beginning? It will save us from redundant changes in code which uses types, and $q service is our ally.

Treat synchronous values as asynchronous with $q service

So how it can be done? Surprisingly easy. Controller loads types as they are loaded with some AJAX call:

// Other code
$ctrl.$onInit = function () {
TypeService.getTypes().then(_bindTypes);
};

function _bindTypes (types) {
$ctrl.types = types;
}

But mentioned earlier TypeService now creates promise with some hard-coded value:

function TypeService ($q) { 
var types = [
{
id: 1,
text: "Small",
},
{
id: 2,
text: "Big",
},
];

function getTypes () {
return $q.when(types);
}

return {
getTypes: getTypes,
};
}

And, from Controller point of view, there is no difference between $q.when(types) and:

function getTypes () { 
return $http.get(typeUrl).then(_getOnlyData);
}
function _getOnlyData (response) {
return response.data;
}

So future changes will take place only in TypeService. Simple, easy to understand and easy to follow.
At this point, I need to say thank you to Codelord for his article, $q.defer: You’re doing it wrong. I realized I can use when() instead of defer()— one line of code instead of four.

Conclusion

I really encourage you to always return a promise, whether possibility to switch from hard-coded value to loaded value exists or not. It’s easier to remember that every service returns promise and we need to use then(), rather than looking into service file every time we forget what it returns, it’s easy to follow and it makes our code more loosely coupled — does not depend on current service implementation.

--

--

Radek Anuszewski
Frontend Weekly

Software developer, frontend developer in AltConnect.pl, mostly playing with ReactJS, AngularJS and, recently, BackboneJS / MarionetteJS.