This article introduces dynamic component creation using the Angular API and CDK Portals, as well as the following: Portals that create components dynamically, similar to aprons or slots in Vue.

Component Development Kit CDK is a Component Development Kit that Angular abstractions when developing Component libraries based on Material Design. It encapsulates some common logic when developing components and has nothing to do with Material Design Design. It can be used to encapsulate its component library or directly used in business development. The code abstraction degree is very high and worth learning. Right now I’m using Portals, Overlay, SelectionModel, Drag and Drop, etc. Official: material. Angular. IO / 英 文 : Material. Angular. cn

Dynamically creating components

Think about application of routing, the general configuration of routing address would be an entrance to this address configuration components, when the match to the routing address will render the component in the specified place, similar to dynamically create components, did not receive the user behavior in the page, I don’t know the area of the page should render the component, When the page loads, the final component to render is determined based on database Settings or user behavior. This time, the code dynamically creates the component to render the target component in the correct place. Sample screenshots

Dynamically create components using the Angular API

The entry component of this route is the PortalsEntryConponent component, as shown in the screenshot above with a dashed border to the right, where the specific rendering component is uncertain.

The first step

Define a placeholder area in the view template where the dynamic component will be rendered, using a #virtualContainer file called portals-entry.component.html

<div class="portals-outlet" >
    <ng-container #virtualContainer>
    </ng-container>
</div>
Copy the code

The second step

Use ViewChild to fetch the logical container file portals-entry.component.ts corresponding to the container

 @ViewChild('virtualContainer', { read: ViewContainerRef })
  virtualContainer: ViewContainerRef;
Copy the code

The third step

Handles click events, dynamically creates a component when the button is clicked, portals-entry.component.ts complete logic

import { TaskDetailComponent } from '.. /task/task-detail/task-detail.component';
@Component({
  selector: 'app-portals-entry',
  templateUrl: './portals-entry.component.html',
  styleUrls: ['./portals-entry.component.scss'],
  providers: [
  ]
})
export class PortalsEntryComponent implements OnInit {
  @ViewChild('virtualContainer', { read: ViewContainerRef })
  virtualContainer: ViewContainerRef;
  constructor(
    private dynamicComponentService: DynamicComponentService,
    private componentFactoryResolver: ComponentFactoryResolver,
    private injector: Injector,
  ) { }
  ngOnInit() {}openTask() {
    const task = new TaskEntity();
    task.id = '1000';
    task.name = 'Write an article about Portals'; const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TaskDetailComponent); const componentRef = this.virtualContainer.createComponent<TaskDetailComponent>( componentFactory, null, this.virtualContainer.injector ); (componentRef.instance as TaskDetailComponent).task = task; // Pass arguments}}Copy the code

Code instructions

