Skip to content

Commit

Permalink
feat: remove content property & add shadow mode detection to render…
Browse files Browse the repository at this point in the history
… property (#258)

BREAKING CHANGE: The `content` property is no longer supported. The `render` property must be used. In some cases, usage of the `shadow` option might be required.
  • Loading branch information
smalluban authored Jun 6, 2024
1 parent 36d6e39 commit 0724600
Show file tree
Hide file tree
Showing 19 changed files with 513 additions and 416 deletions.
121 changes: 49 additions & 72 deletions docs/component-model/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ The cache mechanism uses equality check to compare values (`nextValue` !== `last

## Reserved Keys

There are three reserved property names in the definition:
There are two reserved property names in the definition:

* `tag` - a string which sets the custom element tag name
* `render` and `content`, which expect the value as a function, and have additional options available
* `render` - expects its value as a function for rendering the internal structure of the custom element

## Translation

Expand Down Expand Up @@ -53,7 +53,7 @@ define({
});
```

Usually, the shorthand definition is more readable and less verbose, but the second one gives more control over the property behavior, as it provides additional options.
Usually, the shorthand syntax is more readable and less verbose, but the second one gives more control over the property behavior, as it provides additional options.

## Attributes

Expand Down Expand Up @@ -286,21 +286,31 @@ define({
});
```
## `render` & `content`
## Rendering
The `render` and `content` properties are reserved for the rendering structure of the custom element. The `value` option must be a function, which returns a result of the call to the built-in template engine or a custom update function.
The `render` property is reserved for the creating structure of the custom element. The `value` option must be a function, which returns a result of the call to the built-in template engine.
The library uses internally the `observe` pattern to called function automatically when dependencies change. As the property returns an update function, it can also be called manually, by `el.render()` or `el.content()`.
The library uses the `observe` pattern to call the function automatically when dependencies change. As the property resolves to the update function, it can also be called manually, by `el.render()`.
> You can use built-in [template engine](/component/templates.md) with those properties without additional code
### Element's Content
By default `render` property creates and updates the content of the custom element:
```javascript
define({
tag: "my-element",
name: "",
render: ({ name }) => html`<h1>Hello ${name}!</h1>`,
});
```
### Shadow DOM
Use the `render` key for the internal structure of the custom element, where you can add isolated styles, slot elements, etc.
If the root template of the element includes styles (`css` and `style` helpers, or `<style>` elements) or `<slot>` element, the library will render the content to the shadow DOM:
```javascript
import { define, html } from "hybrids";
The template with styles:
```javascript
define({
tag: "my-element",
name: "",
Expand All @@ -313,88 +323,55 @@ define({
});
```
The `render` property provides unique `options` key for passing additional arguments to `host.attachShadow()` method:
```ts
render: {
value: (host) => { ... },
options: {
mode: "open" | "closed",
delegatesFocus: boolean,
},
...
}
```
The template with `<slot>` element:
```javascript
import { define, html } from "hybrids";

define({
tag: "my-element",
render: {
value: html`<div>...</div>`,
options: { delegatesFocus: true },
},
render: () => html`
<slot></slot>
`,
});
```
### Element's Content
Templates are compiled "just in time", so only the root template can be used to determine the rendering mode. If your nested template includes styles or slots, you must use the `shadow` option to force rendering in the Shadow DOM explicitly.
Use the `content` property for rendering templates in the content of the custom element. By the design, it does not support isolated styles, slot elements, etc.
### Explicit Mode
However, it is the way to build an app-like views structure, which can be rendered as a document content in light DOM. It is easily accessible in developer tools and search engines. For example, form elements (like `<input>`) have to be in the same subtree with the `<form>` element.
Use the `shadow` option of the `render` property to set rendering mode to Shadow DOM or element's content:
```javascript
import { define, html } from "hybrids";

define({
tag: "my-element",
name: "",
content: ({ name }) => html`<h1>Hello ${name}!</h1>`
});
```
### Custom Function
The preferred way is to use a built-in [template engine](/component/templates.md), but you can use any function to update the DOM of the custom element, which accepts the following structure:
```ts
// Disable Shadow DOM
render: {
value: (host) => html`...`,
shadow: false,
...
}

```javascript
import React from "react";
import ReactDOM from "react-dom";

export default function reactify(fn) {
return (host) => {
// get the component using the fn and host element
const Component = fn(host);

// return the update function
return (host, target) => {
ReactDOM.render(Component, target);
}
}
// Force Shadow DOM
render: {
value: (host) => html`...`,
shadow: true,
}
```
```javascript
import reactify from "./reactify.js";
You can use this option for passing custom values to the `host.attachShadow()` method:
function MyComponent({ name }) {
return <div>{name}</div>;
}
```javascript
import { define, html } from "hybrids";

define({
tag: "my-element",
render: reactify(({ name }) => <MyComponent name={name} />),
})
render: {
value: html`<div>...</div>`,
shadow: { mode: "close", delegatesFocus: true },
},
});
```
The above example uses the [`factory` pattern](#factories), to produce a function, which accepts the host element and returns the update function, which has `host` and `target` arguments. The `target` argument in the update function can be a `host` or `host.shadowRoot` depending on the property name.
!> The other properties from the `host` must be called in the main function body (not in the update function), as only then they will be correctly observed
### Reference Internals
Both `render` and `content` properties can be used to reference internals of the custom element. The DOM update process is asynchronous, so to avoid rendering timing issues, always use a property as a reference to the target element. If the property depending on `render` or `content` is called before the first update, the update will be triggered manually by calling the function.
The `render` property can be used to reference internals of the custom element. The DOM update process is asynchronous, so to avoid rendering timing issues, always use the property as a reference to the target element. If the property depending on `render` is called before the first update, the update will be triggered manually by calling the function.
```javascript
import { define, html } from "hybrids";
Expand All @@ -419,8 +396,8 @@ define({
console.log("connected");
return () => console.log("disconnected");
},
observe(host, value, lastValue) {
console.log(`${value} -> ${lastValue}`);
observe(host) {
console.log("rendered");
},
},
});
Expand Down
44 changes: 38 additions & 6 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## v9.0.0

The `v9.0` release brings simplification into the full object property descriptor and moves out some rarely used default behaviors into optional features.
The `v9.0` release brings simplification into the full object property descriptor, removes the `content` property, and moves out some rarely used default behaviors into optional features.

### Descriptors

Expand Down Expand Up @@ -49,11 +49,11 @@ Writable properties are no longer automatically synchronized back to the attribu

Read more about the attribute synchronization in the [Structure](/component-model/structure.md#reflect) section.

### Render and Content
### Render Property

#### Keys
#### Key

The `render` and `content` properties are now reserved and expect an update function as a value (they cannot be used for other purpose). If you defined them as a full descriptor with custom behavior, you must rename them:
The `render` property is now reserved and expects an update function as a value (it cannot be used for other purpose). If you defined it as a full descriptor with custom behavior, you must rename the property:

```javascript
// before
Expand All @@ -74,7 +74,39 @@ The `render` and `content` properties are now reserved and expect an update func
}
```

#### Shadow DOM
#### Content

From now, the `content` property has no special behavior, so it does not render. As the content should not include styles or `<slot>` elements, it is sufficient to just rename the property to `render`:

```javascript
// before
{
content: () => html`...`,
...
}
```

```javascript
// after
{
render: () => html`...`,
...
}
```

If you need to pass styles to the element's content, you can disable Shadow DOM explicitly:

```javascript
{
render: {
value: () => html`...`.css`body { font-size: 14px }`,
shadow: false,
},
...
}
```

#### Options

The options are now part of the `render` descriptor instead of a need to extend the `render` function:

Expand All @@ -91,7 +123,7 @@ The options are now part of the `render` descriptor instead of a need to extend
{
render: {
value: (host) => html`...`,
options: { mode: "close" },
shadow: { mode: "close" },
},
...
}
Expand Down
5 changes: 1 addition & 4 deletions src/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,7 @@ function compile(hybrids, HybridsElement) {
);
}

desc =
key === "render" || key === "content"
? render(key, desc)
: value(key, desc);
desc = key === "render" ? render(desc) : value(key, desc);

if (desc.writable) {
writable.add(key);
Expand Down
8 changes: 6 additions & 2 deletions src/localize.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getPlaceholder } from "./template/utils.js";

import { probablyDevMode } from "./utils.js";
import { compile } from "./template/index.js";

const dictionary = new Map();
Expand Down Expand Up @@ -81,7 +80,7 @@ export function get(key, context, args = []) {
}
if (!msg) {
msg = key;
if ((dictionary.size || translate) && probablyDevMode) {
if (dictionary.size || translate) {
console.warn(
`Missing translation: "${key}"${context ? ` [${context}]` : ""}`,
);
Expand Down Expand Up @@ -166,12 +165,16 @@ export function msg(parts, ...args) {
return getString(parts, args).replace(EXP_REGEX, (_, index) => args[index]);
}

const PLACEHOLDER_MSG = getPlaceholder("msg");
const PLACEHOLDER_SVG = getPlaceholder("svg");

msg.html = function html(parts, ...args) {
const input = getString(parts, args);

return compile(
input.replace(EXP_REGEX, (_, index) => getPlaceholder(index)),
args,
input + PLACEHOLDER_MSG,
false,
true,
);
Expand All @@ -183,6 +186,7 @@ msg.svg = function svg(parts, ...args) {
return compile(
input.replace(EXP_REGEX, (_, index) => getPlaceholder(index)),
args,
input + PLACEHOLDER_MSG + PLACEHOLDER_SVG,
true,
true,
);
Expand Down
51 changes: 19 additions & 32 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export default function render(key, desc) {
export const shadowOptions = new WeakMap();

export default function render(desc) {
if (desc.reflect) {
throw TypeError(`'reflect' option is not supported for '${key}' property`);
throw TypeError(`'reflect' option is not supported for 'render' property`);
}

const { value: fn, observe } = desc;

if (typeof fn !== "function") {
throw TypeError(
`Value for '${key}' property must be a function: ${typeof fn}`,
`Value for 'render' property must be a function: ${typeof fn}`,
);
}

Expand All @@ -22,35 +24,20 @@ export default function render(key, desc) {
},
};

if (key === "render") {
const options = desc.options || {};
const shadow = desc.shadow
? {
mode: desc.shadow.mode || "open",
delegatesFocus: desc.shadow.delegatesFocus || false,
}
: desc.shadow;

const shadowOptions = {
mode: options.mode || "open",
delegatesFocus: options.delegatesFocus || false,
};
return {
value: (host) => {
const updateDOM = fn(host);
shadowOptions.set(host, shadow);

return {
value: (host) => {
const updateDOM = fn(host);
return () => {
const target = host.shadowRoot || host.attachShadow(shadowOptions);
updateDOM(host, target);
return target;
};
},
...rest,
};
} else {
return {
value: (host) => {
const updateDOM = fn(host);
return () => {
updateDOM(host, host);
return host;
};
},
...rest,
};
}
return () => updateDOM(host);
},
...rest,
};
}
Loading

0 comments on commit 0724600

Please sign in to comment.