https://github.com/chihab/elemnt
Custom Elements with strict typing
https://github.com/chihab/elemnt
angular custom-elements web-components web-development-tools
Last synced: 2 months ago
JSON representation
Custom Elements with strict typing
- Host: GitHub
- URL: https://github.com/chihab/elemnt
- Owner: chihab
- Created: 2022-11-29T22:02:43.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2023-03-24T08:27:24.000Z (about 3 years ago)
- Last Synced: 2025-07-13T12:31:23.040Z (11 months ago)
- Topics: angular, custom-elements, web-components, web-development-tools
- Language: TypeScript
- Homepage:
- Size: 107 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# @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
```ts
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`${this.text}`;
}
}
```
In order to have strict type checking we need to create an Angular Component wrapping it.
`@elemnt/angular` makes it simple.
```ts
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: "",
standalone: true,
})
export class ButtonComponent {
e = inject(ElementRef);
z = inject(NgZone);
@Input() text!: Button["text"];
}
@Component({
selector: "app-root",
template: `
✅
✅
`,
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.
```html
❌
❌
```
```ts
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.
```sh
npm add @elemnt/angular
```
```ts
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: "",
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"];
}
```
```ts
@Component({
selector: "app-root",
template: "./app.component.html",
imports: [UiRangeComponent], // 0. Import the Component Wrapper
standalone: true,
})
export class AppComponent {
range = 90;
}
```
```html
✅
✅
```
## 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
```html
Range: {{ range }}
```
```ts
@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
```ts
@Component({
selector: "app-root",
template: ` `,
imports: [UiRangeComponent],
standalone: true,
})
export class AppComponent {
range = 90;
}
```
```ts
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: "",
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
# @elemnt/cem-plugin-angular (WIP)
[@custom-elements-manifest/analyzer](https://github.com/open-wc/custom-elements-manifest) plugin to automatically create Angular wrappers for your custom elements based on your custom elements manifest.
## Usage
### Install:
```bash
npm i -D @elemnt/cem-plugin-angular
```
### Import
`custom-elements-manifest.config.js`:
```js
import angular from 'cem-plugin-angular';
export default {
plugins: [
angular()
]
}
```
### Configuration
`custom-elements-manifest.config.js`:
```js
import angular from 'cem-plugin-angular';
export default {
plugins: [
angular({
/** Directory to write the Angular wrappers */
outDir: './angular',
/** Array of component class names to exclude */
exclude: ['MyElement']
/** Whether to generate a Standalone component for each component, defaults to `true` */
standalone: true,
/** Whether to generate a NgModule for each component, defaults to `false` */
ngModule: false,
/** Whether to register the custom element, defaults to `false` */
register: true,
/** which components should be integrated with ngModel.
* It lets you set what the target prop is (i.e. value), which event will cause the target prop to change */
accessors: [{
event: "input",
prop: "value",
elements: ["ui-text"],
}],
/** A mapper to get the custom element import */
packageMapper: (packageJson, component) => {
return packageJson.name + `/${component.name.toLowerCase()}.js`
}
});
]
}
```