  1. The openTask() method binds to the click event of a button in the template
  2. Import the TaskDetailComponent to dynamically create
  3. The constructor injectorinjectorComponentFactoryResolver dynamically creates the required objects for a component. These instance objects are available only in the context of the component
  4. To create a component using the API, now create a ComponentFactory object based on the component type, and then call createComponent of viewContainer to create the component
  5. Use Componentref. instance to get the created component instance, which is used to set the value of the component’s Task property

other

The ViewContainerRef has a createEmbeddedView method in addition to the createComponent method, which is used to create templates

@ViewChild('customTemplate')
customTemplate: TemplateRef<any>;
this.virtualContainer.createEmbeddedView(this.customTemplate, { name: 'pubuzhixing' });
Copy the code

The createEmbeddedView method’s second argument, which specifies the context parameters for the template

<ng-template #customTemplate let-name="name"><p> Custom template, pass name: {{name}}</p> </ng-template>Copy the code

You can also insert inline view templates directly with ngTemplateOutlet and specify context parameters for the template with ngTemplateOutletContext

<ng-container [ngTemplateOutlet]="customTemplate" [ngTemplateOutletContext]="{ name:'pubuzhixing' }"></ng-container>
Copy the code

summary

Analyzing the Angular API for dynamically creating a component/embedded view, dynamically creating a component requires a component definition or template declaration to be created, and an Angular context to provide where the component is rendered and where the component’s dependencies are retrieved. The viewContainerRef is where the dynamic component is inserted and provides the logical scope of the component. In addition, the dependency injector has been passed in separately. The example uses the logical container injector directly. Example repository: github.com/pubuzhixing…

CDK Portal official documents

Here is a brief description of portal-related content, followed by two usage examples. This content was originally intended to be put at the end, but finally decided to be put at the front. You can first have a simple understanding of Portals. Address: material. Presents. IO/CDK/portal /…

——– Documentation start Portals provide a scalable implementation of rendering dynamic content into an application that encapsulates the Angular process of dynamically creating components

Portals

This Portal refers to the ability to dynamically render a specified location

The UI piece
open slot


The UI piece
open slot


Portal<T> contains abstract classes for dynamic components, which can be TemplatePortal or ComponentPortalCopy the code
methods describe
attach(PortalOutlet): T Attach the current Portal to the host
detach(): void Detach Portal from the host
isAttached: boolean Whether the current Portal is attached to the host
PortalOutlet Hosts the dynamic componentCopy the code
methods describe
attach(Portal): any Append specified Portal
detach(): any Remove the current attached Portal
dispose(): void Permanently frees host resources
hasAttached: boolean Check whether the system is installed on Portal

Code snippet description

CdkPortal

<ng-template cdkPortal> <p>The content of this template is captured by the portal.</p> </ng-template> <! -- OR --> <! <p *cdkPortal> The content of this template is captured by The portal. </p>Copy the code

The Portal can be obtained via ViewChild and ViewChildren. The type should be CdkPortal, as shown below:

Portal@viewChild (CdkPortal) templateCDKPortal: TemplatePortal<any>;Copy the code

ComponentPortal Indicates the Portal of a component type. You can dynamically create this component only when the current component is configured in entryComponents of an NgModule.

 this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
Copy the code

The CdkPortalOutlet directive adds a portal outlet to an ng-template. CdkPortalOutlet specifies the current element as a PortalOutlet, The following code binds userSettingsPortal to this portal-outlet

<! -- Attaches the `userSettingsPortal` from the previous example. --> <ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
Copy the code

—– End

Example usage of Portals

Here we begin by using the new API to fulfill the same requirements as in the previous example, rendering the TaskDetailComponent dynamically in the same location.

The first step

To also set up a host element for rendering dynamic components, you can mount a PortalOutlet on the ng-container element using the cdkPortalOutlet directive

<div class="portals-outlet">
   <ng-container #virtualContainer cdkPortalOutlet>
   </ng-container>
</div>
Copy the code

The second step

Use the same logical element as the section on creating components dynamically using the Angular API, except that the fetch container is of type CdkPortalOutlet, as shown below

@ViewChild('virtualContainer', { read: CdkPortalOutlet })
virtualPotalOutlet: CdkPortalOutlet;
Copy the code

The third step

Create a Portal of type ComponentPortal and attach it to the host virtualPotalOutlet obtained above

