How to define a custom element method with angular elements

1.2k views Asked by At

Our Angular-Elements webcomponent should have the two methods openModal() and closeModal() that can be used as custom-element inside React, Vue or any other library to change the visibility state.

This simple javascript usage should be possible:

<my-modal id="modal"></my-modal>
<button id="openModal" onclick="openModal()">open</button>
<button id="closeModal" onclick="closeModal()">close</button>

<script>
  const modal = document.getElementById("modal");

  function openModal() {
    modal.openModal();
  }

  function closeModal() {
    modal.closeModal();
  }
</script>

The internal attribute visible should be set to true or false so that it can be used by the template.

@Component({
  selector: 'my-modal',
  template: `<p>Attribute visible: {{visible}}</p>`, 
  styles: []
})
export class MyModalComponent {

  // Without `@Input` changes to the variable within `openModal()` 
  // and `closeModal()` will not effect the components state!
  visible = false;

  // Without the `@Input()` here, angular-elements will not
  // map the `openModal()` to our custom-element.
  @Input()
  public openModal(): void {
    console.log("Open Modal")
    this.visible = true;
  }

  @Input()
  public closeModal(): void {
    console.log("Close Modal")
    this.visible = false;
  }
}

Questions:

  1. Why we need to decorate our public component methods (openModal() and closeModal() with @Input()) even if we are not using them as Input-Parameter?
  2. Why we need to decorate the visible-Flag with @Input? This is pretty bad because we expose internal state and furthermore it looks like to be a valid approach?
1

There are 1 answers

0
Markus Dresch On BEST ANSWER

@Input()s are designed for properties, not functions. For your use case you can simply use a getter and return a function there.

As for the visible state not changing: it does change (just check with console.log after the change). However, the change is not detected by angular's change detection, since the call comes out of angulars context. You'd experience the same issue when using setTimeout for example to change a value. To get around this, inject NgZone into your component, and use this.zone.run to have angular detect the changes.

import { Component, Input, NgZone } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<p>Attribute visible: {{visible}}</p>`, 
  styles: []
})
export class MyModalComponent {
  visible = false;

  // use property getters to return functions

  @Input()
  get openModal(): () => void {
    return () => {
     // make change in angular's context so changes are detected by angular
      this.zone.run(() => this.visible = true);
    }
  }

  @Input()
  get closeModal(): () => void {
    return () => {
      // make change in angular's context so changes are detected by angular
      this.zone.run(() => this.visible = false);
    }
  }

  // inject NgZone
  constructor(private zone: NgZone) { }
  
}

Alternatively you may want to play around with OnPush change detection. You'd use Subjects and Observables and a "push" pipe (variant of async pipe) as implemented in this package: https://github.com/rx-angular/rx-angular

// in app.module.ts
import {TemplateModule} from '@rx-angular/template';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    TemplateModule // import module
// ...

import { Component, Input, NgZone } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app-root',
  // use observable with push pipe
  template: `<p>Attribute visible: {{visible$ | push}}</p>`,
  // use OnPush change detection
  changeDetection: ChangeDetectionStrategy.OnPush,
  styles: []
})
export class AppComponent {
  
  // use subjects/observables
  visibleSubject = new BehaviorSubject<boolean>(false);
  visible$ = this.visibleSubject.asObservable();
  
  @Input()
  get openModal(): () => void {
    return () => {
      this.visibleSubject.next(true);
    }
  }

  @Input()
  get closeModal(): () => void {
    return () => {
      this.visibleSubject.next(false);
    }
  }
}

Of course if it's something as simple as changing a boolean flag, you might as well just expose that property as an @Input(), this way it would be bindable in the host application.