Sekrab Garage

Angular programmatically created components

Creating a component dynamically, and programmatically in Angular

Angular May 14, 23
Subscribe to Sekrab Parts newsletter.
Series:

Programmatic components

In Angular 13, the ComponentFactoryResolver became deprecated. Some libraries that depended on it had to be rewritten using the new introduced function createComponent, that is more aligned with the standalone mantra. Let's dig into it and put it to good use.
  1. 8 months ago
    Creating a component dynamically, and programmatically in Angular
  2. 8 months ago
    Inserting Peach component into Rose at runtime
  3. 8 months ago
    Inserting programmatically created components with templates and content projection in Angular
  4. 8 months ago
    Providing services to programmatically created components in Angular
  5. 8 months ago
    Homemade dialog service in Angular

An example use of a dynamically, programmatically created component in Angular is the infamous Dialog. Today, let's go over the documentation of the createComponent and setup the bare minimum to make use of it later.

Follow me on StackBlitz

anchorThe bare minimum

Let’s first create the component that needs to be inserted programmatically, that has a simple yes, and no buttons. The following is the plan:

  • Create the component programmatically from another part of the app. Let’s call her Rose.
  • Insert into a known HTML tag
  • Insert into the HTML body
  • Pass data to Rose
  • Interact with Rose

Once we cover these, we can aspire for more: we will insert a new component into Rose, let’s call it Peach.

anchorRose

Here is rose:

// src/components/rose.partial
@Component({
  template: `
   <button (click)="ok()">Yes</button>
   <button (click)="no()">Cancel</button>
   `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class RosePartialComponent {
  constructor() {
    //
    console.log('created');
  }
  
  ok(): void {
    console.log('ok');
  }
  no(): void {
    console.log('no');
  }
}

In our app component, the following could be anywhere, but for simplicity let’s do that in the app root component.

// main.ts, or app.component

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule],
  template: `
  <button (click)="insertElement()" class="btn">Insert element</button>
	<!-- hosting Rosy -->
  <div id="hostmehere"></div> 
  `,
})
export class App {
  insertElement(): void {
    // in here we shall create Rose and add it to hostmehere element
  }
}
// bootstrap app
bootstrapApplication(App);

According to the documentation and the example provided, we need to:

  1. Bootstrap an application (we already did that). To get the application reference though we can simply inject it in app component constructor.
  2. Locate a DOM node that would be used as a host. We used hostmehere
  3. Get an EnvironmentInjector instance from the ApplicationRef injected token
  4. We can now create a ComponentRef instance. And pass to it the host element and environment injector.
  5. Last step is to register the newly created ref using the ApplicationRef instance, to include the component view into change detection cycles. Let’s put this off for a minute.

Let’s adapt and implement all the above steps

// main.ts

// inject reference to appRef
constructor(private appRef: ApplicationRef) {}

insertElement(): void {
  // in here we shall create Rose and add it to hostmehere element
  const host = <Element>document.getElementById('hostmehere');
  const componentRef = createComponent(RosePartialComponent, {
    hostElement: host,
    // the environment injector instroduced for standalone
    environmentInjector: this.appRef.injector,
  });
}

This should add Rosy to the hostmehere element, and it receives clicks as expected.

anchorAppend programmatically

What if we want to remove the host element, and append to a programmatically created HTML element instead? Two ways

The simple way

The obvious way is to create a new HTML element and append it to document.body

// create a new element the simple way
// don't forget to use the right platform for SSR
const newHost = document.createElement('somenewelement');
document.body.append(newHost);

const componentRef = createComponent(RosePartialComponent, {
  hostElement: newHost,
  environmentInjector: this.appRef.injector,
});

The posh way

We can append the resulting component reference into the body as well. Console logging the componentRef we have access to hostview which contains rootNodes array, which apparently has our HTML element. So let’s append that:

// main.ts
const componentRef = createComponent(RosePartialComponent, {
  environmentInjector: this.appRef.injector,
});

// append the rootNodes root after creation. That works too.
// don't forget to use the right platform for SSR
document.body.append((<any>componentRef.hostView).rootNodes[0]);

Note, I am casting everything to any because that’s beyond the point. But if you insist, the right type is EmbeddedViewRef.

document.body.append((<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0]);

So far so good. Let’s add template variables to Rose.

anchorAttaching the view

If we do the following in Rose’s template:

`{{ something }}`

Upon creating the component, the variable will not reflect changes. To make it part of the change detection cycle, we need to attachView to the application reference.

// main.ts
const componentRef = createComponent(RosePartialComponent, {
  environmentInjector: this.appRef.injector,
});

