Компоненты Q.Palette
Лукап

Лукап

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

Интерфейс поиска реализуется в виде специализированного виджета, который подгружается в интерфейс продукта и использует компонент лукап в виде js-бандла по сети (по аналогии с всеми другими интерфейсами, объединенными в рамках рутового приложения Q.Palette).

Пример лукапа

С чего начать

Для начала необходимо убедиться, что

  1. Конфигурация приложения содержит

  2. В AppModule импортируется QWidgetLoaderModule.forRoot(QCoreService.config.widgetLoader)

  3. В том модуле, где используется q-lookup, импортируется QLookupModule

  4. Встройте компонент в верстку с помощью тэга q-lookup.

В локальном режиме веб-компонент нужно тестировать внутри рутового приложения.

Разработка виджета для лукапа

Общие требования

Для создания и использования компонентов виджета и лукапа необходимо обновить библиотеку QPaletteVisual до версии не ниже 2.2.1.

Структура проекта

Будет хорошей практикой хранить все виджеты проекта в папкеsrc/app/widgets

Разработка компонента виджета

Компонент виджета, который сможет быть подключён к лукапу, должен реализовывать интерфейс QLookupWidgetComponentInterface из библиотеки QPalette:

export type QLookupWidgetComponentFilters = { [fieldName: string]: any };
 
export interface QLookupWidgetComponentInterface {
  filters: QLookupWidgetComponentFilters | string; //значения полей фильтров
  suggestions: EventEmitter<any>; //отдаёт список элементов виджета, отфильтрованный по введённому значению
  selectedItem: EventEmitter<any>; //отдаёт выбранный элемент виджета
  statusChanges: EventEmitter<QLookupWidgetFormStatus>; //отдаёт изменения статуса формы виджета (валидна/не валидна, ошибки)
}

Ниже приведён пример компонента виджета CustomersLookupWidgetComponent, созданный на основе интерфейса QLookupWidgetComponentInterface. Он содержит в себе всю бизнес-логику, связанную с поиском по заданным фильтрам и отображением отфильтрованного списка клиентов.

import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
import {FormBuilder} from '@angular/forms';
 
import {
  QLookupWidgetComponentInterface,
  QLookupWidgetComponentFilters,
  QLookupWidgetFormStatus,
  QLookupWidgetFormErrors
} from '@diasoft/qpalette-visual';
 
@Component({
  ...
})
export class CustomersLookupWidgetComponent implements QLookupWidgetComponentInterface {
  @Input() set filters(filters: QLookupWidgetComponentFilters | string) {
    // При использовании CustomersLookupWidgetComponent как веб-компонента в поле filters
    // приходит объект с фильтрами, закодированный в строку в формате JSON, её нужно раскодировать
    if (typeof filters === 'string') {
      filters = this.parseFilters(filters);
    }
    // Записываем пришедший набор фильтров в поля формы
    this.formGroup.patchValue(filters);
  }
 
  // Объявляем выходные свойства компонента, требуемые интерфейсом, их использование - дальше по коду
  @Output() suggestions = new EventEmitter<any>();
  @Output() selectedItem = new EventEmitter<any>();
  @Output() statusChanges = new EventEmitter<QLookupWidgetFormStatus>();
 
  formGroup = this.formBuilder.group(
    // Конфигурация формы
  );
 
  constructor(private formBuilder: FormBuilder) {}
 
  ngOnInit(): void {
    // Подписываемся на события формы
    this.formGroup.statusChanges.subscribe((status: string) => {
      // Отдаём "наружу" событие смены статуса формы
      this.statusChanges.emit({
        status,
        errors: this.formErrors()
      });
    });
  }
 
  formErrors(): QLookupWidgetFormErrors {
    // Возвращаем ошибки формы
  }
 
  // Привяжите этот обработчик, например, к событию клика на элемент таблицы
  onCustomerSelected($event: any): void {
    // Отдаём "наружу" событие с выбранным элементом таблицы
    this.selectedItem.emit($event.data);
  }
 
