Skip to content

Commit e9bf5f5

Browse files
committed
Replace get and set methods with unified value option
BREAKING CHANGE: `get` and `set` methods must be replaced with `value` option. Properties no longer reflect value to the corresponding attribute by default. New `reflect` option must be set to `true`.
1 parent 09111ad commit e9bf5f5

13 files changed

+481
-315
lines changed

src/cache.js

+26-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function getEntry(target, key) {
3939
key,
4040
target,
4141
value: undefined,
42+
assertValue: undefined,
4243
lastValue: undefined,
4344
resolved: false,
4445
contexts: undefined,
@@ -58,7 +59,11 @@ export function getEntries(target) {
5859
}
5960

6061
let context = null;
61-
export function get(target, key, getter) {
62+
export function getCurrentEntry() {
63+
return context;
64+
}
65+
66+
export function get(target, key, fn) {
6267
const entry = getEntry(target, key);
6368

6469
if (context) {
@@ -88,7 +93,7 @@ export function get(target, key, getter) {
8893
context = entry;
8994
stack.add(entry);
9095

91-
entry.value = getter(target, entry.value);
96+
entry.value = fn(target, entry.assertValue, entry.value);
9297
entry.resolved = true;
9398

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

112-
export function set(target, key, setter, value) {
117+
export function assert(target, key, value) {
113118
const entry = getEntry(target, key);
114-
const newValue = setter(target, value, entry.value);
115119

116-
if (newValue !== entry.value) {
117-
entry.value = newValue;
120+
entry.value = undefined;
121+
entry.assertValue = value;
122+
123+
dispatch(entry);
124+
}
125+
126+
export function set(target, key, fn, value) {
127+
const entry = getEntry(target, key);
128+
const nextValue = fn(target, value, entry.value);
129+
130+
if (nextValue !== entry.value) {
131+
entry.value = nextValue;
132+
entry.assertValue = undefined;
133+
118134
dispatch(entry);
119135
}
120136
}
121137

122-
export function observe(target, key, getter, fn) {
138+
export function observe(target, key, fn, callback) {
123139
const entry = getEntry(target, key);
124140

125141
entry.observe = () => {
126-
const value = get(target, key, getter);
142+
const value = get(target, key, fn);
127143

128144
if (value !== entry.lastValue) {
129-
fn(target, value, entry.lastValue);
145+
callback(target, value, entry.lastValue);
130146
entry.lastValue = value;
131147
}
132148
};
@@ -168,6 +184,7 @@ function invalidateEntry(entry, options) {
168184

169185
if (options.clearValue) {
170186
entry.value = undefined;
187+
entry.assertValue = undefined;
171188
entry.lastValue = undefined;
172189
}
173190

src/children.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function children(
2626
: (hybrids) => hybrids === hybridsOrFn;
2727

2828
return {
29-
get: (host) => walk(host, fn, options),
29+
value: (host) => walk(host, fn, options),
3030
connect(host, key, invalidate) {
3131
const observer = new globalThis.MutationObserver(invalidate);
3232

src/define.js

+63-62
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@ function compile(hybrids, HybridsElement) {
1919
}
2020
} else {
2121
HybridsElement = class extends globalThis.HTMLElement {
22-
connectedCallback() {
23-
for (const key of HybridsElement.settable) {
24-
if (!hasOwnProperty.call(this, key)) continue;
25-
26-
const value = this[key];
27-
delete this[key];
28-
this[key] = value;
22+
constructor() {
23+
super();
24+
25+
for (const [key, attrName] of HybridsElement.writable.entries()) {
26+
if (hasOwnProperty.call(this, key)) {
27+
const value = this[key];
28+
delete this[key];
29+
this[key] = value;
30+
} else {
31+
if (this.hasAttribute(attrName)) {
32+
const value = this.getAttribute(attrName);
33+
this[key] =
34+
(value === "" && typeof this[key] === "boolean") || value;
35+
}
36+
}
2937
}
38+
}
3039

40+
connectedCallback() {
3141
const set = new Set();
3242
disconnects.set(this, set);
3343

@@ -56,60 +66,43 @@ function compile(hybrids, HybridsElement) {
5666

5767
const connects = new Set();
5868
const observers = new Set();
59-
const settable = new Set();
69+
const writableProps = new Map();
6070

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

6474
let desc = hybrids[key];
65-
const type = typeof desc;
66-
67-
if (type === "function") {
68-
if (key === "render") {
69-
desc = render(desc, true);
70-
} else if (key === "content") {
71-
desc = render(desc);
72-
} else {
73-
desc = { get: desc };
74-
}
75-
} else if (type !== "object" || desc === null) {
76-
desc = { value: desc };
77-
} else if (desc.set) {
78-
if (hasOwnProperty.call(desc, "value")) {
79-
throw TypeError(
80-
`Invalid property descriptor for '${key}' property - it must not have 'value' and 'set' properties at the same time.`,
81-
);
82-
}
8375

84-
const attrName = camelToDash(key);
85-
const get = desc.get || ((host, value) => value);
86-
desc.get = (host, value) => {
87-
if (value === undefined) {
88-
value = desc.set(host, host.getAttribute(attrName) || value);
89-
}
90-
return get(host, value);
91-
};
76+
if (typeof desc !== "object" || desc === null) {
77+
desc = { value: desc };
9278
}
9379

94-
if (hasOwnProperty.call(desc, "value")) {
95-
desc = value(key, desc);
96-
} else if (!desc.get) {
97-
throw TypeError(
98-
`Invalid descriptor for '${key}' property - it must contain 'value' or 'get' option`,
99-
);
80+
if (desc.get || desc.set) {
81+
throw TypeError(`'get' and 'set' have been replaced with 'value' option`);
10082
}
10183

102-
if (desc.set) settable.add(key);
84+
desc =
85+
key === "render" || key === "content"
86+
? render(key, desc)
87+
: value(key, desc);
88+
89+
if (desc.writable) {
90+
writableProps.set(key, camelToDash(key));
91+
}
10392

10493
Object.defineProperty(HybridsElement.prototype, key, {
105-
get: function get() {
106-
return cache.get(this, key, desc.get);
107-
},
108-
set:
109-
desc.set &&
110-
function set(newValue) {
111-
cache.set(this, key, desc.set, newValue);
112-
},
94+
get: desc.writable
95+
? function get() {
96+
return cache.get(this, key, desc.value);
97+
}
98+
: function get() {
99+
return cache.get(this, key, (host) => desc.value(host));
100+
},
101+
set: desc.writable
102+
? function assert(newValue) {
103+
cache.assert(this, key, newValue);
104+
}
105+
: undefined,
113106
enumerable: true,
114107
configurable: true,
115108
});
@@ -123,13 +116,15 @@ function compile(hybrids, HybridsElement) {
123116
}
124117

125118
if (desc.observe) {
126-
observers.add((host) => cache.observe(host, key, desc.get, desc.observe));
119+
observers.add((host) =>
120+
cache.observe(host, key, desc.value, desc.observe),
121+
);
127122
}
128123
}
129124

130125
HybridsElement.connects = connects;
131126
HybridsElement.observers = observers;
132-
HybridsElement.settable = settable;
127+
HybridsElement.writable = writableProps;
133128

134129
return HybridsElement;
135130
}
@@ -167,26 +162,32 @@ function update(HybridsElement) {
167162
function define(hybrids) {
168163
if (!hybrids.tag) {
169164
throw TypeError(
170-
"Error while defining hybrids: 'tag' property with dashed tag name is required",
165+
"Error while defining an element: 'tag' property with dashed tag name is required",
171166
);
172167
}
173168

174-
const HybridsElement = globalThis.customElements.get(hybrids.tag);
169+
try {
170+
const HybridsElement = globalThis.customElements.get(hybrids.tag);
175171

176-
if (HybridsElement) {
177-
if (constructors.get(HybridsElement)) {
178-
update(HybridsElement);
179-
compile(hybrids, HybridsElement);
172+
if (HybridsElement) {
173+
if (constructors.get(HybridsElement)) {
174+
update(HybridsElement);
175+
compile(hybrids, HybridsElement);
180176

181-
return hybrids;
177+
return hybrids;
178+
}
179+
180+
throw TypeError(
181+
`Custom element with '${hybrids.tag}' tag name already defined outside of the hybrids context`,
182+
);
182183
}
183184

184-
throw TypeError(
185-
`Custom element with '${hybrids.tag}' tag name already defined outside of the hybrids context`,
186-
);
185+
globalThis.customElements.define(hybrids.tag, compile(hybrids));
186+
} catch (e) {
187+
console.error(`Error while defining '${hybrids.tag}' element:`);
188+
throw e;
187189
}
188190

189-
globalThis.customElements.define(hybrids.tag, compile(hybrids));
190191
return hybrids;
191192
}
192193

src/parent.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function parent(hybridsOrFn) {
2424
? hybridsOrFn
2525
: (hybrids) => hybrids === hybridsOrFn;
2626
return {
27-
get: (host) => walk(host, fn),
27+
value: (host) => walk(host, fn),
2828
connect(host, key, invalidate) {
2929
return invalidate;
3030
},

src/render.js

+53-23
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,58 @@
1-
export default function render(fn, useShadow) {
2-
return {
3-
get: useShadow
4-
? (host) => {
5-
const updateDOM = fn(host);
6-
const target =
7-
host.shadowRoot ||
8-
host.attachShadow({
9-
mode: "open",
10-
delegatesFocus: fn.delegatesFocus || false,
11-
});
12-
return () => {
13-
updateDOM(host, target);
14-
return target;
15-
};
1+
export default function render(key, desc) {
2+
if (desc.writable) {
3+
throw TypeError(`'writable' option is not supported on '${key}' property`);
4+
}
5+
if (desc.reflect) {
6+
throw TypeError(`'reflect' option is not supported on '${key}' property`);
7+
}
8+
9+
const { value: fn, observe } = desc;
10+
11+
if (typeof fn !== "function") {
12+
throw TypeError(`'render' function expected as '${key}' property value`);
13+
}
14+
15+
const shadowOptions = key === "render" && {
16+
mode: "open",
17+
delegatesFocus: fn.delegatesFocus,
18+
};
19+
20+
const rest = {
21+
connect: desc.connect,
22+
observe: observe
23+
? (host, flush) => {
24+
observe(host, flush());
1625
}
17-
: (host) => {
18-
const updateDOM = fn(host);
19-
return () => {
20-
updateDOM(host, host);
21-
return host;
22-
};
26+
: (host, flush) => {
27+
flush();
2328
},
24-
observe(host, flush) {
25-
flush();
29+
};
30+
31+
// Explicitly disable shadow DOM
32+
if (shadowOptions === false) {
33+
return {
34+
value: (host) => {
35+
const updateDOM = fn(host);
36+
return () => {
37+
updateDOM(host, host);
38+
return host;
39+
};
40+
},
41+
...rest,
42+
};
43+
}
44+
45+
// Enable shadow DOM if explicitly set or template function returns a shadow flag
46+
return {
47+
value: (host) => {
48+
const updateDOM = fn(host);
49+
const target = host.shadowRoot || host.attachShadow(shadowOptions);
50+
51+
return () => {
52+
updateDOM(host, target);
53+
return target;
54+
};
2655
},
56+
...rest,
2757
};
2858
}

src/router.js

+3-8
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,7 @@ function setupView(hybrids, routerOptions, parent, nestedParent) {
311311
});
312312
}
313313

314-
const writableParams = [];
315-
316-
for (const key of Object.keys(Constructor.prototype)) {
317-
const desc = Object.getOwnPropertyDescriptor(Constructor.prototype, key);
318-
if (desc.set) writableParams.push(key);
319-
}
314+
const writableParams = [...Constructor.writable.keys()];
320315

321316
if (options.url) {
322317
if (options.dialog) {
@@ -1074,13 +1069,13 @@ function router(views, options) {
10741069
};
10751070

10761071
const desc = {
1077-
get: (host) => {
1072+
value: (host) => {
10781073
const stack = stacks.get(host) || [];
10791074
return stack
10801075
.slice(0, stack.findIndex((el) => !configs.get(el).dialog) + 1)
10811076
.reverse();
10821077
},
1083-
connect: (host, key, invalidate) => {
1078+
connect: (host, _, invalidate) => {
10841079
for (const param of options.params) {
10851080
if (!(param in host)) {
10861081
throw Error(

0 commit comments

Comments
 (0)