neukoellnjenny
neukoellnjenny

Reputation: 180

Insert PrimeNG Checkbox Custom Icon TemplateRef globally (via Directive?) [PrimeNG]

PrimeNG changed the icon mechanics of the ui components, I am updating Angular for that as well from 15 to 17. I want my custom Checkbox Icon.

Touching the html or the logic in the ts file where the p-checkbox is used, is not an option, but maybe using a directive instead.

I want to insert the icon template of the checkbox via directive

I am trying something like this

<p-checkbox checkboxIcon>
  <!-- this is not an OPTION
    <ng-template pTemplate="icon">
      <custom-icon></custom-icon>
    </ng-template>
  -->
</p-checkbox>

I somehow want to create the TemplateRef in the directive and insert it.

@Directive({
  selector: '[checkboxIcon]',
})
export class IconDirective implements OnInit {
  constructor(
    private element: ElementRef,
    @Self() private checkbox: Checkbox
  ) {}

  @HostListener('click', ['$event']) onModelChange(event): void {
    this.appendIconTemplate();
  }

  appendIconTemplate(): void {
    const iconTemplate = document.createElement('ng-template');
    iconTemplate.setAttribute('pTemplate', 'icon');
    iconTemplate.innerHTML = '<custom-icon></custom-icon>';
    this.element.nativeElement.appendChild(iconTemplate);
  }
}

I have a minimal reproduction of the issue here

Found something with @Self() and ComponentFactoryResolver. But never worked with that, can anyone help?

I think those questions here and here want to achieve something similar.

Upvotes: 3

Views: 444

Answers (1)

Tib&#232;re B.
Tib&#232;re B.

Reputation: 1191

So, there's a few things to consider for your needs, firstly, there is no easy and clean way to add a child component using directives, we can use the TemplateRef and ViewContainerRef but the component will be added as a sibling.

We also can't make use of the HostListener in our case, as this will not have the expected behaviour when using it on a structural directive.

So we must take a different approach, as the StackBlitz you shared is in Angular 17, I've made use of signals.

First, we need a new Component to encapsulate the custom icon.

@Component({
  selector: 'appCustomIcon',
  template: `
    <custom-icon></custom-icon>
  `,
})
export class CustomIconContainer {}

Then we can modify the directive to be able to create our CustomIconContainer inside of its embedded view :

@Directive({
  selector: '[checkboxIcon]',
})
export class IconDirective {
  // We will now us an input to know if the icon must be shown
  checkboxIcon = input<boolean>(false);

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {
    // This effect will listen on the changes in our input.
    effect(() => {
      // Using the signal will mark it to be used by the effect.
      const showIcon = this.checkboxIcon();

      // We run the rest of the logic in the untracked function to avoid
      // side effects
      untracked(() => {
        if (showIcon) this.appendIconTemplate();
        else this.resetView();
      });
    });
  }

  appendIconTemplate(): void {
    this.viewContainer.clear();
    const embeddedView = this.viewContainer.createEmbeddedView(
      this.templateRef
    );
    const component = this.viewContainer.createComponent(CustomIconContainer);

    // This is the "hack" that allows us to create the component
    // as a child of the element
    embeddedView.rootNodes[0].appendChild(component.location.nativeElement);
  }

  resetView(): void {
    this.viewContainer.clear();
    this.viewContainer.createEmbeddedView(this.templateRef);
  }
}

To avoid having to rely on a HostListener, we will instead listen on clicks on the checkbox itself, here's the new ChboxDemo class :

export class ChboxDemo {
  protected showCustomIcon = false;
  // The checked boolean is used to keep the state of the checkbox
  // between the view container refreshes
  protected checked = false;

  protected onClick() {
    this.showCustomIcon = !this.showCustomIcon;
  }
}

<div class="card">
  <p-checkbox
    binary="true"
    label="label"

    (click)="onClick()"
    *checkboxIcon="showCustomIcon"
    [(ngModel)]="checked"
  >
  </p-checkbox>
</div>

There was a last small problem where the icon would not show, to fix it, I simpled added a width to the svg element :

<svg style="width: 20px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path
    d="M14.83,12,18,8.82A1,1,0,0,0,18,7.4L16.6,6a1,1,0,0,0-1.42,0L12,9.17,8.82,6A1,1,0,0,0,7.4,6L6,7.4A1,1,0,0,0,6,8.82L9.17,12,6,15.18A1,1,0,0,0,6,16.6L7.4,18a1,1,0,0,0,1.42,0L12,14.83,15.18,18a1,1,0,0,0,1.42,0L18,16.6a1,1,0,0,0,0-1.42Z"
  />
</svg>

Here's your forked StackBlitz with the working solution :

https://stackblitz.com/edit/btpn8a-as1ppw?file=src%2Fapp%2Fdemo%2Fchbox-demo.html

I hope this helps and I'm available for any further questions.

Upvotes: 1

Related Questions