Конструкция «controller as» в AngularJS
15 Jun 2016Сегодня хочу поделиться информацией о конструкции «controller as» в AngularJS, для чего его понадобилось добавлять и разобрать внутренее устройство его работы.
Что это за конструкция «controller as»
Как уже было выше сказано,- это специализированный синтаксис описания контроллера (например, в директиве ng-controller
), который позволяет результат вызова конструктора неявно присвоить в поле сопутствующего (локального) $scope
.
Напомню, что при создании каждого контроллера, в функцию $controller
передаётся объект locals
, который содержит в себе поля $scope
, $element
, $attrs
и $transclude
, присущие экземпляру создаваемого контроллера.
Если ранее, чтобы отобразить данные, предоставляемые контроллером, нам необходимо было явно инжектить $scope
и инициализировать его поле:
app.controller('ProductCtrl', ['$scope', function ($scope) {
$scope.products = ['Apple', 'Banana', 'Milk', 'Coffee'];
}]);
<div ng-controller="ProductCtrl">
<ul>
<li ng-repeat="product in products">
{{ product }}
</li>
</ul>
</div>
Apple
Banana
Milk
Coffee
То теперь, благодаря новому синтаксису, мы можем внутри контроллера оперировать указателем this
, а результат будет автоматически присвоен полю локального $scope
:
app.controller('ProductCtrlAs', [function () {
this.products = ['Apple', 'Banana', 'Milk', 'Coffee'];
}]);
<div ng-controller="ProductCtrlAs as ctrl">
<ul>
<li ng-repeat="product in ctrl.products">
{{ product }}
</li>
</ul>
</div>
Apple
Banana
Milk
Coffee
Важно: В случае ручного вызова
$controller
, вторым параметром необходимо явно передавать объект, содержащий поле$scope
(см. тест'should throw an error if $scope is not provided'
).
Для чего потребовался новый синтаксис
Необходимость данной возможности обусловлена несколькими причинами:
Общая область видимости $scope
’ов вложенных контроллеров при пересечении наименований:
<div ng-controller="TopCtrl">
{{ title || 'undefined;' }}
<div ng-controller="MiddleCtrl">
{{ title || 'undefined;' }}
<div ng-controller="BottomCtrl">
{{ title || 'undefined;' }}
</div>
</div>
</div>
В том случае, когда одна из переменных не инициализирована, то на её замену ищется одноимённая из родительского $scope
($parent
), что может приводить к следующему результату:
I am top ctrl!
I am top ctrl! ($scope MiddleCtrl не содержит значение title, поэтому оно берётся из родительского $scope)
I am bottom ctrl!
Таким образом, в коде появляется неоднозначность и сложность отслеживания источника данных. Для этого, мы можем переписать код представления с использованием синтаксиса «controller as», явно указывая какая переменная из какого $scope
должна браться:
<div ng-controller="TopCtrlAs as top">
{{ top.title || 'undefined;' }}
<div ng-controller="MiddleCtrlAs as middle">
{{ middle.title || 'undefined;' }}
<div ng-controller="BottomCtrlAs as bottom">
{{ bottom.title || 'undefined;' }}
</div>
</div>
</div>
Как итог, переменная будет искаться исключительно в $scope
связанного контроллера:
I am top ctrl!
undefined
I am bottom ctrl!
Внутреннее устройство и реализация
На этапе создания экземпляра контроллера с помощью $controllerProvider
, в том случае, когда первый параметр является строкой (строковое выражение контроллера), проводится поиск вхождения в ней частей регулярного выражения:
var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
[0] TopCtrlAs as top
[1] TopCtrlAs
[2] as top
[3] top
С помощью первого ([1] TopCtrlAs
) элемента (наименование контроллера) ищется зарегистрированный ранее в объекте controllers
конструктор контроллера и создаётся его экземпляр.
При наличии третьего ([3] top
) элемента (наименование поля в $scope
, которому присвоить результат вызова конструктора), вызывается функция addIdentifier
, которая делает простое присвоение:
locals.$scope[identifier] = instance;
Тестовое покрытие
Описанное выше поведение успешно покрыто следующими тестами:
Поиск зарегистрированного ранее контроллера, результат вызова конструктора присваивается в поле foo
объекта $scope
.
it('should publish controller instance into scope', function() {
var scope = {};
$controllerProvider.register('FooCtrl', function() { this.mark = 'foo'; });
var foo = $controller('FooCtrl as foo', {$scope: scope});
expect(scope.foo).toBe(foo);
expect(scope.foo.mark).toBe('foo');
});
Обязательность явной передачи локального объекта $scope
в случае использования синтаксиса «controller as»:
it('should throw an error if $scope is not provided', function() {
$controllerProvider.register('a.b.FooCtrl', function() { this.mark = 'foo'; });
expect(function() {
$controller('a.b.FooCtrl as foo');
}).toThrowMinErr("$controller", "noscp", "Cannot export controller 'a.b.FooCtrl' as 'foo'! No $scope object provided via `locals`.");
});
Бонусы
Как можно заметить из вышесказанного, если не возвращать из конструктора контроллера явно результат, то в локальную переменную $scope
будет присвоен this
. Однако, ничто не мешает нам сделать следующим образом и, всё же, вернуть результат:
app.controller('MainCtrl', function() {
this.message = 'I should be in scope';
return { message: 'Wrong!' };
});
В таком случае, на экран будет выведено именно ‘Wrong!’.
Так же, в процессе разбора исходников, наткнулся на довольно интересный тест, о котором не стоит забывать имея дело с объектами javascript:
it('should throw an exception if a controller is called "hasOwnProperty"', function() {
expect(function() {
$controllerProvider.register('hasOwnProperty', function($scope) {});
}).toThrowMinErr('ng', 'badname', "hasOwnProperty is not a valid controller name");
});
Т.к. регистрация и выявление зарегистрированных ранее контроллеров происходит с помощью объекта controllers
, то запрещено иметь наименование контроллера hasOwnProperty
, дабы в последствии при выявлении controllers[constructor]
не вернуть встроенную функцию языка (или не перегрузить её конструктором контроллера),- весьма предусмотрительно.
См. также:
Follow me online
Join the Discord: https://discord.gg/V4yMGPQzRB
- GitHub: https://github.com/FSou1
- LinkedIn: https://www.linkedin.com/in/maxim-zhukov-dev
- Youtube: https://www.youtube.com/channel/UCz6AZvABoICHPpi43y-Hd5g
- Telegram: https://t.me/seasoneddev