├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierignore ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md ├── src ├── MultiStoreController.ts ├── StoreController.ts ├── index.ts ├── useStores.ts └── withStores.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/no-unused-vars": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | lib -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nano Stores 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nanostores/lit", 3 | "version": "0.2.2", 4 | "description": "Lit integration for Nano Stores, a tiny state manager with many atomic tree-shakable stores", 5 | "keywords": [ 6 | "store", 7 | "state", 8 | "state manager", 9 | "lit" 10 | ], 11 | "author": "Emil Bonne Kristiansen 5 | 6 | [Lit](https://lit.dev/) integration for **[Nano Stores]**, a tiny state manager 7 | with many atomic tree-shakable stores. 8 | 9 | - **Small.** Less than 1 KB. Zero dependencies. 10 | - **Fast.** With small atomic and derived stores, you do not need to call 11 | the selector function for all components on every store change. 12 | - **Tree Shakable.** The chunk contains only stores used by components 13 | in the chunk. 14 | - Was designed to move logic from components to stores. 15 | - It has good **TypeScript** support. 16 | 17 | ## Quick start 18 | 19 | Install it: 20 | 21 | ```bash 22 | npm add nanostores @nanostores/lit # or yarn 23 | ``` 24 | 25 | Use it as a decorator with `@useStores`: 26 | 27 | ```ts 28 | import { LitElement, html } from "lit"; 29 | import { customElement } from "lit/decorators.js"; 30 | import { useStores } from "@nanostores/lit"; 31 | 32 | import { profile } from "./stores/profile.js"; 33 | 34 | @customElement("my-header") 35 | @useStores(profile) 36 | class MyHeader extends LitElement { 37 | render() { 38 | return html`
${profile.get().userId}
`; 39 | } 40 | } 41 | ``` 42 | 43 | Or as a mixin with `withStores`: 44 | 45 | ```ts 46 | import { LitElement, html } from "lit"; 47 | import { customElement } from "lit/decorators.js"; 48 | import { withStores } from "@nanostores/lit"; 49 | 50 | import { profile } from "./stores/profile.js"; 51 | 52 | @customElement("my-header") 53 | class MyHeader extends withStores(LitElement, [profile]) { 54 | render() { 55 | return html`
${profile.get().userId}
`; 56 | } 57 | } 58 | ``` 59 | 60 | Or as a Reactive Controller with `StoreController`: 61 | 62 | ```ts 63 | import { LitElement, html } from "lit"; 64 | import { customElement } from "lit/decorators.js"; 65 | import { StoreController } from "@nanostores/lit"; 66 | 67 | import { profile } from "./stores/profile.js"; 68 | 69 | @customElement("my-header") 70 | class MyHeader extends LitElement { 71 | private profileController = new StoreController(this, profile); 72 | render() { 73 | return html`
${this.profileController.value.userId}
`; 74 | } 75 | } 76 | ``` 77 | 78 | [Nano Stores]: https://github.com/nanostores/nanostores/ 79 | -------------------------------------------------------------------------------- /src/MultiStoreController.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveController, ReactiveControllerHost } from "lit"; 2 | import { Store } from "nanostores"; 3 | 4 | /** 5 | * A `ReactiveController` that subscribes a `LitElement` to several `nanostores` atoms and updates the host element when any of the atoms changes. 6 | * 7 | * @example 8 | * ```ts 9 | * import { atom } from 'nanostores'; 10 | * import { StoreController } from '@nanostores/lit'; 11 | * import { LitElement, html } from 'lit'; 12 | * import { customElement } from 'lit/decorators.js'; 13 | * 14 | * const count1 = atom(0); 15 | * const count2 = atom(0); 16 | * 17 | * @customElement('my-element') 18 | * class MyElement extends LitElement { 19 | * private controller = new MultiStoreController(this, [count1, count2]); 20 | * render() { 21 | * const [$count1, $count2] = controller.values; 22 | * return html\`Count 1: \${count1}\, Count 2: \${count2}\`; 23 | * } 24 | * } 25 | * ``` 26 | */ 27 | export class MultiStoreController< 28 | TAtoms extends [] | ReadonlyArray> 29 | > implements ReactiveController 30 | { 31 | private unsubscribes: undefined | (() => void)[]; 32 | 33 | constructor(private host: ReactiveControllerHost, private atoms: TAtoms) { 34 | host.addController(this); 35 | } 36 | 37 | // Subscribe to the atom when the host connects 38 | hostConnected() { 39 | this.unsubscribes = this.atoms.map((atom) => 40 | atom.subscribe(() => this.host.requestUpdate()) 41 | ); 42 | } 43 | 44 | // Unsubscribe from the atom when the host disconnects 45 | hostDisconnected() { 46 | this.unsubscribes?.forEach((unsubscribe) => unsubscribe()); 47 | } 48 | 49 | /** 50 | * The current values of the atoms. 51 | * @readonly 52 | */ 53 | get values(): { 54 | [K in keyof TAtoms]: ReturnType; 55 | } { 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | return this.atoms.map((atom: Store) => atom.get()) as any; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/StoreController.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveController, ReactiveControllerHost } from "lit"; 2 | import { Store } from "nanostores"; 3 | 4 | /** 5 | * A `ReactiveController` that subscribes a `LitElement` to a `nanostores` atom and updates the host element when the atom changes. 6 | * 7 | * @example 8 | * ```ts 9 | * import { atom } from 'nanostores'; 10 | * import { StoreController } from '@nanostores/lit'; 11 | * import { LitElement, html } from 'lit'; 12 | * import { customElement } from 'lit/decorators.js'; 13 | * 14 | * const count = atom(0); 15 | * 16 | * @customElement('my-element') 17 | * class MyElement extends LitElement { 18 | * private controller = new StoreController(this, count); 19 | * render() { 20 | * const $count = this.controller.value; 21 | * return html\`Count: \${$count}\`; 22 | * } 23 | * } 24 | * ``` 25 | */ 26 | export class StoreController implements ReactiveController { 27 | private unsubscribe: undefined | (() => void); 28 | 29 | constructor( 30 | private host: ReactiveControllerHost, 31 | private atom: Store 32 | ) { 33 | host.addController(this); 34 | } 35 | 36 | // Subscribe to the atom when the host connects 37 | hostConnected() { 38 | this.unsubscribe = this.atom.subscribe(() => { 39 | this.host.requestUpdate(); 40 | }); 41 | } 42 | 43 | // Unsubscribe from the atom when the host disconnects 44 | hostDisconnected() { 45 | this.unsubscribe?.(); 46 | } 47 | 48 | /** 49 | * The current value of the atom. 50 | * @readonly 51 | */ 52 | get value(): AtomType { 53 | return this.atom.get(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { StoreController } from "./StoreController"; 2 | export { MultiStoreController } from "./MultiStoreController"; 3 | export { useStores } from "./useStores"; 4 | export { withStores } from "./withStores"; 5 | -------------------------------------------------------------------------------- /src/useStores.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { ReactiveControllerHost } from "lit"; 3 | import { Store } from "nanostores"; 4 | import { MultiStoreController } from "./MultiStoreController"; 5 | 6 | /** 7 | * A TypeScript decorator that creates a new `MultiStoreController` for the atoms 8 | * @decorator `withStores(atoms)` 9 | * @param atoms The atoms to subscribe to. 10 | * 11 | * @example 12 | * ```ts 13 | * import { LitElement, html } from 'lit'; 14 | * import { customElement } from 'lit/decorators.js'; 15 | * import { atom } from 'nanostores'; 16 | * import { useStores } from '@nanostores/lit'; 17 | * 18 | * const count = atom(0); 19 | * 20 | * @customElement('my-element') 21 | * @useStores(count) 22 | * class MyElement extends LitElement { 23 | * render() { 24 | * return html\`Count: \${count.get()}\`; 25 | * } 26 | * } 27 | * ``` 28 | */ 29 | export function useStores>>( 30 | ...atoms: TAtoms 31 | ) { 32 | return ReactiveControllerHost>( 33 | constructor: TConstructor 34 | ) => { 35 | return class extends constructor { 36 | constructor(...args: any[]) { 37 | super(...args); 38 | new MultiStoreController(this, atoms); 39 | } 40 | }; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/withStores.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { LitElement } from "lit"; 3 | import { Store } from "nanostores"; 4 | import { MultiStoreController } from "./MultiStoreController"; 5 | 6 | /** 7 | * A mixin that subscribes a LitElement to a list of atoms. 8 | * @mixin `withStores` 9 | * @param LitElementClass The LitElement class to extend. 10 | * @param atoms The atoms to subscribe to. 11 | * 12 | * @example 13 | * ```ts 14 | * import { LitElement, html } from 'lit'; 15 | * import { customElement } from 'lit/decorators.js'; 16 | * import { atom } from 'nanostores'; 17 | * import { withStores } from '@nanostores/lit'; 18 | * 19 | * const count = atom(0); 20 | * 21 | * @customElement('my-element') 22 | * class MyElement extends withStores(LitElement, [count]) { 23 | * render() { 24 | * return html\`Count: \${count.get()}\`; 25 | * } 26 | * } 27 | * ``` 28 | */ 29 | export const withStores = < 30 | TLitElementClass extends new (...args: any[]) => LitElement, 31 | TAtoms extends Array> 32 | >( 33 | LitElementClass: TLitElementClass, 34 | atoms: TAtoms 35 | ) => { 36 | return class LitElementWithStores extends LitElementClass { 37 | constructor(...args: any[]) { 38 | super(...args); 39 | new MultiStoreController(this, atoms); 40 | } 41 | } as (new (...args: any[]) => LitElement) & TLitElementClass; 42 | }; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "useDefineForClassFields": false 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "**/__tests__/*"] 14 | } 15 | --------------------------------------------------------------------------------