  // Привяжите этот обработчик, например, к событию фильтрации таблицы
  onCustomersTableFiltered(items: any[]): void {
    // Отдаём "наружу" событие с новыми отфильтрованными записями таблицы
    this.suggestions.emit(items);
  }
 
  // Функция парсинга приходящих "извне" фильтров. Рекомендуемая реализация для всех виджетов.
  parseFilters(filtersJson: string): QLookupWidgetComponentFilters {
    try {
      return JSON.parse(filtersJson);
    } catch (e) {
      return {};
    }
  }
}

Далее на примере этого виджета будет рассмотрен процесс организации файлов виджета и его сборки.

Организация файлов

Каждый виджет должен иметь свой собственный модуль. Для примера создадим в папкеsrc/app/widgets/customers-lookupмодуль виджета CustomersLookupWidgetModule, который будет объявлять CustomersLookupWidgetComponent и любые другие связанные сущности. Помимо этого, поскольку создаваемый лукап будет превращён в полноценный веб-компонент (то есть полноценное Angular-приложение), этот модуль будет являться "входной точкой" для загрузчика Angular в процессе начальной загрузки приложения (веб-компонента) виджета. Ниже приведено основное содержимое этого модуля:

import {DoBootstrap, Injector, NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {createCustomElement} from '@angular/elements';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {BrowserModule} from '@angular/platform-browser';
import {CustomersLookupWidgetComponent} from './customers-lookup-widget.component';
 
@NgModule({
  declarations: [CustomersLookupWidgetComponent],
  exports: [CustomersLookupWidgetComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    CommonModule
  ],
  bootstrap: []
})
export class CustomersLookupWidgetModule implements DoBootstrap {
  constructor(private injector: Injector) {
  }
 
  ngDoBootstrap(): void {
    const myCustomElement = createCustomElement(CustomersLookupWidgetComponent, {
      injector: this.injector,
    });
    customElements.define('cmp-customers-lookup', myCustomElement);
  }
}

На что здесь стоит обратить внимание:

  1. Объявление и экспорт компонента виджета CustomersLookupWidgetComponent.
  2. Импортирование модулей BrowserModule, BrowserAnimationsModule и CommonModule, которые требуются для нормальной работы большинства приложений Angular.
  3. Отключение стандартного процесса начальной загрузки приложения (bootstrap: []) и переопределение этого процесса (метод ngDoBootstrap()).
  4. Создание в процессе начальной загрузки веб-компонента 'cmp-customers-lookup' на основе компонента CustomersLookupWidgetComponent.

Далее в эту же папку нужно положить ещё два файла: main.ts и tsconfig.app.json, содержимое которых приведено ниже:

import {enableProdMode} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {environment} from '../../../environments/environment';
import {CustomersLookupWidgetModule} from './customers-lookup-widget.module.ts';
 
if (environment.production) {
  enableProdMode();
}
 
platformBrowserDynamic().bootstrapModule(CustomersLookupWidgetModule)
  .catch(err => console.error(err));

этот файл будет являться "входной точкой" для сборщика Angular, и

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "extends": "../../../../tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "main.ts",
    "../../../polyfills.ts"
  ],
  "include": [
    "./**/*.d.ts"
  ]
}

этот файл задаёт пути к файлам, которые будут использованы компилятором typescript для генерации конечной сборки виджета. Обратите внимание на пути, в данном случае:

  1. импортируется tsconfig.app.json из корня проекта;
  2. импортируется файл main.ts, созданный на предыдущем шаге;
  3. импортируется polyfills.ts из папки src, лежащей в корне проекта.

Далее необходимо в файле angular.json, лежащем в корне проекта, добавить в разделprojects.accounts.architect.build.configurationsконфигурацию для сборки виджета:

