Angular에서 동적 컨텐츠 생성

Angular에서 동적 컨텐츠 생성

이 글에서 Angular로 동적 컨텐츠를 만드는 몇가지 방법을 보여 드리겠습니다. 사용자 지정 리스트 템플릿, 동적 Component 생성, 런타임 Component 및 모듈 컴파일의 예제를 보여줍니다. 전체 소스 코드는 이 글의 끝부분에서 보실수 있습니다.

제가 작업하고 있는 Developing with Angular에서 Angular 개발에 대한 더 많은 정보를 얻을 수 있습니다.

List item templates

사용자 정의 템플릿을 제공하여 Angular Component를 풍성하게 만드는 방법을 살펴 보겠습니다. 개발자가 선언한 외부 Row(행) 템플릿을 지원하는 간단한 리스트 Component를 작성하며 시작하겠습니다.

List component

먼저 바인딩된 항목 컬렉션을 표시하는 간단한 리스트 Component를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
selector: 'tlist',
template: `
<ul>
<li *ngFor="let item of items">
{{ item.title }}
</li>
</ul>
`
})
export class TListComponent {
@Input()
items: any[] = [];
}

이제 메인 응용 프로그램 Component를 업데이트 하거나 다음 예제와 같이 별도의 데모 Component를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
selector: 'tlist-demo',
template: `
<h1>Templated list</h1>
<tlist [items]="items"></tlist>
`
})
export class AppComponent {
items: any[] = [
{ title: 'Item 1' },
{ title: 'Item 2' },
{ title: 'Item 3' }
];
}

그러면 다음과 같이 정렬되지 않은 HTML 목록이 렌더링됩니다.

Row templates

우리는 객체의 배열에 바인딩하고 정렬되지 않은 표준 HTML리스트를 렌더링하는 간단한 리스트 컴포넌트를 만들었습니다. 그리고 모든 리스트 항목은 title 프로퍼티 값에 바인딩됩니다. 이제 외부 템플릿을 지원하도록 코드를 변경해 보겠습니다. 아래 그림과 같이 코드를 업데이트 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component, Input, ContentChild, TemplateRef } from '@angular/core';
@Component({
selector: 'tlist',
template: `
<ul>
<template ngFor [ngForOf]="items" [ngForTemplate]="template">
</template>
</ul>
`
})
export class TListComponent {
@ContentChild(TemplateRef)
template: TemplateRef<any>;
@Input()
items: any[] = [];
}

이제 TListComponent는 자식 컨텐츠에서 정의될 템플릿을 참조합니다. 그리고 템플릿 내용을 받아 각 *ngFor 항목에 적용합니다. 따라서 이 Component를 사용하는 응용 프로그램 개발자는 다음과 같이 전체 Row 템플릿을 정의할 수 있습니다.

1
2
3
4
5
6
7
<tlist [items]="items">
<template>
<li>
Row template content
</li>
</template>
</tlist>

이제 아래 예제와 같이 업데이트합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component } from '@angular/core';
@Component({
selector: 'tlist-demo',
template: `
<div>
<h2>Templated list</h2>
<tlist [items]="items">
<template let-item="$implicit" let-i="index">
<li>[{{i}}] Hello: {{item.title}}</li>
</template>
</tlist>
</div>
`
})
export class TListComponentDemo {
items: any[] = [
{ title: 'Item 1' },
{ title: 'Item 2' },
{ title: 'Item 3' }
];
}

각 Row에 대한 기본 데이터 바인딩 컨텍스트에 액세스하기 위해 let-item = "$implicit" 프로퍼티를 사용하여 item 변수에 매핑합니다. 따라서 itemTListComponentDemoitems 컬렉션에 있는 항목을 가리키며 title 프로퍼티에 바인딩할 수 있습니다. 또한 let-i = "index"를 통해 i 변수에 Row 인덱스 프로퍼티 값을 할당합니다.

또 다른 개선점은 TListComponent가 더 이상 모든 바인딩된 객체가 title 프로퍼티를 갖도록 강요하지 않는다는 것입니다. 이제 템플릿과 기본 컨텍스트가 모두 응용 프로그램 수준에서 정의됩니다.

변경 사항이 렌더링된 결과는 다음과 같습니다.

동적 Component

또 다른 일반적인 시나리오는 일부 조건에 따라 Component의 내용을 변경하는 것입니다. 예를 들어 아래와 같이 type 프로퍼티 값을 기반으로 다른 하위 Component를 렌더링하는 경우입니다.

1
2
<component type="my-type-1"></component>
<component type="my-type-2"></component>

기본 Component 구조부터 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component({
selector: 'dynamic-content',
template: `
<div>
<div #container></div>
</div>
`
})
export class DynamicContentComponent {
@ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
@Input()
type: string;
}

