Skip to content

Commit

Permalink
Revises copy, upgrades some dependencies, adds testing basics
Browse files Browse the repository at this point in the history
  • Loading branch information
klebba committed Aug 8, 2024
1 parent 70c107a commit e027639
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 254 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ A dead simple starting point for custom elements. It provides the following func
## Installation:

```
# save a local copy
curl https://raw.githubusercontent.com/Netflix/x-element/main/x-element.js > x-element.js
```

or
```
# load it from the web
import XElement from 'https://deno.land/x/element/x-element.js';
```

...or if you're fancy:
```
# use a package manager
npm install @netflix/x-element
```

Expand All @@ -54,3 +57,4 @@ Then...
* http://localhost:8080

See [SPEC.md](./doc/SPEC.md) for all the deets.

34 changes: 16 additions & 18 deletions doc/TEMPLATES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Templates & DOM

Because `x-element` has zero dependencies it ships with an integrated template engine. Developers can choose alternatives according to their preference, e.g. `lit-html`, `React`, etc.
Because `x-element` has zero dependencies it ships with an integrated template engine. Developers can choose alternatives according to their preference, e.g. `lit-html`, `ReactDOM`, etc.

Add a static template function in your `x-element` definition in order to leverage automagical DOM generation and data binding:

```
```javascript
static template(html, { repeat }) {
return ({ options, selectedId }) => {
return html`
Expand All @@ -21,7 +21,7 @@ static template(html, { repeat }) {
The following binding types are supported:

| Type | Example |
| ------------------- | ---------------------- |
| :------------------ | :--------------------- |
| attribute | `<div foo="${bar}">` |
| attribute (boolean) | `<div ?foo="${bar}">` |
| property | `<div .foo="${bar}">` |
Expand All @@ -30,7 +30,7 @@ The following binding types are supported:

Equivalent to:

```
```javascript
const el = document.createElement('my-custom-element');

// attribute value bindings add or modify the attribute value
Expand All @@ -50,7 +50,7 @@ el.appendChild(document.createTextNode(foo))
```

### Important note on serialization during data binding:
Values assigned to DOM attributes are always serialized using `toString()` during assignment. To help you avoid `[object Object]` surprises, properties defined using `x-element` allow you to specify their anticipated type. Properties with scalar types `String`, `Number`, and `Boolean` may be bound to attributes using native serialization. Attempting to bind non-scalar types to attributes will result in an `x-element` error message.
Browsers must serialize values assigned to a DOM node's attributes using `toString()`. Properties with scalar types `String`, `Number`, and `Boolean` are bound to attributes using this native serialization. To help you avoid `[object Object]` surprises, properties defined using `x-element` allow you to specify their anticipated type. Attempting to bind non-scalar types to _attributes_ will result in an `x-element` error message.

The following directives are supported:

Expand All @@ -71,7 +71,7 @@ The following template languages are supported:

Following is a working example using [lit-html](https://lit.dev):

```
```javascript
// 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';
Expand All @@ -87,7 +87,7 @@ export default class BaseElement extends XElement {

Use it in your elements like this:

```
```javascript
// my-custom-element.js
import BaseElement from './base-element.js';

Expand All @@ -99,7 +99,6 @@ class MyCustomElement extends BaseElement {
},
};
}
static template(html, { repeat }) {
return ({ items }) => {
return html`
Expand All @@ -116,7 +115,7 @@ class MyCustomElement extends BaseElement {
customElements.define('my-custom-element', MyCustomElement);
```

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

## Choosing your template engine(s)

Expand All @@ -125,15 +124,14 @@ Because native [custom elements](https://developer.mozilla.org/en-US/docs/Web/AP
- We can 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 your features become more complex some common concerns and conveniences start to emerge (in our case these items became 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.
- 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')`. However 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 that template engines have no awareness of.

Consider the following illustration...

```
Node composition looks like:

```
+-- BODY -------------------------------------+
| |
| +-- DIV #root ----------------------+ |
Expand All @@ -153,9 +151,11 @@ Node composition looks like:
| +-----------------------------------+ |
| |
+---------------------------------------------+
```

The declarative Light DOM representation looks like:

```html
<body>
<div id="root">
<div id="component">
Expand All @@ -169,8 +169,7 @@ 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:

```
```javascript
const root = ReactDOM.createRoot(
document.getElementById('root')
);
Expand All @@ -185,14 +184,13 @@ 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 `<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.
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 contain compatibility issues 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.

Expand All @@ -210,4 +208,4 @@ Key concepts repeated:
* 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
* It's all good
13 changes: 13 additions & 0 deletions doc/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Testing locally

The test runner leverages [x-test](https://github.com/Netflix/x-test) and [puppeteer](https://pptr.dev). At this time this is done in two processes:

```
# serve the source (long running process)
npm start
# run the suite and exit
npm test
```

Test output is recorded in the [TAP format](https://testanything.org).
Loading

0 comments on commit e027639

Please sign in to comment.