Inserting Peach component into Rose at runtime
Tags: angular, webdev, tutorial
Published: 2023-05-21 15:32:22

The great Dialog component
--------------------------

The [Angular Material Dialog](https://material.angular.io/components/dialog/overview) 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 is being created. First, say hello to Peach. What we want, is a component that receives input, emits output, and has its own events.

```ts
// 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');
  }
}
```

Host 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).

```ts
// 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.

Append HTML
------------

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

```ts
// 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,

```ts
// 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.

Rose 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

```ts
// components/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.

```ts
// 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](https://angular.io/api/core/createComponent) documentation, the signature's first parameter `component` is of type: `Type<C>`. [Type](https://angular.io/api/core/Type) is an Angular class that refers to the Component type. So our `open` method can be typed better with `Type<any>`.

```ts
// 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.

```ts
// 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.

```ts
// 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.

Passing 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:

```ts
// 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?

Passing 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)

```ts
// 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`

```ts
// 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

```ts
// 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.

Asking 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.

### 1\. 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.

```ts
// 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.

### 2\. 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`:

```ts
// 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

```ts
// 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?

Emitting 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`

```ts
// 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

```ts
// 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?

Handling 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.

### 1\. Event handlers as options

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

```ts
// 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

```ts
// 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

```ts
// 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);
      }
    });
}
```

### 2\. 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.

```ts
// 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

```ts
// 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.

Standalone
----------

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.

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

Tidy 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](https://stackblitz.com/edit/angular-program-component?file=src%2Flib%2FRoseDialog%2Frose.service.ts)

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

{% embed https://garage.sekrab.com/posts/inserting-peach-component-into-rose-on-runtime %}