container 사용법에 유의하십시오. 주입 지점으로 사용되며, 모든 동적 컨텐츠가 이 요소 아래의 DOM에 삽입됩니다. 또한 ViewContainerRef 유형의 프로퍼티를 사용하면 코드에서 container에 액세스할 수 있습니다.

Component는 나중에 다음과 같이 사용할 수 있습니다.

1
<dynamic-content type="some-value"></dynamic-type>

이제 “type” 값을 기반으로 표시할 두 가지 간단한 Component와 추가적으로 “unknown” 타입에 대한 하나의 대체 Component를 소개합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component({
selector: 'dynamic-sample-1',
template: `<div>Dynamic sample 1</div>`
})
export class DynamicSample1Component {}
@Component({
selector: 'dynamic-sample-2',
template: `<div>Dynamic sample 2</div>`
})
export class DynamicSample2Component {}
@Component({
selector: 'unknown-component',
template: `<div>Unknown component</div>`
})
export class UnknownDynamicComponent {}

또한 해당 문자열을 Component로 변환할 수 있도록 “string” - “type” 매핑이 필요합니다. 별도의 Injectable 서비스 (권장) 또는 Component 구현의 일부일 수 있습니다.

1
2
3
4
5
6
7
8
private mappings = {
'sample1': DynamicSample1Component,
'sample2': DynamicSample2Component
};
getComponentType(typeName: string) {
let type = this.mappings[typeName];
return type || UnknownDynamicComponent;
}

그리고 type 이름이 없을 경우 UnknownDynamicComponent가 자동으로 반환됩니다.

이제 Component를 동적으로 만들 준비가 되었습니다. 다음은 동적 Component 생성에 관심이 있는 주요 블록이 있는 Component의 단순 버전입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export class DynamicContentComponent implements OnInit, OnDestroy {
private componentRef: ComponentRef<{}>;
constructor(
private componentFactoryResolver: ComponentFactoryResolver) {
}
ngOnInit() {
if (this.type) {
let componentType = this.getComponentType(this.type);
let factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.componentRef = this.container.createComponent(factory);
}
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
}

동적으로 생성하려는 모든 Component는 모듈의 entryComponents 섹션에 등록해야합니다.

1
2
3
4
5
6
7
8
9
10
11
@NgModule({
imports: [...],
declarations: [...],
entryComponents: [
DynamicSample1Component,
DynamicSample2Component,
UnknownDynamicComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

이제 세 가지 모든 경우를 테스트 할 수 있습니다.

1
2
3
<dynamic-content type="sample1"></dynamic-content>
<dynamic-content type="sample2"></dynamic-content>
<dynamic-content type="some-other-type"></dynamic-content>

아마도 대부분의 경우 새로 작성된 하위 Component에 런타임 컨텍스트를 전달하려고 할 것입니다.

서로 다른 타입의 동적 Component를 유지하는 가장 쉬운 방법은 공통 인터페이스 또는 추상 클래스를 작성하는 것입니다.

1
2
3
abstract class DynamicComponent {
context: any;
}

간단히 하기 위해, contextany 타입을 사용했습니다. 실제 시나리오에서는 정적 검사의 이점을 얻기위해 타입을 선언할 수 있습니다.

context를 고려하여 이전에 생성된 모든 Component를 업데이트 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export abstract class DynamicComponent {
context: any;
}
@Component({
selector: 'dynamic-sample-1',
template: `<div>Dynamic sample 1 ({{context?.text}})</div>`
})
export class DynamicSample1Component extends DynamicComponent {}
@Component({
selector: 'dynamic-sample-2',
template: `<div>Dynamic sample 2 ({{context?.text}})</div>`
})
export class DynamicSample2Component extends DynamicComponent {}
@Component({
selector: 'unknown-component',
template: `<div>Unknown component ({{context?.text}})</div>`
})
export class UnknownDynamicComponent extends DynamicComponent {}

그리고 동적 Component도 업데이트 해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class DynamicContentComponent implements OnInit, OnDestroy {
...
@Input()
context: any;
...
ngOnInit() {
if (this.type) {
...
let instance = <DynamicComponent> this.componentRef.instance;
instance.context = this.context;
}
}
}

위의 변경 사항으로 이제 부모 Component 내에서 context 객체를 바인딩 할 수 있습니다. 다음은 간단한 데모입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component({
selector: 'dynamic-component-demo',
template: `
<div>
<h2>Dynamic content</h2>
<h3>Context: <input type="text" [(ngModel)]="context.text"></h3>
<dynamic-content type="sample1" [context]="context"></dynamic-content>
<dynamic-content type="sample2" [context]="context"></dynamic-content>
<dynamic-content type="some-other-type" [context]="context"></dynamic-content>
</div>
`
})
export class DynamicContentComponentDemo {
context: any = {
text: 'test'
}
}

런타임에는 세 가지 Component(Fallback된 UnknownDynamicComponent 포함)를 볼 수 있어야합니다. Context 입력란의 텍스트를 변경하면 모든 위젯이 자동으로 업데이트됩니다.

일반적인 사용 사례

정의 파일 (JSON, XML 등)을 기반으로 Form(또는 복합 Component)을 표시해야하는 경우 스키마 또는 상태를 기반으로 최종 컨텐츠를 빌드하는 동적 Component와 여러 동적 컨텐츠 컨테이너에서 빌드된 Form Component를 갖게 될 수 있습니다.

런타임 컴파일

일부 고급 시나리오의 경우 Angular Component 또는 템플릿 편집을 완전히 제어 할 필요가 있을 수 있습니다.

이 장에서는 다음 기능을 구현합니다.

  • 사용자에게 Component 템플릿을 정의하도록 합니다.
  • Component를 즉석에서 컴파일(사용자 정의 템플릿 + 클래스)합니다.
  • 컴포넌트를 생성하면서 NgModule을 즉시 컴파일합니다.
  • 새로 생성된 Component 표시합니다.

구현은 이전 장의 동적 Component를 기반으로 합니다. 그리고 콘텐츠를 삽입하기위한 전용 자리 표시자가있는 기본 Component가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component({
selector: 'runtime-content',
template: `
<div>
<div #container></div>
</div>
`
})
export class RuntimeContentComponent {
@ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
}

사용자가 Component 템플릿을 편집 할 수 있도록 기본 UI를 수정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component({
selector: 'runtime-content',
template: `
<div>
<h3>Template</h3>
<div>
<textarea rows="5" [(ngModel)]="template"></textarea>
</div>
<button (click)="compileTemplate()">Compile</button>
<h3>Output</h3>
<div #container></div>
</div>
`
})
export class RuntimeContentComponent {
template: string = '<div>\nHello, {{name}}\n</div>';
// ...
}

ngModel을 사용하려면 AppModule 내에서 FormsModule을 가져 와서 참조해야합니다.

1
2
3
4
5
6
7
8
9
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [...],
bootstrap: [ AppComponent ]
})
export class AppModule { }

렌더링될 때 다음과 같이 보일 것입니다.

이제 Component 구현의 가장 중요한 부분인 런타임 컴파일입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class RuntimeContentComponent {
private createComponentFactorySync(compiler: Compiler, metadata: Component, componentClass: any): ComponentFactory<any> {
const cmpClass = componentClass || class RuntimeComponent { name: string = 'Denys' };
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [CommonModule], declarations: [decoratedCmp] })
class RuntimeComponentModule { }
let module: ModuleWithComponentFactories<any> = compiler.compileModuleAndAllComponentsSync(RuntimeComponentModule);
return module.componentFactories.find(f => f.componentType === decoratedCmp);
}
}

