Skip to content
Merged
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
136 changes: 115 additions & 21 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ export function app(props, container) {
var globalState
var globalActions

repaint(flush(init(props, (globalState = {}), (globalActions = {}))))
repaint(
flush(
initModule(
props,
(globalState = {}),
(globalActions = {}),
updateGlobalState,
function() {
return globalState
}
)
)
)

return globalActions

Expand Down Expand Up @@ -49,7 +61,20 @@ export function app(props, container) {
)
}

function init(module, state, actions) {
/**
* Initializes the given module:
* - computes the initial state
* - initalize all actions
* - add module.init() to be called before the first render
* - initialize sub-modules
*
* @param module the module to initialize
* @param state the initial state (updated by this function)
* @param actions the actions object (updated by this function)
* @param update the update() function for the current state slice
* @param getState function () => state that returns the current (up-to-date) state slice
*/
function initModule(module, state, actions, update, getState) {
if (module.init) {
callbacks.push(function() {
module.init(state, actions)
Expand All @@ -58,35 +83,104 @@ export function app(props, container) {

assign(state, module.state)

initActions(state, actions, module.actions)

for (var i in module.modules) {
init(module.modules[i], (state[i] = {}), (actions[i] = {}))
initModuleActions(state, actions, module.actions, update, getState)

for (var key in module.modules) {
initModule(
module.modules[key],
// do not override state is already exist in current module
state[key] || (state[key] = {}),
// do not override actions is already exist in current module
actions[key] || (actions[key] = {}),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should solve #438.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mytrill My PR doesn't have this, is it necessary?

updateFor(update, getState, key),
getStateFor(getState, key)
)
}
}

function initActions(state, actions, source) {
Object.keys(source || {}).map(function(i) {
if (typeof source[i] === "function") {
actions[i] = function(data) {
return typeof (data = source[i](state, actions)) === "function"
? typeof (data = data.apply(0, arguments)) === "function"
? data(update)
: update(data)
/**
* Initializes the given actions:
* - bind the moduleActions to the current state slice/actions
* - set state = {} for children actions, if needed
* - recursively initialize children actions
*
* @param moduleActions the current module's actions, contains actions and other action objects
* @param state the initial state object, this is passed here to avoid undefined state when
* computing and action's state slice
* @param actions the initalized actions object (updated by this function)
* @param update the update() function for the actions' relevant state slices
* @param getState function: () => state that return the relevant state slice for the actions
*/
function initModuleActions(state, actions, moduleActions, update, getState) {
Object.keys(moduleActions || {}).map(function(key) {
if (typeof moduleActions[key] === "function") {
actions[key] = function(data) {
return typeof (data = moduleActions[key](
getState(),
actions,
data
)) === "function"
? data(update)
: update(data)
}
} else {
initActions(state[i] || (state[i] = {}), (actions[i] = {}), source[i])
initModuleActions(
state[key] || (state[key] = {}),
(actions[key] = {}),
moduleActions[key],
updateFor(update, getState, key),
getStateFor(getState, key)
)
}
})
}

function update(data) {
return (
typeof data === "function"
? update(data(state))
: data && repaint(assign(state, data)),
state
/**
* Merge the global state with the given result and triggers a repaint.
* This is the update() function for the app's prop.
* @param result the result to merge it, a function (globalState) => result, or falsy to not trigger repaint
* @returns globalState
*/
function updateGlobalState(result) {
return (
typeof result === "function"
? updateGlobalState(result(globalState))
: result && repaint((globalState = merge(globalState, result))),
globalState
)
}

/**
* Wraps the given update() function and return a new update() that can be used for the state slice getState()[prop].
*
* @param update the update() function to wrap
* @param getState function: () => state that returns the parrent's state slice
* (getState()[prop] contains the current state slice)
* @param prop the property of the state slice to create the update() function for
* @param parentResult internal variable added to avoid declaration (saves 1 "var "), should not be set
*
* @returns the new update function specialized for the given state slice
*/
function updateFor(update, getState, prop, parentResult) {
return function(result) {
;(parentResult = {})[prop] = merge(
getState()[prop],
typeof result === "function" ? result(getState()[prop]) : result
)
return update(parentResult)[prop]
}
}

/**
* Function: (getState: () => state, prop: string) => () => state[prop].
*
* @param getState the getter for the state: () => state that gets wrapped
* @param prop the state slice to get
* @returns a new state getter function: () => getState()[prop]
*/
function getStateFor(getState, prop) {
return function() {
return getState()[prop]
}
}

Expand Down
61 changes: 61 additions & 0 deletions test/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,64 @@ test("thunks", done => {
}
}).upAsync(1)
})

test("state immutable", done => {
let initialState
const up = state => ({ value: state.value + 1 })

const actions = app({
state: {
value: 0,
module1: { value: 1 },
module2: { value: 2 }
},
init(state) {
initialState = state
},
actions: {
up,
get(state) {
return _ => state
},
module1: { up }
}
})

const state1 = actions.up()
expect(state1.value).toBe(1)
expect(state1).not.toBe(initialState)
expect(state1.module1).toBe(initialState.module1)
expect(state1.module2).toBe(initialState.module2)

actions.module1.up()
const state2 = actions.get()
expect(state2.value).toBe(state1.value)
expect(state2).not.toBe(state1)
expect(state2.module1).not.toBe(state1.module1)
expect(state2.module2).toBe(state1.module2)

done()
})

test("Allow reuse state", done => {
app({
view: state =>
h(
"div",
{
oncreate() {
console.log(document.body.innerHTML)
expect(document.body.innerHTML).toBe(`<div>{"child":{}}</div>`)
done()
}
},
JSON.stringify(state)
),
state: {},
actions: {
recurse(state) {
return { child: state }
}
}
}).recurse()
})
9 changes: 9 additions & 0 deletions test/modules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ test("modules", done => {
},
actions: {
up(state) {
expect(state).toEqual({
value: 0,
bar: {
text: "hello"
}
})
return { value: state.value + 1 }
}
},
Expand All @@ -36,6 +42,9 @@ test("modules", done => {
},
actions: {
change(state) {
expect(state).toEqual({
text: "hello"
})
return { text: "hola" }
}
}
Expand Down