[Разработка веб-сайтов, JavaScript, Angular, TypeScript] Давайте сделаем переиспользуемый компонент tree view в Angular

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Окт-2020 16:31

Я разрабатываю несколько Angular-библиотек, поэтому люблю делать простые и легко переиспользуемые решения для разработчиков. Недавно один из подписчиков в Твиттере спросил меня, как сделать компонент, который выводил бы его данные в виде иерархического дерева — tree view. Я люблю подобные задачи, потому что они дают возможность покрыть много различных юзкейсов с минимальным количеством логики внутри. В этой статье опишу, как я размышляю, когда решаю подобные задачи.
Дисклеймер: эта статья-туториал рассчитана на аудиторию изучающих Angular. Если вы понимаете, как сделать рекурсивный тип, рекурсивный компонент и преобразовать в нем данные, переданные функцией-обработчиком, можете ее пропустить.
Итак, что нам нужно?В первую очередь нам надо понять, с какими данными мы будем работать. Что описывает такую древовидную структуру?Здесь первым приходит в голову многомерный массив: если мы встретили в нем элемент, то просто покажем его. Если встретили вложенный массив, то погружаемся на уровень ниже.Давайте опишем такой тип в TypeScript:
export type MultidimensionalArray<string> =
| string
| ReadonlyArray<MultidimensionalArray<string>>;
Это будет работать благодаря TypeScript recursive type references и позволит нам использовать подобную структуру в качестве данных:
readonly items: MultidimensionalArray<string> = [
    "Hello",
    ["here", "is", ["some", "structured"], "Data"],
    "Bye"
];
Каждый элемент это («строка» или массив из («строка» или массив из («строка» или …)))… Добро пожаловать в рекурсию!Что ж, готово? Но здесь можно найти одну небольшую проблему. Мы планировали сделать легко переиспользуемый компонент, и, скорее всего, пользователи нашего компонента используют в своих приложениях не только строки в качестве данных.Но эта проблема легко решается. Давайте воспользуемся TypeScript generics:
export type MultidimensionalArray<T> =
| T
| ReadonlyArray<MultidimensionalArray<T>>;
Теперь у нас есть крепкая типизация и мы можем начать кодить что-нибудь настоящее!Рекурсивный Angular-компонентAngular поддерживает рекурсию в компонентах. Эта фича позволит нам нарисовать tree view, строя из компонентов ровно такую же структуру, которую имеет наш массив.Давайте создадим компонент для отображения tree view:
В классе компонента нам определенно нужен инпут для значения — тот самый элемент или массив элементов или массивов элементов и так далееКроме того, я сделаю еще один геттер isArray Его можно будет использовать в компоненте для проверки, а также завязать на него HostBinding, чтобы можно было легко разделить случаи массива и отдельного элемента в стилях.
@Component({
    selector: "m-dimensional-view",
    templateUrl: "./m-dimensional-view.template.html",
    styleUrls: ["./m-dimensional-view.styles.less"],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MultidimensionalViewComponent<T> {
    @Input()
    value: MultidimensionalArray<T> = [];
    @HostBinding("class._array")
    get isArray(): boolean {
        return Array.isArray(this.value);
    }
}
В шаблоне же нам нужно рассмотреть два кейса с помощью isArray-геттера и *ngIfЕсли у нас массив, то мы можем проитерировать по каждому его элементу через *ngFor передав элемент в m-dimensional-view следующего уровня, — так мы и получим необходимую нам рекурсию. Если у нас отдельный элемент, давайте просто отобразим его, заодно покинув рекурсию. 
<ng-container *ngIf="isArray; else itemView">
<m-dimensional-view
    *ngFor="let item of value"
    [value]="item"
></m-dimensional-view>
</ng-container>
<ng-template #itemView>
    {{ value }}
</ng-template>
На этой стадии мы можем написать простейшие стили, чтобы убедиться, что наша задумка работает.
:host {
    display: block;
    &._array {
      margin-left: 20px;
    }
}
Просто margin-left для каждого уровня вложенности, написано на LESSДавайте взглянем, что мы получили:
Компонент работает корректно и может показывать строки или любой произвольный объект с методом toString (интерполяция {{value}}приводит значение к строчному виду по умолчанию).Но все же разработчики, которые будут переиспользовать наш компонент, редко имеют данные с реализованными toString-методами. Если они будут орудовать обычными объектами, то их дерево будет состоять исключительно из [object Object]Поддержка данных любого типа Проблема предыдущего решения может быть легко устранена с помощью хендлеров — функций-обработчиков. Это такие функции, которые принимают в себя элемент и отвечают на какой-то вопрос. В нашем случае вопрос будет звучать так: «Какое строчное представление этого элемента?».Давайте добавим еще один инпут к нашему компоненту с подобным хендлером:
@Component({})
export class MultidimensionalViewComponent<T> {
    // ...
    @Input()
    stringify: (item: T) => string = (item: T) => String(item);
    // ...
}
Разработчик может передать функцию, которая приведет элемент к строке. По умолчанию же будет нативный String.Также не забудем добавить обработку значения в шаблон:
<ng-container *ngIf="isArray; else itemView">
<m-dimensional-view
  *ngFor="let item of value"
  [stringify]="stringify"
  [value]="item"
></m-dimensional-view>
</ng-container>
<ng-template #itemView>
   {{stringify(value)}}
</ng-template>
Теперь мы используем stringify для значения, если в нашем компоненте отдельный элемент, или передаем ее для значения на следующем уровне вложенности.Мы можем добавить любые стили для нашего дерева. Мой пример будет немного необычным: подписчику, по запросу которого я готовил этот компонент, было нужно дерево, которое стартует сверху и отрисовывает структуру линиями из точек.Вот по этой ссылке можно увидеть весь получившийся код в действии: Открыть StackblitzОтдельное спасибо Waterplea за щепотку CSS-магии, чтобы сделать пример более лаконичным:
Хотя постойте…А вдруг мы захотим добавить к пункту ссылку или иконку?Мы можем пойти еще дальше и позволить кастомизировать компонент шаблонами ng-polymorheus. Они тоже поддерживают строки и обработчики, но еще позволяют представить значение как любой кастомный шаблон или компонент.Давайте установим ng-polymorheus:npm i @tinkoff/ng-polymorpheusВ нем содержится специальный тип для «строка», или «обработчик», или «шаблон», или «компонент». Импортируем его и немного перепишем класс:
import { PolymorpheusContent } from "@tinkoff/ng-polymorpheus";
// ...
@Component({
  selector: "m-dimensional-view",
  templateUrl: "./m-dimensional-view.template.html",
  styleUrls: ["./m-dimensional-view.styles.less"],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MultidimensionalViewComponent<T> {
  @Input()
  value: MultidimensionalArray<T> = [];
  @Input()
  content: PolymorpheusContent = "";
  @HostBinding("class._array")
  get isArray(): boolean {
    return Array.isArray(this.value);
  }
}
В шаблоне компонента нам нужно заменить функцию stringify на polymorpheus-outlet. Этот компонент создаст блок с контентом. Если контент будет строкой или числом, то блок покажет их значение. Если контент — функция, шаблон или компонент, то мы сможем получить значение благодаря context и кастомизировать контент под каждый конкретный элемент.Теперь мы готовы создать более хитрый пример. Давайте посмотрим на массив из папок и файлов с различными иконками:
readonly itemsWithIcons: MultidimensionalArray<Node> = [
    {
      title: "Documents",
      icon: "https://www.flaticon.com/svg/static/icons/svg/210/210086.svg"
    },
    [
      {
        title: "hello.doc",
        icon: "https://www.flaticon.com/svg/static/icons/svg/2306/2306060.svg"
      },
      {
        title: "table.csv",
        icon: "https://www.flaticon.com/svg/static/icons/svg/2306/2306046.svg"
      }
    ]
];
Добавим шаблон polymorheus для кастомизации, он будет передаваться как контент в компонент вывода дерева:
<m-dimensional-view
    [value]="itemsWithIcons"
    [content]="itemView"
></m-dimensional-view>
<ng-template #itemView let-icon="icon" let-title="title">
    <img alt="icon" width="16" [src]="icon" />
    {{title}}
</ng-template>
В этом шаблоне у нас есть доступ к полям объекта элемента из контекста, который пробрасывается внутри tree view компонента. Когда мы пишем let-icon мы получаем локальную переменную со строкой, которую можем использовать внутри этого ng-template. Самим шаблоном будет картинка с иконкой и название папки или файла:
Вот три примера с ng-polymorheus: Открыть StackblitzИтогоИтак, мы написали один тип, один компонент с двумя инпутами и несколько строк HTML. Да, изначальная проблема не очень сложна, но наше маленькое решение содержит множество идей, которые можно использовать при проектировании других компонентов.Я же просто показал, как размышляю, когда делаю подобные решения, чтобы их мог переиспользовать любой разработчик для своего конкретного кейса.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_angular, #_typescript, #_angular, #_reusable, #_component, #_tree_view, #_folder_structure, #_structure, #_blog_kompanii_tinkoff (
Блог компании Tinkoff
)
, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_angular, #_typescript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 02-Май 11:00
Часовой пояс: UTC + 5