Skip to content

Commit c82e380

Browse files
fix: add force option to invalidate callback (#167)
* feat: add force option to invalidate callback * test: invalidate force option * style(cache): combine invalidate options into single object * style: implement PR suggestions * Fix missing value clear Co-authored-by: Dominik Lubański <[email protected]>
1 parent 1a7ab52 commit c82e380

File tree

6 files changed

+74
-47
lines changed

6 files changed

+74
-47
lines changed

Diff for: docs/basics/descriptor.md

+31-25
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ get: (host: Element, lastValue: any) => {
6565
}
6666
```
6767

68-
* **arguments**:
69-
* `host` - an element instance
70-
* `lastValue` - last cached value of the property
71-
* **returns (required)**:
72-
* `nextValue` - a value of the current state of the property
68+
- **arguments**:
69+
- `host` - an element instance
70+
- `lastValue` - last cached value of the property
71+
- **returns (required)**:
72+
- `nextValue` - a value of the current state of the property
7373

7474
`get` calculates the current property value. The returned value is always cached. The cache mechanism works between properties defined by the library (even between different elements). If your `get` method does not use other properties, it won't be called again (then, the only way to update the value is to assert new value or call `invalidate` from `connect` method).
7575

@@ -79,8 +79,8 @@ In the following example, the `get` method of the `name` property is called agai
7979

8080
```javascript
8181
const MyElement = {
82-
firstName: 'John',
83-
lastName: 'Smith',
82+
firstName: "John",
83+
lastName: "Smith",
8484
name: {
8585
get: ({ firstName, lastName }) => `${firstName} ${lastName}`,
8686
},
@@ -102,12 +102,12 @@ set: (host: Element, value: any, lastValue: any) => {
102102
}
103103
```
104104

105-
* **arguments**:
106-
* `host` - an element instance
107-
* `value` - a value passed to assertion (ex., `el.myProperty = 'new value'`)
108-
* `lastValue` - last cached value of the property
109-
* **returns (required)**:
110-
* `nextValue` - a value of the property, which replaces cached value
105+
- **arguments**:
106+
- `host` - an element instance
107+
- `value` - a value passed to assertion (ex., `el.myProperty = 'new value'`)
108+
- `lastValue` - last cached value of the property
109+
- **returns (required)**:
110+
- `nextValue` - a value of the property, which replaces cached value
111111

112112
Every assertion of the property calls `set` method (like `myElement.property = 'new value'`). If returned `nextValue` is not equal to `lastValue`, cache of the property invalidates. However, `set` method does not trigger `get` method automatically. Only the next access to the property (like `const value = myElement.property`) calls `get` method. Then `get` takes `nextValue` from `set` as the `lastValue` argument, calculates `value` and returns it.
113113

@@ -118,7 +118,7 @@ const MyElement = {
118118
power: {
119119
set: (host, value) => value ** value,
120120
},
121-
}
121+
};
122122

123123
myElement.power = 10; // calls 'set' method and set cache to 100
124124
console.log(myElement.power); // Cache returns 100
@@ -142,12 +142,12 @@ connect: (host: Element, key: string, invalidate: Function) => {
142142
}
143143
```
144144

145-
* **arguments**:
146-
* `host` - an element instance
147-
* `key` - a property key name
148-
* `invalidate` - a callback function, which invalidates cached value
149-
* **returns (not required):**
150-
* `disconnect` - a function (without arguments)
145+
- **arguments**:
146+
- `host` - an element instance
147+
- `key` - a property key name
148+
- `invalidate` - a callback function, which invalidates cached value
149+
- **returns (not required):**
150+
- `disconnect` - a function (without arguments)
151151

152152
When you insert, remove or relocate an element in the DOM tree, `connect` or `disconnect` is called synchronously (in the `connectedCallback` and `disconnectedCallback` callbacks of the Custom Elements API).
153153

@@ -156,7 +156,7 @@ You can use `connect` to attach event listeners, initialize property value (usin
156156
If the third party code is responsible for the property value, you can use `invalidate` callback to notify that value should be recalculated (within next access). For example, it can be used to connect to async web APIs or external libraries:
157157

158158
```javascript
159-
import reduxStore from './store';
159+
import reduxStore from "./store";
160160

161161
const MyElement = {
162162
name: {
@@ -166,6 +166,12 @@ const MyElement = {
166166
};
167167
```
168168

169+
`invalidate` can take an options object argument.
170+
171+
| Option | Type | Default | Description |
172+
| ------ | --------- | ------- | --------------------------------------------------------------------------------------------------------------- |
173+
| force | `boolean` | false | When true, the invalidate call will always trigger a rerender, even if the property's identity has not changed. |
174+
169175
> Click and play with [redux](redux.js.org) library integration example:
170176
>
171177
> [![Edit <redux-counter> web component built with hybrids library](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/redux-counter-web-component-built-with-hybrids-library-jrqzp?file=/src/ReduxCounter.js)
@@ -181,10 +187,10 @@ observe: (host: Element, value: any, lastValue: any) => {
181187
}
182188
```
183189

184-
* **arguments**:
185-
* `host` - an element instance
186-
* `value` - current value of the property
187-
* `lastValue` - last cached value of the property
190+
- **arguments**:
191+
- `host` - an element instance
192+
- `value` - current value of the property
193+
- `lastValue` - last cached value of the property
188194

189195
When property cache invalidates (directly by the assertion or when one of the dependency invalidates) and `observe` method is set, the change detection mechanism adds the property to the internal queue. Within the next animation frame (using `requestAnimationFrame`) properties from the queue are checked if they have changed, and if they did, `observe` method of the property is called. It means, that `observe` method is asynchronous by default, and it is only called for properties, which value is different in the time of execution of the queue (in the `requestAnimationFrame` call).
190196

Diff for: src/cache.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -189,32 +189,36 @@ function deleteEntry(entry) {
189189
gcList.add(entry);
190190
}
191191

192-
function invalidateEntry(entry, clearValue, deleteValue) {
192+
function invalidateEntry(entry, options) {
193193
entry.depState = 0;
194194
dispatchDeep(entry);
195195

196-
if (clearValue) {
196+
if (options.clearValue) {
197197
entry.value = undefined;
198198
values.delete(entry);
199199
}
200200

201-
if (deleteValue) {
201+
if (options.deleteEntry) {
202202
deleteEntry(entry);
203203
}
204+
205+
if (options.force) {
206+
entry.state += 1;
207+
}
204208
}
205209

206-
export function invalidate(target, key, clearValue, deleteValue) {
210+
export function invalidate(target, key, options = {}) {
207211
if (contexts.size) {
208212
throw Error(
209213
`Invalidating property in chain of get calls is forbidden: '${key}'`,
210214
);
211215
}
212216

213217
const entry = getEntry(target, key);
214-
invalidateEntry(entry, clearValue, deleteValue);
218+
invalidateEntry(entry, options);
215219
}
216220

217-
export function invalidateAll(target, clearValue, deleteValue) {
221+
export function invalidateAll(target, options = {}) {
218222
if (contexts.size) {
219223
throw Error(
220224
"Invalidating all properties in chain of get calls is forbidden",
@@ -224,7 +228,7 @@ export function invalidateAll(target, clearValue, deleteValue) {
224228
const targetMap = entries.get(target);
225229
if (targetMap) {
226230
targetMap.forEach(entry => {
227-
invalidateEntry(entry, clearValue, deleteValue);
231+
invalidateEntry(entry, options);
228232
});
229233
}
230234
}

Diff for: src/define.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ function compile(Hybrid, descriptors) {
7272

7373
if (config.connect) {
7474
callbacks.push(host =>
75-
config.connect(host, key, () => {
76-
cache.invalidate(host, key);
75+
config.connect(host, key, options => {
76+
cache.invalidate(host, key, {
77+
force: options && options.force === true,
78+
});
7779
}),
7880
);
7981
}
@@ -102,13 +104,11 @@ function update(Hybrid, lastHybrids) {
102104

103105
Object.keys(hybrids).forEach(key => {
104106
const type = typeof hybrids[key];
105-
cache.invalidate(
106-
node,
107-
key,
107+
const clearValue =
108108
type !== "object" &&
109-
type !== "function" &&
110-
hybrids[key] !== prevHybrids[key],
111-
);
109+
type !== "function" &&
110+
hybrids[key] !== prevHybrids[key];
111+
cache.invalidate(node, key, { clearValue });
112112
});
113113

114114
node.connectedCallback();

Diff for: src/store.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ function setupModel(Model, nested) {
272272
invalidate: () => {
273273
if (!invalidatePromise) {
274274
invalidatePromise = resolvedPromise.then(() => {
275-
cache.invalidate(config, config, true);
275+
cache.invalidate(config, config, { clearValue: true });
276276
invalidatePromise = null;
277277
});
278278
}
@@ -1040,14 +1040,14 @@ function clear(model, clearValue = true) {
10401040
}
10411041

10421042
if (config) {
1043-
cache.invalidate(config, model.id, clearValue, true);
1043+
cache.invalidate(config, model.id, { clearValue, deleteEntry: true });
10441044
} else {
10451045
if (!configs.get(model) && !lists.get(model[0])) {
10461046
throw Error(
10471047
"Model definition must be used before - passed argument is probably not a model definition",
10481048
);
10491049
}
1050-
cache.invalidateAll(bootstrap(model), clearValue, true);
1050+
cache.invalidateAll(bootstrap(model), { clearValue, deleteEntry: true });
10511051
}
10521052
}
10531053

@@ -1271,7 +1271,7 @@ function store(Model, options = {}) {
12711271
},
12721272
connect: options.draft
12731273
? (host, key) => () => {
1274-
cache.invalidate(host, key, true);
1274+
cache.invalidate(host, key, { clearValue: true });
12751275
clear(Model, false);
12761276
}
12771277
: undefined,

Diff for: test/spec/cache.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ describe("cache:", () => {
6969
expect(spy).not.toHaveBeenCalled();
7070
});
7171

72+
it("runs getter again if force option is true", () => {
73+
Object.defineProperty(target, "otherKey", {
74+
get: () => get(target, "otherKey", () => "value"),
75+
});
76+
77+
get(target, "key", () => target.otherKey);
78+
invalidate(target, "otherKey", { force: true });
79+
80+
get(target, "key", spy);
81+
82+
expect(spy).toHaveBeenCalledTimes(1);
83+
});
84+
7285
it("forces getter to be called if validation fails", () => {
7386
get(target, "key", () => get(target, "otherKey", () => "value"));
7487
get(target, "key", spy, () => false);
@@ -140,7 +153,7 @@ describe("cache:", () => {
140153

141154
it("clears cached value", () => {
142155
get(target, "key", () => "value");
143-
invalidate(target, "key", true);
156+
invalidate(target, "key", { clearValue: true });
144157

145158
get(target, "key", spy);
146159
expect(spy).toHaveBeenCalledWith(target, undefined);
@@ -172,7 +185,7 @@ describe("cache:", () => {
172185

173186
expect(getEntries(target).length).toBe(1);
174187

175-
invalidateAll(target, true);
188+
invalidateAll(target, { clearValue: true });
176189
expect(getEntries(target)).toEqual([
177190
jasmine.objectContaining({
178191
value: undefined,

Diff for: types/index.d.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ export = hybrids;
33
export as namespace hybrids;
44

55
declare namespace hybrids {
6+
interface InvalidateOptions {
7+
force?: boolean;
8+
}
9+
610
interface Descriptor<E, V> {
711
get?(host: E & HTMLElement, lastValue: V | undefined): V;
812
set?(host: E & HTMLElement, value: any, lastValue: V | undefined): V;
913
connect?(
1014
host: E & HTMLElement & { [property in keyof E]: V },
1115
key: keyof E,
12-
invalidate: Function,
16+
invalidate: (options?: InvalidateOptions) => void,
1317
): Function | void;
1418
observe?(host: E & HTMLElement, value: V, lastValue: V): void;
1519
}

0 commit comments

Comments
 (0)