diff --git a/docs-src/documentation/web-components.page.ts b/docs-src/documentation/web-components.page.ts index 11405b5e..585f2550 100644 --- a/docs-src/documentation/web-components.page.ts +++ b/docs-src/documentation/web-components.page.ts @@ -19,126 +19,383 @@ export default ({ docsMenu, content: html` ${Heading(page.name)} -

- Native - Web Components - are powerful but the API is a bit complex to scale. The good - news is that this library can enhance and simplify that - experience in a way the API is approachable. -

-

- Below is an example of a simple input field web component with - only two props and small template. -

- ${CodeSnippet( - 'class TextField extends HTMLElement {\n' + - ' static observedAttributes = ["value", "disabled"];\n' + - ' value = "";\n' + - ' disabled = false;\n' + - ' \n' + - ' connectedCallback() {\n' + - ' this.innerHTML = ``;\n' + - ' \n' + - ' this.updateField()\n' + - ' }\n' + - ' \n' + - ' attributeChangedCallback(name, oldValue, newValue) {\n' + - ' // parse the new value\n' + - ' try {\n' + - ' this[name] = JSON.parse(newValue)\n' + - ' this.updateField();\n' + - ' } catch(e) {\n' + - ' console.warn(`invalid "${name}" value`, newValue)\n' + - ' }\n' + - ' }\n' + - ' \n' + - ' updateField() {\n' + - " const input = this.querySelector('input');\n" + - ' \n' + - ' input.value = this.value;\n' + - ' input.disabled = this.disabled;\n' + - ' }\n' + - '}\n' + - '\n' + - 'customElements.define("text-field", TextField)', - 'typescript' - )} -

- You can see that it is a little complex for a simple input field - component. Now imagine if we had a component with more props and - with complex values as well as a more elaborated template. -

-

- We can simplify it a little by introducing this library - capabilities so no need for DOM manipulations and tracking is - needed. -

- ${CodeSnippet( - 'class TextField extends HTMLElement {\n' + - ' static observedAttributes = ["value", "disabled"];\n' + - ' value = "";\n' + - ' disabled = false;\n' + - ' template = html`\n' + - ' \n' + - ' `\n' + - ' \n' + - ' connectedCallback() {\n' + - ' this.template.render(this); // render template\n' + - ' }\n' + - ' \n' + - ' attributeChangedCallback(name, oldValue, newValue) {\n' + - ' // parse the new value\n' + - ' try {\n' + - ' this[name] = JSON.parse(newValue)\n' + - ' this.template.update(); // update template\n' + - ' } catch(e) {\n' + - ' console.warn(`invalid "${name}" value`, newValue)\n' + - ' }\n' + - ' }\n' + - '}\n' + - '\n' + - 'customElements.define("text-field", TextField)', - 'typescript' - )} - ${Heading('WebComponent solution', 'h3')} -

- There is already a Markup based web components solution you can - use to make your web component creation easy and still take - advantage of Markup reactive template. -

- ${CodeSnippet( - 'class TextField extends WebComponent {\n' + - ' static observedAttributes = ["value", "disabled"];\n' + - ' value = "";\n' + - ' disabled = false;\n' + - ' \n' + - ' render() {\n' + - ' return html`\n' + - ' \n' + - ' `\n' + - ' }\n' + - '}\n' + - '\n' + - 'customElements.define("text-field", TextField)', - 'typescript' - )} -

- You can learn more about this powerful tool by checking the - WebComponent package. -

+

Before Semicolon offers a Web Component package built on top of Markup that + simplifies and extends native API with new capabilities and reactivity.

+ ${CodeSnippet('import { WebComponent, html } from \'@beforesemicolon/web-component\'\n' + + 'import stylesheet from \'./counter-app.css\' assert { type: \'css\' }\n' + + '\n' + + 'interface Props {\n' + + ' label: string\n' + + '}\n' + + '\n' + + 'interface State {\n' + + ' count: number\n' + + '}\n' + + '\n' + + 'class CounterApp extends WebComponent {\n' + + ' static observedAttributes = [\'label\']\n' + + ' label = \'+\' // defined props default value\n' + + ' initialState = {\n' + + ' // declare initial state\n' + + ' count: 0,\n' + + ' }\n' + + ' stylesheet = stylesheet\n' + + '\n' + + ' countUp(e: Event) {\n' + + ' e.stopPropagation()\n' + + ' e.preventDefault()\n' + + '\n' + + ' this.setState(({ count }) => ({ count: count + 1 }))\n' + + ' this.dispatch(\'click\')\n' + + ' }\n' + + '\n' + + ' render() {\n' + + ' return html`\n' + + '

${this.state.count}

\n' + + ' \n' + + ' `\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'counter-app\', CounterApp)', 'typescript')} +

In your HTML you can simply use the tag normally.

+ ${CodeSnippet('', 'html')} + + ${Heading('Installation', 'h3')} +

Via npm:

+ ${CodeSnippet('npm install @beforesemicolon/web-component', 'vim')} +

Via yarn:

+ ${CodeSnippet('yarn add @beforesemicolon/web-component', 'vim')} +

Via CDN:

+ ${CodeSnippet('\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '', 'html')} + + ${Heading('Create component', 'h3')} +

To create a component, all you need to do is create a class that extends WebComponent then define it.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + '...\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('ShadowRoot', 'h4')} +

By default, all components you create add a ShadowRoot in open mode.

+

If you don't want ShadowRoot in your components, you can set the shadow property to false.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' config = {\n' + + ' shadow: false,\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('mode', 'h5')} +

You can set the mode your ShadowRoot should be created with by setting the mode property. By default, it is set to open.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' config = {\n' + + ' mode: \'closed\',\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('delegatesFocus', 'h5')} +

You may also set whether the ShadowRoot delegates focus by setting the delegatesFocus. By default, it is set to false.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' config = {\n' + + ' delegatesFocus: \false,\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('Internals', 'h3')} +

WebComponent exposes the ElementInternals + via the internals property that you can access for accessibility purposes.

+ ${CodeSnippet('class TextField extends WebComponent {\n' + + ' static formAssociated = true // add this to form-related components\n' + + ' static observedAttributes = [\'disabled\', \'placeholder\']\n' + + ' disabled = false\n' + + ' placeholder = \'\'\n' + + '\n' + + ' render() {\n' + + ' return html`\n' + + ' \n' + + ' `\n' + + ' }\n' + + '}\n' + + '\n' + + 'const field = new TextField()\n' + + '\n' + + 'field.internals // ElementInternals object', 'javascript')} + + ${Heading('Content Root', 'h3')} +

WebComponent exposes the root of the component via the contentRoot property. + If the component has a shadowRoot, it will expose it here regardless of the mode. + If not, it will be the component itself.

+ ${CodeSnippet('const field = new TextField()\n' + + '\n' + + 'field.contentRoot // ShadowRoot object', 'javascript')} +

This is not to be confused with the Node returned by calling the getRootNode() + on an element. The getRootNode will return the element context root node and contentRoot will contain the node where the template was rendered to.

+ ${Heading('Root', 'h3')} +

The root is about where the component was rendered at. It can either be the document itself, or the ancestor element shadow root.

+ ${Heading('Props', 'h3')} +

If your component expects props (inputs), you can set the observedAttributes static array with all the attribute names.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' static observedAttributes = [\'type\', \'disabled\', \'label\']\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

To define the default values for your props, simply define a property in the class with same name and provide the value.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' static observedAttributes = [\'type\', \'disabled\', \'label\']\n' + + ' type = \'button\'\n' + + ' disabled = false\n' + + ' label = \'\'\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

To read your reactive props you can access the props property in the class. This is what it is recommended to be used in the + template if you want the template to react to prop changes. Check the templating section for more.

+ ${CodeSnippet('interface Props {\n' + + ' type: \'button\' | \'reset\' | \'submit\'\n' + + ' disabled: boolean\n' + + ' label: string\n' + + '}\n' + + '\n' + + 'class MyButton extends WebComponent {\n' + + ' static observedAttributes = [\'type\', \'disabled\', \'label\']\n' + + ' type = \'button\'\n' + + ' disabled = false\n' + + ' label = \'\'\n' + + '\n' + + ' constructor() {\n' + + ' super()\n' + + '\n' + + ' console.log(this.props) // contains all props as getter functions\n' + + ' this.props.disabled() // will return the value\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + + ${Heading('State', 'h3')} +

The state is based on Markup state + which means it will pair up with your template just fine.

+ ${Heading('initialState', 'h4')} +

To start using state in your component simply define the initial state with the initialState property.

+ ${CodeSnippet('interface State {\n' + + ' loading: boolean\n' + + '}\n' + + '\n' + + 'class MyButton extends WebComponent<{}, State> {\n' + + ' initialState = {\n' + + ' loading: false,\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('setState', 'h4')} +

If you have state, you will need to update it. To do that you can call the setState method with a whole or + partially new state object or simply a callback function that returns the state.

+ ${CodeSnippet('interface State {\n' + + ' loading: boolean\n' + + '}\n' + + '\n' + + 'class MyButton extends WebComponent<{}, State> {\n' + + ' initialState = {\n' + + ' loading: false,\n' + + ' }\n' + + '\n' + + ' constructor() {\n' + + ' super()\n' + + '\n' + + ' this.setState({\n' + + ' loading: true,\n' + + ' })\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

if you provide a partial state object it will be merged with the current state object. No need to spread state when updating it.

+ ${Heading('Render', 'h3')} +

Not all components need an HTML body but in case you need one, you can use the render method to return either a + Markup template, a string, or a DOM element.

+ ${CodeSnippet('import { WebComponent, html } from \'@beforesemicolon/web-component\'\n' + + '\n' + + 'class MyButton extends WebComponent {\n' + + ' render() {\n' + + ' return html`\n' + + ' \n' + + ' `\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('Templating', 'h4')} +

In the render method you can return a string, a DOM element or a + Markup template.

+ ${Heading('Stylesheet', 'h3')} +

You have the ability to specify a style for your component either by providing a CSS string or a + CSSStyleSheet.

+ ${CodeSnippet('import { WebComponent, html } from \'@beforesemicolon/web-component\'\n' + + 'import buttonStyle from \'./my-button.css\' assert { type: \'css\' }\n' + + '\n' + + 'class MyButton extends WebComponent {\n' + + ' stylesheet = buttonStyle\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

Where the style is added will depend on whether the shadow option is true or false. + If the component has shadow style will be added to its own content root. + Otherwise, style will be added to the closest root node the component was rendered in. + It can be the document itself or root of an ancestor web component.

+ ${Heading('css', 'h4')} +

you can use the css utility to define your style inside the component as well.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' stylesheet = css`\n' + + ' :host {\n' + + ' display: inline-block;\n' + + ' }\n' + + ' button {\n' + + ' color: blue;\n' + + ' }\n' + + ' `\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

It helps your IDE give you better CSS syntax highlight and autocompletion but it does not perform any computation + to your CSS at this point.

+ ${Heading('updateStylesheet', 'h4')} +

You can always manipulate the stylesheet property according to the CSSStyleSheet properties. + For when you want to replace the stylesheet completely with another, you can use the updateStylesheet method + and provide either a string or a new instance of CSSStyleSheet.

+ ${Heading('Events', 'h3')} +

Components can dispatch custom events of any name and include data. For that, you can use the dispatch method.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' handleClick = (e: Event) => {\n' + + ' e.stopPropagation()\n' + + ' e.preventDefault()\n' + + '\n' + + ' this.dispatch(\'click\')\n' + + ' }\n' + + '\n' + + ' render() {\n' + + ' return html`\n' + + ' \n' + + ' `\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('Lifecycles', 'h3')} +

You could consider the constructor and render method as some type of "lifecycle" where anything inside the + constructor happen when the component is instantiated and everything in the render method + happens before the onMount.

+ ${Heading('onMount', 'h4')} +

The onMount method is called whenever the component is added to the DOM.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onMount() {\n' + + ' console.log(this.mounted)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

You may always use the mounted property to check if the component is in the DOM or not.

+

You have the option to return a function to perform cleanups which is executed like onDestroy.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onMount() {\n' + + ' return () => {\n' + + ' // handle cleanup\n' + + ' }\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('onUpdate', 'h4')} +

The onUpdate method is called whenever the component props are updated via the setAttribute + or changing the props property on the element instance directly.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onUpdate(name: string, newValue: unknown, oldValue: unknown) {\n' + + ' console.log(`prop ${name} updated from ${oldValue} to ${newValue}`)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

The method will always tell you, which prop and its new and old value.

+ ${Heading('onDestroy', 'h4')} +

The onDestroy method is called whenever the component is removed from the DOM.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onDestroy() {\n' + + ' console.log(this.mounted)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('onAdoption', 'h4')} +

The onAdoption method is called whenever the component is moved from one document to another. + For example, when you move a component from an iframe to the main document.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onAdoption() {\n' + + ' console.log(document)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} + ${Heading('onError', 'h4')} +

The onError method is called whenever the component fails to perform internal actions. + These action can also be related to code executed inside any lifecycle methods, render, state or style update.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onError(error: Error) {\n' + + ' console.log(document)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

You may also use this method as a single place to expose and handle all the errors.

+ ${CodeSnippet('class MyButton extends WebComponent {\n' + + ' onClick() {\n' + + ' execAsyncAction().catch(this.onErrror)\n' + + ' }\n' + + '\n' + + ' onError(error) {\n' + + ' // handle error\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} +

You can also enhance components so all errors are handled in the same place.

+ ${CodeSnippet('// have your global componenent that extends WebComponent\n' + + '// and that you can use to handle all global related things, for example, error tracking\n' + + 'class Component extends WebComponent {\n' + + ' onError(error: Error) {\n' + + ' trackError(error)\n' + + ' console.error(error)\n' + + ' }\n' + + '}\n' + + '\n' + + 'class MyButton extends Component {\n' + + ' onClick() {\n' + + ' execAsyncAction().catch(this.onErrror)\n' + + ' }\n' + + '}\n' + + '\n' + + 'customElements.define(\'my-button\', MyButton)', 'javascript')} `, })