  portalOpenTask() { this.virtualPotalOutlet.detach(); const taskDetailCompoentPortal = new ComponentPortal<TaskDetailComponent>( TaskDetailComponent ); const ref = this.virtualPotalOutlet.attach(taskDetailCompoentPortal); // The task argument can also be passed through ref.instance}Copy the code

summary

Here is a sample implementation of dynamic component creation using ComponentPortal. Portal also has a subclass, TemplatePortal, which is implemented for templates. In summary, using Portals can greatly simplify your code logic. Example repository: github.com/pubuzhixing…

Portals source code analysis

The above is just the simplest way to use Portal, but let’s discuss its source code implementation to get a better understanding

ComponentPortal

The constructor for ComponentPortal is defined as a component type. The constructor for ComponentPortal is defined as a component type. The constructor for ComponentPortal is defined as a component type

exportclass ComponentPortal<T> extends Portal<ComponentRef<T>> { constructor( component: ComponentType<T>, viewContainerRef? : ViewContainerRef | null, injector? : Injector | null, componentFactoryResolver? : ComponentFactoryResolver | null) { super(); this.component = component; this.viewContainerRef = viewContainerRef; this.injector = injector; this.componentFactoryResolver = componentFactoryResolver; }}Copy the code

The other two arguments to the ComponentPortal constructor

viewContainerRef

injector


viewContainerRef

The optional argument is attached to a PortalOutlet by default. If you pass the viewContainerRef argument, then ComponentPortal is attached to that viewContaierRef instead of the element on which the current PortalOutlet is located.


injector

This parameter is optional. By default, the PortalOutlet logical container injector is used. If passed in, the dynamically created component uses the injector as its injector.

BasePortalOutlet

BasePortalOutlet provides a partial implementation of attaching ComponentPortal and TemplatePortal. Let’s look at some of the attach method code (just to show some of the logic)

  /** Attaches a portal. */
  attach(portal: Portal<any>): any {
    if(! portal) { throwNullPortalError(); }if (portal instanceof ComponentPortal) {
      this._attachedPortal = portal;
      return this.attachComponentPortal(portal);
    } else if (portal instanceof TemplatePortal) {
      this._attachedPortal = portal;
      return this.attachTemplatePortal(portal);
    }
    throwUnknownPortalTypeError();
  }
Copy the code

Before attaching, the createComponent or createEmbeddedView method of ViewContainerRef is called based on whether the Portal type is a component or template. For interest in this section check out the source file portal-cache. ts.

DomPortalOutlet

DomPortalOutlet can insert a Portal into the DOM outside of an Angular application context. Consider our previous example, Whether you implement it yourself or use CdkPortalOutlet, you insert a template or component into the host ViewContainerRef in an Angular context, and DomPortalOutlet is one

Out of Angular Context



Look at all

if(! this._outlet) { this._outlet = new DomPortalOutlet(this._document.createElement('div'), this._componentFactoryResolver, this._appRef, this._injector); } const element: HTMLElement = this._template.elementRef.nativeElement; element.parentNode! .insertBefore(this._outlet.outletElement, element); this._portal.attach(this._outlet, context);Copy the code

The above code first creates an object of type DomPortalOutlet, _outlet. DomPortalOutlet is a DOM host that is not in any Angular ViewContainerRef. Now look at its four constructor arguments

Parameter names type instructions
outletElement
Element The document element created
_componentFactoryResolver
ComponentFactoryResolver At first I didn’t understand what this instance object was for, but later I looked up the information, it is probably used to compile the component or template to be created
_appRef
ApplicationRef An associated object of the current Angular application
_defaultInjector
Injector Injector object

Explanation: this section is about

Out of Angular Context

No template or Component can be rendered into the specified DOM outside of the Component Tree that is actually rendered.

Complex sample

The PortalInjector instance has configured the Injector for another business component and has configured tokens. The PortalInjector instance has configured the Injector for another business component and has configured tokens.

Business Component TaskListComponent

The file task-list.com ponent. Ts

@ Component ({, the selector:'app-task-list',
  templateUrl: './task-list.component.html',
  styleUrls: ['./task-list.component.scss'],
  providers: [TaskListService]
})
export class TaskListComponent implements OnInit {
  constructor(public taskListService: TaskListService) {}
}
Copy the code

The component-level provider is configured with TaskListService

Define TaskListService

Get the task list data and save it in the property Tasks

TaskListComponent template

Bind the TaskListService. tasks property data directly in the template

Modify the parent PortalsEntryComponent

Because PortalOutlet is in the parent component, the logic for clicking on the task list to create a dynamic component is the portals-entry.component.ts response from the parent component

   @ViewChild('taskListContainer', { read: TaskListComponent })
  taskListComponent: TaskListComponent; 
  ngOnInit() {
    this.taskListComponent.openTask = task => {
      this.portalCreatTaskModel(task);
    };
  }
portalCreatTaskModel(task: TaskEntity) {
    this.virtualPotalOutlet.detach();
    const customerTokens = new WeakMap();
    customerTokens.set(TaskEntity, task);
    const portalInjector = new PortalInjector(
      this.taskListViewContainerRef.injector,
      customerTokens
    );
    const taskModelCompoentPortal = new ComponentPortal<TaskModelComponent>(
      TaskModelComponent,
      null,
      portalInjector
    );
    this.virtualPotalOutlet.attach(taskModelCompoentPortal);
  }
Copy the code

The constructor for ComponentPortal has been passed the parameter PortalInjector of type PortalInjector, which has inherited from Injector

The PortalInjector constructor has two arguments

  1. The first parameter is to provide a basis of injector injector, here use taskListViewContainerRef. Injector, TaskListViewContainerRef is the viewContainerRef of the business TaskListComponent
    @ViewChild('taskListContainer', { read: ViewContainerRef })
    taskListViewContainerRef: ViewContainerRef;
    Copy the code

    The new component’s injector comes from the TaskListComponent

  2. The second parameter is to provide a token. WeakMap is a key/value pair. Its key can only be an object of reference type. Using the set methodcustomerTokens.set(TaskEntity, task);.

A new task detail component, TaskModelComponent

task-model.component.ts

  constructor(
    public task: TaskEntity,
    private taskListService: TaskListService
  ) {}
Copy the code

That’s right, get the TaskEntity instance and the TaskListService instance via injector injection.

summary

This example is relatively complex and just illustrates that you can pass in a specific Injector for dynamically created components.

conclusion

I want to write the use of Portals mainly because I saw the implementation of ThyDialog in our component library. I think these uses are clever, so I want to share them. Example repository: github.com/pubuzhixing… Component warehouse: github.com/worktile/ng…

expand

ViewContainerRef

Angula. Cn explanation: Represents a container that can attach one or more views to a component, which can contain both a host view (created when the component is instantiated with the createComponent() method) and an embedded view (created when TemplateRef is instantiated with the createEmbeddedView() method). ViewContainerRef is a unit of Angular logic that corresponds to a component or HTML element in a page in a different logical form. It also has hierarchies, but hierarchies don’t correspond to hierarchies in the component tree. Take the implementation of ComponentPortal in Portals, for example, and pass in a viewContainerRef, a code fragment, in the constructor

/**
 * A `ComponentPortal` is a portal that instantiates some Component upon attachment.
 */
export class ComponentPortal<T> extends Portal<ComponentRef<T>> {
  /**
   * [Optional] Where the attached component should live in Angular's * Logical * Component tree. * This is different from where the component *renders*, Which is determined by the PortalOutlet. * This is different from where the component is actually rendered, The actual location is determined by PortalOutlet * The origin is necessary when The host is outside of The Angular application context This parameter is mandatory when the host is outside the Angular context */ viewContainerRef? : ViewContainerRef | null; constructor( component: ComponentType
      
       , viewContainerRef? : ViewContainerRef | null, injector? : Injector | null, componentFactoryResolver? : ComponentFactoryResolver | null) { // ... }}
      Copy the code

Some viewContainerRef has carried on the simple translation, but I still don’t know how it is with the real implementation of logical component tree rendering component tree set different level, after his attempt when setting viewContainerRef component is rendered in the incoming viewContainerRef. attribute

element
injector



element





injector


The injector bubbles

WeakMap

At first, I was confused about the implementation of WeakMap because I did not know about WeakMap, so I checked relevant materials of WeakMap

A WeakMap object is a set of key/value pairs where the keys are weakly referenced. The key name must be an object, and the value can be arbitrary. Key names are weak references to objects. When objects are reclaimed, WeakMap automatically removes the corresponding key-value pairs. WeakMap structure helps prevent memory leaks. It can be compared with Map, in which key can be of various types, while WeakMap must be an object. In this way, WeakMap can be used to expand the attribute value of the object without modifying the object of the original reference type, and does not affect the garbage collection of the object of the reference type. With the disappearance of the object, the expanded attribute will disappear.


Worktile’s website: Worktile.com

Author: Zhenxing Yang, Engineer, Worktile

This article was first published on Worktile’s official blog.