Skip to content

Commit

Permalink
Adds docs about templating, minor housekeeping
Browse files Browse the repository at this point in the history
  • Loading branch information
klebba committed Apr 26, 2024
1 parent 5aeaeba commit 81dd526
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 24 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@

A dead simple starting point for custom elements. It provides the following functionality:

- Efficient element generation and data binding via an integrated templating engine
- ...or use another engine (e.g., [lit-html](https://lit.dev))
- Efficient DOM generation and data binding using your preferred [templating engine](./doc/TEMPLATES.md)
- Automatic `.property` to `[attribute]` reflection (opt-in)
- Automatic `[attribute]` to `.property` synchronization (one-directional, on connected)
- Simple and efficient property observation and computation
Expand Down Expand Up @@ -54,4 +53,4 @@ npm install && npm start
Then...
* http://localhost:8080

See [SPEC.md](./SPEC.md) for all the deets.
See [SPEC.md](./doc/SPEC.md) for all the deets.
30 changes: 14 additions & 16 deletions PUBLISHING.md → doc/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,21 @@ procedure feels more comfortable. By publishing, the “Publish” GitHub Action
workflow will be triggered and if all the tests pass, it’ll publish to all the
registries we care about.

Before you start — make sure your on the `main` branch and up to date. E.g., by
running `git checkout main && git pull origin main`.
Before you start — make sure you are on the `main` branch and up to date.
E.g., by running `git checkout main && git pull origin main`.

## Publishing with Manual Prepublish

1. Edit `package.json`, `package-lock.json`, and `deno.json` files to
set the new version. Don’t forget that `package-lock.json` should be edited
in multiple places to account for the self-package’s version (i.e., `""`).
For example, set the `"version"` key to a value of `"1.0.0-rc.57"`.
2. Add / commit those files. By convention, set the message to the new version
(e.g., `git commit --message="1.0.0-rc.57"`).
3. Tag the commit using an annotated tag. By convention, set the name to the new
version _prefixed with a “v”_ and set the message to the new version
(e.g., `git tag --annotate "v1.0.0-rc.57" --message="1.0.0-rc.57"`).
4. Push the new commit / tags by running `git push origin main --follow-tags`.
5. Find [the tag you just pushed](https://github.com/Netflix/x-element/tags) in
the GitHub UI and click the “Create release” option. Add any additional
1. Edit `package.json` and `deno.json` files to set the new version.
2. Run `npm install` to derive `package-lock.json` using the new version.
3. Commit the changed files. Follow the convention for the commit message.
Example: `git commit --message="1.0.0-rc.57"`
4. Tag the commit using an annotated tag. Follow the convention with
version _prefixed with a “v”_ and set the message to the new version.
Example: `git tag --annotate "v1.0.0-rc.57" --message="1.0.0-rc.57"`
5. Push changes and tags using `git push origin main --follow-tags`.
6. Find [the tag you just pushed](https://github.com/Netflix/x-element/tags)
in the GitHub UI and click the “Create release” option. Add any additional
release information (including to check the box if it’s a “pre-release”).

## Publishing with Assisted Prepublish (`bump`)
Expand All @@ -33,6 +31,6 @@ running `git checkout main && git pull origin main`.
and provide the version to bump to (e.g., `npm run bump 1.0.0-rc.57`). Note,
keywords like `major`, `minor`, `patch`, etc. _are_ supported.
2. Push the new commit / tags by running `git push origin main --follow-tags`.
3. Find [the tag you just pushed](https://github.com/Netflix/x-element/tags) in
the GitHub UI and click the “Create release” option. Add any additional
3. Find [the tag you just pushed](https://github.com/Netflix/x-element/tags)
in the GitHub UI and click the “Create release” option. Add any additional
release information (including to check the box if it’s a “pre-release”).
4 changes: 2 additions & 2 deletions SPEC.md → doc/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ And use it in your markup:

## Rendering

XElement has a built-in templating engine to efficiently turn interpolated html markup into DOM nodes.
XElement has a built-in templating engine to efficiently manage DOM generation and data binding using native [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).

It is also possible to integrate third party rendering engines. Here is an example using [lit-html]: [demo/lit-html/base-element.js](./demo/lit-html/base-element.js)
It is also possible to use third party rendering engines. Check out the (template guide)[./doc/TEMPLATES.md] to learn more.

## Properties

Expand Down
149 changes: 149 additions & 0 deletions doc/TEMPLATES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Templates & DOM

Because `x-element` has zero dependencies it also ships with an integrated template engine. However

## Customizing your base class

Developers can choose to override the default template engine that ships with `x-element` according to their preference.
Following is a working example using [lit-html](https://lit.dev):

```
// base-element.js
import XElement from 'https://deno.land/x/element/x-element.js';
import { html, render as litRender, svg } from 'https://unpkg.com/[email protected]/lit-html.js';
import { repeat } from 'https://unpkg.com/[email protected]/directives/repeat.js';
export default class BaseElement extends XElement {
static get templateEngine() {
const render = (container, template) => litRender(template, container);
return { render, html, repeat };
}
}
```

Use it in your elements like this:

```
// my-custom-element.js
import BaseElement from './base-element.js';
class MyCustomElement extends BaseElement {
static get properties() {
return {
items: {
type: Array,
},
};
}
static template(html, { repeat }) {
return ({ items }) => {
return html`
<div id="container">
${repeat(items, item => item.id, item => html`
<div id="${item.id}">${item.label}</div>
`)}
</div>
`;
};
}
}
customElements.define('my-custom-element', MyCustomElement);
```

A more complete implementation with all of the Lit directives can be viewed [here](../demo/lit-html/).

## Choosing your template engine(s)

Because native [custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) are now part of the browser specification it is important to distinguish `x-element` from other popular JavaScript frameworks. **The manner in which custom elements are defined is framework agnostic.** Here's more explanation:

- We register a new custom element `my-custom-element` within the current page context using a native browser API: `customElements.define('my-custom-element', MyCustomElement);`
- If the features of our custom element are really basic, we could do this easily without any libraries. As the feature becomes more complex some common boilerplate starts to emerge; this spurred the creation of the `x-element` project.
- Regardless of the manner in which the element has been defined, the current page context now guarantees a relationship between the new tag `<my-custom-element>` and the class `MyCustomElement`. This concept is critical to understand because this normalization liberates developers from the need to choose a single framework (or framework version) to define their features.
- Note that it is possible to create a DOM node named `my-custom-element` _before_ the custom element has been defined via `customElements.define('my-custom-element', MyCustomElement)`. This can be done using declarative HTML like `<my-custom-element></my-custom-element>` or with imperative API calls like `const el = document.createElement('my-custom-element')`. At this stage the `my-custom-element` DOM node is functionally equivalent to a `span`.
- When `my-custom-element` is eventually defined within the page context all instances of that element are instantly "upgraded" using the `MyCustomElement` class. This is the second important concept: DOM composition is independent from custom element definition. This decoupling enables composible feature developers to have flexibility when selecting a DOM template engine. Because child nodes within `my-custom-element` can be fully encapsulated using the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) creating and managing them becomes an implementation detail.

Consider the following illustration...

```
Node composition looks like:
+-- BODY -------------------------------------+
| |
| +-- DIV #root ----------------------+ |
| | | |
| | +-- DIV #component -------+ | |
| | | | | |
| | | DIV #light | | |
| | | | | |
| | +-------------------------+ | |
| | | |
| | +-- MY-CUSTOM-ELEMENT ----+ | |
| | | | | |
| | | DIV #shadow | | |
| | | | | |
| | +-------------------------+ | |
| | | |
| +-----------------------------------+ |
| |
+---------------------------------------------+
The declarative Light DOM representation looks like:
<body>
<div id="root">
<div id="component">
<div id="light"></div>
</div>
<my-custom-element></my-custom-element>
<my-app>
</body>
```

We can generate these nodes any way we prefer while leveraging `my-custom-element`. In this scenario we will use `React` and `ReactDOM` to accomplish this:

```
const root = ReactDOM.createRoot(
document.getElementById('root')
);
const example = (
<>
<div id="component">
<div id="global"></div>
</div>
<my-custom-element/>
</>
);
root.render(example);
```

A working example can be found (here)[../demo/react/]

### Important note regarding React versions before React 19

Because `my-custom-element` has no bound properties, the above example works as expected. `ReactDOM` will generate and attach `<my-custom-element>` to your root just like any other native element. However **React 18 and all prior versions remain incompatible with custom elements**, due to a variety of past design decisions that were deliberated at length [here](https://github.com/facebook/react/issues/11347). In short, React's original property binding and event management system predates the custom element specification. Addressing the incompatibility causes breaking changes to the framework which needed careful consideration.

Fortunately the React team recently [announced support for custom elements](https://react.dev/blog/2024/04/25/react-19#support-for-custom-elements) in its next major version, React 19.

---

## Summary

Features distributed as custom elements are framework and library agnostic. Thus custom elements can integrate with [any modern framework](https://custom-elements-everywhere.com/). By using native ShadowDOM encapsulation developers can choose the manner in which they manage the DOM while avoiding the risk of vendor lock-in.

Key concepts repeated:

* Custom elements are not a framework (native feature)
* Custom elements provide DOM, JS and CSS encapsulation (native feature)
* Developers can choose a framework to manage the DOM within their custom element
* Developers can choose a framework to manage the DOM that leverages their custom elements
* Developers can work with custom elements without using any framework at all (native feature)
* Developers can mix and match frameworks within the same page context
* It's all good baby
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
],
"devDependencies": {
"@netflix/eslint-config": "^3.0.0",
"eslint": "^9.0.0",
"puppeteer": "^22.0.0",
"tap-parser": "^15.3.1"
"eslint": "^9.1.0",
"puppeteer": "^22.7.1",
"tap-parser": "^15.3.2"
},
"contributors": [
{
Expand Down

0 comments on commit 81dd526

Please sign in to comment.