0.1.7 • Published 1 year ago

@elemnt/angular v0.1.7

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

@elemnt/angular (WIP)

Custom Elements with strict typing.

✅ No need for CUSTOM_ELEMENTS_SCHEMA ✅ Works with any Custom Element ✅ Strictly Typed properties ✅ Good experience with Angular Language Service ✅ Easy way to create Value Accessors (ngModel) ✅ Small runtime overhead (~1KB)

In a nutshell

Given the Custom Element below

import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";

@customElement("ui-button")
export class Button extends LitElement {
  @property({ type: String })
  text: string = "";

  protected render() {
    return html`<button>${this.text}</button>`;
  }
}

In order to have strict type checking we need to create an Angular Component wrapping it.

@elemnt/angular makes it simple.

import { Component, ElementRef, inject, Input, NgZone } from "@angular/core";
import { Element } from "@elemnt/angular";

import type { UiRange } from "ui-range";
import "ui-range/ui-range.js";

@Element()
@Component({
  selector: "ui-button",
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: "<ng-content></ng-content>",
  standalone: true,
})
export class ButtonComponent {
  e = inject(ElementRef);
  z = inject(NgZone);
  @Input() text!: Button["text"];
}

@Component({
  selector: "app-root",
  template: `
    <!-- Wrong properties are not accepted --> ✅
    <ui-range
      [range]="'This is not a number'" // Wrong Type is not accepted ✅
      [unitx]="'kg'" // Typo in property is not accepted ✅
    ></ui-range>
    
    <!-- 2. Wrong Selector is not accepted -->  ✅
    <ui-rangerrr [range]="75" [unit]="'❤'"></ui-rangerrr>
  `,
  imports: [UiRangeComponent], // Import the Component Wrapper
  standalone: true,
})
export class AppComponent {
  range = 90;
}

The issue with CUSTOM_ELEMENTS_SCHEMA

In order to use a custom element in our template we usually add CUSTOM_ELEMENTS_SCHEMA to the schemas of the NgModule or Standalone Component.

CUSTOM_ELEMENTS_SCHEMA allows any custom-tag with any property without type checking.

<!-- CUSTOM_ELEMENTS_SCHEMA is dangerous!--> ❌

<!-- Wrong properties -->
<ui-range
  [range]="'This is not a number'" // Wrong Type is accepted ❌
  [unitx]="'kg'" // Typo in property is ignored ❌
></ui-range>

<!-- Wrong selector is accepted -->
<ui-rangerrr [range]="75" [unit]="'❤'"></ui-rangerrr> ❌
import "ui-range/ui-range.js";

@Component({
  selector: "app-root",
  template: "./app.component.html",
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // This is dangerous for your templates ❌
  standalone: true,
})
export class AppComponent {}

@elemnt/angular

@elemnt/angular helps creating tiny component wrappers.

npm add @elemnt/angular
import { Element, ElementProvider } from "@elemnt/angular";

// 1. Import Custom Element
import "ui-range/ui-range.js";
import type { UiRange } from "ui-range";

// 2. Decorate with @Element
@Element()
@Component({
  // 3. Use the name of your custom element
  selector: "ui-range",
  template: "<ng-content></ng-content>",
  standalone: true,
})
export class UiRangeComponent {
  // 4. Inject ElementREf and NgZone, this are used by the Element decorator
  e = inject(ElementRef);
  z = inject(NgZone);
  // 5. Add the Inputs you need
  @Input() value!: UiRange["value"];
  @Input() unit!: UiRange["unit"];
  @Input() interval!: UiRange["interval"];
}
@Component({
  selector: "app-root",
  template: "./app.component.html",
  imports: [UiRangeComponent], // 0. Import the Component Wrapper
  standalone: true,
})
export class AppComponent {
  range = 90;
}
<!-- wrong properties are not accepted --> ✅
<ui-range
  [range]="'This is not a number'" // Wrong Type is not accepted
  [unitx]="'kg'" // Typo in property is not accepted
></ui-range>

<!-- wrong selector is not accepted -->  ✅
<ui-rangerrr [range]="75" [unit]="'❤'"></ui-rangerrr>

Custom Form Components

In order to attach the default value accessor to a custom element we can add ngDefaultControl attribute but it does not work with Custom Events

<ui-range
  ngDefaultControl // Does not match our Custom Element 'range' event ❌
  [(ngModel)]="range" ❌
  [unit]="{'wrong': 'type'}"  ❌
  [unknown]="2"  ❌
></ui-range>
<p>Range: {{ range }}</p>
@Component({
  selector: "app-root",
  template: "./app.component.html",
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // This is dangerous for your templates ❌
  standalone: true,
})
export class AppComponent {
  range = 90;
}

@elemnt/angular

@Component({
  selector: "app-root",
  template: ` <ui-range [(ngModel)]="range" unit="kg"></ui-range> `,
  imports: [UiRangeComponent],
  standalone: true,
})
export class AppComponent {
  range = 90;
}
import { Component, forwardRef, Input } from "@angular/core";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { Element, ElementValueAccessor } from "@elemnt/angular";

import type { UiRange } from "ui-range";
import "ui-range/ui-range.js";

// 1. Add binding configuration
//    Default is { prop: "value", event: "input" }
@Element({ accessor: { prop: "value", event: "range" } })
@Component({
  selector: "ui-range",
  template: "<ng-content></ng-content>",
  standalone: true,

  // 2. Provide the component to NG_VALUE_ACCESSOR
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => UiRangeComponent),
      multi: true,
    },
  ],
})
export class UiRangeComponent extends ElementValueAccessor {
  // 3. Extend ElementValueAccessor

  @Input() value!: UiRange["value"];
  @Input() unit!: UiRange["unit"];
  @Input() interval!: UiRange["interval"];
}

Component overhead

The @Element() Component wrapper is lightweight, here is a comparaison of the usage a basic component used in the template with CUSTOM_ELEMENTS_SCHEMA and with @elemnt/angular

CUSTOM_ELEMENTS_SCHEMA

Initial Chunk Files       | Names  |  Raw Size | Estimated Transfer Size
main.8ab91566fd99e31c.js  | main   | 101.30 kB |                30.97 kB

@elemnt/angular

Initial Chunk Files       | Names  |  Raw Size | Estimated Transfer Size
main.756c16a61c1d76e0.js  | main   | 104.81 kB |                31.89 kB

Runtime overhead

  • < 1kb for a single component.
  • @Element bundled code is shared/not duplicated if used in multiple component wrappers

License

MIT © Chihab Otmani

0.1.7

1 year ago

0.1.6

1 year ago

0.1.5

1 year ago

0.1.4

1 year ago

0.1.3

1 year ago

0.1.2

1 year ago

0.1.1

1 year ago

0.1.0

1 year ago