A versatile component loader that serves as a shared service for resolving components

Utilizing the ComponentFactoryResolver is necessary to dynamically incorporate components. In my scenario, I have a modal window that is loaded dynamically upon clicking a button on the view. The code for this operation appears as follows:

let componentFactory = this.componentFactoryResolver.resolveComponentFactory(XYZComponent);
let viewContainerRef = this.containerHost.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(componentFactory);

let _XYZComponent = componentRef.instance as XYZComponent;

_XYZComponent.close.subscribe(() => {
    componentRef.destroy();
});

Each time I want to utilize this modal window, I must use similar code with just the component name changing.

I am interested in creating a shared service for this purpose but I am struggling to find an ideal solution that considers dynamically loaded components.

Does anyone have suggestions on how to create a suitable service for using this code efficiently?

Answer №1

When dealing with loading components dynamically, it is essential to register these components within the entryComponents of your modal component decorator. In addition, since these components are classes in TypeScript, you must import them from the relevant component where your modal is called. To streamline this process, it's recommended to gather all dynamic components in a single file and export them as an array.

For example, create a file named dynamic-components.ts to store all potential dynamic components:

import { FooComponent } from './path/to/foo';
import { BarComponent } from './path/to/bar';
import { BazComponent } from './path/to/baz';

// Export all dynamic components
export const dynamicComponents = [
  FooComponent, BarComponent, BazComponent
]

In your modal component, simply spread these components into the entryComponents property:

import { dynamicComponents } from './path/to/dynamic-components';
@Component({
  //...
  entryComponents: [ ...dynamicComponents ]
})
export class ModalComponent {}

Now, within your modal component, you can create a method that renders a component based on the provided component name and metadata to handle properties dynamically. This approach enhances the flexibility of your modal by enabling the rendering of components with varying inputs and outputs.

Instead of hardcoding the component in the method, extract it from the dynamicComponents array. You can do this by utilizing the name property of JavaScript classes, which matches the parameter passed to your function with the component names in dynamicComponents.

export class ModalComponent {
  //...
  createComponent(name: string, metadata: any) {
    const viewContainerRef = this.entryContainer.viewContainerRef;
    const cmpClass = dynamicComponents.find(cmp => cmp.name === name);
    const cmpToCreate = new DynamicComponent(cmpClass, metadata);

    const componentFactory = this.cmpFactoryResolver.resolveComponentFactory(cmpToCreate.component)

    viewContainerRef.clear();

    const cmpRef = viewContainerRef.createComponent(componentFactory);

    // Patch input values ...
    for (let input in metadata.inputs) {
      if (metadata.inputs.hasOwnProperty(input)) {
        cmpRef.instance[input] = metadata.inputs[input];
      }
    }

    // Subscribe to outputs
    for (let output in metadata.outputs) {
      if (metadata.outputs.hasOwnProperty(output)) {
        console.log('hasOutput', metadata.outputs[output]);
        cmpRef.instance[output].subscribe(metadata.outputs[output]);
      }
    }
  }
}

To ensure proper type handling, define the DynamicComponent class as follows:

export class DynamicComponent {
  constructor(public component: Type<any>, data: any) {}
}

The rest of the function remains similar to what you already have, with the additional use of the metadata parameter. This object contains inputs and outputs for the instantiated components. When calling the method, provide the component name and respective metadata:

export class SomeComponentThatRendersTheModal() {
  renderFooComponent() {
    // Assume there's a modal service to open the modal
    this.modalService.openModal();
    this.modalService.createComponent(
      'FooComponent', { 
        inputs: { fooInputTest: 'kitten' },
        outputs: { fooOutputTest: handleOutput } 
      }
    );
  }

  // Handle the output event
  handleOutput(emittedEvent) {
    // Implement functionality here
  }
}

Finally, adjust the metadata structure or how you manage input values and output handlers according to your requirements. This setup provides a foundational guide on creating components dynamically.


For a visual demonstration, check out the associated demo. I hope this explanation clarifies the process for you.

10/22/21 Update:

Note that directly accessing the name property of a Function object may cause issues once the code gets minified for production. To address this concern, refer to the documented workaround provided in the angular repo.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Return a 404 status code in Angular 5

I'm facing a frustrating issue that is causing me trouble. For some reason, I can't get the 404 status to work with Angular 5. Here are the steps I've taken: In my server.ts file, I included const { AppServerModuleNgFactory, LAZY_MODUL ...

The ko.mapping function is throwing an error because it cannot access the property 'fromJS' which is undefined

I am currently exploring typescript and attempting to integrate knockout.mapping into my project, but I'm facing some challenges. Despite installing the necessary libraries for knockout and knockout.mapping, along with their respective "@types" decla ...

What is the reason behind the input value becoming empty after a reset?

