angularjs-directive – Stepan Suvorov Blog https://stepansuvorov.com/blog Release 2.0 Sun, 22 Jan 2017 11:36:32 +0000 en-US hourly 1 https://wordpress.org/?v=6.3.1 Understanding of component/directive attribute binding types https://stepansuvorov.com/blog/2016/02/understanding-of-componentdirective-attribute-binding-types/ https://stepansuvorov.com/blog/2016/02/understanding-of-componentdirective-attribute-binding-types/#comments Tue, 23 Feb 2016 20:29:02 +0000 http://stepansuvorov.com/blog/?p=3036 Continue reading ]]> You probably know that directive component can have 4 different attribute-scope bindings:

  • @” – bind a local scope property to the value of DOM attribute
  • =” – set up a bidirectional binding between a local scope property and an expression passed via the attribute (also for collection “=*” and “=?” if attribute is optional)
  • <” – set up a one-way (one-directional) binding between a local scope property and an expression passed via the attribute
  • &” – provides a way to execute an expression in the context of the parent scope

So I decided to uncover magic of this symbols and recreate their functionality by using attributes from the link function.

Let’s take an example of a directive with all binding types:

[javascript]
app.directive(‘myDir’, function() {
return {
scope: {
x: ‘@’,
y: ‘=’,
z: ‘<‘,
f: ‘&’
},
template
};
});
[/javascript]

template could be something like this:

<div>{{x}} <input ng-model="x"></div>
<div>Hello {{y}}  <input ng-model="y"></div>
<div>Hello {{z}}  <input ng-model="z"></div>
<div>Hello {{f()}}  <input></div>

and integration is just:

<my-dir x="Hello {{name}}" y="name" z="name" f="getName()"></my-dir>

Now we will create directive without all this scope magic param notation, but with the same binding functionality, just by using attributes of link function:

<my-dir2 x="Hello{{name}}" y="name" z="name" f="getName()"></my-dir2>

and the definition:

[javascript]
app.directive(‘myDir2’, function($interpolate, $parse) {
return {
scope: {},
template,
link: function(scope, element, attrs) {}
};
});
[/javascript]

Inside the link function we will use scope.$parent – link to parent scope and attrs – directive attributes.

@

Value binding is simple, only thing that we need to do is to observe attribute and update the value:

[javascript]
attrs.$observe(‘x’, value => scope.x = value);
[/javascript]

to have immediate access inside link function we should probably also add:

[javascript]
scope.x = $interpolate(attrs.x)(scope.$parent);
[/javascript]

we are using $interpolate service here that will parse expression that you put into you attribute( attrs.$observe will do it by default).

<

One way binding. The same logic like we did for first one, only in this case we need to use $watch instead of $observe (now it’s property, not attribute change) and $parse instead of $interpolate(only one property not the expression):

[javascript]
scope.$watch(() => $parse(attrs.z)(scope.$parent), newParentValue => scope.z = newParentValue);
[/javascript]

and again to make it accessible in link function:

[javascript]
scope.z = $parse(attrs.z)(scope.$parent);
[/javascript]

=

Two-way binding. The most difficult one, because you need to synchronise value of the parent scope as well. Comparing to previous one you also need to store lastValue to understand which value has been updated: parent or directive one.

So first lest just try to parse the attribute:

[javascript]
expressionFn = $parse(attrs.y);
[/javascript]

after we can get attribute value

[javascript]
scope.y = expressionFn(scope.$parent)
[/javascript]

and store it like a lastValue:

[javascript]
lastValue = scope.y;
[/javascript]

now we should setup a watcher to check whether something was changed:

[javascript]
scope.$watch(() => {
let parentValue = expressionFn(scope.$parent);
if (angular.equals(parentValue, scope.y)) {
return;
}
});
[/javascript]

so if parentValue and scope.y are the same we finish the function, but if not we need to synchronise either parent or directive scope. How to define whine one to synchronise? We will make comparison with lastValue:

[javascript]
if (!angular.equals(parentValue, lastValue)) {
scope.y = parentValue;
}
[/javascript]

if they are not equal – we should sync directive scope property, otherwise – parent property. But how to do it? It could be done with help of special assign method:

[javascript]
expressionFn.assign(scope.$parent, parentValue = scope.y);
[/javascript]

And now all together:

[javascript]
let expressionFn = $parse(attrs.y);
let lastValue = scope.y = expressionFn(scope.$parent);
scope.$watch(() => {
let parentValue = expressionFn(scope.$parent);
if (angular.equals(parentValue, scope.y)) {
return;
}

if (!angular.equals(parentValue, lastValue)) {
scope.y = parentValue;
} else {
expressionFn.assign(scope.$parent, parentValue = scope.y);
}

return lastValue = parentValue;
});
[/javascript]

 

&

Execute with parent scope context. Like a piece of cake:

[javascript]
scope.f = (locals) => $parse(attrs.f)(scope.$parent, locals);
[/javascript]

 

All these examples were shown only to explain you what’s happening inside Angular and you should not use scope.$parent instead of attribute binding notation.

]]>
https://stepansuvorov.com/blog/2016/02/understanding-of-componentdirective-attribute-binding-types/feed/ 1
AngularJS: от directive() к component() https://stepansuvorov.com/blog/2016/02/angularjs-%d0%be%d1%82-directive-%d0%ba-component/ https://stepansuvorov.com/blog/2016/02/angularjs-%d0%be%d1%82-directive-%d0%ba-component/#comments Wed, 17 Feb 2016 21:06:11 +0000 http://stepansuvorov.com/blog/?p=3019 Continue reading ]]> пост был подготовлен на основе статьи Exploring the Angular 1.5 .component() method от Todd Moto.

