htmlangulartypescriptweb-componentangular-elements

How to pass an object programmatically to an Angular custom element (@angular/elements)?


I’m trying to pass an object as an input to an Angular custom element created with @angular/elements, not via HTML attributes (since setAttribute only supports strings).

Here’s a minimal example:

import {
  Component,
  Input,
  Injector,
  inject,
  NO_ERRORS_SCHEMA,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { CommonModule } from '@angular/common';

@Component({
  imports: [CommonModule],
  selector: 'foo',
  template: `foo works! {{ data.test() | json }}`,
})
export class Foo {
  @Input() name = '';
  @Input() data: any;
}

@Component({
  selector: 'app-root',
  template: `
    <h1>Hello from {{ name }}!</h1>
    <a target="_blank" href="https://angular.dev/overview">
      Learn more about Angular
    </a>
    <app-foo></app-foo>
  `,
  schemas: [NO_ERRORS_SCHEMA],
})
export class App {
  name = 'Angular';
  injector = inject(Injector);

  constructor() {
    const el = createCustomElement(Foo, { injector: this.injector });
    customElements.define('app-foo', el);

    const e = document.querySelector('app-foo') as any;
    queueMicrotask(() => {
      e.data = {
        test: () => {
          console.log('in test');
          return 'test';
        },
      };
    });
  }
}

bootstrapApplication(App);

I expect the <app-foo> element to print "foo works! test" in the template and log "in test" to the console. However, the template doesn’t update — it looks like the @Input() isn’t detecting the object assignment.

Question: How can I pass a complex object (with functions or nested data) programmatically to an Angular Element so that the component receives it and the template updates?

Notes:

Stackblitz Demo


Solution

  • Your code, just needs to have the e property moved inside the queueMicrotask callback, because only then, will the latest value be set, when the callback is called.

    In your code, the e is evaluated before queueMicrotask hence you are facing this issue.

    constructor() {
      const el = createCustomElement(Foo, { injector: this.injector });
      customElements.define('app-foo', el);
      queueMicrotask(() => {
        const e = document.querySelector('app-foo') as any;
        e.data = {
          test: () => {
            console.log('in test');
            return 'test';
          },
        };
      });
    }
    

    Full Code:

    import {
      Component,
      Input,
      Injector,
      inject,
      NO_ERRORS_SCHEMA,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { createCustomElement } from '@angular/elements';
    import { CommonModule } from '@angular/common';
    
    @Component({
      imports: [CommonModule],
      selector: 'foo',
      template: ` foo works! {{ data.test() | json }}`,
    })
    export class Foo {
      @Input() name = '';
      @Input() data: any;
    }
    
    @Component({
      selector: 'app-root',
      template: `
        <h1>Hello from {{ name }}!</h1>
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
        <app-foo />
      `,
      schemas: [NO_ERRORS_SCHEMA],
    })
    export class App {
      name = 'Angular';
      injector = inject(Injector);
    
      constructor() {
        const el = createCustomElement(Foo, { injector: this.injector });
        customElements.define('app-foo', el);
        queueMicrotask(() => {
          const e = document.querySelector('app-foo') as any;
          e.data = {
            test: () => {
              console.log('in test');
              return 'test';
            },
          };
        });
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo