Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(wip): local state #11

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions examples/todo/components/Form.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react'
import { View, LocalState } from '../../../src'

const {
state,
setState
} = LocalState({
schema: {
name: "",
age: 0,
},

setter: (mutation) => {
return {
name: mutation.name ? mutation.name.toLowerCase() : "",
age: mutation.age
? mutation.age > 0
? mutation.age * 2
: mutation.age * 10
: mutation.age + 1,
}
},

getter: (state) => {
const [
firstName, lastName
] = state.name.split(" ")

return {
firstName,
lastName,
}
}
})

export default View(
() =>
<div>
<pre>
{
<p>
name: { state.name }
age: { state.age }
</p>
}
</pre>
<li>
<label htmlFor="name">Name</label>
<input type="text" name="name" value={state.name} onChange={({ target: { value } }) => setState("name")(value)}/>
</li>
<li>
<label htmlFor="name">Age</label>
<input type="text" name="name" value={state.age} onChange={({ target: { value }}) => setState("age")(value)}/>
</li>
</div>
)
8 changes: 8 additions & 0 deletions examples/todo/components/TodoList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ import { createTodo, login, logout } from '../actions.jsx'
import { userState, todoState } from '../state.jsx'

import ListItem from './ListItem.jsx'
import Form from "./Form"

console.log({
todoState, userState
})

export default View(() => {
return (
<div>
<Form />
<br/>
<hr/>
Hi, we have {todoState.size} total todos.
Pending:
<ul>
Expand Down
7 changes: 2 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { configure } from 'mobx'

configure({ enforceActions: 'strict', isolatedGlobalState: true })

export * from 'mobx'

export { Model, Propose } from './model'
export { State } from './state'
export { observer as View } from 'mobx-react'
export { observer as View } from 'mobx-react'
export { LocalState } from "./local-state"
195 changes: 195 additions & 0 deletions src/local-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
*
* @example:
* import { LocalState } from "samx"
*
* const { state, setState } = LocalState({
* schema: {
* },
* setter: (mutation) => {
* return mutation
* },
* getter: (state) => {
* return state
* },
* })
* */

import { observable, set } from "mobx"

const LocalStateError = (msg) => new Error(msg)

const Errors = {
setter: {
InvalidMutation: LocalStateError("Invalid Mutation"),
notAllowed: LocalStateError("Operation not allowed"),
},
setState: {
args: {
one: LocalStateError("Invalid arguments. First argument should be either a string prop name or a mutation object"),
two: LocalStateError("Invalid arguments. First argument must be a string, second must be an object/number/string"),
invalid: LocalStateError("Invalid argument list.")
}
},
Constructor: {
InvalidArgs: {
first: {
schema: LocalStateError("Schema must be an object"),
}
}
},
getter: {
prop: {
invalid: LocalStateError("Invalid property access. No such property is defined in the getter."),
}
},
}

/**
* Encapsulated Mobx.set for validation and interfacing purposes
*
* @param {Mobx.observable} observable
* @param {Object} mutation
*/
const Setter = (interceptor) => (observableState, mutation) => {
console.log(observableState, mutation)
Object.keys(mutation).forEach(
(key) => {
if (typeof mutation[key] === "function") {
throw Errors.setter.InvalidMutation
}
}
)

if (typeof interceptor === "function") {
const newMutation = interceptor(toJS(observableState), mutation)
set(observableState, newMutation)
} else {
set(observableState, mutation)
}

return true
}

/**
*
* @example:
* first:
* setState({ property: value })
*
* second:
* setState("property")(value)
*
* third:
* setState("property", value)
*/
const SetState = (observableState, interceptor) => (...args) => {
const interceptedSetter = Setter(interceptor)
const argsLength = args.length

switch (argsLength) {
case 1:
const arg = args[0]
if (typeof arg === "object") {
return interceptedSetter(observableState, arg)
} else if (typeof arg === "string") {
return (mutation) => interceptedSetter(observableState, mutation)
} else throw Errors.setState.args.one

case 2 || argsLength > 2:
const [ first, second ] = args

if (
typeof first === "string"
&& (
typeof second === "object"
|| typeof second === "string"
|| typeof second === "number"
)
) {
return interceptedSetter(observableState, {
[first]: second,
})
} else throw Errors.setState.args.two

default:
throw Errors.setState.args.invalid
}
}

/**
*
* @param {Object} opts
* @param {Object} opts.schema
* @optional {Function} opts.setter
* @optional {Function} opts.getter
*
*/
const LocalState = (opts) => {
const options = {}

if (typeof opts.schema !== "object") {
throw Errors.Constructor.InvalidArgs.first.schema
}

options.schema = opts.schema

if (opts.setter && typeof opts.setter === "Function") {
options.setter = opts.setter
}

if (opts.getter && typeof opts.getter === "Function") {
options.getter = opts.getter
}

const observableState = observable(options.schema)

const state = new Proxy({
__data: observableState,

getCurrentState() {
return toJS(observableState)
},

__getObservable() {
return observableState
},

}, {
get(target, prop) {
console.log({ target, prop })
if (typeof target[prop] === "function") {
const r = Reflect.get(...arguments)
console.log({ target, prop, value: target[prop], r })
return r
}

if (options.getter) {
const currentValue = options.getter(target.__data)

if (prop in Object.keys(currentValue)) {
return currentValue[prop]
} else {
throw Errors.getter.prop.invalid
}
} else {
return target.__data[prop]
}
},

set() {
throw Errors.setter.notAllowed
}
})

const setState = SetState(state.__getObservable(), options.setter || undefined)

return {
state,
setState,
}
}

export {
LocalState,
}