Currently, I am working with an input element reference: @ViewChild("inputSearch", { static: false }) This is how the template looks like: <input tabindex="0" type="text" (keydown)="keydownInputSearch($event)" #inputSearch autocomplete="off" ...

Enhancing the Look of Typeahead in ng-bootstrap

I'm finding it difficult to customize the appearance of the dropdown menu in ng bootstrap typeahead. The input styling works fine (the component only includes an input/typeahead), but I am unable to style the dropdown. I've tried using both the ...

Defining the type of the createAction() function in TypeScript within the context of Redux Toolkit

Lately, I have been delving into the redux-toolkit library but I am struggling with understanding the type declaration of the createAction function as demonstrated below. The createAction function returns a PayloadActionCreator which includes a generic of ...

Angular 2 Rapid Launch: Incorrect Encoding of JavaScript Files

I am brand new to learning angular 2, so I am currently attempting to get things up and running following this guide: https://angular.io/guide/quickstart The issue I am facing has left me quite puzzled. Whenever I receive any JS files as a response, they ...

Checking the status of a checkbox in Ionic 2

Is there a way to check if an ion-checkbox is marked as checked in Ionic 2? <ion-item *ngFor="let a of q.Answers"> <ion-label>{{a.AnswerDescription}}</ion-label> <ion-checkbox (click)="addValue($event)"></ion-chec ...

Authenticating to Google APIs using Node.js within a lambda function: A step-by-step guide

I'm encountering an issue when trying to connect a Google sheet to an AWS Lambda function. While the code runs smoothly during local testing, upon deployment to the function, I receive an error message indicating that the credentials.json file cannot ...

Encountering a Circular JSON stringify error on Nest.js without a useful stack trace

My application is being plagued by this critical error in production: /usr/src/app/node_modules/@nestjs/common/services/console-logger.service.js:137 ? `${this.colorize('Object:', logLevel)}\n${JSON.stringify(message, (key, value ...

Sorting an array of numbers in TypeScript using the array sort function

Looking to organize an array based on ID, comparing it with another array of numbers var items:[] = [{ item:{id:1},item:{id:2},item:{id:3},item:{id:4} }] var sorted:[] = [1,3,2,4]; Output: var items:[] = [{ item:{id:1},item:{id:3},item: ...

Inquiry regarding classifications, Maps, and subcategories in typescript

In my project, I have a class called Chart that has various subclasses like BarChart and TimeseriesChart, all of which extend the base Chart class. To create these charts, I use a method named buildChart. This method uses an enum called ChartsEnum (example ...

Angular service not triggering timer

I've developed a timer service file in Angular for countdown functionality import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class TimerUtilService { initial_module_timer_value = 300; sec ...

Calculating different percentages from a reduced map containing JSON data

Forgive me in advance if there's a similar question out there, but after a day of searching, I couldn't find what I need. I'm calling an API that responds with data, and I've extracted the necessary details to create a JSON file with t ...

Initiate the recalculation of the computed property

Currently, I am attempting to trigger an update in a computed property from my viewmodel to the UI. However, I am only able to retrieve the initial value and not any subsequent values. When trying to modify the property, it fails due to it not being recogn ...

What should you do when Node.js is unable to locate a module using the specified path?

Encountering the following error: Unable to import main.handler module. Error: Cannot find module '/function/code/skill/index' imported from /function/code/main.js. Stack: Nodejs 18, TypeScript 5.3.3. Interestingly, there is no error with import ...

What is the process for importing a function dynamically in a Next.js TypeScript environment?

Currently, I am utilizing a React modal library known as react-st-modal, and I am attempting to bring in a hook named useDialog. Unfortunately, my code is not functioning as expected and appears like this: const Dialog = dynamic<Function>( import(& ...

Angular Universal experiences issues with route functionality after page reloads

Recently deployed an Angular Universal web app on Firebase. While all routes within the application are functioning properly, encountering errors when trying to access them externally. Below is the error message: functions: Starting execution of "ssr" ⚠ ...

Using `@HostListener` with `e: TouchEvent` is known to trigger a crash in Firefox, displaying the error message "ReferenceError: TouchEvent is not defined."

When using @HostListener with the event parameter explicitly typed as a TouchEvent, it triggers Firefox to crash and display an error message: ReferenceError: TouchEvent is not defined. This can be illustrated with the following example: @HostListener ...

Adjust the size of every card in a row when one card is resized

At the top of the page, I have four cards that are visible. Each card's height adjusts based on its content and will resize when the window size is changed. My goal is to ensure that all cards in the same row have equal heights. To see a demo, visit: ...

Set up a remapping for Istanbul to encompass every source file

Currently, I am setting up my Ionic 2 application with Angular 2 and TypeScript to produce code coverage reports for my test files. For unit testing and coverage report generation, I am utilizing Jasmine, Karma, and remap-istanbul. I came across an inform ...