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 committed Jun 7, 2024
1 parent 36d6e39 commit b5e9894
Show file tree
Hide file tree
Showing 28 changed files with 571 additions and 434 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import Details from "./details.js";
const Home = define({
[router.connect]: { stack: [Details, ...] },
tag: "app-home",
content: () => html`
render: () => html`
<template layout="column">
<h1>Home</h1>
<nav layout="row gap">
Expand All @@ -118,7 +118,7 @@ const Home = define({
export define({
tag: "app-router",
stack: router(Home),
content: ({ stack }) => html`
render: ({ stack }) => html`
<template layout="column">
${stack}
</template>
Expand All @@ -139,7 +139,7 @@ Create CSS layouts in-place in templates, even without using Shadow DOM, but sti
```javascript
define({
tag: "app-home-view",
content: () => html`
render: () => html`
<template layout="column center gap:2">
<div layout="grow grid:1|max">
<h1>Home</h1>
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import Details from "./details.js";
const Home = define({
[router.connect]: { stack: [Details, ...] },
tag: "app-home",
content: () => html`
render: () => html`
<template layout="column">
<h1>Home</h1>
<nav layout="row gap">
Expand All @@ -124,7 +124,7 @@ const Home = define({
export define({
tag: "app-router",
stack: router(Home),
content: ({ stack }) => html`
render: ({ stack }) => html`
<template layout="column">
${stack}
</template>
Expand All @@ -145,7 +145,7 @@ Create CSS layouts in-place in templates, even without using Shadow DOM, but sti
```javascript
define({
tag: "app-home-view",
content: () => html`
render: () => html`
<template layout="column center gap:2">
<div layout="grow grid:1|max">
<h1>Home</h1>
Expand Down
2 changes: 1 addition & 1 deletion docs/component-model/definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ import Home from './views/home.js';

const App = {
stack: router(Home),
content: () => html`
render: () => html`
<template layout="column">
...
${stack}
Expand Down
2 changes: 1 addition & 1 deletion docs/component-model/layout-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The feature supports both `content` and `render` properties of the component's d
```js
define({
tag: "my-app-view",
content: () => html`
render: () => html`
<template layout="column">
...
</template>
Expand Down
126 changes: 61 additions & 65 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 or `<slot>` elements, the library renders the content to the shadow DOM:
```javascript
import { define, html } from "hybrids";
The template with inline styles:
```javascript
define({
tag: "my-element",
name: "",
Expand All @@ -313,88 +323,74 @@ 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`
<div id="container">
<slot></slot>
</div>
`,
});
```
### Element's Content
!> Templates are compiled "just in time", so only the root template can be used to determine the rendering mode
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.
If your nested template includes styles or slots, you must use the `shadow` option to force rendering in the Shadow DOM explicitly:
```javascript
import { define, html } from "hybrids";

define({
tag: "my-element",
name: "",
content: ({ name }) => html`<h1>Hello ${name}!</h1>`
show: false,
render: {
value: ({ show }) => html`
<div id="container">
${show && html`<slot></slot>`}
</div>
`,
shadow: true,
},
});
```
### Custom Function
You can use the `shadow` option to force both rendering modes:
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 (even if the template includes styles or slot elements)
render: {
value: (host) => html`...`.css`...`,
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 `shadow` option for passing custom arguments 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 +415,8 @@ define({
console.log("connected");
return () => console.log("disconnected");
},
observe(host, value, lastValue) {
console.log(`${value} -> ${lastValue}`);
observe(host) {
console.log("rendered");
},
},
});
Expand Down
4 changes: 2 additions & 2 deletions docs/component-model/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ The built-in `html.transition` plugin utilizes the [Transition API](https://deve
define({
tag: 'my-app',
stack: router([Home]),
content: ({ stack }) => html`
render: ({ stack }) => html`
<header>...</header>
<main>${stack}</main>
...
Expand All @@ -560,7 +560,7 @@ The transition API can be customized by the CSS properties. The DOM elements mig
```javascript
export default define({
tag: 'my-app-home',
content: () => html`
render: () => html`
<template layout="column">
...
<div layout="view:card"></div>
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Otherwise, you can use it directly from a number of CDNs, which provide a hybrid
define({
tag: "hello-world",
name: '',
content: ({ name }) => html`<p>Hello ${name}!</p>`,
render: ({ name }) => html`<p>Hello ${name}!</p>`,
});
</script>
```
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
4 changes: 2 additions & 2 deletions docs/router/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface MyView {
export default define<MyView>({
[router.connect]: { url: "/my-view" },
tag: "my-view",
content: ({ param }) => html`
render: ({ param }) => html`
<p>${param}</p>
`,
});
Expand Down Expand Up @@ -60,7 +60,7 @@ interface MyView {
const MyView: Component<MyView> = {
[router.connect]: { url: "/my-view" },
tag: "my-view",
content: ({ param }) => html`
render: ({ param }) => html`
<p>${param}</p>
`,
};
Expand Down
Loading

0 comments on commit b5e9894

Please sign in to comment.