Не повторяйся

В разработке программного обеспечения существует концепция DRY: Don't Repeat Yourself. Это означает, что вам следует повторно использовать функциональность, а не писать ее с нуля. Однако я часто вижу, что этот принцип игнорируется, когда дело доходит до создания компонентов, что странно, поскольку компоненты существуют именно для этого.

При переходе от одного компонента к другому в вашем приложении меняются два параметра: Поведение и Контент.

Когда поведение меняется, вам нужно написать новую функциональность, чтобы выразить это поведение пользователю. Два поведения не часто используют один и тот же контент, но часто одно и то же поведение используется в нескольких местах.

Создание модального окна

Возьмем простой вариант использования - скромное модальное окно. Одно модальное окно предупреждает пользователя о том, что ему необходимо зарегистрироваться, и представляет форму для этого. В другом модальном окне отображается информация о продукте, по которому они щелкнули.

Поведение в обоих случаях одинаково: предоставить пользователю контент в модальном окне, который можно закрыть одним щелчком мыши. Все модальные окна имеют одинаковый стиль, но содержимое немного отличается. Давайте посмотрим, как код может выглядеть для обоих:

Обратите внимание, что они функционально одинаковы, но реализация немного отличается? У одного есть накладка, у другого нет. На одном есть кнопка «закрыть», а на другом - «X», чтобы закрыть ее. Разметка и стиль у них различаются. Это типично для проектов, в которых не используются повторно компоненты, и приводит к отсутствию единообразия брендинга и взаимодействия с пользователем.

Это разумно надуманный пример, поэтому код очень похож. Вряд ли здесь сходство осталось бы незамеченным. Но на самом деле модальные окна были бы более сложными, а различия более заметными. Иногда различия между двумя компонентами настолько велики, что мы не замечаем сходства.

Давайте вместо этого рассмотрим, как мы бы хотели выглядеть.

Это намного яснее и последовательнее. Мы можем установить заголовок как свойство, а содержимое будет помещено в теги «my-modal». Так как же будет достигнута эта магия?

Тег ng-Content

Этот маленький ребенок - один из помощников Angular в проектировании контента. Он действует как заполнитель для всего, что вы помещаете в открывающий и закрывающий теги объявления текущего компонента. Давайте посмотрим на реализацию my-modal:

Ключевым моментом здесь является тег ng-content, который упрощает и упрощает добавление контента. Когда ваш компонент визуализируется, тег ng-content заменяется тем, что вы вставили между открывающим и закрывающим тегами компонента.

Итак, учитывая следующий компонент:

Если я включу этот компонент так:

Тогда это выглядит так:

В этом случае мы обходимся простой привязкой свойств для заголовка, но что, если нам нужно что-то более сложное? Что, если заголовок должен включать подзаголовок? Давайте покажем пример того, как это может работать:

Здесь в заголовке есть дополнительный тег ng-content, за исключением того, что у него есть свойство select. Это указывает модальному компоненту найти что-либо, соответствующее этому селектору, и включить его сюда. Вы можете использовать любой селектор CSS, что позволяет нам легко иметь несколько слотов контента в нашем модальном окне.

Проблема решена, да?

Шаблоны

Тег ng-content хорош для модального окна, но как насчет списка? Я часто обнаруживал, что повторяю ту же логику для списков просто потому, что элементы списка были немного разными, и я не знал ничего лучше. Несмотря на признание сходства между двумя похожими компонентами списка, я не знал, как создать единый общий компонент, объединяющий общую логику.

Оказывается, сделать это довольно просто. Вот очень простой пример, отражающий логику * ngFor:

Давайте разберемся, что происходит:

  1. Компонент списка включает тег ng-template, в котором должно отображаться содержимое каждого элемента.
  2. Тег ng-template списка предоставляет выход, который сообщает ему, где найти фактический шаблон. Эта часть может сбивать с толку, поскольку вы предполагаете, что тег под названием ng-template на самом деле является шаблоном. Это так, но в данном случае это скорее заполнитель, откладывающий фактическую часть шаблона на что-то еще.
  3. Мы передаем в компонент templateRef через свойство ввода template. Это ссылка на наш фактический шаблон (бит, содержащий HTML-код, который мы хотим отобразить для каждого элемента)
  4. Чтобы передать каждый элемент массива в наш шаблон, мы присоединяем объект контекста к внутреннему ng-template.
  5. Каждый раз, когда мы потребляем этот компонент списка, мы создаем еще один ng-template, за исключением того, что на этот раз мы фактически предоставляем разметку, которую хотим отобразить. Свойство item доступно через контекст, поэтому мы можем использовать его для отображения данных строки, и мы передаем templateRef, давая шаблону productTemplate идентификатор.

Все вышеперечисленное кажется излишним, если просто объединить простой цикл ngFor. Но давайте посмотрим на более сложный пример, основанный на реальном проекте, над которым я недавно работал:

В этом примере происходит гораздо больше:

  • Теперь существует шаблон по умолчанию, так как большинство данных, которые мне нужно было поместить в эти списки, имели аналогичные основные поля (имя и описание).
  • Элементы можно удалять
  • Элементы могут содержать ссылку на другую страницу
  • Строки с нечетными номерами должны иметь другой цвет фона.
  • Предметы можно сгруппировать по категориям
  • Но иногда мне нужен отдельный шаблон для каждого элемента

Этот конкретный проект на самом деле был перестройкой - когда я впервые попробовал это, я фактически повторил большую часть вышеупомянутой логики с помощью копирования и вставки. Это сделало обслуживание этих компонентов очень громоздким и ненадежным. Когда у меня появилась возможность написать это снова с нуля, я постарался сделать это правильно.

Представьте, что вся эта логика повторяется полдюжины раз в вашем приложении. Вышеупомянутый компонент означает, что мой шаблон для списка теперь состоит только из одной строки. Я могу настроить список или легко расширить функциональность списков по всему проекту. Накладные расходы на создание настраиваемого компонента окупились, как только я использовал компонент списка во второй раз.

Заключение

Я был бы удивлен, если бы это было все, что нужно знать о проекции контента в Angular - я недостаточно умен, чтобы все это усвоить. Но это информация, которую я в 2018 году хотел бы узнать, поэтому я в 2019 году записываю ее для меня в 2020 году на случай, если я забуду. Надеюсь, это сделает вашу жизнь немного проще!