// attach view to make it part of change detection cycle
this.appRef.attachView(componentRef.hostView);

document.body.append(
  (<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0]
);

anchorPassing inputs

To set input after creation, all we need to do is use setInput of component reference:

// main.ts
// set inputs
componentRef.setInput('something', 'something else');

// rose component
// create input
@Input() something: string = 'somevalue';

We can also set the value of public properties through the instance property.

componentRef.instance.something = 'anything else';

We can also call public methods in our instance the same way. We’re going to use that later to try and remove the component from within the component itself.

anchorDetach and destroy.

In order to move on to other routes, especially that we appended directly to the body, we need a way to destroy the component and detach it from the change detection cycle.

// maint.ts
// remove element using its component reference
this.appRef.detachView(componentRef.hostView);
componentRef.destroy();

anchorEmit output

If Rose has an output emitted, the EventEmitter in Angular is an extension of RxJS Subject, thus to listen to those events, we can subscribe to them.

// rose.component
// create output
@Output() onSomething: EventEmitter<string> = new EventEmitter<string>();

// in main.ts
// listen to output events
componentRef.instance.onSomething.subscribe((data: string) => {
  console.log(data);
});

anchorPut it in

Let me make the removal of the element, from inside the element itself. That should be handy. In Rose, let’s add a remove button and attach it to an event.

// rose:
@Component({
  template: `
  // ... add remove button
    <button class="btn-fake" (click)="remove()">Remove</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RosePartialComponent {
  // add output 
  @Output() onRemove: EventEmitter<string> = new EventEmitter<string>();
  
  // emit on click
  remove(): void {
    this.onRemove.emit('remove element');
  }
}

In main component, let’s listen to the remove button to remove element. And since we are subscribing, let’s also unsubscribe.

// main.ts
insertElement(): void {
  // ...
  // listen to Output events
  const ref = componentRef.instance.onRemove.subscribe((data: string) => {
    // remove element, then unsubscribe
    this.appRef.detachView(componentRef.hostView);
    componentRef.destroy();
    ref.unsubscribe();
  });
}

With those basic ingredients, let’s rewrite our toast service to get rid of the toast element, and make it programmatic.

anchorPutting it to good use. The toast as an example.

In our previous trip to error handling, we created a toast service that controls a single toast element, showing, and hiding it upon request. We can use this method to insert the component programmatically upon initialization of the Toast state service, and that will reduce some code lurking around, specifically the toast host element in the app root. This change is superficial and would not alter any behavior.

When initialized, the toast component injects the same service immediately, causing a circular dependency issue. To avoid that, we have a very simple solution, create the element upon first time Show.

We shall use a flag to detect the existence of the component.

// services/toast/toast.state
// rewrite

@Injectable({ providedIn: 'root' })
export class Toast extends StateService<IToast> {
  
  // v16 inject application reference
  constructor(private appRef: ApplicationRef) {
    // ...
  }

  // v16 use a flag
  private created: boolean;
  // add component programmatically
  private addComponent() {
    // v16 check component if it does not exist, create it
    if (this.created) {
      return;
    }
    const componentRef = createComponent(ToastPartialComponent, {
      environmentInjector: this.appRef.injector
    });
  
    this.appRef.attachView(componentRef.hostView);
  
    // append to body
    document.body.append((<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0]);
  
    this.created = true;
  }

  Show(code: string, options?: IToast) {
    // v16 add the component
    this.addComponent();
    // ... eveyrthing else stays the same
  }
}

Also remove any reference to ToastPartialComponent, and gr-toast. Now that we create the component programmatically, we don't even need the selector. But we do need to turn the component into standalone, and import its required modules:

// services/toast/toast.partial

// turn it into standalone, and drop the selector
@Component({
  standalone: true,
  imports: [CommonModule],
  // ...
})
export class ToastPartialComponent {
  constructor(public toastState: Toast) {}
}

Have a look at it on StackBlitz.

Dialog

Time to make a bigger use of it, let’s adapt it to create a dialog service, where Peach is going to be displayed in a Rose component, upon request. Let’s sleep on that till next Tuesday, or any day of the week.

Thanks for reading this far, did you miss me?

  1. 8 months ago
    Creating a component dynamically, and programmatically in Angular
  2. 8 months ago
    Inserting Peach component into Rose at runtime
  3. 8 months ago
    Inserting programmatically created components with templates and content projection in Angular
  4. 8 months ago
    Providing services to programmatically created components in Angular
  5. 8 months ago
    Homemade dialog service in Angular