Skip to content

Commit 0724600

Browse files
authored
feat: remove content property & add shadow mode detection to render 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.
1 parent 36d6e39 commit 0724600

19 files changed

+513
-416
lines changed

docs/component-model/structure.md

+49-72
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ The cache mechanism uses equality check to compare values (`nextValue` !== `last
1212

1313
## Reserved Keys
1414

15-
There are three reserved property names in the definition:
15+
There are two reserved property names in the definition:
1616

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

2020
## Translation
2121

@@ -53,7 +53,7 @@ define({
5353
});
5454
```
5555

56-
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.
56+
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.
5757

5858
## Attributes
5959

@@ -286,21 +286,31 @@ define({
286286
});
287287
```
288288
289-
## `render` & `content`
289+
## Rendering
290290
291-
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.
291+
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.
292292
293-
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()`.
293+
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()`.
294294
295-
> You can use built-in [template engine](/component/templates.md) with those properties without additional code
295+
### Element's Content
296+
297+
By default `render` property creates and updates the content of the custom element:
298+
299+
```javascript
300+
define({
301+
tag: "my-element",
302+
name: "",
303+
render: ({ name }) => html`<h1>Hello ${name}!</h1>`,
304+
});
305+
```
296306
297307
### Shadow DOM
298308
299-
Use the `render` key for the internal structure of the custom element, where you can add isolated styles, slot elements, etc.
309+
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:
300310
301-
```javascript
302-
import { define, html } from "hybrids";
311+
The template with styles:
303312
313+
```javascript
304314
define({
305315
tag: "my-element",
306316
name: "",
@@ -313,88 +323,55 @@ define({
313323
});
314324
```
315325
316-
The `render` property provides unique `options` key for passing additional arguments to `host.attachShadow()` method:
317-
318-
```ts
319-
render: {
320-
value: (host) => { ... },
321-
options: {
322-
mode: "open" | "closed",
323-
delegatesFocus: boolean,
324-
},
325-
...
326-
}
327-
```
326+
The template with `<slot>` element:
328327
329328
```javascript
330-
import { define, html } from "hybrids";
331-
332329
define({
333330
tag: "my-element",
334-
render: {
335-
value: html`<div>...</div>`,
336-
options: { delegatesFocus: true },
337-
},
331+
render: () => html`
332+
<slot></slot>
333+
`,
338334
});
339335
```
340336
341-
### Element's Content
337+
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.
342338
343-
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.
339+
### Explicit Mode
344340
345-
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.
341+
Use the `shadow` option of the `render` property to set rendering mode to Shadow DOM or element's content:
346342
347-
```javascript
348-
import { define, html } from "hybrids";
349-
350-
define({
351-
tag: "my-element",
352-
name: "",
353-
content: ({ name }) => html`<h1>Hello ${name}!</h1>`
354-
});
355-
```
356-
357-
### Custom Function
358-
359-
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:
343+
```ts
344+
// Disable Shadow DOM
345+
render: {
346+
value: (host) => html`...`,
347+
shadow: false,
348+
...
349+
}
360350

361-
```javascript
362-
import React from "react";
363-
import ReactDOM from "react-dom";
364-
365-
export default function reactify(fn) {
366-
return (host) => {
367-
// get the component using the fn and host element
368-
const Component = fn(host);
369-
370-
// return the update function
371-
return (host, target) => {
372-
ReactDOM.render(Component, target);
373-
}
374-
}
351+
// Force Shadow DOM
352+
render: {
353+
value: (host) => html`...`,
354+
shadow: true,
375355
}
376356
```
377357
378-
```javascript
379-
import reactify from "./reactify.js";
358+
You can use this option for passing custom values to the `host.attachShadow()` method:
380359
381-
function MyComponent({ name }) {
382-
return <div>{name}</div>;
383-
}
360+
```javascript
361+
import { define, html } from "hybrids";
384362

385363
define({
386364
tag: "my-element",
387-
render: reactify(({ name }) => <MyComponent name={name} />),
388-
})
365+
render: {
366+
value: html`<div>...</div>`,
367+
shadow: { mode: "close", delegatesFocus: true },
368+
},
369+
});
389370
```
390371
391-
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.
392-
393-
!> 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
394-
395372
### Reference Internals
396373
397-
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.
374+
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.
398375
399376
```javascript
400377
import { define, html } from "hybrids";
@@ -419,8 +396,8 @@ define({
419396
console.log("connected");
420397
return () => console.log("disconnected");
421398
},
422-
observe(host, value, lastValue) {
423-
console.log(`${value} -> ${lastValue}`);
399+
observe(host) {
400+
console.log("rendered");
424401
},
425402
},
426403
});

docs/migration.md

+38-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## v9.0.0
44

5-
The `v9.0` release brings simplification into the full object property descriptor and moves out some rarely used default behaviors into optional features.
5+
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.
66

77
### Descriptors
88

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

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

52-
### Render and Content
52+
### Render Property
5353

54-
#### Keys
54+
#### Key
5555

56-
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:
56+
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:
5757

5858
```javascript
5959
// before
@@ -74,7 +74,39 @@ The `render` and `content` properties are now reserved and expect an update func
7474
}
7575
```
7676

77-
#### Shadow DOM
77+
#### Content
78+
79+
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`:
80+
81+
```javascript
82+
// before
83+
{
84+
content: () => html`...`,
85+
...
86+
}
87+
```
88+
89+
```javascript
90+
// after
91+
{
92+
render: () => html`...`,
93+
...
94+
}
95+
```
96+
97+
If you need to pass styles to the element's content, you can disable Shadow DOM explicitly:
98+
99+
```javascript
100+
{
101+
render: {
102+
value: () => html`...`.css`body { font-size: 14px }`,
103+
shadow: false,
104+
},
105+
...
106+
}
107+
```
108+
109+
#### Options
78110

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

@@ -91,7 +123,7 @@ The options are now part of the `render` descriptor instead of a need to extend
91123
{
92124
render: {
93125
value: (host) => html`...`,
94-
options: { mode: "close" },
126+
shadow: { mode: "close" },
95127
},
96128
...
97129
}

src/define.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,7 @@ function compile(hybrids, HybridsElement) {
8585
);
8686
}
8787