"widget-customers-lookup": {
  "main": "src/app/widgets/customers-lookup/main.ts",
  "tsConfig": "src/app/widgets/customers-lookup/tsconfig.app.json",
  "outputPath": "dist/widgets/customers-lookup"
}

Здесь:

  1. main - путь к файлу main.ts, созданному ранее;
  2. tsConfig - путь к файлу tsconfig.app.json, созданному ранее;
  3. outputPath - путь к папке, в которую будет помещена конечная сборка виджета.

В package.json, расположенном в корне проекта в секции scripts нужно добавить команду для сборки виджета:

"build:widget-accounts-lookup": "ng build --prod --output-hashing none --single-bundle true --configuration widget-accounts-lookup"

Здесь build:widget-accounts-lookup - название команды (алиас), а параметр--configuration widget-accounts-lookupзадаёт конфигурацию, которую мы ранее объявили в файле angular.json.

Сборка

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

npm run build:widget-customers-lookup

В папкеdist/widgets/customers-lookupпоявится сборка для нового виджета с входным файлом main.js.

Сборка нескольких виджетов

Повторив шаги, описанные в предыдущих разделах, для каждого из виджетов, можно создать отдельные конфигурации и команды для сборки виджетов, и запускать их поочерёдно.

Дополнительная информация

Существуют различия меджу виджетами:

  • <q-widget> - подключает обычные виджеты, а именно любые куски кода, обернутые в веб-компонент, у которых есть инпуты и аутпуты.
  • <q-lookup> - подключает виджеты лукапа, т.е. функционал поиска, обернутый в веб-компонент, со строго определенным программным интерфейсом.

Разработка и использование лукапа

Общие требования

Для создания и использования компонентов виджета и лукапа необходимо обновить библиотеку QPaletteVisual до версии не ниже 2.2.1.

Схема взаимодействия лукапа и виджета

Общая схема взаимодействия лукапа и виджета прикладных проектов представлена на рисунке ниже:

Что здесь изображено:

  1. Прикладной проект 1 подключает где-то в шаблоне формы компонент QLookup из библиотеки QPaletteVisual, задав в его параметре remoteWidgetComponentUrl адрес компонента виджета в формате <имя ui-сервиса>: <имя виджета>.
  2. На основании заданного параметра remoteWidgetComponentUrl лукап формирует полный URL к файлу main.js собранного виджета на сервере, где лежит Прикладной проект 2, и подгружает его в Прикладной проект 1.
  3. После подгрузки виджета QLookup готов к работе и может взаимодействовать с виджетом как с обычным веб-компонентом через стандартный интерфейс виджета, Прикладной проект 1 продолжает работать с компонентом QLookup через интерфейс лукапа.

По умолчанию, полный URL к бандлу формируется таким образом:http://mdpgateway.<хост>/<имя ui-сервиса>/widgets/<имя виджета>/main.js.

Использование Multiselect

Для работы мультиселекта требуется добавления параметра при вызове q-lookup компонента и доработка функции поиска в прикладном виджете.

formControlNameлукапа требуется инициализировать пустым массивом[].

Для получения в виджете флага, что сейчас активен режим мультиселекта, требуется добавить в него @Input() multiselect. Это необходимо, если виджет работает в двух режимах и требуется динамически их переключать, в других случаях проще работать с виджетом либо в одиночном, либо в мультиселект режиме.

multiselectInner = false;
 
@Input() set multiselect(value: boolean) {
  this.multiselectInner = value;
}
 
get multiselect(): boolean {
  return this.multiselectInner;
}

Подготовка p-table для мутиселекта.

События(onRowSelect), onRowUnselect) и (onHeaderCheckboxToggle)срабатывают при взаимодействии с чекбоксами таблицы. Для отправки значения в лукап, требуется сделатьthis.selectedItem.emit(this.selectedAccount)в функцииonCustomerSelected

Интерфейс лукапа

Компонент QLookup реализует интерфейс QLookupComponentInterface из библиотеки QPaletteVisual, приведённый ниже:

