├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── App.svelte ├── components │ ├── Footer.svelte │ ├── Header.svelte │ ├── Main.svelte │ └── TodoItem.svelte ├── constants │ ├── TodoFilters.js │ └── keyCodes.js ├── index.html ├── main.js └── store │ ├── filters.js │ ├── index.js │ └── todos.js ├── index.d.ts ├── index.js ├── package.json ├── test └── index.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn-error.log 3 | 4 | .cache/ 5 | node_modules/ 6 | dist/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | 3 | npm-debug.log 4 | yarn-error.log 5 | yarn.lock 6 | 7 | *.test.js 8 | .travis.yml 9 | 10 | node_modules/ 11 | example/ 12 | coverage/ 13 | .cache/ 14 | dist/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - node 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 1.0.0 6 | - Support for node.js < v10 and storeon < v3 is dropped ([07552](https://github.com/storeon/svelte/commit/07552faead6ef118b29c579a27b668c537a33321)) 7 | 8 | ## 0.6.3 9 | - Fix reactivity for boolean state (by [Mayke](https://github.com/maykefreitas)) ([e6a290](https://github.com/storeon/svelte/commit/e6a290580dc1d2d350c611d0e3f6dac30a49625e)) 10 | 11 | ## 0.6.2 12 | - Fix publishing ([deeb73](https://github.com/storeon/svelte/commit/deeb73ab87e7c8498a74e874e5032fa03024deac)) 13 | 14 | ## 0.6.1 15 | - Add ESModules support ([3b4988](https://github.com/storeon/svelte/commit/3b4988d6342b4310827b55239911f4fd984eeace)) 16 | 17 | ## 0.6.0 18 | - Make API more convinient ([e01d03](https://github.com/storeon/svelte/commit/e01d0312f63be29eb23e1228be4712ed7212d293)) 19 | 20 | ## 0.5.2 21 | - Better typings ([8f8225](https://github.com/storeon/svelte/commit/8f82254e59a0f5dcf3e537c8368f02eece679c48)) 22 | 23 | ## 0.5.1 24 | - Fix error typo ([38a47b](https://github.com/storeon/svelte/commit/38a47b1b03b2c758f9bc7d15e81b6f597f02e9d4)) 25 | - Reduce lib size ([8af38d](https://github.com/storeon/svelte/commit/8af38d4a9d1204278c42af36d8fba108597ed458)) 26 | - Reduce rerender using subscribers map instead of array ([7e77af](https://github.com/storeon/svelte/commit/43e58291d91b40ee105aa51ebc4e5a9d216f71df)) 27 | 28 | ## 0.5.0 29 | - change API: smaller, better DX ([7e77af](https://github.com/storeon/svelte/commit/7e77afd3288d684c341fcbe6453e8ff2dd3985fd)) 30 | 31 | ## 0.4.1 32 | - update dependencies ([0c71ea](https://github.com/storeon/svelte/commit/0c71ea83ce0c1f4a5f206ee5af62b0fc0aff170e)) 33 | 34 | ## 0.4.0 35 | - add support for storeon 0.9 ([eeed35](https://github.com/storeon/svelte/commit/eeed35e8f7cad204356d9044ff595535732081ab)) 36 | 37 | ## 0.3.0 38 | 39 | - simplify API ([57d4ba](https://github.com/storeon/svelte/commit/57d4ba1f1d50ef48313ce23cebe2671f49ac813d)) 40 | 41 | ## 0.2.1 42 | 43 | - fix type definition 44 | 45 | ## 0.2.0 46 | 47 | - simplify API 48 | - update readme 49 | 50 | ## 0.1.0 51 | 52 | - Add `README.md`. 53 | - Add `CHANGELOG.md`. 54 | - Add `LICENSE`. 55 | - Add `.npmignore`. 56 | - Add `eslint`. 57 | 58 | ## 0.0.1 59 | 60 | - Initial release. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Dmytro Mostovyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storeon Svelte 2 | 3 | [![npm version](https://badge.fury.io/js/%40storeon%2Fsvelte.svg)](https://www.npmjs.com/package/@storeon/svelte) 4 | [![Build Status](https://travis-ci.org/storeon/svelte.svg?branch=master)](https://travis-ci.org/storeon/svelte) 5 | 6 | 7 | Storeon logo by Anton Lovchikov 8 | 9 | [Svelte] is the smallest JS framework, but even so, it contains many built-in features. One of them is a `svelte/store`. But why we need to use a third-party store? `@storeon/svelte` has several advantages compared with the built-in one. 10 | 11 | - **Size**. 179 bytes (+ Storeon itself) instead of 485 bytes (minified and gzipped). 12 | - **Ecosystem**. Many additional [tools] can be combined with a store. 13 | - **Speed**. Bind components to the changes in the exact store that you need. 14 | 15 | [storeon]: https://github.com/storeon/storeon 16 | [tools]: https://github.com/storeon/storeon#tools 17 | [svelte]: https://github.com/sveltejs/svelte 18 | [size limit]: https://github.com/ai/size-limit 19 | [demo]: https://codesandbox.io/s/admiring-beaver-edi8m 20 | [article]: https://evilmartians.com/chronicles/storeon-redux-in-173-bytes 21 | 22 | ## Install 23 | ```sh 24 | npm install -S @storeon/svelte 25 | ``` 26 | or 27 | ```sh 28 | yarn add @storeon/svelte 29 | ``` 30 | ## How to use ([Demo]) 31 | 32 | Create store using `storeon` module: 33 | 34 | #### `store.js` 35 | 36 | ```javascript 37 | import { createStoreon } from 'storeon' 38 | 39 | let counter = store => { 40 | store.on('@init', () => ({ count: 0 })) 41 | store.on('inc', ({ count }) => ({ count: count + 1 })) 42 | } 43 | 44 | export const store = createStoreon([counter]) 45 | ``` 46 | 47 | Using TypeScript you can pass `State` and `Events` interface to the `createStoreon` function: 48 | 49 | #### `store.ts` 50 | 51 | ```typescript 52 | import { StoreonModule, createStoreon } from 'storeon' 53 | 54 | interface State { 55 | count: number 56 | } 57 | 58 | interface Events { 59 | 'inc': undefined 60 | 'set': number 61 | } 62 | 63 | let counter = (store: StoreonModule) => { 64 | store.on('@init', () => ({ count: 0 })) 65 | store.on('inc', ({ count }) => ({ count: count + 1 })) 66 | store.on('set', (_, event) => ({ count: event})) 67 | }; 68 | 69 | export const store = createStoreon([counter]) 70 | ``` 71 | 72 | #### `App.svelte` 73 | 74 | Provide store to Svelte Context using `provideStoreon` from `@storeon/svelte` 75 | 76 | ```html 77 | 84 | 85 | 86 | ``` 87 | 88 | Import `useStoreon` function from our `@storeon/svelte` module and use it for getting state and dispatching new events: 89 | 90 | #### `Child.svelte` 91 | 92 | ```html 93 | 102 | 103 |

The count is {$count}

104 | 105 | 106 | ``` 107 | Using typescript you can pass `State` and `Events` interfaces to `useStoreon` function to be full type safe 108 | ```html 109 | 119 | 120 |

The count is {$count}

121 | 122 | 123 | ``` 124 | 125 | ## Usage with [@storeon/router](https://github.com/storeon/router) 126 | If you want to use the @storeon/svelte with the `@storeon/router` you should import the `router.createRouter` from `@storeon/router` and add this module to `createStoreon` 127 | 128 | #### `store.js` 129 | ```js 130 | import { createStoreon } from 'storeon' 131 | import { createRouter } from '@storeon/router'; 132 | 133 | const store = createStoreon([ 134 | createRouter([ 135 | ['/', () => ({ page: 'home' })], 136 | ['/blog', () => ({ page: 'blog' })], 137 | ]) 138 | ]) 139 | ``` 140 | 141 | And use it like: 142 | #### `App.svelte` 143 | ```html 144 | 151 | 152 | 153 | ``` 154 | #### `Child.svelte` 155 | ```html 156 | 162 | 163 | You can access the router like default svelte store via $: 164 | {$route.match.page} 165 | ``` 166 | -------------------------------------------------------------------------------- /example/App.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /example/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | {numActive} {numActive === 1 ? 'item' : 'items'} left 18 | 19 | 24 | {#if numCompleted} 25 | 28 | {/if} 29 |
30 | -------------------------------------------------------------------------------- /example/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

todos

17 | 23 |
24 | -------------------------------------------------------------------------------- /example/components/Main.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | {#if $todos.length > 0} 34 |
35 | 42 | 43 | 44 |
    45 | {#each filtered as todo (todo.id)} 46 | 47 | {/each} 48 |
49 | 50 |
51 |
52 | {/if} 53 | -------------------------------------------------------------------------------- /example/components/TodoItem.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
  • 34 |
    35 | 36 | 37 | 38 |
    39 | 40 | {#if editing} 41 | 49 | {/if} 50 |
  • 51 | -------------------------------------------------------------------------------- /example/constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /example/constants/keyCodes.js: -------------------------------------------------------------------------------- 1 | export const ENTER = 13 2 | export const ESCAPE = 27 3 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Storeon Svelte • TodoMVC 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | import 'todomvc-app-css/index.css' 3 | 4 | const app = new App({ target: document.body }) 5 | 6 | export default app 7 | -------------------------------------------------------------------------------- /example/store/filters.js: -------------------------------------------------------------------------------- 1 | import { SHOW_ALL } from '../constants/TodoFilters' 2 | 3 | export const filters = store => { 4 | store.on('@init', () => ({ filter: SHOW_ALL })) 5 | store.on('filter/set', (_, filter) => ({ filter })) 6 | } 7 | -------------------------------------------------------------------------------- /example/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStoreon } from 'storeon' 2 | 3 | import { todos } from './todos' 4 | import { filters, filterRoutes } from './filters' 5 | 6 | export const store = createStoreon([todos, filters, filterRoutes]) 7 | -------------------------------------------------------------------------------- /example/store/todos.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-unsupported-features/es-syntax */ 2 | function randomId () { 3 | return Math.random().toString() 4 | } 5 | 6 | export const todos = store => { 7 | store.on('@init', () => ({ todos: [] })) 8 | store.on('todo/add', (state, text) => ({ 9 | todos: [...state.todos, { id: randomId(), text, completed: false }] 10 | })) 11 | store.on('todo/delete', (state, id) => ({ 12 | todos: state.todos.filter(todo => todo.id !== id) 13 | })) 14 | store.on('todo/edit', (state, { id, text }) => ({ 15 | todos: state.todos.map(todo => todo.id === id ? { ...todo, text } : todo) 16 | })) 17 | store.on('todo/complete', (state, id) => ({ 18 | todos: state.todos.map( 19 | todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo 20 | ) 21 | })) 22 | store.on('todo/complete_all', state => { 23 | let marked = state.todos.every(todo => todo.completed) 24 | return { todos: state.todos.map(todo => ({ ...todo, completed: !marked })) } 25 | }) 26 | store.on('todo/clear_completed', state => ({ 27 | todos: state.todos.filter(todo => todo.completed === false) 28 | })) 29 | } 30 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { StoreonStore, StoreonDispatch, createStoreon } from 'storeon'; 2 | 3 | type Subscriber = (value: T) => void; 4 | 5 | type Unsubscriber = () => void; 6 | 7 | type Subscribable = { 8 | [K in keyof State]: { 9 | subscribe: (run: Subscriber) => Unsubscriber; 10 | }; 11 | } 12 | 13 | export declare function provideStoreon(store: StoreonStore): void 14 | export declare function useStoreon(...keys: (keyof State)[]): Subscribable & { 15 | dispatch: StoreonDispatch> 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { getContext, setContext } = require('svelte') 2 | 3 | const STORE = typeof Symbol !== 'undefined' ? Symbol('storeon') : '@@storeon' 4 | 5 | function provideStoreon (store) { 6 | setContext(STORE, store) 7 | } 8 | 9 | function useStoreon (...keys) { 10 | let store = getContext(STORE) 11 | if (process.env.NODE_ENV !== 'production' && !store) { 12 | throw new Error( 13 | 'Could not find storeon context value.' + 14 | 'Please ensure you provide store using "provideStoreon" function' 15 | ) 16 | } 17 | 18 | let subscribers = {} 19 | 20 | let makeSubscribable = key => { 21 | let subscribe = run => { 22 | let state = store.get() 23 | 24 | subscribers[key] = run 25 | run(state[key]) 26 | 27 | return () => { 28 | delete subscribers[key] 29 | } 30 | } 31 | 32 | return { subscribe } 33 | } 34 | 35 | store.on('@changed', (_, changed) => { 36 | keys.forEach(key => { 37 | if (key in changed && subscribers[key]) { 38 | subscribers[key](changed[key]) 39 | } 40 | }) 41 | }) 42 | 43 | let data = {} 44 | keys.forEach(key => { 45 | data[key] = makeSubscribable(key) 46 | }) 47 | data.dispatch = store.dispatch 48 | return data 49 | } 50 | 51 | module.exports = { provideStoreon, useStoreon } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storeon/svelte", 3 | "version": "1.0.0", 4 | "description": "A tiny (187 bytes) connector for Storeon and Svelte", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": "https://github.com/storeon/svelte.git", 8 | "author": "Dmytro Mostovyi ", 9 | "license": "MIT", 10 | "scripts": { 11 | "start": "parcel serve example/index.html --open", 12 | "lint": "eslint ./*.js", 13 | "test": "jest --coverage && yarn lint && size-limit" 14 | }, 15 | "sideEffects": false, 16 | "peerDependencies": { 17 | "storeon": "^3.0.0", 18 | "svelte": "^3.20.0" 19 | }, 20 | "devDependencies": { 21 | "@logux/eslint-config": "^42.1.0", 22 | "@size-limit/dual-publish": "^4.7.0", 23 | "@size-limit/preset-small-lib": "^4.7.0", 24 | "dual-publish": "^0.11.0", 25 | "eslint": "^7.12.1", 26 | "eslint-config-standard": "^16.0.1", 27 | "eslint-plugin-import": "^2.22.1", 28 | "eslint-plugin-jest": "^24.1.0", 29 | "eslint-plugin-node": "^11.1.0", 30 | "eslint-plugin-prefer-let": "^1.1.0", 31 | "eslint-plugin-prettierx": "^0.14.0", 32 | "eslint-plugin-promise": "^4.2.1", 33 | "eslint-plugin-security": "^1.4.0", 34 | "eslint-plugin-standard": "^4.0.2", 35 | "eslint-plugin-unicorn": "^23.0.0", 36 | "husky": "^4.3.0", 37 | "jest": "^26.6.1", 38 | "lint-staged": "^10.5.1", 39 | "parcel-bundler": "^1.12.4", 40 | "parcel-plugin-svelte": "^4.0.6", 41 | "size-limit": "^4.7.0", 42 | "storeon": "^3.1.1", 43 | "svelte": "^3.29.4", 44 | "todomvc-app-css": "^2.3.0", 45 | "typescript": "^4.0.5" 46 | }, 47 | "lint-staged": { 48 | "*.js": [ 49 | "eslint --fix", 50 | "git add" 51 | ] 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "pre-commit": "lint-staged" 56 | } 57 | }, 58 | "browserslist": [ 59 | "last 1 chrome versions" 60 | ], 61 | "size-limit": [ 62 | { 63 | "name": "core", 64 | "import": { 65 | "index.js": "{ provideStoreon, useStoreon }" 66 | }, 67 | "limit": "179 B", 68 | "ignore": [ 69 | "svelte" 70 | ] 71 | } 72 | ], 73 | "eslintConfig": { 74 | "extends": "@logux/eslint-config", 75 | "rules": { 76 | "node/no-unpublished-require": "off", 77 | "func-style": "off" 78 | } 79 | }, 80 | "eslintIgnore": [ 81 | "node_modules" 82 | ], 83 | "keywords": [ 84 | "storeon", 85 | "state", 86 | "svelte" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | let { getContext, setContext } = require('svelte') 2 | let { createStoreon } = require('storeon') 3 | 4 | let { useStoreon, provideStoreon } = require('..') 5 | 6 | jest.mock('svelte') 7 | 8 | function setupStore () { 9 | function counter (store) { 10 | store.on('@init', () => { 11 | return { count: 0, foo: 'baz', loading: false } 12 | }) 13 | store.on('inc', state => { 14 | return { count: state.count + 1 } 15 | }) 16 | store.on('toggle', state => { 17 | return { loading: !state.loading } 18 | }) 19 | } 20 | 21 | return createStoreon([counter]) 22 | } 23 | 24 | function setupContextStore () { 25 | getContext.mockImplementationOnce(() => setupStore()) 26 | } 27 | 28 | beforeEach(() => { 29 | setupContextStore() 30 | }) 31 | 32 | afterEach(() => { 33 | setContext.mockReset() 34 | getContext.mockReset() 35 | }) 36 | 37 | it('set store to svelte context', () => { 38 | let store = setupStore() 39 | setContext.mockImplementationOnce(() => {}) 40 | provideStoreon(store) 41 | 42 | expect(setContext).toHaveBeenCalledWith(expect.anything(), store) 43 | expect(setContext).toHaveBeenCalledTimes(1) 44 | }) 45 | 46 | it('get store from svelte context', () => { 47 | let store = useStoreon() 48 | 49 | expect(store).toBeDefined() 50 | expect(store.dispatch).toBeDefined() 51 | }) 52 | 53 | it('get error if store context not provided', () => { 54 | getContext.mockRestore() 55 | 56 | expect(() => useStoreon()).toThrow(Error) 57 | }) 58 | 59 | it('should start with init value', () => { 60 | let { count } = useStoreon('count') 61 | 62 | count.subscribe(value => expect(value).toBe(0)) 63 | }) 64 | 65 | it('should be reactive', () => { 66 | let currentValue 67 | let { count, dispatch } = useStoreon('count') 68 | 69 | count.subscribe(value => { currentValue = value }) 70 | 71 | expect(currentValue).toBe(0) 72 | 73 | dispatch('inc') 74 | 75 | expect(currentValue).toBe(1) 76 | }) 77 | 78 | it('should not emit changes on other dispatches', () => { 79 | let fooSpyCb = jest.fn() 80 | let countSpyCb = jest.fn() 81 | 82 | let { foo, count, dispatch } = useStoreon('foo', 'count') 83 | 84 | foo.subscribe(fooSpyCb) 85 | count.subscribe(countSpyCb) 86 | 87 | expect(fooSpyCb).toHaveBeenCalledWith('baz') 88 | expect(fooSpyCb).toHaveBeenCalledTimes(1) 89 | expect(countSpyCb).toHaveBeenCalledWith(0) 90 | expect(countSpyCb).toHaveBeenCalledTimes(1) 91 | 92 | countSpyCb.mockReset() 93 | dispatch('inc') 94 | 95 | expect(countSpyCb).toHaveBeenCalledWith(1) 96 | expect(countSpyCb).toHaveBeenCalledTimes(1) 97 | }) 98 | 99 | it('should to be unsubscribed', () => { 100 | let currentValue 101 | let { count, dispatch } = useStoreon('count') 102 | 103 | let unsubscribe = count.subscribe(value => { currentValue = value }) 104 | 105 | expect(currentValue).toBe(0) 106 | 107 | dispatch('inc') 108 | 109 | expect(currentValue).toBe(1) 110 | 111 | unsubscribe() 112 | 113 | dispatch('inc') 114 | 115 | expect(currentValue).toBe(1) 116 | }) 117 | 118 | it('should work with boolean values', () => { 119 | let currentValue 120 | let { loading, dispatch } = useStoreon('loading') 121 | 122 | let unsubscribe = loading.subscribe(value => { currentValue = value }) 123 | 124 | expect(currentValue).toBe(false) 125 | 126 | dispatch('toggle') 127 | 128 | expect(currentValue).toBe(true) 129 | 130 | dispatch('toggle') 131 | 132 | expect(currentValue).toBe(false) 133 | 134 | unsubscribe() 135 | 136 | dispatch('toggle') 137 | 138 | expect(currentValue).toBe(false) 139 | }) 140 | --------------------------------------------------------------------------------