88-
desc =
89-
key === "render" || key === "content"
90-
? render(key, desc)
91-
: value(key, desc);
88+
desc = key === "render" ? render(desc) : value(key, desc);
9289

9390
if (desc.writable) {
9491
writable.add(key);

src/localize.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getPlaceholder } from "./template/utils.js";
22

3-
import { probablyDevMode } from "./utils.js";
43
import { compile } from "./template/index.js";
54

65
const dictionary = new Map();
@@ -81,7 +80,7 @@ export function get(key, context, args = []) {
8180
}
8281
if (!msg) {
8382
msg = key;
84-
if ((dictionary.size || translate) && probablyDevMode) {
83+
if (dictionary.size || translate) {
8584
console.warn(
8685
`Missing translation: "${key}"${context ? ` [${context}]` : ""}`,
8786
);
@@ -166,12 +165,16 @@ export function msg(parts, ...args) {
166165
return getString(parts, args).replace(EXP_REGEX, (_, index) => args[index]);
167166
}
168167

168+
const PLACEHOLDER_MSG = getPlaceholder("msg");
169+
const PLACEHOLDER_SVG = getPlaceholder("svg");
170+
169171
msg.html = function html(parts, ...args) {
170172
const input = getString(parts, args);
171173

172174
return compile(
173175
input.replace(EXP_REGEX, (_, index) => getPlaceholder(index)),
174176
args,
177+
input + PLACEHOLDER_MSG,
175178
false,
176179
true,
177180
);
@@ -183,6 +186,7 @@ msg.svg = function svg(parts, ...args) {
183186
return compile(
184187
input.replace(EXP_REGEX, (_, index) => getPlaceholder(index)),
185188
args,
189+
input + PLACEHOLDER_MSG + PLACEHOLDER_SVG,
186190
true,
187191
true,
188192
);

src/render.js

+19-32
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
export default function render(key, desc) {
1+
export const shadowOptions = new WeakMap();
2+
3+
export default function render(desc) {
24
if (desc.reflect) {
3-
throw TypeError(`'reflect' option is not supported for '${key}' property`);
5+
throw TypeError(`'reflect' option is not supported for 'render' property`);
46
}
57

68
const { value: fn, observe } = desc;
79

810
if (typeof fn !== "function") {
911
throw TypeError(
10-
`Value for '${key}' property must be a function: ${typeof fn}`,
12+
`Value for 'render' property must be a function: ${typeof fn}`,
1113
);
1214
}
1315

@@ -22,35 +24,20 @@ export default function render(key, desc) {
2224
},
2325
};
2426

25-
if (key === "render") {
26-
const options = desc.options || {};
27+
const shadow = desc.shadow
28+
? {
29+
mode: desc.shadow.mode || "open",
30+
delegatesFocus: desc.shadow.delegatesFocus || false,
31+
}
32+
: desc.shadow;
2733

28-
const shadowOptions = {
29-
mode: options.mode || "open",
30-
delegatesFocus: options.delegatesFocus || false,
31-
};
34+
return {
35+
value: (host) => {
36+
const updateDOM = fn(host);
37+
shadowOptions.set(host, shadow);
3238

33-
return {
34-
value: (host) => {
35-
const updateDOM = fn(host);
36-
return () => {
37-
const target = host.shadowRoot || host.attachShadow(shadowOptions);
38-
updateDOM(host, target);
39-
return target;
40-
};
41-
},
42-
...rest,
43-
};
44-
} else {
45-
return {
46-
value: (host) => {
47-
const updateDOM = fn(host);
48-
return () => {
49-
updateDOM(host, host);
50-
return host;
51-
};
52-
},
53-
...rest,
54-
};
55-
}
39+
return () => updateDOM(host);
40+
},
41+
...rest,
42+
};
5643
}

0 commit comments

Comments
 (0)