export class QLookupComponent implements QLookupComponentInterface {
  /**
   * Устанавливает режим отображения виджета лукапа.
   *
   * Режим 'modal' выводит виджет в модальном окне при щелчке по значку "лупы" в поле лукапа.
   *
   * Режим 'container' выводит виджет в контейнере, заданном в поле displayContainer.
   */
  @Input() displayMode: QLookupDisplayTypes;
 
  /**
   * Контейнер, в котором отображать виджет. Задаётся в виде обычного CSS-селектора.
   * Например, для контейнера с HTML-атрибутом id, равным 'someContainerId', задайте селектор '#someContainerId'.
   *
   * Используется только при displayMode='container'.
   */
  @Input() displayContainer: string;
 
  /**
   * Поле фильтра виджета, по которому будет вестись поиск.
   */
  @Input() lookupField: string;
 
  /**
   * Заголовок, который будет выведен в поля лукапа.
   */
  @Input() caption: string;
 
  /**
   * Заголовок модального окна. Используется при displayMode='modal'.
   */
  @Input() modalCaption: string;
 
  /**
   * Класс компонента виджета, который будет использоваться для поиска значений в лукапе.
   *
   * Не используйте одновременно с remoteWidgetComponentUrl, поля взаимоисключающие!
   * Используется только при подключении виджета в "локальном" режиме.
   */
  @Input() widgetComponentClass: Type<any>;
 
  /**
   * URL виджета, который должен быть подгружен с сервера. Задаётся в формате: servicename:widgetname
   */
  @Input() remoteWidgetComponentUrl: string;
 
  /**
   * Имя виджета, который должен быть подгружен с сервера. Задаётся в формате селектора, например: cmp-widgetname
   */
  @Input() widgetComponentSelector: string;
 
  /**
   * Возвращает варианты, найденные виджетом при вводе пользователем значений в лукапе.
   */
  @Output() suggestions = new EventEmitter<any>();
 
  /**
   * Возвращает выбранный пользователем элемент из списка найденных виджетом.
   */
  @Output() selectedItem = new EventEmitter<any>();
 
  /**
   * Возвращает статус формы виджета при событиях валидации.
   */
  @Output() statusChanges = new EventEmitter<QLookupWidgetFormStatus>();
 
  /**
   * Событие инициализации лукапа. Срабатывает, когда лукап готов к работе.
   */
  @Output() initialized = new EventEmitter<void>();

События suggestions, selectedItem и statusChanges полностью дублируют те, что определены интерфейсом виджета. Ими можно воспользоваться для получения обратного взаимодействия с лукапом.

Событие initialized срабатывает в момент полной подгрузки виджета. Пока этого не произойдёт, поле лукапа будет заблокировано.

Подключение лукапа в прикладной проект

Рассмотрим процесс подключения лукапа на примере модуля, реализующего форму кредитной заявки. Назовём модуль этой формы CreditsFormModule, в нём объявим компонент с формой CreditsFormComponent и подключим QLookupModule из библиотеки QPaletteVisual:

import {QLookupModule} from '@diasoft/qpalette-visual';
 
@NgModule({
  declarations: [CreditsFormComponent],
  exports: [CreditsFormComponent],
  imports: [
    QLookupModule
  ]
})
export class CreditsFormModule {
}

Далее внутри шаблона компонента формы подключим компонент лукапа

