From a39b21c0fc3ba6c5887d893134577d694a169e8e Mon Sep 17 00:00:00 2001 From: George Payne Date: Tue, 19 May 2020 16:53:10 +0200 Subject: [PATCH 1/3] [23] add delete method to observable map --- src/observable-map.ts | 18 ++++++++++++++++++ src/types.ts | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/observable-map.ts b/src/observable-map.ts index edb75bb..1150dfe 100644 --- a/src/observable-map.ts +++ b/src/observable-map.ts @@ -10,6 +10,7 @@ export const createObservableMap = ( get: [], set: [], reset: [], + delete: [], }; const reset = (): void => { @@ -40,6 +41,16 @@ export const createObservableMap = ( } }; + const deleteProperty =

(propName: P & string) => { + const success = states.delete(propName); + + if (success) { + handlers.delete.forEach((cb) => cb(propName)); + } + + return success; + } + const state = (typeof Proxy === 'undefined' ? {} : new Proxy(defaultState, { @@ -62,6 +73,9 @@ export const createObservableMap = ( set(propName as any, value); return true; }, + deleteProperty(_, propName) { + return deleteProperty(propName as any); + } })) as T; const on: OnHandler = (eventName, callback) => { @@ -95,12 +109,16 @@ export const createObservableMap = ( if (subscription.reset) { on('reset', subscription.reset); } + if (subscription.delete) { + on('delete', subscription.delete); + } }); return { state, get, set, + delete: deleteProperty, on, onChange, use, diff --git a/src/types.ts b/src/types.ts index 1ab1f57..c64ab56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ export interface Handlers { get: GetEventHandler[]; reset: ResetEventHandler[]; set: SetEventHandler[]; + delete: DeleteEventHandler[]; } export type SetEventHandler = ( @@ -11,12 +12,14 @@ export type SetEventHandler = ( oldValue: any ) => void; export type GetEventHandler = (key: keyof StoreType) => void; +export type DeleteEventHandler = (key: keyof StoreType) => void; export type ResetEventHandler = () => void; export type DisposeEventHandler = () => void; export interface OnHandler { (eventName: 'set', callback: SetEventHandler): () => void; (eventName: 'get', callback: GetEventHandler): () => void; + (eventName: 'delete', callback: DeleteEventHandler): () => void; (eventName: 'dispose', callback: DisposeEventHandler): () => void; (eventName: 'reset', callback: ResetEventHandler): () => void; } @@ -32,6 +35,7 @@ export interface Subscription { newValue: StoreType[KeyFromStoreType], oldValue: StoreType[KeyFromStoreType] ): void; + delete?(key: KeyFromStoreType): void; reset?(): void; } @@ -43,6 +47,10 @@ export interface Setter {

(propName: P & string, value: T[P]): void; } +export interface DeleteProperty { +

(propName: P & string): void; +} + export interface ObservableMap { /** * Proxied object that will detect dependencies and call @@ -77,6 +85,17 @@ export interface ObservableMap { */ set: Setter; + /** + * Only useful if you need to support IE11. + * + * @example + * const { store, ...store } = createStore>({ a: { id: 'a' } }); + * + * delete state.a; // If you don't need to support IE11, use this way. + * store.delete('a'); // If you need to support IE11, use this other way. + */ + delete: DeleteProperty; + /** * Register a event listener, you can listen to `set`, `get` and `reset` events. * From 5a49866729113e56b5e0d395942f7417b3c1dbc5 Mon Sep 17 00:00:00 2001 From: George Payne Date: Wed, 20 May 2020 09:39:05 +0200 Subject: [PATCH 2/3] Add delete method - update stencil subscription to listen for deletes - add example usage in test-app --- src/subscriptions/stencil.ts | 10 +++- test-app/src/components.d.ts | 13 ++++ .../display-map-store/display-map-store.tsx | 60 +++++++++++++++++++ test-app/src/index.html | 1 + test-app/src/utils/map-store.ts | 13 ++++ 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 test-app/src/components/display-map-store/display-map-store.tsx create mode 100644 test-app/src/utils/map-store.ts diff --git a/src/subscriptions/stencil.ts b/src/subscriptions/stencil.ts index 3093729..b648486 100644 --- a/src/subscriptions/stencil.ts +++ b/src/subscriptions/stencil.ts @@ -35,7 +35,7 @@ export const stencilSubscription = ({ on }: ObservableMap) => { const elm = getRenderingRef(); if (elm) { appendToMap(elmsToUpdate, propName as string, elm); - } + } }); on('set', (propName) => { @@ -46,6 +46,14 @@ export const stencilSubscription = ({ on }: ObservableMap) => { cleanupElements(elmsToUpdate); }); + on('delete', (propName) => { + const elements = elmsToUpdate.get(propName as string); + if (elements) { + elmsToUpdate.set(propName as string, elements.filter(forceUpdate)); + } + cleanupElements(elmsToUpdate); + }) + on('reset', () => { elmsToUpdate.forEach((elms) => elms.forEach(forceUpdate)); cleanupElements(elmsToUpdate); diff --git a/test-app/src/components.d.ts b/test-app/src/components.d.ts index c7828b8..ddb2ff9 100644 --- a/test-app/src/components.d.ts +++ b/test-app/src/components.d.ts @@ -10,6 +10,8 @@ export namespace Components { "storeKey": "hola" | "adios"; "storeValue": string; } + interface DisplayMapStore { + } interface DisplayStore { "storeKey": "hello" | "goodbye"; } @@ -24,6 +26,12 @@ declare global { prototype: HTMLChangeStoreElement; new (): HTMLChangeStoreElement; }; + interface HTMLDisplayMapStoreElement extends Components.DisplayMapStore, HTMLStencilElement { + } + var HTMLDisplayMapStoreElement: { + prototype: HTMLDisplayMapStoreElement; + new (): HTMLDisplayMapStoreElement; + }; interface HTMLDisplayStoreElement extends Components.DisplayStore, HTMLStencilElement { } var HTMLDisplayStoreElement: { @@ -38,6 +46,7 @@ declare global { }; interface HTMLElementTagNameMap { "change-store": HTMLChangeStoreElement; + "display-map-store": HTMLDisplayMapStoreElement; "display-store": HTMLDisplayStoreElement; "simple-store": HTMLSimpleStoreElement; } @@ -47,6 +56,8 @@ declare namespace LocalJSX { "storeKey"?: "hola" | "adios"; "storeValue"?: string; } + interface DisplayMapStore { + } interface DisplayStore { "storeKey"?: "hello" | "goodbye"; } @@ -54,6 +65,7 @@ declare namespace LocalJSX { } interface IntrinsicElements { "change-store": ChangeStore; + "display-map-store": DisplayMapStore; "display-store": DisplayStore; "simple-store": SimpleStore; } @@ -63,6 +75,7 @@ declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { "change-store": LocalJSX.ChangeStore & JSXBase.HTMLAttributes; + "display-map-store": LocalJSX.DisplayMapStore & JSXBase.HTMLAttributes; "display-store": LocalJSX.DisplayStore & JSXBase.HTMLAttributes; "simple-store": LocalJSX.SimpleStore & JSXBase.HTMLAttributes; } diff --git a/test-app/src/components/display-map-store/display-map-store.tsx b/test-app/src/components/display-map-store/display-map-store.tsx new file mode 100644 index 0000000..01b136d --- /dev/null +++ b/test-app/src/components/display-map-store/display-map-store.tsx @@ -0,0 +1,60 @@ +import { Component, Host, h } from '@stencil/core'; +import { state, Item } from '../../utils/map-store'; + +@Component({ + tag: 'display-map-store', + shadow: false, +}) +export class DisplayMapStore { + private currentId: number = 0; + + render() { + return ( + +

+ + +
+ +
+ {/* Always try to display the first 10 items (adds subscription for all) */} + {Array.from({ length: 10 }, (_, i) => { + const item = state[`id-${i}`]; + + if (!item) return null; + + return ( +
+
{item.name}
+
{item.created.toLocaleDateString()}
+
+ ); + })} +
+ + ); + } + + private addToMap = () => { + const item: Item = { + id: `id-${this.currentId}`, + name: `item-${this.currentId}`, + created: new Date(), + }; + + state[item.id] = item; + + this.currentId++; + }; + + private deleteFromMap = () => { + this.currentId--; + delete state[`id-${this.currentId}`]; + }; +} diff --git a/test-app/src/index.html b/test-app/src/index.html index a9d0ed9..f0ac3f0 100644 --- a/test-app/src/index.html +++ b/test-app/src/index.html @@ -13,5 +13,6 @@ + diff --git a/test-app/src/utils/map-store.ts b/test-app/src/utils/map-store.ts new file mode 100644 index 0000000..444e2e9 --- /dev/null +++ b/test-app/src/utils/map-store.ts @@ -0,0 +1,13 @@ +import { createStore } from '@stencil/store'; + +export interface Item { + id: string; + name: string; + created: Date; +} + +const store = createStore>({}); + +export const dispose = store.dispose; +export const state = store.state; +export const reset = store.reset; From c8ea436c3f19d47a7191fe94459b9342e20ca0d6 Mon Sep 17 00:00:00 2001 From: George Payne Date: Wed, 20 May 2020 10:40:38 +0200 Subject: [PATCH 3/3] only subscribe to store access in the current render --- src/subscriptions/stencil.ts | 41 ++++++++++++++++++- .../display-map-store/display-map-store.tsx | 17 +++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/subscriptions/stencil.ts b/src/subscriptions/stencil.ts index b648486..0644c7e 100644 --- a/src/subscriptions/stencil.ts +++ b/src/subscriptions/stencil.ts @@ -22,6 +22,43 @@ const cleanupElements = debounce((map: Map) => { export const stencilSubscription = ({ on }: ObservableMap) => { const elmsToUpdate = new Map(); + const elmsToSubscriptions = new Map(); + const cleanupMap = new Map void>(); + + const reverseCleanup = (elm, propName) => { + if (cleanupMap.has(elm)) { + cleanupMap.get(elm)(); + } else { + const previous = elmsToSubscriptions.get(elm) || []; + + elmsToSubscriptions.delete(elm); + + const clean = debounce(() => { + const current = elmsToSubscriptions.get(elm); + + for (const key of previous) { + if (current.includes(key)) continue; + + const elements = elmsToUpdate.get(key).filter((el) => el !== elm); + + if (elements.length) { + elmsToUpdate.set(key, elements); + } else { + elmsToUpdate.delete(key); + } + + console.log(elmsToUpdate); + } + + cleanupMap.delete(elm); + }, 0); + + cleanupMap.set(elm, clean); + } + + appendToMap(elmsToSubscriptions, elm, propName as string); + }; + if (typeof getRenderingRef === 'function') { // If we are not in a stencil project, we do nothing. @@ -35,7 +72,9 @@ export const stencilSubscription = ({ on }: ObservableMap) => { const elm = getRenderingRef(); if (elm) { appendToMap(elmsToUpdate, propName as string, elm); - } + reverseCleanup(elm, propName); + } + }); on('set', (propName) => { diff --git a/test-app/src/components/display-map-store/display-map-store.tsx b/test-app/src/components/display-map-store/display-map-store.tsx index 01b136d..3bdc8cf 100644 --- a/test-app/src/components/display-map-store/display-map-store.tsx +++ b/test-app/src/components/display-map-store/display-map-store.tsx @@ -7,6 +7,7 @@ import { state, Item } from '../../utils/map-store'; }) export class DisplayMapStore { private currentId: number = 0; + private deletionIndex: number = 0; render() { return ( @@ -17,9 +18,8 @@ export class DisplayMapStore {
- {/* Always try to display the first 10 items (adds subscription for all) */} - {Array.from({ length: 10 }, (_, i) => { - const item = state[`id-${i}`]; + {Array.from({ length: this.currentId + 10 - this.deletionIndex }, (_, i) => { + const item = state[`id-${i + this.deletionIndex}`]; if (!item) return null; @@ -54,7 +54,14 @@ export class DisplayMapStore { }; private deleteFromMap = () => { - this.currentId--; - delete state[`id-${this.currentId}`]; + const id = Object.keys(state)[0]; + + const toDelete = state[id]; + console.log('will delete', toDelete); + + if (id) { + delete state[id]; + this.deletionIndex++; + } }; }