Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add component docs #181

Merged
merged 3 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions app/docs/md/conventions/components.md
Original file line number Diff line number Diff line change
@@ -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 `<my-card></my-card>` 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 → <my-message></my-message>
app/components/my-link → <my-link></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 → <blog-comment></blog-comment>
app/components/blog/comment-form → <blog-comment-form></blog-comment-form>
```

<!-- doc-callout level="none" mark="📄">

**[Learn about Enhance Elements](/docs/elements)**

</doc-callout -->

## @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.

<doc-code filename="app/components/my-card.mjs">

```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`
<style>
:host {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
color: black;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
}
.card-img {
width: 100%;
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: 500;
}
</style>
<slot name="image"></slot>
<div class="card-body font-sans">
<h5 class="card-title">${title}</h5>
<slot></slot>
</div>
`
}

static get observedAttributes() {
return [ 'title' ]
}

titleChanged(value) {
this.heading.textContent = value
}
}

customElements.define('my-card', MyCard)
```

</doc-code>

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
<my-card enhanced=”✨”></my-card>
```

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.

<doc-callout level="none" mark="✨">

**[Learn about the HTML render function](/docs/elements/html)**

</doc-callout>

## Rehydration
Enhance Components handle rehydration by listening to attribute changes. Any change to an attribute listed in the `observedAttributes` will trigger a `<attribute name>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`.

<doc-code filename="app/components/my-card.mjs">

```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`
<style>
:host {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
color: black;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
}
.card-img {
width: 100%;
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: 500;
}
</style>
<slot name="image"></slot>
<div class="card-body font-sans">
<h5 class="card-title">${title}</h5>
<slot></slot>
</div>
`
}

static get observedAttributes() {
return [ 'title' ]
}
}

customElements.define('my-card', MyCard)
```

</doc-code>

Once added the `MorphdomMixin` will handle updating the DOM whenever an `observedAttributes` is modified. The `<attribute name>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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on a Morphdom doc that should be linked to here in a callout.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could optionally make a mention about how Morphdom does a String based diff on an the actual HTML element and not a virtual DOM diff so every element you want diffed needs to have a string change or unique id.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think I should put that in the callout below or part of the paragraph?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't hurt to put it something in the paragraph as well


<doc-callout level="caution" mark="⚠️">

### 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment. It may help to call out string based diff vs vdom diffing in this callout.


</doc-callout>

## 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:

<doc-code filename="app/components/my-card.mjs">

```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`
<style>
:host {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
color: black;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.25rem;
}
.card-img {
width: 100%;
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.card-title {
margin-bottom: 0.75rem;
font-size: 1.25rem;
font-weight: 500;
}
</style>
<slot name="image"></slot>
<div class="card-body font-sans">
<h5 class="card-title">${title}</h5>
<slot></slot>
</div>
`
},
connected() {
console.log('CONNECTED')
},
disconnected() {
console.log('DISCONNECTED')
}
})
```

</doc-code>
13 changes: 13 additions & 0 deletions app/docs/md/conventions/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,6 +49,17 @@ Elements must be [named](https://html.spec.whatwg.org/multipage/custom-elements.

</doc-callout>

## 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 `<my-card></my-card>`.

<doc-callout level="none" mark="🧱">

**[Read more about components →](/docs/conventions/components)**

</doc-callout>

## 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`.
Expand Down
1 change: 1 addition & 0 deletions app/docs/nav-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const data = [
{ slug: 'css', label: 'CSS' },
'pages',
'elements',
'components',
{ slug: 'api', label: 'APIs' },
'browser',
'public',
Expand Down
3 changes: 3 additions & 0 deletions scripts/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ Neovim
nvim-treesitter
Treesitter's
parsers
renderable
(R|r)ehydration
performant