  1. В lookupField передаём название поля виджета, по которому будет вестись поиск.
  2. В caption передаём название поля (плейсхолдер), которое будет выведено в лукапе.
  3. В remoteWidgetComponentUrl передаём адрес виджета на сервере по описанной ранее схеме.
  4. В widgetComponentSelector указываем селектор виджета, который был ранее задан функцией customElements.define(...) в модуле виджета (см. Разработка виджета для лукапа).
  5. В displayMode указываем режим отображения виджета. В данном случае виджет будет открыт в модальном окне по нажатию на кнопку лупы.

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

В данном примере виджет будет выведен в блоке div сid="lookupContainer", определённом в шаблоне.

Настройка сборки для доставки лукапа на стенд

Для настройки сборки необходимо внести изменения в файлdocker/Dockerfile-pbcui.txt

К уже имеющимся строкам, необходимо добавить новые, для копирования main.js собранного банда лукапа

COPY PBC/ИМЯ компонента(папки)/dist/имя папки из angular.json/app/files/widgets/customerslookup(имя вашего лукапа)

После установки на стенд, он будет доступен по ссылке:http://mdpgateway. <хост>/ <имя ui-сервиса>/widgets/ <имя лукапа>/main.js

Для Qpalette версии 5 порядок такой:

  1. Настроить виджеты так, чтобы купалет за ними ходил по какому-то относительному адресу. Инструкция по ссылке (opens in a new tab). (параметр widgetLoader -> bundleUrl)
  2. Настроить проксирование с localhost на удалённый стенд через proxy.conf.js (см. ссылку (opens in a new tab), раздел "Прокси")

Для Qpalette версии 3 порядок порядок такой же, только базовый URL указывается не в конфигурации, а при подключении модуля через QWidgetLoaderModule.forRoot().

Локальная разработка лукапа и виджета

Для локальной разработки необходимо выполнить следующие действия:

  1. Локально запустить свой ui-продукт

  2. Скопировать меню из menu.json и добавить его в настройки проксирования в запущенном приложении. Таким образом, при клике по пунктам меню, запросы будут направляться к локальной сборке.

  3. На данном этапе, при загрузке виджета, в поле лукапа будет выведена ошибка, так как ui-продукт запускается локально, но при этом выполняются относительные запросы направленные к удалённому стенду. Чтобы отправлять запросы к локально запущенному серверу, нужно обратиться к консоли разработчика, скопировать часть url виджета /api/ <имя ui-сервиса>/ <имя компонента>/widgets/ <имя виджета>/main.js , добавить его в настройки проксирования и выбрать свободный порт (например, 4211).

  4. В проекте в angular.json в раздел projects.accounts.architect.serve.configurations необходимо добавить конфигурацию для запуска виджета:

  5. Затем в package.json добавить команду для запуска виджета, где указываем выбранный в настройках проксирования порт:

  6. После всех выполненных шагов можно запустить виджет и вести разработку локально.

Дополнительная информация

Использование lookupField

Под лукапом лежит компонент autocomplete. Ссылка на документацию по Autocomplete (opens in a new tab)

При реализации виджета необходимо внутри него добавить таблицу, которая работает с источником данных. Например, используемые для виджета данные - это массив объектов следующего типа:

[
  {
    id: 1,
    fullName: 'Иван Иванов',
    accountNumber: 82089979,
    balance: 312312,
    date: '01/01/2020',
    status: 'Активный'
  },
  {
    id: 2,
    fullName: 'Василий Васильев',
    accountNumber: 98287271,
    balance: 29837223,
    date: '01/02/2010',
    status: 'Активный'
  }
]

Если вы укажете в lookupField поле id - поиск будет делаться по нему, и оно же будет выведено в этом поле, а при вводе будет выводиться список именно из id, а не из fullName.

Таким образом, в lookupField вы указываете поле, по которому вы будете искать и которое будет выведено в поле и в раскрывающемся списке при вводе.

Если вы хотите, например, чтобы в поле отображался fullName, а поиск осуществлялся по id, вы можете использовать один из вариантов:

  1. Фильтрация

    Для этого необходимо выполнить следующие действия:

    • Указать fullName в качестве lookupField.
    • Щёлкнуть по кнопке лупы в поле лукапа.
    • В открывшемся окне отфильтровать записи по id, затем щёлкнуть нужную запись.

    В результате в поле лукапа вставится fullName, хотя поиск осуществлялся по id.

  2. Primary key

    Используя данный способ, вам надо будет:

    • Убедиться, что версия библиотеки visual не ниже 5.2.3

    • Добавить в виджет лукапа:

      1. аутпут @Output() initialValue;
      2. инпут @Input() primaryKeyValue, который будет находить нужную запись по ID и бросать событие initialValue с найденной записью
    • В прикладной форме, в которой подключается лукап, для подгрузки записи лукапа по ID вызвать у компонента лукапа метод setValueByPrimaryKeyValue с передачей ему ID записи

    Пример:

    Set id = "1" Set id = "2"

  3. Предустановленные фильтры

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

    • Через шаблон:

    • В компоненте:

    Пример:

    Предустановить фильтры Предустановить скрытые фильтры Сброс фильтров

Лукап в формах

Вы можете использовать лукап как FormControl (opens in a new tab) или NgModel (opens in a new tab).

Рассмотрим использование лукапа как FormControl.

В коде компонента опишем форму, у которого поле customers является обязательным

form = new FormGroup({
  customer: new FormControl('Иван Иванов')
});
 
submit(): void {
  if (this.form.invalid) {
    this.form.updateValueAndValidity();
    return;
  }
  this.httpClient.post('/api/customer', this.form.value);
}

В шаблоне компонента используем обычную форму, а лукап используем в качестве одного из полей формы

Таким образом, значение в лукапе будут являться значением поля customers данной формы.

Посмотреть примеры использования можно в разделе "Примеры".

Размеры

Лукап как и другие элементы форм поддерживает три размера:small,medium,large. Чтобы включить нужный размер передайте значение в полеsize. Значением по умолчанию является значениеlarge

Лукап как фильтр

Чтобы использовать лукап как фильтр, оберните его в компонент q-filter-wrapper.

Примеры

Долгий запрос к бэкенду

В реальной жизни виджеты сначала делают запрос к REST API, и только потом отдают ответ клиенту. А значит, между вводом текста в лукап и получением списка значений может пройти время - в этот промежуток времени в поле лукапа будет отображаться спиннер. Пример приведён ниже.

Обновитесь до Q.Palette 5.3.13 и выше для работы этого функционала.

Если вы подключили в свой проект библиотечный UI, который содержит виджеты лукапов, вы можете их также подключить, проделав следующие шаги.

Введите название вашего UI-сервиса, компонента, подключаемого библиотечного UI и виджета - и мы подставим их в инструкции ниже:

Имя сервиса прикладного UI * Имя компонента прикладного UI * Имя пакета библиотечного UI * Имя подключаемого виджета *

В файле angular.json найдите секцию projects.[project].architect.build.options.assets, где [project] - это название вашего проекта, в который вы хотите подключить лукап.

Добавьте в данную секцию следующий блок кода, где libraryWidgetForm.installedLibraryPackage - название подключенного библиотечного UI.

Теперь после каждой сборки проекта в папке assets/ libraryWidgetForm.installedLibraryPackage будет находиться бандл самого библиотечного UI, а также все бандлы виджетов (как правило, находятся в папке assets/ libraryWidgetForm.installedLibraryPackage /widgets).

Подключите лукап с указанием полного относительного пути к виджету в параметре remoteWidgetComponentUrl:

Обратите внимание, что адрес начинается с пути /api/ libraryWidgetForm.service / libraryWidgetForm.component , который является путем к веб-компоненту, в который вы подключаете библиотечный UI. Подставьте вместо него адрес своего веб-компонента. Также обратите внимание на папку /widgets/ libraryWidgetForm.widget : виджет может оказаться в другой папке по вине разработчика виджета - в этом случае поищите виджет вручную в папке /assets/ libraryWidgetForm.installedLibraryPackage вашего компонента и подставьте правильный путь.

Ниже пример лукапа, подключающего виджет из библиотечного UI нашей документации:* Предустановка значения

  • Блокировка

  • Валидация required

    Validate

  • Валидация типа и длины вводимых значений