В Angular 1.5 нам был представлен новый метод .component(), который намного проще чем .directive() и при этом он использует все лучшее по умолчанию. Метод .component() также позволит разработчикам писать в Angular2 стиле, то есть сделает переход на вторую версию максимально безболезненным.

В этом посте мы попробуем параллельно разобрать старый и новый подходы для создания компонентов.

На примере простой директивы counter мы посмотрим как можно создать компонент с аналогичной функциональностью:

[javascript]
.directive(‘counter’, function counter() {
return {
scope: {},
bindToController: {
count: ‘=’
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count–;
}
this.increment = increment;
this.decrement = decrement;
},
controllerAs: ‘counter’,
template: [

<div class="todo">’,
‘<input type="text" ng-model="counter.count">’,
‘<button type="button" ng-click="counter.decrement();">-</button>’,
‘<button type="button" ng-click="counter.increment();">+</button>’,
‘</div>

].join(”)
};
});
[/javascript]

вот как это будет выглядеть:

Объект вместо Функции

Начнем наш анализ со способа задания и обратим внимание на то, что параметры в компонент передаются как объект (а не функция, что было в директиве):

[javascript]
// до
.directive(‘counter’, function counter() {
return {
};
});

// после
.component(‘counter’, {

});
[/javascript]

Scope и BindToController становятся просто Bindings

В директиве мы можем задавать scope 3-мя способами: родительский(скоуп не создается), наследник от родительского, изолированный. Со временем мы приходим к выводу, что изолированный скоуп, где мы четко задаем входящие параметры, наилучший вариант. Так же каждый раз для изолированного скоупа нам приходиться прописывать bindToController, чтобы прокинуть данные со скоупа непосредственно на контроллер директивы.

Свойство компонента bindings позволяет использовать 2 в одном, так как компонент использует изолированный скоуп по умолчанию:

[javascript]
// before
.directive(‘counter’, function counter() {
return {
scope: {},
bindToController: {
count: ‘=’
}
};
});

// after
.component(‘counter’, {
bindings: {
count: ‘=’
}
});
[/javascript]

Controller и ControllerAs

Ничего не изменилось в способе задания контроллера, однако теперь controllerAs параметр по умолчанию, который задан как “$ctrl“: то есть если мы в контроллере напишем:

[javascript]
this.x = 5;
[/javascript]

то в шаблоне компонента потом можно будет обратиться вот так:

[html]
<div>{{$ctrl.x}}</div>
[/html]

Итак, что у нас получилось с контроллером для обоих случаев:

[javascript]
// до
.directive(‘counter’, function counter() {
return {
scope: {},
bindToController: {
count: ‘=’
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count–;
}
this.increment = increment;
this.decrement = decrement;
},
controllerAs: ‘counter’
};
});

// после
.component(‘counter’, {
bindings: {
count: ‘=’
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count–;
}
this.increment = increment;
this.decrement = decrement;
}
});
[/javascript]

Я очень упростил для понимания пункт из статьи, поэтому рекомендую также заглянуть в оригинал.

Шаблоны

В определении шаблонов есть небольшое различие: шаблон компонента может задаваться как функция, в которую инжектятся элемент и атрибуты:

[javascript]
{

template: function ($element, $attrs) {
// access to $element and $attrs
return ‘…’;
}

}
[/javascript]

Улучшенное require

Да, это свершилось! Теперь мы можем задать имя для контроллера, подключаемого к нашему компоненту, и обратиться к нему из контроллера( до этого только из метода link, а в контроллер оно попадало только путем ужасных костылей):

[javascript]
{

require: {
parent: ‘^parentComponent’
},
controller: function () {
// use this.parent to access required Objects
this.parent.foo();
}

}
[/javascript]

В данном случае мы определили подключаемый контроллер на свойстве parent.

Одностороннее связывание

Еще одна фишка компонентов и Angular1.5 это одностороннее связывание, которое определяется следующим синтаксисом:

[javascript]
{

bindings: {
oneWay: ‘<‘,
twoWay: ‘=’
},

}
[/javascript]

если мы задали свойство oneWay таким образом, то оно будет реагировать на изменения внешнего связанного объекта, при этом свои изменения передавать “наружу” не будет. И да, сразу отвечу на вопрос, который у вас наверное появился: работает только в одну сторону.

Нет никакого нового концепта

Если вы посмотрите на исходный код, то увидите что разработчики AngularJS особо не парились и сделали метод component() просто оболочкой над directive().

Обновляемся до Angular2

Как уже было сказано: использование метода .component() серьезно упростит переход на Angular2. Посмотрите как будет выглядеть ваш компонент во второй версии фреймворка(конечно, с новым синтаксисом шаблонов):

[javascript]
var Counter = ng
.Component({
selector: ‘counter’,
template: [

<div class="todo">’,
‘<input type="text" [(ng-model)]="count">’,
‘<button type="button" (click)="decrement();">-</button>’,
‘<button type="button" (click)="increment();">+</button>’,
‘</div>

].join(”)
})
.Class({
constructor: function () {
this.count = 0;
},
increment: function () {
this.count++;
},
decrement: function () {
this.count–;
}
});
[/javascript]

]]>
https://stepansuvorov.com/blog/2016/02/angularjs-%d0%be%d1%82-directive-%d0%ba-component/feed/ 10