Тонкости AngularJS: select внутри шаблона директивы

в 14:14, , рубрики: angular, AngularJS, javascript, JS, select, директивы

Эта статья будет описывать решение одной конкретной задачи, а также на примере показывать как работает $transclude.

Задача такая: сделать директиву, обертку для select-а. Предположим, что мы хотим одним тегом добавлять сразу и селект и label к нему (потом можно будет туда добавить ошибки заполнения, но мы для простоты не будем этого делать). В общем то, на первый взгляд выглядит все просто.

Сделаем директиву и назовем ее field. Использовать будем так:

<field title="Цвет" type="select" ng-model="selectedColor" options="color.id as color.name for color in colors"></field>


Мы придумали несколько атрибутов:

  • title — Название поля
  • type — Тип поля (у нас будет только select, но вдруг потом...)
  • ng-model — Переменная, которая хранит выбранное значение
  • options — Чтобы задать список для селекта мы будем пользоваться синтаксисом англяровского ngOptions

Обратите внимание, что переменные selectedColor и colors из примера, это переменные scope в котором мы использовали директиву. Мы специально их указали через атрибуты, чтобы директива получила к ним доступ.

Код директивы:

angular.module('directives').directive('field', function () {
return {
	//директива - это название тега
	restrict: 'E', 

	// то, что мы передали в директиву
	scope: {
		ngModel: "=", 
		title: "@",
		type: "@",
		options: "@"
	},

	// адрес шаблончика
	templateUrl: '/tpl/fields/select.html',
});

Код шаблона:

<div class="field">
	<label>{{title}}</label>
	<select ng-model="ngModel" ng-options="{{options}}"></select>
</div>

Вживую: codepen.io/Dzorogh/pen/umCKG?editors=101

Вроде, все выглядит просто и должно работать. Запускаем, проверяем в отладчике —

<field title="Цвет" type="select" ng-model="selectedColor" options="color.id as color.name for color in colors">
	<label>Цвет</label>
	<select ng:options="color.id as color.name for color in colors" ng:model="ngModel"></select>
</field>

Вроде неплохо… только где все options? Почему colors — пуст (точнее, undefined)?

Дело в том, что когда мы указываем в директиве параметр scope, то все тело директивы оборачивается каменной стеной — isolate scope. Это изолированый скоуп не дает тем, кто внутри, видеть внешние scope. Но, соответственно, все те переменные (ngModel, type, title, options) что мы указали параметре директивы, будут добавлены в наш изолированый scope и «привязаны» к внешним переменным.

И тут мы можем увидеть проблему — мы не добавили colors к тем переменным. И это правильно, директива, разумеется, не должна знать что там за массив мы хотим использовать. Мы уже указали, что используем этот массив, написав его в атрибуте options.

Чтобы получить доступ к массиву, мы должны хотя бы знать название переменной. Поэтому самый лучший вариант — парсить options. Нам тут помогает исходный код angular.js, там есть регулярка для ngOptions.

Но подождите, даже получив название переменной из внешенго scope, как мы сможем ее добавить внутрь?

Собственно, самое интересное

Чтобы связывать внешние переменный с внутренними после иницализации директивы нужно использовать такую штуку, как "$transclude". Вызов этой функции дает доступ к внешнему скоупу и можно просто получить любую переменную оттуда и «передать» в скоуп директивы.

Новая директива, с регуляркой и трансклюдом:

.directive('field', function() {

  // позаимствовано из исходников angularjs
  var NG_OPTIONS_REGEXP = /^s*(.*?)(?:s+ass+(.*?))?(?:s+groups+bys+(.*))?s+fors+(?:([$w][$w]*)|(?:(s*([$w][$w]*)s*,s*([$w][$w]*)s*)))s+ins+(.*?)(?:s+tracks+bys+(.*?))?$/;
  };

  return {
    restrict: 'E',
    scope: {
      ngModel: "=",
      type: "@",
      title: "@",
      options: "@"
    },

    // "Включаем" возможность использовать трансклюд
    transclude: true,
    templateUrl: '/fields/select.html',

    // $transclude всегда идет 5м параметром.
    link: function (scope, element, attrs, controller, $transclude) {
      if (scope.type == 'select') {
        // Парсинг названия массива значений
        var parsedOptions = attrs.options.match(NG_OPTIONS_REGEXP);

        // убираем фильтры (они пишутся после | )
        var optionsArray = /^(.*) |/.exec(parsedOptions[7]) || parsedOptions[7];

        // optionsArray - название (только название) массива, в котором содержится список значений.
        // чтобы получить к нему доступ, нужно взять его из общего Scope
        // (того, где появилась директива) и добавить в наш.
        $transclude(function (clone, $outerScope) {
          scope[optionsArray] = $outerScope[optionsArray];
        });
      }
    } 
  }
});

Конечный вариант в CodePen: codepen.io/Dzorogh/pen/gBbyI

Автор: Dzorogh

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js