diff --git a/.vscode/settings.json b/.vscode/settings.json index 54845c0..67aa678 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.ignoreWords": [ diff --git a/demo/store.ts b/demo/store.ts index c6ca836..5c2e7f1 100644 --- a/demo/store.ts +++ b/demo/store.ts @@ -84,9 +84,9 @@ const createStore = (): Store => { console.log("throttledSubmitEvent called"); }); - debouncedSubmitEvent.trigger(submitted); - throttledSubmitEvent.trigger(submitted); - logEffectRunner.trigger(submitted); + debouncedSubmitEvent.on(submitted); + throttledSubmitEvent.on(submitted); + logEffectRunner.on(submitted); resetEvent.subscribe(() => { demoFormModule.reset(); diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json index 5b9e24a..3d271c9 100644 --- a/docs/.vitepress/cache/deps/_metadata.json +++ b/docs/.vitepress/cache/deps/_metadata.json @@ -1,23 +1,23 @@ { - "hash": "d045bc4a", - "browserHash": "51a78d22", + "hash": "f9af5a9c", + "browserHash": "1765bf14", "optimized": { "vue": { "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "89eb42af", + "fileHash": "5ec59935", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../../node_modules/@vue/devtools-api/lib/esm/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "4e25a332", + "fileHash": "5c166a54", "needsInterop": false }, "@theme/index": { "src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js", "file": "@theme_index.js", - "fileHash": "bf82decb", + "fileHash": "7f8b60bb", "needsInterop": false } }, diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bd2806d..68381da 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -13,10 +13,22 @@ export default defineConfig({ { text: "Entities", items: [ + { + text: "AbstractEntity", + link: "/entities/abstract-entity", + }, { text: "State", link: "/entities/state", }, + { + text: "PersistState", + link: "/entities/persist-state", + }, + { + text: "ComputedState", + link: "/entities/computed-state", + }, ], }, { diff --git a/docs/entities/abstract-entity.md b/docs/entities/abstract-entity.md new file mode 100644 index 0000000..36623db --- /dev/null +++ b/docs/entities/abstract-entity.md @@ -0,0 +1,34 @@ +# AbstractEntity + +The base entity. +You cannot create an instance of it, but every entity in `svitore` inherits its behavior + +## subscribe + +Subscribe to entity + +**Interface:** + +```ts +subscribe(subscriber: (data: T) => void): () => void +``` + +## unsubscribe + +Unsubscribe from entity + +**Interface:** + +```ts +unsubscribe(subscriber: ((data: T) => void) | Entity): void +``` + +## release + +Remove all subscribers + +**Interface:** + +```ts +release(): void +``` diff --git a/docs/entities/computed-state.md b/docs/entities/computed-state.md new file mode 100644 index 0000000..dd5dc6c --- /dev/null +++ b/docs/entities/computed-state.md @@ -0,0 +1,3 @@ +# ComputedState + +Derived state entity diff --git a/docs/entities/persist-state.md b/docs/entities/persist-state.md new file mode 100644 index 0000000..63e2699 --- /dev/null +++ b/docs/entities/persist-state.md @@ -0,0 +1,43 @@ +# PersistState + +Entity to store a state and save the value in the storage + +_Has the same api as [State](/entities/state)_ + +## constructor + +```ts +constructor( + state: T, + storageKey: string, + storage?: Storage, // globalThis.Storage +): PersistState +``` + +**Example:** + +```ts +const $m = new SvitoreModule(); + +const firstName = $m.PersistState( + "Alex", + "firstName", + globalThis.sessionStorage +); +``` + +This code means to create a state with a default value of `"Alex"` and store the value in the storage by the key `"firstName"` + +::: info +By default `localStorage` is used, but you can use any storage, including your own custom storage by implementing the `Storage` interface +::: + +## clear + +Delete state from storage + +**Interface:** + +```ts +clear(): void +``` diff --git a/docs/entities/state.md b/docs/entities/state.md index 32d46f7..72651a3 100644 --- a/docs/entities/state.md +++ b/docs/entities/state.md @@ -1,3 +1,133 @@ # State -## Description +Entity to store the state + +_Has the same api as [AbstractEntity](/entities/abstract-entity)_ + +## constructor + +```ts +constructor(state: T): State +``` + +## get + +Return current state + +**Interface:** + +```ts +get(): T +``` + +**Example:** + +```ts +const $m = new SvitoreModule(); + +const counter = $m.State(0); + +counter.get(); // 0 +``` + +## changeOn + +Method to change the state using an event + +**Interface:** + +```ts +changeOn(event: Event): this; +``` + +```ts +changeOn( + event: Event, + selector: (payload: Payload, state: T, prevState: T) => T +): this; +``` + +**Example:** + +```ts +const $m = new SvitoreModule(); + +const counter = $m.State(0); +const counterChanged = $m.Event(); + +counter.changeOn(counterChanged); +``` + +You can use in several ways + +if the event type matches the state type - short form: + +```ts +counter.changeOn(counterChanged); +``` + +if you want to transform the value + +```ts +counter.changeOn(counterChanged, (payload) => payload * 10); +``` + +if you want to calculate a value based on the state + +```ts +counter.changeOn(counterChanged, (payload, state) => state + payload); +``` + +## resetOn + +Reset state to default value + +**Interface:** + +```ts +resetOn(event: Event): this +``` + +**Example:** + +```ts +const $m = new SvitoreModule(); + +const counter = $m.State(5); +const counterChanged = $m.Event(); +const reset = $m.Event(); + +counter.changeOn(counterChanged).resetOn(reset); + +counterChanged.dispatch(10); + +reset.dispatch(); + +counter.get(); // 5 +``` + +## getPrev + +Return prev state + +**Interface:** + +```ts +getPrev(): T +``` + +**Example:** + +```ts +const $m = new SvitoreModule(); + +const counter = $m.State(0); +const counterChanged = $m.Event(); + +counter.changeOn(counterChanged); + +counterChanged.dispatch(10); + +counter.getPrev(); // 0 +counter.get(); // 10 +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index fa3e566..2d6e286 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -135,12 +135,9 @@ This code means: Let's connect our entities. ```ts -fetchGitHubUsersEffect.trigger(searchChanged); +fetchGitHubUsersEffect.on(searchChanged); -githubUsersUpdated.trigger( - fetchGitHubUsersEffect.fulfilled, - ({ result }) => result -); +githubUsersUpdated.on(fetchGitHubUsersEffect.fulfilled, ({ result }) => result); ``` Our application is already functioning. @@ -180,12 +177,9 @@ gitHubUsers.subscribe(console.log); gitHubUsers.changeOn(githubUsersUpdated); search.changeOn(searchChanged); -fetchGitHubUsersEffect.trigger(searchChanged); +fetchGitHubUsersEffect.on(searchChanged); -githubUsersUpdated.trigger( - fetchGitHubUsersEffect.fulfilled, - ({ result }) => result -); +githubUsersUpdated.on(fetchGitHubUsersEffect.fulfilled, ({ result }) => result); ``` The application works, but there are a few problems @@ -211,13 +205,10 @@ const fetchGitHubUsersEffect = $module.Effect( gitHubUsers.changeOn(githubUsersUpdated); searchState.changeOn(searchChanged); -requestSent.trigger(searchChanged); // [!code ++] -fetchGitHubUsersEffect.trigger(requestSent); // [!code ++] -fetchGitHubUsersEffect.trigger(searchChanged); // [!code --] -githubUsersUpdated.trigger( - fetchGitHubUsersEffect.fulfilled, - ({ result }) => result -); +requestSent.on(searchChanged); // [!code ++] +fetchGitHubUsersEffect.on(requestSent); // [!code ++] +fetchGitHubUsersEffect.on(searchChanged); // [!code --] +githubUsersUpdated.on(fetchGitHubUsersEffect.fulfilled, ({ result }) => result); ``` Effect support auto-cancellation, which will help us cancel previous requests. @@ -290,12 +281,9 @@ requestSent.applyMiddleware((context, next) => { gitHubUsers.changeOn(githubUsersUpdated); searchState.changeOn(searchChanged); -requestSent.trigger(searchChanged); -fetchGitHubUsersEffect.trigger(requestSent); -githubUsersUpdated.trigger( - fetchGitHubUsersEffect.fulfilled, - ({ result }) => result -); +requestSent.on(searchChanged); +fetchGitHubUsersEffect.on(requestSent); +githubUsersUpdated.on(fetchGitHubUsersEffect.fulfilled, ({ result }) => result); searchChanged.dispatch("a"); ``` diff --git a/docs/index.md b/docs/index.md index f61a7eb..56d2659 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,20 +14,6 @@ hero: link: https://github.com/vitlolik/svitore features: - - title: State - link: /state - - title: ComputedState - link: /computed-state - - title: Event - link: /event - - title: DebouncedEvent - link: /debounced-event - - title: ThrottledEvent - link: /throttled-event - - title: Effect - link: /effect - - title: Effect runner - link: /effect-runner - - title: Reaction - link: /reaction + - title: Entities + link: /entities/state --- diff --git a/src/entities/computed-state.ts b/src/entities/computed-state.ts index afab6dc..0d0a9df 100644 --- a/src/entities/computed-state.ts +++ b/src/entities/computed-state.ts @@ -11,13 +11,13 @@ class ComputedState< const selector = args.pop() as SelectorCallback; const states = args as unknown as States; - const getStateData = (): T => + const getState = (): T => selector(...(states.map((state) => state.get()) as any)); - super(getStateData()); + super(getState()); this.unsubscribes = states.map((state) => - state.subscribe(() => this.notify(getStateData())) + state.subscribe(() => this.notify(getState())) ); } diff --git a/src/entities/effect-runner.test.ts b/src/entities/effect-runner.test.ts index 8d18aa8..f67fb18 100644 --- a/src/entities/effect-runner.test.ts +++ b/src/entities/effect-runner.test.ts @@ -216,7 +216,7 @@ describe("EffectRunner", () => { }); }); - describe("trigger", () => { + describe("on", () => { test("should subscribe on another entity", async () => { const effect = new Effect((value: string) => Promise.resolve(value)); const triggerEvent = new Event(); @@ -226,7 +226,7 @@ describe("EffectRunner", () => { while: ({ fulfilled }) => fulfilled < 1, }); effectRunner.start = mockStart; - effectRunner.trigger(triggerEvent); + effectRunner.on(triggerEvent); triggerEvent.dispatch("test"); @@ -243,10 +243,7 @@ describe("EffectRunner", () => { while: ({ fulfilled }) => fulfilled < 1, }); effectRunner.start = mockStart; - effectRunner.trigger( - triggerEvent, - (numericValue) => numericValue + "_test" - ); + effectRunner.on(triggerEvent, (numericValue) => numericValue + "_test"); triggerEvent.dispatch(10); diff --git a/src/entities/effect-runner.ts b/src/entities/effect-runner.ts index 39833da..371a501 100644 --- a/src/entities/effect-runner.ts +++ b/src/entities/effect-runner.ts @@ -96,16 +96,16 @@ class EffectRunner< this.end("stopped"); } - trigger(entity: Entity): this; - trigger( + on(entity: Entity): this; + on( entity: Entity, selector: (payload: EntityPayload) => Params ): this; - trigger( + on( entity: Entity, selector?: ((payload: any) => Params) | undefined ): this { - return super.trigger(entity, (payload) => { + return super.on(entity, (payload) => { this.start(selector ? selector(payload) : payload); }); } diff --git a/src/entities/effect.test.ts b/src/entities/effect.test.ts index 602bf50..0551b26 100644 --- a/src/entities/effect.test.ts +++ b/src/entities/effect.test.ts @@ -142,13 +142,13 @@ describe("effect", () => { expect(subscriber).toHaveBeenCalledTimes(0); }); - describe("trigger", () => { + describe("on", () => { test("should subscribe on another entity", async () => { const effect = new Effect((value: string) => Promise.resolve(value)); const triggerEvent = new Event(); const mockRun = vi.fn(); - effect.trigger(triggerEvent); + effect.on(triggerEvent); effect.run = mockRun; triggerEvent.dispatch("test"); @@ -162,7 +162,7 @@ describe("effect", () => { const triggerEvent = new Event(); const mockRun = vi.fn(); - effect.trigger(triggerEvent, (numericValue) => numericValue + "_test"); + effect.on(triggerEvent, (numericValue) => numericValue + "_test"); effect.run = mockRun; triggerEvent.dispatch(10); diff --git a/src/entities/effect.ts b/src/entities/effect.ts index 954ef4b..e8eaa3d 100644 --- a/src/entities/effect.ts +++ b/src/entities/effect.ts @@ -76,16 +76,16 @@ class Effect< } } - trigger(entity: Entity): this; - trigger( + on(entity: Entity): this; + on( entity: Entity, selector: (payload: EntityPayload) => Params ): this; - trigger( + on( entity: Entity, selector?: ((payload: any) => Params) | undefined ): this { - return super.trigger(entity, (payload) => { + return super.on(entity, (payload) => { this.run(selector ? selector(payload) : payload); }); } diff --git a/src/entities/services/abstract-event.test.ts b/src/entities/services/abstract-event.test.ts index d674833..d8e52f9 100644 --- a/src/entities/services/abstract-event.test.ts +++ b/src/entities/services/abstract-event.test.ts @@ -142,13 +142,13 @@ describe("abstract-event", () => { }); }); - describe("trigger", () => { + describe("on", () => { test("should subscribe on another entity", () => { const event = new Event(); const triggerEvent = new Event(); const mockDispatch = vi.fn(); - event.trigger(triggerEvent); + event.on(triggerEvent); event.dispatch = mockDispatch; triggerEvent.dispatch("test"); @@ -162,7 +162,7 @@ describe("abstract-event", () => { const triggerEvent = new Event(); const mockDispatch = vi.fn(); - event.trigger(triggerEvent, (numericValue) => numericValue + "_test"); + event.on(triggerEvent, (numericValue) => numericValue + "_test"); event.dispatch = mockDispatch; triggerEvent.dispatch(10); diff --git a/src/entities/services/abstract-event.ts b/src/entities/services/abstract-event.ts index 42af5fa..5e5aeff 100644 --- a/src/entities/services/abstract-event.ts +++ b/src/entities/services/abstract-event.ts @@ -56,13 +56,13 @@ abstract class AbstractEvent extends Entity { return this; } - trigger(entity: Entity): this; - trigger( + on(entity: Entity): this; + on( entity: Entity, selector: (payload: EntityPayload) => Payload ): this; - trigger(entity: Entity, selector?: (payload: any) => Payload): this { - return super.trigger(entity, (payload) => { + on(entity: Entity, selector?: (payload: any) => Payload): this { + return super.on(entity, (payload) => { this.dispatch(selector ? selector(payload) : payload); }); } diff --git a/src/entities/services/entity.test.ts b/src/entities/services/entity.test.ts index ea235b3..0d8f820 100644 --- a/src/entities/services/entity.test.ts +++ b/src/entities/services/entity.test.ts @@ -3,11 +3,11 @@ import { Entity } from "./entity"; describe("entity", () => { class TestEntity extends Entity { - trigger( + on( entity: Entity, subscriber: (payload: EntityPayload) => void ): this { - return super.trigger(entity, subscriber); + return super.on(entity, subscriber); } notify(params: T): void { return super.notify(params); @@ -88,19 +88,19 @@ describe("entity", () => { const anotherEntity = new TestEntity(); const mockSubscriber = vi.fn(); - entity.trigger(anotherEntity, mockSubscriber); + entity.on(anotherEntity, mockSubscriber); anotherEntity.notify(); expect(mockSubscriber).toHaveBeenCalledOnce(); }); - test("should unsubscribe from trigger entity", () => { + test("should unsubscribe to entity", () => { const entity = new TestEntity(); const anotherEntity = new TestEntity(); const mockSubscriber = vi.fn(); - entity.trigger(anotherEntity, mockSubscriber); + entity.on(anotherEntity, mockSubscriber); entity.unsubscribe(anotherEntity); anotherEntity.notify(); @@ -120,7 +120,7 @@ describe("entity", () => { entity.subscribe(firstSubscriber); entity.subscribe(secondSubscriber); entity.subscribe(thirdSubscriber); - entity.trigger(secondEntity, triggerSubscriber); + entity.on(secondEntity, triggerSubscriber); entity.release(); diff --git a/src/entities/services/entity.ts b/src/entities/services/entity.ts index ba0b92e..adab578 100644 --- a/src/entities/services/entity.ts +++ b/src/entities/services/entity.ts @@ -4,7 +4,7 @@ type Subscriber = (data: T) => void; abstract class Entity { private subscribers: Set> = new Set(); - private triggerMap: Map, () => void> = new Map(); + private onMap: Map, () => void> = new Map(); subscribe(subscriber: Subscriber): () => void { this.subscribers.add(subscriber); @@ -12,10 +12,10 @@ abstract class Entity { return () => this.unsubscribe(subscriber); } - unsubscribe(subscriber: Subscriber | Entity): void { + unsubscribe(subscriber: Subscriber | Entity): void { if (subscriber instanceof Entity) { - this.triggerMap.get(subscriber)?.(); - return void this.triggerMap.delete(subscriber); + this.onMap.get(subscriber)?.(); + return void this.onMap.delete(subscriber); } this.subscribers.delete(subscriber); @@ -31,23 +31,23 @@ abstract class Entity { } } - protected trigger( + protected on( entity: Entity, subscriber: (payload: EntityPayload) => void ): this { - if (this.triggerMap.has(entity)) return this; + if (this.onMap.has(entity)) return this; - this.triggerMap.set(entity, entity.subscribe(subscriber)); + this.onMap.set(entity, entity.subscribe(subscriber)); return this; } release(): void { - for (const unsubscribe of this.triggerMap.values()) { + for (const unsubscribe of this.onMap.values()) { unsubscribe(); } - this.triggerMap.clear(); + this.onMap.clear(); this.subscribers.clear(); } } diff --git a/src/entities/state.ts b/src/entities/state.ts index db4c866..5560237 100644 --- a/src/entities/state.ts +++ b/src/entities/state.ts @@ -18,7 +18,7 @@ class State extends AbstractState { event: Event, selector?: (payload: any, state: Data, prevState: Data) => Data ): this { - return this.trigger(event, (payload) => { + return this.on(event, (payload) => { this.notify( selector ? selector(payload, this.get(), this.getPrev()) : payload );