-
Notifications
You must be signed in to change notification settings - Fork 3
/
exitable.es6.js
163 lines (127 loc) · 4.76 KB
/
exitable.es6.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import mithril from 'mithril'
// Registry of controllers and corresponding root nodes
const roots = new Map()
// A record of recent view outputs for every root-level component
const history = new WeakMap()
// Whether the current draw is being used to revert to its previous state
let reverting = false
// Register views to bind their roots to the above
// so we can provide exit animations with the right DOM reference
const register = view =>
function registeredView( ctrl ){
const output = view( ...arguments )
// In case of a registered exit animation...
if( ctrl.exit ){
let node = output
// If the view output is an array, deal with the first element
while( node.length )
node = node[ 0 ]
const { config } = node.attrs
// Map the root / first child element to the component instance
node.attrs.config = function superConfig( el, init, ctxt, snapshot ){
roots.set( ctrl, el )
if( history.has( ctrl ) )
history.set( ctrl, snapshot )
if( ctrl.enter && !init )
ctrl.enter( el )
if( config )
return config.apply( this, arguments )
}
}
return output
}
// Root components (those mounted or routed) are the source of redraws.
// Before they draw, there is no stateful virtual DOM.
// Therefore their view execution is the source of all truth in what is currently rendered.
const root = ( { view, ...component } ) =>
Object.assign( component, {
view : function rootView( ctrl ){
// If we are in the middle of a reversion, we just want to patch
// Mithril's internal virtual DOM HEAD to what it was before the
// last output
if( reverting )
return history.get( ctrl )
// All previously registered exitable components are saved here
const previous = Array.from( roots )
// Then we reset
roots.clear()
// Execute the view, registering all exitables
let output = register( view ).apply( this, arguments )
// Record the output, we will need to return to this state if the next draw has exits
history.set( ctrl, output )
// Now, set up a list of confirmed exits
const exits = []
// For every previous exitable instance...
for( let [ ctrl, el ] of previous )
// ...if it hasn't re-registered...
if( !roots.has( ctrl ) )
// It's gone! Call the exit method and keep its output.
exits.push( ctrl.exit( el ) )
// If we have exits...
if( exits.length ){
// Noop this draw
output = { subtree : 'retain' }
// Freeze the draw process
mithril.startComputation()
// ...until all exits have resolved
Promise.all( exits ).then( () => {
// We now need to revert Mithril's internal virtual DOM head so that
// it will correctly patch the live DOM to match the state in which
// components are removed: it currently believes that already happend
// Because it ran the diff before we told it to retain the subtree at
// the last minute
reverting = true
// Next draw should not patch, only diff
mithril.redraw.strategy( 'none' )
// Force a synchronous draw despite being frozen
mithril.redraw( true )
// Now it's as if we were never here to begin with
reverting = false
// Resume business as usual
mithril.endComputation()
} )
}
return output
}
} )
// Helper: transform each value in an object. Even in ES7, this is painfully contrived.
const reduce = ( object, transformer ) =>
Object.keys( object ).reduce(
( output, key ) =>
Object.assign( output, {
[ key ] : transformer( object[ key ] )
} ),
{}
)
const hooks = {
// m.component invocations produce virtual DOM.
// We need to intercede to get at the view.
component : ( component, ...rest ) =>
mithril.component( Object.assign( component, {
view : register( component.view )
} ), ...rest ),
// Mount and Route need to register root components for snapshot logic
mount : ( el, component ) =>
mithril.mount( el, root( component ) ),
route : ( el, path, map ) => map
? mithril.route( el, path, reduce( map, root ) )
: mithril.route( ...arguments )
}
// The patched Mithril API
export default Object.assign(
function( first ){
if( first.view )
return hooks.component( ...arguments )
const output = mithril( ...arguments )
output.children.forEach( child => {
if( 'view' in child )
// ...and get their views to register controllers and root nodes
Object.assign( child, {
view : register( child.view )
} )
} )
return output
},
mithril,
hooks
)