3

I'm building a few WebComponent that I'll be using across different libraries/frameworks, but mostly with plain vanilla JavaScript. I'm considering using Angular Elements for this, and built a sample one following few documents.

It works fine, with all natural Angular goodies built in. But, what I'm struggling with is, reading data out of the web component from plain JavaScript, something like var result = myComponent.value.

  • Note that this data I'm reading is not primitive, or a single value. It's like an object with many details from a complex WebComponent
  • I'd want something like a readonly property
  • Standard Angular documents for components always use @Output() with an EventEmitter for this purpose. But since I'll be using the component as WebComponent outside Angular world, I cannot (and don't want to) use that approach. Also, I do not want to notify on each change of the value, but just read the value when needed

If I'm writing a standard WebComponent in plain JavaScript, I can do this

class MyComponent extends HTMLElement {

    constructor() {
        super();
        this.name = 'Guest';
        this.count = 0;
    }

    // more code...

    // THIS result PROPERTY IS USED TO READ DATA FROM THE WEB COMPONENT
    get result() {
        return { name: this.name, count: this.count }; // <<== to read data
    }

    connectedCallback() {
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                styles....
            </style>
            <h1>MyComponent</h1>
            More HTML...
        `;
    }
}

customElements.define('my-component', MyComponent);

// Then I can use <my-component></my-component> in my application HTML
// And in the JavaScript code, I can read data from my-component as

const result = document.querySelector('my-component').result;
// which would be something like { name: 'Guest', count: 0 }

Is there a way to do this in AngularElements ?

Arghya C
  • 9,805
  • 2
  • 47
  • 66

1 Answers1

0

Ok, so I have got 2 workarounds to deal with it for now! They are more like hacks as (1) they are not done the standard Angular way, and (2) might break in future with implementation changes within Angular. Below are the 2 ways I could achieve an output property for a WebComponent built with Angular Elements

(I'm using "@angular/core": "~9.1.11" & "@angular/elements": "^9.1.11")

  1. My colleague pointed out that createCustomElement only exposes inputs as properties. See code here. SO the hack is, to mark a property as Input() then just read it from outside like a normal property! It's very non-intuitive and doesn't follow any standard semantics. And it's interesting that Angular does not complain if I have just a getter for a property decorated with @Input()
  2. A better approach, as was pointed out by Danny '365CSI' Engelman in the comments, we can define a new property on the native HTML element, using Object.defineProperty

Note that, instead of option (1), a better approach could be to enhance/extend the logic in Angular to include other properties from the component class as the element properties, maybe with a new custom decorator like @outputProperty.

Sample code with implementation of both the above approaches

// The component
import { Component, OnInit, ViewEncapsulation, Input, ElementRef } from '@angular/core';

@Component({
    // selector, templateUrl & styleUrls
    encapsulation: ViewEncapsulation.ShadowDom,
})
export class MyComponent implements OnInit {

    constructor(
        private element: ElementRef,
    ) {
        // THIS IS APPROACH 2 => we are defining a 'result2' property on the HTMLElement
        Object.defineProperty(this.element.nativeElement, 'result2', {
            get: () => { return { name: this.name, count: this.count }; },
            enumerable: true,
        });
    }

    public name = 'World';
    public count = 0;

    // More code for component functionalities and state management...

    // Just note that this does NOT work i.e. cannot read it from outside
    public get result0(): ElementMetadata {
        return { name: this.name, count: this.count };
    }
    public set result0(value) { console.log(`result value set to ${value}`); }

    // THIS IS APPROACH 1 => we are marking 'result1' with a getter, as @Input
    @Input()
    public get result1(): ElementMetadata {
        return { name: this.name, count: this.count };
    }
}

// The module (no changes made specifically for this)
export class AppModule {
    constructor(private injector: Injector) { }

    ngDoBootstrap() {
        const custElement = createCustomElement(
            MyComponent,
            { injector: this.injector },
        );
        customElements.define('my-component', custElement);
    }
}
Arghya C
  • 9,805
  • 2
  • 47
  • 66