AngularJS and $q: get hard-coded values asynchronously — like they came from server side
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?
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.