diff --git a/README.md b/README.md index 5fb6d1b..ca861e5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/PUBLISHING.md b/doc/PUBLISHING.md similarity index 53% rename from PUBLISHING.md rename to doc/PUBLISHING.md index b8d0458..ab6dcaf 100644 --- a/PUBLISHING.md +++ b/doc/PUBLISHING.md @@ -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`) @@ -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”). diff --git a/SPEC.md b/doc/SPEC.md similarity index 96% rename from SPEC.md rename to doc/SPEC.md index 81ced85..a5f77c4 100644 --- a/SPEC.md +++ b/doc/SPEC.md @@ -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 diff --git a/doc/TEMPLATES.md b/doc/TEMPLATES.md new file mode 100644 index 0000000..5128c41 --- /dev/null +++ b/doc/TEMPLATES.md @@ -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/lit-html@3.1.2/lit-html.js'; +import { repeat } from 'https://unpkg.com/lit-html@3.1.2/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` +
+ ${repeat(items, item => item.id, item => html` +
${item.label}
+ `)} +
+ `; + }; + } +} + +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 `` 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 `` 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: + + +
+
+
+
+ + + + +``` + +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 = ( + <> +
+
+
+ + +); + +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 `` 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 diff --git a/package.json b/package.json index 1fb6bca..c61c9da 100644 --- a/package.json +++ b/package.json @@ -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": [ {