diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5112867 --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ + +# Created by https://www.gitignore.io/api/node,windows,linux,macos + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# End of https://www.gitignore.io/api/node,windows,linux,macos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a2cd0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Atomix Interactions + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bde1394 --- /dev/null +++ b/README.md @@ -0,0 +1,311 @@ +# Readme + +## Optice + +Like redux, but `no reducers`, `no actions` + +### Usage + +```bash +npm install optice +``` + +```js +import { L, createStore } from 'optice' + +const initialState = { + user: { + name: '', + email: '', + }, + company: { + name: '', + okato: 0, + } +} + +const store = createStore(initialState) + +const userLens = L.prop('user') +const companyLens = L.prop('company') + +const nameLens = L.prop('name') +const emailLens = L.create( + (state) => state.email, + (email) => (state) => ({ ...state, email }), +) +// same as `L.prop('email')` + +const userNameLens = L.compose(userLens, nameLens) +const companyNameLens = L.compose(companyLens, nameLens) + +const fetchUser = async () => ({ name: 'Foo Bar', email: 'foo.bar@company.com' }) + +const loadUser = () => async ({ updateState }) => { + updateState(userLens, await fetchUser()) +} +``` + +In console should be this output: + +``` +BEFORE { name: '', email: '' } { name: '', okato: 0 } +UPDATE { name: 'Foo Bar', email: 'foo.bar@company.com' } { name: '', okato: 0 } +UPDATE { name: 'Foo Bar', email: 'foo.bar@company.com' } { name: 'Company', okato: 0 } +``` + + +### API + +#### `createStore` + +``` +createStore(initialState): Store +``` +Create new store object. + + +#### `Store.execute` + +``` +store.execute(command): result +``` + +Run command function with store methods `({ getState, setState, updateState, readState, execute })`. +Immediatly return result of command. + +```js +const command = ({ getState, setState, updateState, readState, execute }) => { + // read, update state + // or execute another command + return 1 +} + +store.execute(command) === 1 +``` + +#### `Store.getState` + +``` +store.getState(): State +``` + +Just return current state + +#### `Store.readState` + +``` +store.readState(lens: Lens): Value +``` + +Read value from state through lens. + +```js +const lens = L.prop('data') + +const initialState = { + data: 1 +} + +const store = createStore(initialState) +const value = store.readState(lens) + +console.assert(value === 1) +``` + + +#### `Store.setState` + +``` +store.setState(newState: State): void +``` + +*It notify all subscribers*. Replace state, returns nothing. + +```js + +const initial = { a: 1, b: 2 } + +const store = createStore(initial) + +store.subscribe(state => { + console.log('updated') + console.assert(state.a === 5) +}) +store.setState({ a: 5, b: 10 }) +``` + +#### `Store.subscribe` + +``` +store.subscribe(listener: Function): Function +``` + +Add listener to subscribers list, returns `unsubscribe` function. + + +```js +const store = createStore({ a: 1 }) + +const unsubscribe = store.subscribe(state => { + console.log('update', state) +}) + +store.setState({ a: 2 }) // > update { a: 2 } + +unsubscribe() +store.setState({ a: 3 }) // nothing +``` + +#### `Store.updateState` + +``` +store.updateState(lens: Lens, valueOrFunc: any | Function): void +``` + +*It notify all subscribers*. Update piece of state through lens. +If function passed update state with `L.over`, else use `L.write` to just set value. + +```js +const lens = L.prop('a') + +const store = createStore({ a: 1 }) + +store.updateState(lens, 2) +console.assert(store.getState().a === 2) + +store.updateState(lens, value => value + 1) +console.assert(store.getState().a === 3) +``` + +#### `L.create` + +``` +L.create(getter: Function, setter: Function): Lens +``` + +Create new lens. + +Getter is just function that received state. Should return piece of state. + +Setter is function that received passed value, return function that received state. Should return new version of passed state. + +Getter and Setter should be pure functions. + +```js +const lens = L.create( + state => state.value, + value => state => ({ ...state, value }), +) +``` + +#### `L.view` + +``` +L.view(state: State, lens: Lens): any +``` + +Read value from state through lens. + +```js +const lens = L.create( + state => state.value, + value => state => ({ ...state, value }), +) + +const state = { value: 'foo' } +const value = view(state, lens) + +console.assert(value === 'foo') +``` + +#### `L.write` + +``` +L.write(state: State, lens: Lens, value: any): State +``` + +Immutable update piece of state through lens. Return new version of state. + +```js +const state = { foo: { bar: 1 } } + +const fooLens = L.prop('foo') +const barLens = L.prop('bar') +const fooBarLens = L.compose(fooLens, barLens) + +const newState = L.write(state, fooBarLens, 2) +console.assert(newState.foo.bar === 2) +``` + +#### `L.over` + +``` +L.over(state: State, lens: Lens, fn: (value) => value): State +``` + +Like `L.write` but use function to update value. Return new version of state. + + +```js +const state = { foo: 100 } + +const fooLens = L.prop('foo') +const updater = (value) => value + value + +const newState = L.over(state, fooLens, updater) +console.assert(L.read(state, fooLens) === 200) +``` + + +#### `L.compose` + +``` +L.compose(...lenses: Lens[]): Lens +``` + +> L.compose can be used only for this lenses + +Perform lens composition. Returns one lens. + +If passed no lens, returns empty lens `L.create(s => s, v => s => s)`. + +If passed one lens, returns its. + +```js +const a = L.create( + state => state.a, + value => state => ({ ...state, a: value }) +) + +const b = L.create( + state => state.b, + value => state => ({ ...state, b: value }) +) + +const ab = L.compose(a, b) + +// same as + +const ab2 = L.create( + state => state.a.b, + value => state => ({ ...state, a: { ...state.a, b: value } }), +) +``` + +#### `L.prop` + +``` +L.prop(name: string): Lens +``` + +Makes lens to read/write property. + +```js +const fooLens = L.prop('foo') +// Same as +const fooLens2 = L.create( + state => state.foo, + value => state => ({ ...state, value: foo }) +) +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..54a3fb6 --- /dev/null +++ b/index.js @@ -0,0 +1,99 @@ + + +const create = (getter, setter) => ({ getter, setter }) + +const view = (state, lens) => lens.getter(state) +const write = (state, lens, data) => lens.setter(data)(state) +const over = (state, lens, fn) => L.write(state, lens, fn(L.view(state, lens))) + +const compose = (...lenses) => { + const len = lenses.length; + + if (len === 0) { + return L.create(s => s, v => s => s) + } + + if (len === 1) { + return lenses[0] + } + + return L.create( + (state) => lenses.reduce((current, lens) => L.view(current, lens), state), + (value) => function traverse(state, index = 0) { + const lens = lenses[index] + return index === len + ? value + : lens.setter(traverse(lens.getter(state), index + 1))(state) + } + ) +} + +const prop = (name) => L.create( + (state) => state[name], + (value) => (state) => ({ ...state, [name]: value }), +) + +const L = { + create, + view, + write, + over, + compose, + prop, +} + +const createStore = (initialState) => { + let lastState = initialState + const listeners = [] + + const subscribe = (fn) => { + let isSubscribed = true + listeners.push(fn) + + return function unsubscribe() { + if (isSubscribed) { + const index = listeners.indexOf(fn) + listeners.splice(index, 1) + isSubscribed = false + } + } + } + + const update = () => { + for (const listener of listeners) { + listener(lastState) + } + } + + const getState = () => lastState + + const setState = (newState) => { + lastState = newState + update() + } + + const updateState = (lens, dataOrFn) => { + typeof dataOrFn === 'function' + ? setState(L.over(getState(), lens, dataOrFn)) + : setState(L.write(getState(), lens, dataOrFn)) + } + + const readState = (lens) => + L.view(getState(), lens) + + const execute = (command) => command({ getState, setState, updateState, readState, execute }) + + return { + execute, + getState, + readState, + setState, + subscribe, + updateState, + } +} + +module.exports = { + L, + createStore, +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a07d3e2 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "optice", + "version": "0.1.0", + "description": "Select and update your store with lenses", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/atomixinteractions/optice.git" + }, + "keywords": [ + "store", + "redux", + "react", + "lens", + "ramda", + "function", + "traverse", + "reduce", + "update", + "state", + "compose", + "prop", + "initial", + "set", + "read" + ], + "contributors": [ + "Sergey Sova (https://sergeysova.com/)", + "Viacheslav Bereza (http://betula.co/)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/atomixinteractions/optice/issues" + }, + "homepage": "https://github.com/atomixinteractions/optice#readme" +}