Лукап
Компонент «Лукап» - это строка поиска с расширенными возможностями. Данный компонент позволяет выполнять поиск необходимых значений, которые предоставляются внешним модулем. Эти значения допустимо вводить в поле для ввода, тогда подходящие результаты отображаются в виде выпадающего списка. Для расширенного поиска по параметрам доступно открытие (виджета) модального окна.
Интерфейс поиска реализуется в виде специализированного виджета, который подгружается в интерфейс продукта и использует компонент лукап в виде js-бандла по сети (по аналогии с всеми другими интерфейсами, объединенными в рамках рутового приложения Q.Palette).
Пример лукапа
С чего начать
Для начала необходимо убедиться, что
-
Конфигурация приложения содержит
-
В AppModule импортируется QWidgetLoaderModule.forRoot(QCoreService.config.widgetLoader)
-
В том модуле, где используется q-lookup, импортируется QLookupModule
-
Встройте компонент в верстку с помощью тэга 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);
}
}
На что здесь стоит обратить внимание:
- Объявление и экспорт компонента виджета CustomersLookupWidgetComponent.
- Импортирование модулей BrowserModule, BrowserAnimationsModule и CommonModule, которые требуются для нормальной работы большинства приложений Angular.
- Отключение стандартного процесса начальной загрузки приложения (
bootstrap: []
) и переопределение этого процесса (методngDoBootstrap()
). - Создание в процессе начальной загрузки веб-компонента '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 для генерации конечной сборки виджета. Обратите внимание на пути, в данном случае:
- импортируется tsconfig.app.json из корня проекта;
- импортируется файл main.ts, созданный на предыдущем шаге;
- импортируется 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"
}
Здесь:
- main - путь к файлу main.ts, созданному ранее;
- tsConfig - путь к файлу tsconfig.app.json, созданному ранее;
- 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 подключает где-то в шаблоне формы компонент QLookup из библиотеки QPaletteVisual, задав в его параметре remoteWidgetComponentUrl адрес компонента виджета в формате
<имя ui-сервиса>: <имя виджета>
. - На основании заданного параметра remoteWidgetComponentUrl лукап формирует полный URL к файлу main.js собранного виджета на сервере, где лежит Прикладной проект 2, и подгружает его в Прикладной проект 1.
- После подгрузки виджета 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 {
}
Далее внутри шаблона компонента формы подключим компонент лукапа
- В lookupField передаём название поля виджета, по которому будет вестись поиск.
- В caption передаём название поля (плейсхолдер), которое будет выведено в лукапе.
- В remoteWidgetComponentUrl передаём адрес виджета на сервере по описанной ранее схеме.
- В widgetComponentSelector указываем селектор виджета, который был ранее задан функцией
customElements.define(...)
в модуле виджета (см. Разработка виджета для лукапа). - В 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 порядок такой:
- Настроить виджеты так, чтобы купалет за ними ходил по какому-то относительному адресу. Инструкция по ссылке (opens in a new tab). (параметр widgetLoader -> bundleUrl)
- Настроить проксирование с localhost на удалённый стенд через proxy.conf.js (см. ссылку (opens in a new tab), раздел "Прокси")
Для Qpalette версии 3 порядок порядок такой же, только базовый URL указывается не в конфигурации, а при подключении модуля через QWidgetLoaderModule.forRoot().
Локальная разработка лукапа и виджета
Для локальной разработки необходимо выполнить следующие действия:
-
Локально запустить свой ui-продукт
-
Скопировать меню из menu.json и добавить его в настройки проксирования в запущенном приложении. Таким образом, при клике по пунктам меню, запросы будут направляться к локальной сборке.
-
На данном этапе, при загрузке виджета, в поле лукапа будет выведена ошибка, так как ui-продукт запускается локально, но при этом выполняются относительные запросы направленные к удалённому стенду. Чтобы отправлять запросы к локально запущенному серверу, нужно обратиться к консоли разработчика, скопировать часть url виджета
/api/ <имя ui-сервиса>/ <имя компонента>/widgets/ <имя виджета>/main.js
, добавить его в настройки проксирования и выбрать свободный порт (например, 4211). -
В проекте в angular.json в раздел
projects.accounts.architect.serve.configurations
необходимо добавить конфигурацию для запуска виджета: -
Затем в package.json добавить команду для запуска виджета, где указываем выбранный в настройках проксирования порт:
-
После всех выполненных шагов можно запустить виджет и вести разработку локально.
Дополнительная информация
Использование 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, вы можете использовать один из вариантов:
-
Фильтрация
Для этого необходимо выполнить следующие действия:
- Указать fullName в качестве lookupField.
- Щёлкнуть по кнопке лупы в поле лукапа.
- В открывшемся окне отфильтровать записи по id, затем щёлкнуть нужную запись.
В результате в поле лукапа вставится fullName, хотя поиск осуществлялся по id.
-
Primary key
Используя данный способ, вам надо будет:
-
Убедиться, что версия библиотеки visual не ниже 5.2.3
-
Добавить в виджет лукапа:
- аутпут
@Output() initialValue
; - инпут
@Input() primaryKeyValue
, который будет находить нужную запись по ID и бросать событие initialValue с найденной записью
- аутпут
-
В прикладной форме, в которой подключается лукап, для подгрузки записи лукапа по ID вызвать у компонента лукапа метод
setValueByPrimaryKeyValue
с передачей ему ID записи
Пример:
Set id = "1" Set id = "2"
-
-
Предустановленные фильтры
Для этого необходимо свойству 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
-
Валидация типа и длины вводимых значений