Конструкция «controller as» в AngularJS
Сегодня хочу поделиться информацией о конструкции «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]
не вернуть встроенную функцию языка (или не перегрузить её конструктором контроллера),- весьма предусмотрительно.
См. также: