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${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('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
.
You can set the mode your ShadowRoot
should be created with by setting the mode property. By default, it is set to open
.
You may also set whether the ShadowRoot
delegates focus by setting the delegatesFocus. By default, it is set to false
.
WebComponent exposes the ElementInternals
+ via the internals
property that you can access for accessibility purposes.
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.
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.
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.
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 WebComponentThe 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.
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.
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.
In the render
method you can return a string, a DOM element or a
+ Markup template.
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.
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
.
Components can dispatch
custom events of any name and include data. For that, you can use the dispatch method.
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.
The onMount
method is called whenever the component is added to the DOM.
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.
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.
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.
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.
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')} `, })