Element
Simple TypeScript wrapper for creating a Web Component.
npm install @pictogrammers/element
Example Usage: Element-Hello-World
Basics
To make things easier setup the project assuming the custom element <hello-world message="Hello World!"></hello-world>
.
📂 src/
📂 hello/
📂 world/
📃 world.ts
📃 world.html
📃 world.css
📃 jest.config.json
📃 package.json
📃 tsconfig.json
📃 webpack.config.js
Class (world.ts
)
import { Component, Prop, Part } from '@pictogrammers/element';
import template from "./world.html";
import style from './world.css';
@Component({
selector: 'hello-world',
style,
template
})
export default class HelloWorld extends HTMLElement {
@Prop() message = 'Hello World';
@Part() $message: HTMLDivElement;
render(changes) {
if (changes.message) {
this.$message.textContent = this.message;
}
}
}
Template (world.html
)
<div part="message">Default!</div>
CSS Styles (world.css
)
:host {
display: block;
}
[part=message] {
}
Normalizing Props
It is recommended to use primitives for props where possible. To make this easier functions are provided to normalize values for booleans, integers, numbers, and strings.
import { Component, Prop, normalizeBoolean } from '@pictogrammers/element';
@Prop(normalizeBoolean) selected = false;
Which is equivalent to...
import { Component, Prop, normalizeBoolean } from '@pictogrammers/element';
#selected = false;
@Prop()
get selected() {
return this.#selected;
}
set selected(value: string | boolean) {
this.#selected = normalizeBoolean(value);
}
Note: Instead of ever using get
/ set
always use the render
method for managing changes to prevent unncessary operations.
normalizeInt
- Wrapper for parseInt(`${value}`, 10)
.
normalizeFloat
- Wrapper for parseFloat(`${value}`)
.
normalizeBoolean
- Handles bool
type including string 'true'
/ 'false'
.
normalizeString
- Wrapper for `${value}`
.
Template Loops
Components can create repeated lists of other components by using the forEach
utility. Any updates will sync values to the component provided in the type function.
import { forEach } from '@pictogrammers/element';
import UiItem from 'ui/item';
@Prop() options: any[] = [];
@Prop() #options: any[] = [];
connectedCallback() {
forEach({
container: this.$items,
items: this.options,
type: (item) => {
return UiItem;
},
create: ($item, item) => {
},
connect: ($item, item, $items) => {
},
disconnect: ($item, item, $items) => {
},
update: ($item, item, $items) => {
},
minIndex: (items) => {
return 0;
},
maxIndex: (items) => {
return items.length;
}
});
}
Methods
Components can have methods for performing actions. For instance validating or resetting a form.
import { Component } from '@pictogrammers/element';
@Component({
selector: 'hello-world'
})
export default class HelloWorld extends HTMLElement {
method(arg) {
}
#privateMethod(arg) {
}
}
Advanced
Starting with a simple component can allow one to extend it with more features later on. This can be done by extending components.
📂 src/
📂 hello/
📂 world/
📃 world.ts
📃 world.html
📃 world.css
📂 worldButton/
📃 worldButton.ts
📃 worldButton.html
📃 worldButton.css
TypeScript (worldButton.ts
)
import { Component, Prop, Part } from '@pictogrammers/element';
import HelloWorld from '../world/world';
import style from './worldButton.css';
import template from './worldButton.html';
@Component({
selector: 'hello-world-button',
style,
template
})
export default class HelloWorldButton extends HelloWorld {
@Part() $button: HTMLButtonElement;
renderCallback() {
this.$button.addEventListener('click', () => {
alert(this.message);
});
}
}
Template (worldButton.html
)
<button part="button">
<parent />
</button>
CSS Styles (worldButton.css
)
[part=button] {
border-radius: 0.25rem;
border: #ddd;
color: #222;
}
@Local(key: string)
To access localStorage
values bind them to a class level property with a Map
type.
@Local('store') store = new Map([
['toggle', false]
]);
@Local('store') #store = new Map([
['someobj', null]
]);
this.store.get('toggle');
this.store.set('toggle' true);
Development
npm run build
npm link
npm link @pictogrammers/element
After making changes run build.
npm run build
Always run tests before submitting any updates.
npm test
Utility Base Class
Some other notes about unique use cases that are handled.
Optional Component()
Config
Utility base classes can be defined without a config. These are rarely used, but are supported.
import { Component } from '@pictogrammers/element';
@Component()
export default class HelloOverlay extends HtmlElement {
static open() {
}
close() {
}
}
Jest Utils
selectComponent<T>(tag: string): T
selectPart<T>(component: HTMLElement, name: string): T
getProps(tag: string): string[]
Basic
import { selectComponent, getProps } from '@pictogrammers/element';
import './world';
import HelloWorld from './world';
const HELLO_WORLD = 'hello-world';
describe('hello-world', () => {
const DEFAULT_MESSAGE = 'None';
beforeEach(() => {
var c = document.createElement(HELLO_WORLD);
document.body.appendChild(c);
});
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
it('should be registered', () => {
expect(customElements.get(HELLO_WORLD)).toBeDefined();
});
it('should only expose known props', () => {
const props = getProps(HELLO_WORLD);
expect(props.length).toBe(2);
expect(props).toContain('message');
expect(props).toContain('count');
});
});