Skip to content

Commit a8a0eaa

Browse files
feat(overmind-components): added overmind components
1 parent 0c3722b commit a8a0eaa

File tree

21 files changed

+858
-321
lines changed

21 files changed

+858
-321
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
"vue-hot-reload-api": "2.3.0",
5858
"vue-styled-components": "1.3.0",
5959
"ws": "7.0.0",
60-
"react-split-pane": "0.1.87"
60+
"react-split-pane": "0.1.87",
61+
"snabbdom": "0.7.3"
6162
},
6263
"devDependencies": {
6364
"@babel/core": "7.4.5",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
src
2+
jest.config.js
3+
tsconfig.json
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# overmind-components
2+
3+
[https://overmindjs.org](https://overmindjs.org)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
collectCoverage: true,
3+
collectCoverageFrom: ['src/**/*.{t,j}s?(x)', '!src/**/*.d.ts'],
4+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest',
7+
},
8+
testRegex: '\\.test\\.tsx?$',
9+
testPathIgnorePatterns: [
10+
'/dist/',
11+
'/es/',
12+
'/lib/',
13+
'<rootDir>/node_modules/',
14+
],
15+
transformIgnorePatterns: ['<rootDir>/node_modules/'],
16+
coveragePathIgnorePatterns: ['<rootDir>/node_modules/'],
17+
haste: {
18+
// This option is needed or else globbing ignores <rootDir>/node_modules.
19+
providesModuleNodeModules: ['overmind-react'],
20+
},
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "overmind-components",
3+
"version": "1.0.0-alpha1",
4+
"description": "Functional actions",
5+
"author": "Christian Alfoni <[email protected]>",
6+
"license": "MIT",
7+
"repository": "git+https://github.com/cerebral/overmind.git",
8+
"main": "lib/index.js",
9+
"module": "es/index.js",
10+
"types": "lib/index.d.ts",
11+
"scripts": {
12+
"build": "npm run build:lib & npm run build:es",
13+
"build:lib": "tsc --outDir lib --module commonjs",
14+
"build:es": "tsc --outDir es --module es2015",
15+
"clean": "rimraf es lib coverage",
16+
"typecheck": "tsc --noEmit",
17+
"test": "jest --runInBand",
18+
"test:watch": "jest --watch --updateSnapshot --coverage false",
19+
"prebuild": "npm run clean",
20+
"postbuild": "rimraf {lib,es}/**/__tests__",
21+
"posttest": "npm run typecheck"
22+
},
23+
"keywords": [
24+
"state",
25+
"sideeffects",
26+
"app",
27+
"framework"
28+
],
29+
"files": [
30+
"lib",
31+
"es",
32+
"react"
33+
],
34+
"dependencies": {
35+
"overmind": "next",
36+
"snabbdom": "^0.7.3"
37+
},
38+
"devDependencies": {
39+
"@types/node": "^10.12.21",
40+
"tslib": "^1.9.3"
41+
}
42+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`React should allow using component as normal, even when not connected 1`] = `
4+
<h1>
5+
bar
6+
</h1>
7+
`;
8+
9+
exports[`React should allow using component as normal, even when not connected 2`] = `
10+
<h1>
11+
nada
12+
</h1>
13+
`;
14+
15+
exports[`React should allow using hooks 1`] = `
16+
<h1>
17+
bar
18+
</h1>
19+
`;
20+
21+
exports[`React should allow using mocked Overmind 1`] = `
22+
<h1>
23+
bar
24+
</h1>
25+
`;
26+
27+
exports[`React should be able to use Provider with connect 1`] = `
28+
<h1>
29+
bar
30+
</h1>
31+
`;
32+
33+
exports[`React should connect actions and state to class components 1`] = `
34+
<h1>
35+
bar
36+
</h1>
37+
`;
38+
39+
exports[`React should connect state and actions to stateless components 1`] = `
40+
<h1>
41+
bar
42+
</h1>
43+
`;
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { EventType, IConfiguration, Overmind } from 'overmind'
2+
import { VALUE } from 'proxy-state-tree'
3+
import * as snabbdom from 'snabbdom'
4+
import h from 'snabbdom/h'
5+
import attributesModule from 'snabbdom/modules/attributes'
6+
import classModule from 'snabbdom/modules/class'
7+
import eventlistenersModule from 'snabbdom/modules/eventlisteners'
8+
import propsModule from 'snabbdom/modules/props'
9+
import styleModule from 'snabbdom/modules/style'
10+
import thunk from 'snabbdom/thunk'
11+
12+
import { SELF, getName, normalizeAttrs } from './utils'
13+
14+
const patch = snabbdom.init([
15+
classModule,
16+
propsModule,
17+
attributesModule,
18+
styleModule,
19+
eventlistenersModule,
20+
])
21+
22+
let _app
23+
24+
export interface IComponent<Config extends IConfiguration, Props = {}> {
25+
(
26+
props: Props,
27+
overmind: {
28+
state: Overmind<Config>['state']
29+
actions: Overmind<Config>['actions']
30+
effects: Overmind<Config>['effects']
31+
}
32+
): any
33+
}
34+
35+
declare global {
36+
namespace JSX {
37+
interface IntrinsicElements {
38+
self: React.DetailedHTMLProps<
39+
React.HtmlHTMLAttributes<HTMLHtmlElement> & {
40+
onMount?: (el: HTMLElement) => void | (() => void)
41+
},
42+
HTMLHtmlElement
43+
>
44+
}
45+
}
46+
}
47+
48+
declare module 'react' {
49+
interface HTMLAttributes<T>
50+
extends React.AriaAttributes,
51+
React.DOMAttributes<T> {
52+
onInsert?: (el: HTMLElement) => void
53+
onUpdate?: (el: HTMLElement) => void
54+
onDestroy?: (el: HTMLElement) => void
55+
onEnter?: (el: HTMLElement) => void
56+
onLeave?: (el: HTMLElement) => void
57+
class?: {
58+
[key: string]: boolean
59+
}
60+
}
61+
}
62+
63+
const thunks = new Map()
64+
65+
let nextId = 0
66+
let nextComponentId = {}
67+
let nextComponentInstanceId = {}
68+
69+
export const createElement = (tag, attrs, ...children) => {
70+
if (typeof tag === 'function') {
71+
const memo = Object.keys(attrs || {}).reduce(
72+
(aggr, key) => {
73+
const value = attrs[key]
74+
if (value && value[VALUE] && !value[VALUE].__rescope) {
75+
Object.defineProperty(value[VALUE], '__rescope', {
76+
get: () => {
77+
return (tree) => _app.proxyStateTree.rescope(value, tree)
78+
},
79+
})
80+
}
81+
return [...aggr, key, value && value[VALUE] ? value[VALUE] : value]
82+
},
83+
children.length
84+
? ['children', children.reduce((aggr, item) => aggr.concat(item), [])]
85+
: []
86+
)
87+
88+
const name = getName(tag.name)
89+
return thunk(
90+
name,
91+
attrs && attrs.key,
92+
thunks.get(tag) ||
93+
thunks
94+
.set(tag, (...props) => {
95+
const tree = _app.proxyStateTree.getTrackStateTree()
96+
const propsObject = {}
97+
while (props.length) {
98+
const [key, value] = props.splice(0, 2)
99+
propsObject[key] =
100+
value && value.__rescope ? value.__rescope(tree) : value
101+
}
102+
let vnode
103+
let elm
104+
105+
function onFlush(_, __, flushId) {
106+
if (elm.__overmind.tree !== tree) {
107+
tree.dispose()
108+
return
109+
}
110+
111+
tree.track(update)
112+
113+
const newNode = tag(propsObject, {
114+
state: tree.state,
115+
actions: _app.actions,
116+
})
117+
tree.stopTracking()
118+
119+
patch(vnode, newNode)
120+
vnode = newNode
121+
122+
_app.eventHub.emitAsync(EventType.COMPONENT_UPDATE, {
123+
componentId: elm.__overmind.componentId,
124+
componentInstanceId: elm.__overmind.componentInstanceId,
125+
name,
126+
flushId,
127+
paths: Array.from(tree.pathDependencies) as any,
128+
})
129+
}
130+
131+
tree.track(onFlush)
132+
133+
vnode = tag(propsObject, {
134+
state: tree.state,
135+
actions: _app.actions,
136+
})
137+
138+
if (vnode.sel !== SELF) {
139+
throw new Error(
140+
`You are not returning a <self /> element from the component ${name} `
141+
)
142+
}
143+
144+
vnode.data.hook = vnode.data.hook || {}
145+
const insert = vnode.data.hook.insert
146+
const update = vnode.data.hook.update
147+
const destroy = vnode.data.hook.destroy
148+
149+
vnode.data.hook = {
150+
insert: (actualVnode) => {
151+
if (!(name in nextComponentId)) {
152+
nextComponentId[name] = nextId++
153+
}
154+
if (!(name in nextComponentInstanceId)) {
155+
nextComponentInstanceId[name] = 0
156+
}
157+
158+
nextComponentInstanceId[name] = nextId++
159+
160+
elm = actualVnode.elm
161+
vnode.elm = elm
162+
elm.__overmind = Object.assign(elm.__overmind || {}, {
163+
name,
164+
tree,
165+
componentId: nextComponentId,
166+
componentInstanceId: nextComponentInstanceId[name],
167+
})
168+
169+
insert && insert(elm)
170+
171+
_app.eventHub.emitAsync(EventType.COMPONENT_ADD, {
172+
componentId: elm.__overmind.componentId,
173+
componentInstanceId: elm.__overmind.componentInstanceId,
174+
name,
175+
paths: Array.from(tree.pathDependencies) as any,
176+
})
177+
},
178+
update: (_, vnode) => {
179+
elm = vnode.elm
180+
elm.__overmind.tree = tree
181+
update && update(vnode.elm)
182+
},
183+
destroy: (vnode) => {
184+
elm = vnode.elm
185+
tree.dispose()
186+
destroy && destroy(vnode.elm)
187+
_app.eventHub.emitAsync(EventType.COMPONENT_REMOVE, {
188+
componentId: elm.__overmind.componentId,
189+
componentInstanceId: elm.__overmind.componentInstanceId,
190+
name,
191+
})
192+
},
193+
}
194+
195+
tree.stopTracking()
196+
197+
return vnode
198+
})
199+
.get(tag),
200+
memo
201+
)
202+
}
203+
204+
return h(
205+
tag,
206+
normalizeAttrs(tag, attrs),
207+
children.reduce((aggr, item) => aggr.concat(item), [])
208+
)
209+
}
210+
211+
export const render = (app, vnode, container) => {
212+
_app = app
213+
patch(container, vnode())
214+
}

0 commit comments

Comments
 (0)