위 코드는 사용자 지정 메타 데이터와 선택적으로 Component 클래스를 받습니다. 클래스가 제공되어 있지 않은 경우, 대신 RuntimeComponent를 미리 정의된 name 프로퍼티와 함께 사용됩니다. 이것은 우리가 테스트를 위해 사용할 것입니다. 그런 다음 결과 Component는 제공된 메타 데이터로 장식(Decorate)됩니다.

다음으로, RuntimeComponentModule 모듈은 미리 정의된 CommonModule을 import(필요한 경우 목록을 확장 할 수 있음)하고, declarations 섹션의 일부로 이전에 생성되어 장식(Decorate)된 Component로 만들어집니다.

마지막으로 이 함수는 Angular의 Compiler 서비스를 사용하여 모듈과 포함된 Component를 컴파일합니다. 컴파일된 모듈은 기본 Component 팩토리에 대한 액세스를 제공하며 이는 정확히 우리가 필요로하는 것입니다.

마지막 단계에서는 다음 코드로 Compile 버튼을 연결합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class RuntimeContentComponent {
compileTemplate() {
let metadata = {
selector: `runtime-component-sample`,
template: this.template
};
let factory = this.createComponentFactorySync(this.compiler, metadata, null);
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
this.componentRef = this.container.createComponent(factory);
}
}

사용자가 Compile 버튼을 클릭할 때 마다 템플릿을 가져 와서 (RuntimeComponent 클래스가 미리 정의된 name 프로퍼티를 사용하여) 새 Component를 컴파일하고 렌더링합니다.

어디에서든 Component 템플릿을 저장하고 즉시 Component를 생성하려는 경우 가장 좋은 옵션입니다.

소스코드

GitHub 저장소에서 모든 소스 코드와 실제 예제 프로젝트를 얻을 수 있습니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://denysvuika.blog/2017/10/07/dynamic-content-in-angular/]

참고

공유하기