Динамический рендеринг компонентов в Angular 2

в 15:19, , рубрики: angular 2, angular 4, AngularJS, Leaflet, TypeScript, Разработка веб-сайтов, метки:

Вступительное слово

В процессе работы над проектом на Angular 2 с использованием карт возникла следующая задача: требуется срендерить свой ангуляровский компонент в стандартный popup leaflet’а. В данной статье динамический рендеринг компонентов будет рассмотрен в разрезе именно этой задачи, однако аналогичным образом можно использовать эту информацию в собственных кейсах.

Постановка задачи

Начальный проект находится по ссылке. Это angular 2+ приложение, к которому подключена библиотека для работ с картой leaflet.js. В MapService есть методы для создания карты, добавления маркеров на неё и центровки на маркерах. MapComponent – компонент для отображения карты. Для сборки проекта используется webpack 2. Если запустить приложение, то перед нами появится карта с маркером, к которому привязан popup следующего вида:

marker.bindPopup(`
    	    <h3>Leaflet PopUp</h1>
	        <p>Some text</p>
	        <p *ngIf="false">Should be deleted from DOM if it was angular component because of ngIf = false<p>
    	`);

Кликнем на него и увидим следующую картину:

Карта с открытым popup

В DOM находится элемент с текстом “Should be deleted from DOM...”, который хотелось бы удалить используя *ngIf, однако в popup просто так нельзя записать код ангуляра, чтобы он тут же заработал. Именно здесь на сцену выходит динамический рендеринг компонентов ангуляра.

Решение задачи

Для начала создадим компонент, который мы хотим динамически рендерить:

@Component({
    selector: 'custom-popup',
    template: require('./custom-popup.component.html')
})
export class CustomPopUpComponent {
	public inputData: any;

	private title: string = 'Angular component';
	private array: Array<string> = ['this', 'array', 'was viewed', 'by', 'ngFor!'];
}

Его template:

<div>
	<h1>{{title}}</h1>
	<p>{{inputData}}</p>
	<p *ngFor="let text of array">{{text}}</p>
	<p *ngIf="false">Should be deleted from DOM if it was angular component because of ngIf = false</p>
</div>

Далее создадим новый сервис dynamic-render.service.ts:

@Injectable()
export class RenderService {

	private componentRef: ComponentRef<any>;

	constructor(private ngZone: NgZone,
	            private injector: Injector,
	            private appRef: ApplicationRef,
	            private componentFactoryResolver: ComponentFactoryResolver) { }

	public attachCustomPopUpsToMap(map: Map) {
		this.ngZone.run(() => {
			map.on("popupopen",
			       (e: any) => {
				       const popup = e.popup;

				       const compFactory = this.componentFactoryResolver.resolveComponentFactory(popup.options.popupComponentType);
				       this.componentRef = compFactory.create(this.injector);

				       this.componentRef.instance.geoObject = popup.options.object;

				       this.appRef.attachView(this.componentRef.hostView);

				       let div = document.createElement('div');
				       div.appendChild(this.componentRef.location.nativeElement);

				       popup.setContent(div);
			       });
		});
	}
}

Так как addListener запускается вне зоны ангуляра, нам нужно самим вручную добавить его туда. Таким образом каждый раз при открытии popup’а вызывается componentFactory, которая создаёт компонент, который мы прокинули в поле options. Далее мы можем с помощью instance этого компонента записать в его поля данные, которые мы так же можем прокинуть в options popup’а. В данном примере мы назначаем полю inputData компонента данные из options.data. Затем создаем div элемент, к которому прикрепляем наш только что созданный компонент и назначаем его в качестве контента popup’у.

Замечание: этот код написан для angular 2.3.0+. Для более ранних версий это решение будет выглядеть следующим образом. Вместо

 this.appRef.attachView(this.componentRef.hostView);

нужно будет написать

if (this.appRef['attachView']) {
	this.appRef['attachView'](this.componentRef.hostView);
	this.componentRef.onDestroy(() => {
		this.appRef['detachView'](this.componentRef.hostView);
	});
}
else {
	this.appRef['registerChangeDetector'](this.componentRef.changeDetectorRef);
	this.componentRef.onDestroy(() => {
		this.appRef['unregisterChangeDetector'](this.componentRef.changeDetectorRef);
	});
}

Запровайдим RenderService в MapModule. Также обязательно нужно добавить в MapModule в declarations и entryComponents наш CustomPopUpComponent. Вызовем renderService и добавим возможность для нашего элемента карты рендерить в popup’ах ангуляровские компоненты, после чего прикрепим к маркеру кастомный компонент:

this.renderService.attachCustomPopUpsToMap(this.mapService.getMap());
let options = {
		    data: 'you can provide here anything you want',
		    popupComponentType: CustomPopUpComponent
	    };
let myPopUp = L.popup(options);
marker.bindPopup(myPopUp);

В поле data прокинем данные для компонента, в popupComponentType – сам компонент. Такую конструкцию можно обернуть в интерфейс для удобства использования, но в рамках данного примера делать этого не будем, статья не об этом. Для корректного отображения немного подправим стили, после чего можно запускать приложение. Кликнув по маркеру, видим, что наш компонент среднерился в popup’е leaflet:

Наш компонент срендерился!

Заключение

Нам удалось значительно расширить функционал стандратных popup’ов leaflet в связке с angular 2+. В качестве бонуса наши компоненты получают анимацию открытия/закрытия, изменение размера при зуме и другие стандартные вещи leaflet.

Исходники проекта, в котором реализовано всё описанное в статье находятся здесь.

Автор: bodryi

Источник

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


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