From f75729f8a08d908ddf7fc2d3529f30f8add38423 Mon Sep 17 00:00:00 2001 From: macdonst Date: Wed, 15 Nov 2023 14:44:48 -0500 Subject: [PATCH] Add component docs Signed-off-by: macdonst --- app/docs/md/conventions/components.md | 253 ++++++++++++++++++++++++++ app/docs/md/conventions/structure.md | 13 ++ app/docs/nav-data.mjs | 1 + 3 files changed, 267 insertions(+) create mode 100644 app/docs/md/conventions/components.md diff --git a/app/docs/md/conventions/components.md b/app/docs/md/conventions/components.md new file mode 100644 index 00000000..68e4733c --- /dev/null +++ b/app/docs/md/conventions/components.md @@ -0,0 +1,253 @@ +--- +title: Components +--- + +Components are another option for reusable building blocks of your Enhance application. They are single file components wrapping your HTML, CSS and JavaScript in a portable web component. Components live in the `app/components/` folder in Enhance projects. + +## Naming + +The file name of your component will be the tag name you author with. Meaning `app/components/my-card.mjs` will be authored as `` in your HTML page. Enhance components are HTML custom elements, so they [require two or more words separated by a dash](/docs/elements). + +``` +app/components/my-message → +app/components/my-link → +``` + +When a project grows to include more elements than can comfortably fit in a single folder, they can be divided into sub-directories inside `app/components/`. +The folder name becomes part of the custom element tag name: + +``` +app/components/blog/comment → +app/components/blog/comment-form → +``` + + + +## @enhance/custom-element +Components are web components — meaning: they extend `HTMLElement` like a vanilla web components and provide you all the [lifecycle methods](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_the_lifecycle_callbacks) `connectedCallback`, `disconnectedCallback`, `adoptedCallback`, and `attributeChangedCallback` you would expect. + +When you write an Enhance component you will extend the `CustomElement` class from the `@enhance/custom-element` package. These single file components allow you to take advantage of slotting and style scoping in the [light DOM](https://en.wikipedia.org/wiki/Document_Object_Model) while avoiding [some of the issues](https://begin.com/blog/posts/2023-11-10-head-toward-the-light-dom) the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) creates. + + + +```javascript +import CustomElement from '@enhance/custom-element' + +export default class MyCard extends CustomElement { + connectedCallback() { + this.heading = this.querySelector('h5') + } + + render({ html, state }) { + const { attrs={} } = state + const { title='default' } = attrs + return html` + + +
+
${title}
+ +
+ ` + } + + static get observedAttributes() { + return [ 'title' ] + } + + titleChanged(value) { + this.heading.textContent = value + } +} + +customElements.define('my-card', MyCard) +``` + +
+ +You may be thinking that the `render` function looks familiar and you would be right. These `render` functions are Enhance Elements. This enables us to share rendering logic between the client side and server side so any Enhance Component will be server side renderable. + +When an Enhance component is server-side rendered it is "enhanced" with an attribute to indicate that the slotting algorithm and style transform have already been run. The attribute look like this: + +```html + +``` + +The client-side code will look for this attribute and only run if your component hasn’t already been "enhanced". Thus avoiding an unnecessary render pass. + +If you are have an existing Enhance Element you can always import it into your Component and use it as your `render` function. + + + +**[Learn about the HTML render function](/docs/elements/html)** + + + +## Rehydration +Enhance Components handle rehydration by listening to attribute changes. Any change to an attribute listed in the `observedAttributes` will trigger a `Changed` method. For example if you are observing the `title` attribute of our `my-card` component any time that attribute value is updated the `titleChanged` method will be executed. This enables you to write surgical DOM updates which will always be the most performant way to update your page. + +## DOM Diffing +Other frameworks supply a DOM diffing solution and Enhance Components are no different but we believe it should be on an opt-in basis. To enabled DOM diffing in our `my-card` component we will add the `MorphdomMixin` class from `@enhance/morphdom-mixin`. + + + +```javascript +import CustomElement from '@enhance/custom-element' +import MorphdomMixin from '@enhance/morphdom-mixin' + +export default class MyCard extends MorphdomMixin(CustomElement) { + render({ html, state }) { + const { attrs={} } = state + const { title='default' } = attrs + return html` + + +
+
${title}
+ +
+ ` + } + + static get observedAttributes() { + return [ 'title' ] + } +} + +customElements.define('my-card', MyCard) +``` + +
+ +Once added the `MorphdomMixin` will handle updating the DOM whenever an `observedAttributes` is modified. The `Changed` methods are no longer necessary. Instead on an attribute change the `render` method will be re-run and the output will be compared against the current DOM. Only the modified DOM nodes will be updated. + + + +### Lists + +When working with lists of data in the DOM it is highly advisable to add a unique attribute to the list item like an `id` or `key`. This will assist MorphDOM in determining what items have changed in the list. + + + +## Reducing Boilerplate +Many other web components provide a way of reducing the amount of boilerplate code one needs to write. Enhance provides the `@enhance/element` package which builds upon the `CustomElement` and `MorphdomMixin` classes while providing a more succinct way of writing Enhance Components. + +Revisiting our `my-card` component we get: + + + +```javascript +import enhance from '@enhance/element' + +enhance('my-card', { + attrs: [ 'title' ], + init(element) { + console.log('My Card: ', element) + }, + render({ html, state }) { + const { attrs={} } = state + const { title='default' } = attrs + return html` + + +
+
${title}
+ +
+ ` + }, + connected() { + console.log('CONNECTED') + }, + disconnected() { + console.log('DISCONNECTED') + } +}) +``` + +
diff --git a/app/docs/md/conventions/structure.md b/app/docs/md/conventions/structure.md index edfe499e..d1161daf 100644 --- a/app/docs/md/conventions/structure.md +++ b/app/docs/md/conventions/structure.md @@ -10,6 +10,8 @@ app │ └── index.mjs ├── browser ........... browser JavaScript │ └── index.mjs +├── components ........ single file web components +│ └── my-card.mjs ├── elements .......... custom element pure functions │ └── my-header.mjs ├── pages ............. file-based routing @@ -47,6 +49,17 @@ Elements must be [named](https://html.spec.whatwg.org/multipage/custom-elements. +## Components +The components folder is where you keep your single file web components. These components are rendered server side but also include client-side code for additional interactivity. Allowing you to add progressive enhancements to your component in one file. + +Components follow the same rule as Elements and must be [named](https://html.spec.whatwg.org/multipage/custom-elements.html#prod-potentialcustomelementname) with one or more words separated by a dash `my-card.mjs` which corresponds to the tag name you author in your HTML pages — for example ``. + + + +**[Read more about components →](/docs/conventions/components)** + + + ## API The `api` folder is preconfigured to expose data to your file-based routes. For example, the file `app/api/index.mjs` will automatically pass state to `app/pages/index.mjs` as well as expose an endpoint for standard REST verbs like `get` and `post`. diff --git a/app/docs/nav-data.mjs b/app/docs/nav-data.mjs index b383561a..f1f4adf1 100644 --- a/app/docs/nav-data.mjs +++ b/app/docs/nav-data.mjs @@ -29,6 +29,7 @@ export const data = [ { slug: 'css', label: 'CSS' }, 'pages', 'elements', + 'components', { slug: 'api', label: 'APIs' }, 'browser', 'public',