Sekrab Garage

Angular programmatically created components

Inserting Peach component into Rose at runtime

Angular May 22, 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

anchorThe great Dialog component

The Angular Material Dialog component is—among other things—a component inserted into the body, then another generic component inserted into it. Let’s now try to insert a Peach component, in a Rose component after it has been created. First, say hello to Peach. What we want, is a component that receives input, emits output, and has its own events.

// components/peach.partial

// simple peach component
@Component({
  template: `
    Hello {{ peachSomething }} <br>
    <button class="btn-rev" (click)="ok()">Yes Iniside peech</button>
    <button class="btn-rev" (click)="clickPeach()">On peach event</button>
   `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PeachPartialComponent {
  @Input() peachSomething: string = 'default peach';
  @Output() onPeach: EventEmitter<string> = new EventEmitter<string>();

  constructor() {
    console.log('created');
  }
  ok(): void {
    console.log('peach ok');
  }
  clickPeach(): void {
    this.onPeach.emit('peach clicked');
  }
}

anchorHost it

In Rose, we need to create a tag that will host Peach. We can append it to the root element, but that would not be very beneficial, we really want to decide ahead of time where it shall go. So in Rose, we update and add an HTML element (we later will investigate another option).

// rose component updated
template: `
  // ... add a dom for my child
  <div class="box">
    <div id="mychild"></div>
  </div>
  //...
`

So that pretty much looks like appending Peach directly to Rose. Would that work? Let’s see.

anchorAppend HTML

One way to go about this is gain reference to the mychild element, then append our new component. Straight forward.

// in app root component

// gain reference to my child after creating Rose
const child = (<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0].querySelector('#mychild')

// create peach
const peachRef = createComponent(PeachPartialComponent, {
    // host peach in mychild reference
    hostElement: child,
    environmentInjector: this.appRef.injector
 });

// attach
this.appRef.attachView(peachRef.hostView);

Let’s investigate inputs and outputs and see if it all works the same way,

// listen to outputs of peach
const ss = peachRef.instance.onPeach.subscribe((data: string) => {
	console.log('onpeach called');
});

// assign input:
peachRef.instance.peachSomething = 'new peach something';

This works perfectly fine. Let’s put that to use.

anchorRose service

We are going to make our own dialog service that does the following:

  • Create Rose and insert in body
  • Pass Peach and append to Rose
  • Listen and respond
  • Detach and clean

Working backwards, we need to be able to do the following in our calling component

// component/dialog.component

// somewhere in our code, open dialog using our service
// Dialog in this case is Rose service
const dialog = this.roseService.open(PeachComponent);
// pass few properties to Rose:
dialog.title = "opening peach";
// pass some properties to Peach
dialog.child.prop1 = 'my new peach';
// listen 
dialog.onclose.subscribe((data: any) => {
	console.log('closed rose');
});

// we are going to combine these props as we go along

So the Rose service initially needs to create a Rose component when open is called.

// lib/rosedialog/rose.service

// inject in root
@Injectable({ providedIn: 'root' })
export class RoseService {

  constructor(
    // bring in the application ref
    private appRef: ApplicationRef,
    // use platform document 
    @Inject(DOCUMENT) private doc: Document
  ) { }

  // open method, will implement
  public open(c: any, options: any) {
    
    // first create a Rose component
    const componentRef = createComponent(RosePartialComponent, {
      environmentInjector: this.appRef.injector
    });
    
    // append to body, we will use platform document for this
    this.doc.body.append((<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0])

    // attach view
    this.appRef.attachView(componentRef.hostView);

    // when closed destroy (onClose is an Output event of Rose)
    const s = componentRef.instance.onClose.subscribe(() => {
      this.appRef.detachView(componentRef.hostView);
      componentRef.destroy();
      s.unsubscribe();
    });

    // gain reference to my child after creating Rose
    const child = (<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0].querySelector('#rosechild')

    // now create c, and append to rose host view
    const childRef = createComponent(c, {
      environmentInjector: this.appRef.injector,
      hostElement: child
    });

    // attach that as well
    this.appRef.attachView(childRef.hostView);
  }
}

Looking back at the createComponent documentation, the signature’s first parameter component is of type: Type<C>. Type is an Angular class that refers to the Component type. So our open method can be typed better with Type<any>.

// better typing for c
public open(c: Type<any>, options: any) {

}

Rose herself now should look like a dialog box, I’m removing most of the styling for now for simplicity. Rose has a header with a title, a body, and a close button.

// lib/rosedialog/rose.partial

@Component({
  template: `
    <div class="rose-overlay" >
      <div class="rose">
        <div class="rose-header">
         <h6 class="rose-title" >{{ title }}</h6>
          <button type="button" class="rose-close" (click)="close()">Close</button>
        </div>
        <div class="rose-body" id="rosechild">
          <!-- peach will be inserted here -->
        </div>
      </div>
    </div>
   `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class RosePartialComponent {

  // a property, does not have to be an input
  @Input() title: string = 'Default tile';

  // an event that we will catch to destroy Rose
  @Output() onClose: EventEmitter<any> = new EventEmitter<any>();

  // some public function we can use from within Peach
  close(data: any): void {
    this.onClose.emit(data);
  }
}

Let’s test this first before we add properties to Rose.

// components/dialog.component

// in the template
<button (click)="openRose()" class="btn">Open Rose</button>

// in the code behind
constructor(private roseService: RoseService) {
	// it's really is that simple:
	openRose(): void {
    this.roseService.open(PeachPartialComponent, { something: 'anything' });
  }
}

So now we opened a Rose component, with Peach in it. We also closed Rose on a click of a close button. We want to do more, but so far, it really is that simple.

anchorPassing properties to Rose

The first thing we want to do is pass the title property to Rose. Let’s pass that in the options param of the open method:

// lib/rosedialog/rose.service

// redifine open method
public open(c: Type<any>, options?: {title?: string}) {
  // ...
  // after attaching RoseView, let's assign title
  this.appRef.attachView(componentRef.hostView);

  // assign title
  componentRef.instance.title = options?.title || '';

  // ...
}

You might think it needs more. Like an interface called IRoseOptions. But we both know that’s just bells and whistles. What if we want to pass an object to Peach itself?

anchorPassing data to Peach

The easiest way to do that is simply pass the object to Peach. Is that possible? (Remember from our first article, we can also use Input and setInputs, but setting a public property to instance is simpler)

// rose.service

// add data of type: any
public open(c: Type<any>, options?: {title?: string, data?: any}) {
	// ...
	
	// after creating the child component, pass properties directly
	childRef.instance.data = options?.data;
}

Peach now should have a definition for data

// peach.component

export class PeachPartialComponent implements OnInit, AfterViewInit {
  // public property to set
  data: any;

  constructor() {
	  // this is undefined
    console.log(this.data);
  }

  ngOnInit(): void {
	  // this propbably has value
    console.log(this.data);
  }

  ngAfterViewInit(): void {
	  // this most definitly has value
    console.log(this.data);
  }
}

We might get access to that data as early as OnInit, but it's generally safer to wait till AfterViewInit. Trying out from our root component

// components/dialog.component

openRose(): void {
  const ref = this.roseService.open(PeachPartialComponent,
    {
      title: 'Peach says hello',
      data: 'some string'
    });
}

Notice, I made a big statement by saying that the data is available on AfterViewInit, but you and I know that Angular is too unpredictable, I have run into situations where I had to pull off some tricks. I hope with the new Angular 16 we will resolve less often to those tricks.

anchorAsking Peach to close Rose

What if I want to call a method from Peach to close Rose? There are two ways to about that, passing a reference to Rose itself to Peach, or creating an event in Peach, and catching it to close Rose.

anchor1. Passing Rose as a reference

One way to do that is to pass a reference to Rose inside of Peach, then the method this.rose.close() can be called inside of Peach.

// rose.service

open(...) {
	// ...
	// we can pass Rose partial to the new component
	childRef.instance.rose = componentRef.instance;
}

// peach.component

export class PeachPartialComponent implements OnInit, AfterViewInit { 
	// optionally define data and rose
	data: any;
	rose!: RosePartialComponent;
	// lets use rose to close
	someClick() {
		this.rose.close();
	}

}

You might think this is too clumsy, it is. But it is also that simple and straightforward.

anchor2. Sharing an event with Rose

Another way is to call a predefined event in Peach. In our example we had onPeach event. Let’s make another one: onChildClose:

// peach.component

@Output() onChildClose: EventEmitter<any> = new EventEmitter<any>();

// then call it in a method
clickPeach(): void {
  this.onChildClose.emit('close rose');
}

Now all we have to do is subscribe to that event in our Rose services

// rose.service
// ... 
open(...) {
  // subscribe to onChildClose if it exists, and destory Rose	
  childRef.instance.onChildClose?.subscribe(() => {
    this.appRef.detachView(componentRef.hostView);
    componentRef.destroy();
  });
}

Too clumsy? We’ll figure it out later. Let’s move on. How do we listen to events from the outside world?

anchorEmitting events from Rose

The most straightforward way to do it is to assign functions to the options property, then call these functions on events happening in Rose. Here is an example of onclose

// rose.service

// define an extra parameter to options
public open(c: Type<any>, options?: {
	title?: string, 
	data?: any,
	// onclose or onemit or onopen ... etc 
	onclose?: (data: any) => void}) {

	// then call these functions when you want
	const s = componentRef.instance.onClose.subscribe((data: any) => {
   
    // call onclose if exists
    if (options?.onclose) {
      options.onclose(data);
    }
    // then destroy
    this.appRef.detachView(componentRef.hostView);
    componentRef.destroy();
    s.unsubscribe();
  });
}

Then in our root component, we use it like this

// components/dialog.component

openRose(): void {
 this.roseService.open(PeachPartialComponent,
    {
      title: 'Peach says hello',
      data: 'some string',
      onclose: () => {
        console.log('closed');
      }
    });

}

This works because we know that onclose should be called on Rose closing, and we can add onopen, or oninit, etc. But what if we just want Peach to emit data?

anchorHandling Peach events

There are two ways for this as well. We either pass the event handler as an option to Rose service (if you wish to be a control freak), or we can directly catch events from Peach, no need to let Rose know about it.

anchor1. Event handlers as options

For example we can wire the onPeach event to be a property for Rose service.

// rose.service

// add yet another function
public open(c: Type<any>, options?: {
  title?: string,
  data?: any,
  onPeach?: (data: any) => void,
  onclose?: () => void}) {
		
	// at the end, emit subscribe if it exists
	childRef.instance.onPeach?.subscribe((data: any) => {
      options?.onPeach?.(data);
  });
}

Trigger the event in Peach

// peach.partial

// create an event 
@Output() onPeach: EventEmitter<string> = new EventEmitter<string>();

// trigger it with some data inside peach
clickPeach(): void {
  this.onPeach.emit('peach clicked');
}

Finally, make use of it in app root

// components/dialog.component

// openRose
openRose(): void {
 this.roseService.open(PeachPartialComponent,
    {
      title: 'Peach says hello',
      data: 'some string',
      onclose: () => {
       console.log('closed');
      },
      // pass custom event handlers
      onPeach: (data: string) => {
        console.log('do seomthing with', data);
      }
    });
}

anchor2. Keep Rose out of it

We can return a reference to the new component (Peach) and deal with it directly, No need to map all properties and events. This is definitely more robust solution, and a lot less strict.

// rose.service

@Injectable({ providedIn: 'root' })
export class RoseService {
  // open method
  public open(...) {
    // ...
  
    // return refrence to peach
    return childRef.instance;
  }
}

Now in our application, we can do the following

// components/dialog.component

openRose(): void {
  const peachRef =  this.roseService.open(PeachPartialComponent,
  {
    //... 
  });

  // its easier to deal with Peach directly:
  peachRef.onPeach.subscribe((data: any) => {
    console.log('do something with', data);
  });
}

So we kept onclose inside Rose service because we want to detach and get rid of Rose before making other calls. But everything else can be done directly on Peach component. There is no need for Rose service to get in between them.

anchorStandalone

Noticed how we did not once add standalone flag to Rose and Peach? That’s because we did not need to import any modules. If we are to use a *ngIf for example, we need to turn them into standalone, and import the CommonModule. This is much cleaner than the old solution where we had to house the different components in some module that imports needed modules.

// Peach and Rose should turn into standalone when using common module directives
@Component({
  // ...
  standalone: true,
  imports: [CommonModule]
})

anchorTidy up

Is it too clumsy? It is. And we can do something about it. But before we go with our housekeeping, there are two other ways besides an HTML node to add content. That, in addition to passing provider services, is coming on next Tuesday. 😴

The Rose service as we stand can be found on StackBlitz

Thank you for reading this far, if you have comments or questions, let me know.

  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