Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace get and set methods with unified value option #232

Merged
merged 11 commits into from
May 21, 2024
424 changes: 247 additions & 177 deletions docs/component-model/structure.md

Large diffs are not rendered by default.

105 changes: 103 additions & 2 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,106 @@
# Migration Guide

## v9.0.0

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

### Descriptors

The `value` option is now required in the object descriptor, and it replaces the `get` and `set` methods for defining computed property:

```javascript
// before
customName: {
get(host, value) {...},
set(host, value) {...},
...
}
```

```javascript
// after
customName: {
value(host, value) { ... },
...
}
```

Read more about the full object property descriptor in the [Structure](/component-model/structure.md#value) section.

### Attributes

Writable properties are no longer automatically synchronized back to the attribute. You must set the `reflect` option to enable the synchronization:

```javascript
// before
{
isAdmin: false,
render: () => html`...`.css`:host([is-admin]) { ... }`,
}
```

```javascript
// after
{
isAdmin: { value: false, reflect: true },
...
}
```

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

### Render and Content

#### Names

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:

```javascript
// before
{
render: {
get(host) { return "some" },
}
...
}
```

```javascript
// after
{
customRender: {
get(host) { return "some" },
}
}
```

#### Shadow DOM

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

```javascript
// before
{
render: Object.assign((host) => html`...`, { mode: "close" }),
...
}
```

```javascript
// after
{
render: {
value: (host) => html`...`,
options: { mode: "close" },
},
...
}
```

### Store Errors

For better developer experience, the `store.get()` and `store.set()` methods throw type errors immediately, instead of returning a model in error state. This is not a breaking change, but the information can help you to find the issue faster.

## v8.0.0

### Browser Support
Expand Down Expand Up @@ -93,7 +194,7 @@ The `render` factory is no longer supported - you must set `render` or `content`
If you update the DOM using another property name, you must create a custom factory for the property. You can follow the old implementation of the `render` factory available here:
https://github.com/hybridsjs/hybrids/blob/v6.1.0/src/render.js
<https://github.com/hybridsjs/hybrids/blob/v6.1.0/src/render.js>
#### Templates
Expand Down Expand Up @@ -155,7 +256,7 @@ import { Component } from "hybrids";
const MyElement: Component<MyElement> = { ... };
```
### Store
### Store Factory
#### Identifier
Expand Down
35 changes: 26 additions & 9 deletions src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function getEntry(target, key) {
key,
target,
value: undefined,
assertValue: undefined,
lastValue: undefined,
resolved: false,
contexts: undefined,
Expand All @@ -58,7 +59,11 @@ export function getEntries(target) {
}

let context = null;
export function get(target, key, getter) {
export function getCurrentValue() {
return context?.value;
}

export function get(target, key, fn) {
const entry = getEntry(target, key);

if (context) {
Expand Down Expand Up @@ -88,7 +93,7 @@ export function get(target, key, getter) {
context = entry;
stack.add(entry);

entry.value = getter(target, entry.value);
entry.value = fn(target, entry.assertValue);
entry.resolved = true;

context = lastContext;
Expand All @@ -109,24 +114,35 @@ export function get(target, key, getter) {
return entry.value;
}

export function set(target, key, setter, value) {
export function assert(target, key, value) {
const entry = getEntry(target, key);
const newValue = setter(target, value, entry.value);

if (newValue !== entry.value) {
entry.value = newValue;
entry.value = undefined;
entry.assertValue = value;

dispatch(entry);
}

export function set(target, key, fn, value) {
const entry = getEntry(target, key);
const nextValue = fn(target, value, entry.value);

if (nextValue !== entry.value) {
entry.value = nextValue;
entry.assertValue = undefined;

dispatch(entry);
}
}

export function observe(target, key, getter, fn) {
export function observe(target, key, fn, callback) {
const entry = getEntry(target, key);

entry.observe = () => {
const value = get(target, key, getter);
const value = get(target, key, fn);

if (value !== entry.lastValue) {
fn(target, value, entry.lastValue);
callback(target, value, entry.lastValue);
entry.lastValue = value;
}
};
Expand Down Expand Up @@ -166,6 +182,7 @@ function invalidateEntry(entry, options) {

if (options.clearValue) {
entry.value = undefined;
entry.assertValue = undefined;
entry.lastValue = undefined;
}

Expand Down
2 changes: 1 addition & 1 deletion src/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function children(
: (hybrids) => hybrids === hybridsOrFn;

return {
get: (host) => walk(host, fn, options),
value: (host) => walk(host, fn, options),
connect(host, key, invalidate) {
const observer = new globalThis.MutationObserver(invalidate);

Expand Down
109 changes: 49 additions & 60 deletions src/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ function compile(hybrids, HybridsElement) {
constructor() {
super();

for (const fn of HybridsElement.settable) {
fn(this);
}

for (const key of Object.keys(this)) {
const value = this[key];
delete this[key];
this[key] = value;
for (const key of HybridsElement.writable) {
if (hasOwnProperty.call(this, key)) {
const value = this[key];
delete this[key];
this[key] = value;
} else {
const value = this.getAttribute(camelToDash(key));

if (value !== null) {
this[key] =
(value === "" && typeof this[key] === "boolean") || value;
}
}
}
}

Expand Down Expand Up @@ -65,72 +70,43 @@ function compile(hybrids, HybridsElement) {

const connects = new Set();
const observers = new Set();
const settable = new Set();
const writable = new Set();

for (const key of Object.keys(hybrids)) {
if (key === "tag") continue;

let desc = hybrids[key];
const type = typeof desc;

if (type === "function") {
if (key === "render") {
desc = render(desc, true);
} else if (key === "content") {
desc = render(desc);
} else {
desc = { get: desc };
}
} else if (type !== "object" || desc === null) {
desc = { value: desc };
} else if (desc.set) {
if (hasOwnProperty.call(desc, "value")) {
throw TypeError(
`Invalid property descriptor for '${key}' property - it must not have 'value' and 'set' properties at the same time.`,
);
}

const attrName = camelToDash(key);
const get = desc.get || ((host, value) => value);
desc.get = (host, value) => {
if (value === undefined) {
value = desc.set(host, host.getAttribute(attrName) || value);
}
return get(host, value);
};
}

if (hasOwnProperty.call(desc, "value")) {
desc = value(key, desc);
} else if (!desc.get) {
if (typeof desc !== "object" || desc === null) {
desc = { value: desc };
} else if (!hasOwnProperty.call(desc, "value")) {
throw TypeError(
`Invalid descriptor for '${key}' property - it must contain 'value' or 'get' option`,
`The 'value' option is required for '${key}' property of the '${hybrids.tag}' element`,
);
}

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

if (desc.writable) {
writable.add(key);
}

Object.defineProperty(HybridsElement.prototype, key, {
get: function get() {
return cache.get(this, key, desc.get);
return cache.get(this, key, desc.value);
},
set:
desc.set &&
function set(newValue) {
cache.set(this, key, desc.set, newValue);
},
set: desc.writable
? function assert(newValue) {
cache.assert(this, key, newValue);
}
: undefined,
enumerable: true,
configurable: true,
});

if (desc.set) {
const attrName = camelToDash(key);
settable.add((host) => {
const value = host.getAttribute(attrName);
if (value !== null) {
host[key] = (value === "" && typeof host[key] === "boolean") || value;
}
});
}

if (desc.connect) {
connects.add((host) =>
desc.connect(host, key, () => {
Expand All @@ -140,13 +116,15 @@ function compile(hybrids, HybridsElement) {
}

if (desc.observe) {
observers.add((host) => cache.observe(host, key, desc.get, desc.observe));
observers.add((host) =>
cache.observe(host, key, desc.value, desc.observe),
);
}
}

HybridsElement.connects = connects;
HybridsElement.observers = observers;
HybridsElement.settable = settable;
HybridsElement.writable = writable;

return HybridsElement;
}
Expand Down Expand Up @@ -181,13 +159,23 @@ function update(HybridsElement) {
updateQueue.set(HybridsElement, constructors.get(HybridsElement));
}

const tags = new Set();
function define(hybrids) {
if (!hybrids.tag) {
throw TypeError(
"Error while defining hybrids: 'tag' property with dashed tag name is required",
"Error while defining an element: 'tag' property with dashed tag name is required",
);
}

if (tags.has(hybrids.tag)) {
throw TypeError(
`Error while defining '${hybrids.tag}' element: tag name is already defined`,
);
}

if (!tags.size) deferred.then(() => tags.clear());
tags.add(hybrids.tag);

const HybridsElement = globalThis.customElements.get(hybrids.tag);

if (HybridsElement) {
Expand All @@ -204,6 +192,7 @@ function define(hybrids) {
}

globalThis.customElements.define(hybrids.tag, compile(hybrids));

return hybrids;
}

Expand Down
2 changes: 1 addition & 1 deletion src/parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function parent(hybridsOrFn) {
? hybridsOrFn
: (hybrids) => hybrids === hybridsOrFn;
return {
get: (host) => walk(host, fn),
value: (host) => walk(host, fn),
connect(host, key, invalidate) {
return invalidate;
},
Expand Down