@srfnstack/fntags 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/fntags",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "author": "Robert Kempton <r@snow87.com>",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/srfnstack/fntags",
@@ -52,8 +52,8 @@
52
52
  "scripts": {
53
53
  "test": "cp src/*.mjs docs/lib/ && npm run lint && cypress run --spec \"test/**\" --headless -b chrome",
54
54
  "cypress": "cypress run --headed --spec test/** -b chrome",
55
- "lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test docs",
56
- "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test docs",
55
+ "lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test",
56
+ "lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test",
57
57
  "typedef": "rm -rf src/*.mts* && tsc",
58
58
  "docs": "typedoc --plugin typedoc-plugin-markdown --out docs/types --json docs/types.json ./src/*.mjs && node scripts/generateApi.js",
59
59
  "generate-api": "node scripts/generateApi.js",
package/src/fntags.d.mts CHANGED
@@ -47,7 +47,6 @@ export function h<T extends HTMLElement | SVGElement>(tag: string, ...children:
47
47
  * will not be reflected correctly.
48
48
  * @property {(path: string, value: any, fillWithObjects: boolean)=>void} setPath Set a value at the given property path
49
49
  * @property {(subscriber: (newState: T, oldState: T)=>void) => void} subscribe Register a callback that will be executed whenever the state is changed
50
- * @property {(reinit: boolean)=>{}} reset Remove all of the observers and optionally reset the value to it's initial value
51
50
  * @property {boolean} isFnState A flag to indicate that this is a fnstate object
52
51
  */
53
52
  /**
@@ -96,6 +95,23 @@ export function getAttrs(children: any): object;
96
95
  * @return {T} The styled element
97
96
  */
98
97
  export function styled<T extends HTMLElement | SVGElement>(style: object | string, tag: string, children: object[] | Node[]): T;
98
+ /**
99
+ * Create a compiled template function. The returned function takes a single object that contains the properties
100
+ * defined in the template.
101
+ *
102
+ * This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
103
+ * the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
104
+ * one by one and can speed up situations where a similar element is created many times.
105
+ *
106
+ * You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
107
+ * not be updated when the state changes because they will not be bound to the cloned element.
108
+ * All state bindings must be passed in the context to the compiled template to work correctly.
109
+ *
110
+ * @param {(any)=>Node} templateFn A function that returns a html node.
111
+ * @return {(any)=>Node} A function that takes a context object and returns a rendered node.
112
+ *
113
+ */
114
+ export function fntemplate(templateFn: (any: any) => Node): (any: any) => Node;
99
115
  /**
100
116
  * A container for a state value that can be bound to.
101
117
  */
@@ -161,10 +177,6 @@ export type FnStateObj<T> = {
161
177
  * Register a callback that will be executed whenever the state is changed
162
178
  */
163
179
  subscribe: (subscriber: (newState: T, oldState: T) => void) => void;
164
- /**
165
- * Remove all of the observers and optionally reset the value to it's initial value
166
- */
167
- reset: (reinit: boolean) => {};
168
180
  /**
169
181
  * A flag to indicate that this is a fnstate object
170
182
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OA6C1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cA+KlB;AAySD;;;;GAIG;AACH,iCAHW,GAAG,GACD,IAAI,CAuBhB;AAqFD;;;;GAIG;AACH,6BAHW,GAAG,GACD,OAAO,CAInB;AAED;;;;GAIG;AACH,mCAHW,GAAG,GACF,MAAM,CAIjB;AAED;;;;;;;;;;GAUG;AACH,kEALW,MAAM,GAAC,MAAM,OACb,MAAM,YACN,MAAM,EAAE,GAAC,IAAI,EAAE,KAezB;;;;;;;;;kCAnpBmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAE/C,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;sCAEI,CAAC,YAAY,CAAC,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;mCACvC,CAAC,YAAY,CAAC,KAAG,MAAM,KAAK,MAAM;;;;yCAC7B,GAAG,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;+CACrB,GAAG,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7C,GAAG,KAAG,IAAI;;;;cAGhB,MAAK,GAAG;;;;;qBACC,CAAC,KAAG,IAAI;;;;;;oBAEV,MAAM,KAAG,GAAG;;;;oBAGZ,MAAM,SAAS,GAAG,mBAAmB,OAAO,KAAG,IAAI;;;;uCAClC,CAAC,YAAY,CAAC,KAAG,IAAI,KAAK,IAAI;;;;oBAC7C,OAAO,KAAG,EAAE;;;;eACrB,OAAO;;;;;sDAKqB,CAAC,KAAG,CAAC"}
1
+ {"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OA6C1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cAoIlB;AAwWD;;;;GAIG;AACH,iCAHW,GAAG,GACD,IAAI,CAuBhB;AAqFD;;;;GAIG;AACH,6BAHW,GAAG,GACD,OAAO,CAInB;AAED;;;;GAIG;AACH,mCAHW,GAAG,GACF,MAAM,CAIjB;AAED;;;;;;;;;;GAUG;AACH,kEALW,MAAM,GAAC,MAAM,OACb,MAAM,YACN,MAAM,EAAE,GAAC,IAAI,EAAE,KAezB;AAOD;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;kCAzvBmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAE/C,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;sCAEI,CAAC,YAAY,CAAC,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;mCACvC,CAAC,YAAY,CAAC,KAAG,MAAM,KAAK,MAAM;;;;yCAC7B,GAAG,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;+CACrB,GAAG,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7C,GAAG,KAAG,IAAI;;;;cAGhB,MAAK,GAAG;;;;;qBACC,CAAC,KAAG,IAAI;;;;;;oBAEV,MAAM,KAAG,GAAG;;;;oBAGZ,MAAM,SAAS,GAAG,mBAAmB,OAAO,KAAG,IAAI;;;;uCAClC,CAAC,YAAY,CAAC,KAAG,IAAI,KAAK,IAAI;;;;eACtD,OAAO;;;;;sDAKqB,CAAC,KAAG,CAAC"}
package/src/fntags.mjs CHANGED
@@ -97,7 +97,6 @@ function hasNs (val) {
97
97
  * will not be reflected correctly.
98
98
  * @property {(path: string, value: any, fillWithObjects: boolean)=>void} setPath Set a value at the given property path
99
99
  * @property {(subscriber: (newState: T, oldState: T)=>void) => void} subscribe Register a callback that will be executed whenever the state is changed
100
- * @property {(reinit: boolean)=>{}} reset Remove all of the observers and optionally reset the value to it's initial value
101
100
  * @property {boolean} isFnState A flag to indicate that this is a fnstate object
102
101
  */
103
102
 
@@ -138,6 +137,8 @@ export function fnstate (initialValue, mapKey) {
138
137
  return newState
139
138
  }
140
139
  }
140
+ // make context available to static functions to avoid declaring functions on every new state
141
+ ctx.state._ctx = ctx
141
142
 
142
143
  /**
143
144
  * Bind this state to the given element
@@ -145,7 +146,7 @@ export function fnstate (initialValue, mapKey) {
145
146
  * @param {((T)=>(Node|any))?} [element] The element to bind to. If not a function, an update function must be passed. If not passed, defaults to the state's value
146
147
  * @returns {()=>Node}
147
148
  */
148
- ctx.state.bindAs = (element) => doBindAs(ctx, element ?? ctx.state)
149
+ ctx.state.bindAs = doBindAs
149
150
 
150
151
  /**
151
152
  * Bind the values of this state to the given element.
@@ -156,7 +157,7 @@ export function fnstate (initialValue, mapKey) {
156
157
  * @param {(childState: FnState)=>(Node|any)} element A function that receives each element wrapped as a fnstate and produces an element
157
158
  * @returns {Node}
158
159
  */
159
- ctx.state.bindChildren = (parent, element) => doBindChildren(ctx, parent, element)
160
+ ctx.state.bindChildren = doBindChildren
160
161
 
161
162
  /**
162
163
  * Bind to a property of an object stored in this state instead of the state itself.
@@ -166,46 +167,46 @@ export function fnstate (initialValue, mapKey) {
166
167
  * @param {string} prop The object property to bind as
167
168
  * @returns {()=>Node}
168
169
  */
169
- ctx.state.bindProp = (prop) => doBindAs(ctx, (st) => st[prop])
170
+ ctx.state.bindProp = doBindProp
170
171
 
171
172
  /**
172
173
  * Bind attribute values to state changes
173
174
  * @param {(()=>(string|any))?} [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
174
175
  * @returns {()=>(string|any)} A function that calls the passed function, with some extra metadata
175
176
  */
176
- ctx.state.bindAttr = (attribute) => doBindAttr(ctx.state, attribute ?? ctx.state)
177
+ ctx.state.bindAttr = doBindAttr
177
178
 
178
179
  /**
179
180
  * Bind style values to state changes
180
181
  * @param {(()=>string)?} [style] A function that returns a style's value. If not passed, defaults to the state's value
181
182
  * @returns {()=>Node} A function that calls the passed function, with some extra metadata
182
183
  */
183
- ctx.state.bindStyle = (style) => doBindStyle(ctx.state, style ?? ctx.state)
184
+ ctx.state.bindStyle = doBindStyle
184
185
 
185
186
  /**
186
187
  * Bind select and deselect to an element
187
188
  * @param {(()=>(Node|any))?} [element] The element to bind to. If not passed, defaults to the state's value
188
189
  * @returns {()=>Node}
189
190
  */
190
- ctx.state.bindSelect = (element) => doBindSelect(ctx, element ?? ctx.state)
191
+ ctx.state.bindSelect = doBindSelect
191
192
 
192
193
  /**
193
194
  * Bind select and deselect to an attribute
194
195
  * @param {(()=>(string|any))?} [attribute] A function that returns an attribute value. If not passed, defaults to the state's value
195
196
  * @returns {()=>(string|any)} A function that calls the passed function, with some extra metadata
196
197
  */
197
- ctx.state.bindSelectAttr = (attribute) => doBindSelectAttr(ctx, attribute ?? ctx.state)
198
+ ctx.state.bindSelectAttr = doBindSelectAttr
198
199
 
199
200
  /**
200
201
  * Mark the element with the given key as selected. This causes the bound select functions to be executed.
201
202
  */
202
- ctx.state.select = (key) => doSelect(ctx, key)
203
+ ctx.state.select = doSelect
203
204
 
204
205
  /**
205
206
  * Get the currently selected key
206
207
  * @returns {any}
207
208
  */
208
- ctx.state.selected = () => ctx.selected
209
+ ctx.state.selected = doSelected
209
210
 
210
211
  ctx.state.isFnState = true
211
212
 
@@ -213,7 +214,7 @@ export function fnstate (initialValue, mapKey) {
213
214
  * Perform an Object.assign() on the current state using the provided update
214
215
  * @param {T} [update]
215
216
  */
216
- ctx.state.assign = (update) => ctx.state(Object.assign(ctx.currentValue, update))
217
+ ctx.state.assign = doAssign
217
218
 
218
219
  /**
219
220
  * Get a value at the given property path, an error is thrown if the value is not an object
@@ -222,26 +223,7 @@ export function fnstate (initialValue, mapKey) {
222
223
  * will not be reflected correctly.
223
224
  * @param {string} [path] a json path type path that points to a property
224
225
  */
225
- ctx.state.getPath = (path) => {
226
- if (typeof path !== 'string') {
227
- throw new Error('Invalid path')
228
- }
229
- if (typeof ctx.currentValue !== 'object') {
230
- throw new Error('Value is not an object')
231
- }
232
- return path
233
- .split('.')
234
- .reduce(
235
- (curr, part) => {
236
- if (part in curr) {
237
- return curr[part]
238
- } else {
239
- return undefined
240
- }
241
- },
242
- ctx.currentValue
243
- )
244
- }
226
+ ctx.state.getPath = doGetPath
245
227
 
246
228
  /**
247
229
  * Set a value at the given property path
@@ -249,50 +231,24 @@ export function fnstate (initialValue, mapKey) {
249
231
  * @param {any} value The value to set the path to
250
232
  * @param {boolean} fillWithObjects Whether to replace non object values with new empty objects.
251
233
  */
252
- ctx.state.setPath = (path, value, fillWithObjects = false) => {
253
- const s = path.split('.')
254
- const parent = s
255
- .slice(0, -1)
256
- .reduce(
257
- (current, part) => {
258
- if (fillWithObjects && typeof current[part] !== 'object') {
259
- current[part] = {}
260
- }
261
- return current[part]
262
- },
263
- ctx.currentValue
264
- )
265
-
266
- if (parent && typeof parent === 'object') {
267
- parent[s.slice(-1)] = value
268
- ctx.state(ctx.currentValue)
269
- } else {
270
- throw new Error(`No object at path ${path}`)
271
- }
272
- }
234
+ ctx.state.setPath = doSetPath
273
235
 
274
236
  /**
275
237
  * Register a callback that will be executed whenever the state is changed
276
238
  * @param {(newValue:T,oldValue:T)=>void} callback
277
239
  * @return {()=>void} a function to stop the subscription
278
240
  */
279
- ctx.state.subscribe = (callback) => doSubscribe(ctx, ctx.observers, callback)
280
-
281
- /**
282
- * Remove all the observers and optionally reset the value to it's initial value
283
- * @param {boolean} reInit whether to reset the state to it's initial value
284
- */
285
- ctx.state.reset = (reInit) => doReset(ctx, reInit, initialValue)
241
+ ctx.state.subscribe = doSubscribe
286
242
 
287
243
  return ctx.state
288
244
  }
289
245
 
290
- function doSubscribe (ctx, list, listener) {
246
+ function doSubscribe (callback) {
247
+ const ctx = this._ctx
291
248
  const id = ctx.nextId++
292
- list.push({ id, fn: listener })
249
+ ctx.observers.push({ id, fn: callback })
293
250
  return () => {
294
- list.splice(list.findIndex(l => l.id === id), 1)
295
- list = null
251
+ ctx.observers.splice(ctx.observers.findIndex(l => l.id === id), 1)
296
252
  }
297
253
  }
298
254
 
@@ -305,7 +261,9 @@ const subscribeSelect = (ctx, callback) => {
305
261
  parentCtx.selectObservers[key].push(callback)
306
262
  }
307
263
 
308
- const doBindSelectAttr = function (ctx, attribute) {
264
+ function doBindSelectAttr (attribute) {
265
+ attribute = attribute ?? this
266
+ const ctx = this._ctx
309
267
  const attrFn = (attribute && !attribute.isFnState && typeof attribute === 'function')
310
268
  ? (...args) => attribute(args.length > 0 ? args[0] : ctx.selected)
311
269
  : attribute
@@ -325,37 +283,32 @@ function createBoundAttr (attr) {
325
283
  return boundAttr
326
284
  }
327
285
 
328
- function doBindAttr (state, attribute) {
286
+ function doBindAttr (attribute) {
287
+ attribute = attribute ?? this
329
288
  const boundAttr = createBoundAttr(attribute)
330
289
  boundAttr.init = (attrName, element) => {
331
- setAttribute(attrName, attribute.isFnState ? attribute() : attribute(state()), element)
332
- state.subscribe((newState, oldState) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(newState, oldState), element))
290
+ setAttribute(attrName, attribute.isFnState ? attribute() : attribute(this()), element)
291
+ this.subscribe((newState, oldState) => setAttribute(attrName, attribute.isFnState ? attribute() : attribute(newState, oldState), element))
333
292
  }
334
293
  return boundAttr
335
294
  }
336
295
 
337
- function doBindStyle (state, style) {
296
+ function doBindStyle (style) {
297
+ style = style ?? this
338
298
  if (typeof style !== 'function') {
339
299
  throw new Error('You must pass a function to bindStyle')
340
300
  }
341
301
  const boundStyle = () => style()
342
302
  boundStyle.isBoundStyle = true
343
303
  boundStyle.init = (styleName, element) => {
344
- element.style[styleName] = style.isFnState ? style() : style(state())
345
- state.subscribe((newState, oldState) => { element.style[styleName] = style.isFnState ? style() : style(newState, oldState) })
304
+ element.style[styleName] = style.isFnState ? style() : style(this())
305
+ this.subscribe((newState, oldState) => { element.style[styleName] = style.isFnState ? style() : style(newState, oldState) })
346
306
  }
347
307
  return boundStyle
348
308
  }
349
309
 
350
- function doReset (ctx, reInit, initialValue) {
351
- ctx.observers = []
352
- ctx.selectObservers = {}
353
- if (reInit) {
354
- ctx.currentValue = initialValue
355
- }
356
- }
357
-
358
- function doSelect (ctx, key) {
310
+ function doSelect (key) {
311
+ const ctx = this._ctx
359
312
  const currentSelected = ctx.selected
360
313
  ctx.selected = key
361
314
  if (ctx.selectObservers[currentSelected] !== undefined) {
@@ -366,7 +319,65 @@ function doSelect (ctx, key) {
366
319
  }
367
320
  }
368
321
 
369
- function doBindChildren (ctx, parent, element) {
322
+ function doSelected () {
323
+ return this._ctx.selected
324
+ }
325
+
326
+ function doAssign (update) {
327
+ return this(Object.assign(this._ctx.currentValue, update))
328
+ }
329
+
330
+ function doGetPath (path) {
331
+ const ctx = this._ctx
332
+ if (typeof path !== 'string') {
333
+ throw new Error('Invalid path')
334
+ }
335
+ if (typeof ctx.currentValue !== 'object') {
336
+ throw new Error('Value is not an object')
337
+ }
338
+ return path
339
+ .split('.')
340
+ .reduce(
341
+ (curr, part) => {
342
+ if (part in curr) {
343
+ return curr[part]
344
+ } else {
345
+ return undefined
346
+ }
347
+ },
348
+ ctx.currentValue
349
+ )
350
+ }
351
+
352
+ function doSetPath (path, value, fillWithObjects = false) {
353
+ const ctx = this._ctx
354
+ const s = path.split('.')
355
+ const parent = s
356
+ .slice(0, -1)
357
+ .reduce(
358
+ (current, part) => {
359
+ if (fillWithObjects && typeof current[part] !== 'object') {
360
+ current[part] = {}
361
+ }
362
+ return current[part]
363
+ },
364
+ ctx.currentValue
365
+ )
366
+
367
+ if (parent && typeof parent === 'object') {
368
+ parent[s.slice(-1)] = value
369
+ this(ctx.currentValue)
370
+ } else {
371
+ throw new Error(`No object at path ${path}`)
372
+ }
373
+ }
374
+
375
+ function doBindProp (prop) {
376
+ return this.bindAs((st) => st[prop])
377
+ }
378
+
379
+ function doBindChildren (parent, element) {
380
+ const ctx = this._ctx
370
381
  parent = renderNode(parent)
371
382
  if (parent === undefined || parent.nodeType === undefined) {
372
383
  throw new Error('You must provide a parent element to bind the children to. aka Need Bukkit.')
@@ -384,11 +395,11 @@ function doBindChildren (ctx, parent, element) {
384
395
  }
385
396
  ctx.currentValue = ctx.currentValue.map(v => v.isFnState ? v : fnstate(v))
386
397
  ctx.bindContexts.push({ element, parent })
387
- ctx.state.subscribe((_, oldState) => {
398
+ this.subscribe((_, oldState) => {
388
399
  if (!Array.isArray(ctx.currentValue)) {
389
400
  console.warn('A state used with bindChildren was updated to a non array value. This will be converted to an array of 1 and the state will be updated.')
390
401
  new Promise((resolve) => {
391
- ctx.state([ctx.currentValue])
402
+ this([ctx.currentValue])
392
403
  resolve()
393
404
  }).catch(e => {
394
405
  console.error('Failed to update element: ')
@@ -441,11 +452,17 @@ const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
441
452
  }
442
453
  }
443
454
 
444
- const doBindSelect = (ctx, element) =>
445
- doBind(ctx, element, (elCtx) => subscribeSelect(ctx, updateReplacer(ctx, element, elCtx)))
455
+ function doBindSelect (element) {
456
+ element = element ?? this
457
+ const ctx = this._ctx
458
+ return doBind(ctx, element, (elCtx) => subscribeSelect(ctx, updateReplacer(ctx, element, elCtx)))
459
+ }
446
460
 
447
- const doBindAs = (ctx, element) =>
448
- doBind(ctx, element, (elCtx) => ctx.state.subscribe(updateReplacer(ctx, element, elCtx)))
461
+ function doBindAs (element) {
462
+ const ctx = this._ctx
463
+ const el = element ?? this
464
+ return doBind(ctx, el, (elCtx) => this.subscribe(updateReplacer(ctx, el, elCtx)))
465
+ }
449
466
 
450
467
  /**
451
468
  * Reconcile the state of the current array value with the state of the bound elements
@@ -479,16 +496,18 @@ function arrangeElements (ctx, bindContext, oldState) {
479
496
 
480
497
  const keys = {}
481
498
  const keysArr = []
482
- const oldStateMap = oldState && oldState.reduce((acc, v) => {
483
- const key = keyMapper(ctx.mapKey, v.isFnState ? v() : v)
484
- acc[key] = v
485
- return acc
486
- }, {})
487
-
499
+ let oldStateMap = null
488
500
  for (const i in ctx.currentValue) {
489
501
  let valueState = ctx.currentValue[i]
490
502
  // if the value is not a fnstate, we need to wrap it
491
503
  if (valueState === null || valueState === undefined || !valueState.isFnState) {
504
+ if (oldStateMap === null) {
505
+ oldStateMap = oldState && oldState.reduce((acc, v) => {
506
+ const key = keyMapper(ctx.mapKey, v.isFnState ? v() : v)
507
+ acc[key] = v
508
+ return acc
509
+ }, {})
510
+ }
492
511
  // check if we have an old state for this key
493
512
  const key = keyMapper(ctx.mapKey, valueState)
494
513
  if (oldStateMap && oldStateMap[key]) {
@@ -740,3 +759,81 @@ const stringifyStyle = style =>
740
759
  typeof style === 'string'
741
760
  ? style
742
761
  : Object.keys(style).map(prop => `${prop}:${style[prop]}`).join(';')
762
+
763
+ /**
764
+ * Create a compiled template function. The returned function takes a single object that contains the properties
765
+ * defined in the template.
766
+ *
767
+ * This allows fast rendering by pre-creating a dom element with the entire template structure then cloning and populating
768
+ * the clone with data from the provided context. This avoids the work of having to re-execute the tag functions
769
+ * one by one and can speed up situations where a similar element is created many times.
770
+ *
771
+ * You cannot bind state to the initial template. If you attempt to, the state will be read, but the elements will
772
+ * not be updated when the state changes because they will not be bound to the cloned element.
773
+ * All state bindings must be passed in the context to the compiled template to work correctly.
774
+ *
775
+ * @param {(any)=>Node} templateFn A function that returns a html node.
776
+ * @return {(any)=>Node} A function that takes a context object and returns a rendered node.
777
+ *
778
+ */
779
+ export function fntemplate (templateFn) {
780
+ if (typeof templateFn !== 'function') {
781
+ throw new Error('You must pass a function to fntemplate.')
782
+ }
783
+
784
+ const bindingsByPath = []
785
+
786
+ const initContext = prop => {
787
+ const placeholder = (element, type, attr) => {
788
+ if (!element._tpl_bind) element._tpl_bind = []
789
+ element._tpl_bind.push({ prop, type, attr })
790
+ }
791
+ placeholder.isTemplatePlaceholder = true
792
+ return placeholder
793
+ }
794
+
795
+ const root = templateFn(initContext)
796
+
797
+ const traverse = (node, path) => {
798
+ if (node._tpl_bind) {
799
+ bindingsByPath.push({ path: [...path], binds: node._tpl_bind })
800
+ delete node._tpl_bind
801
+ }
802
+
803
+ let child = node.firstChild
804
+ let i = 0
805
+ while (child) {
806
+ traverse(child, [...path, i])
807
+ child = child.nextSibling
808
+ i++
809
+ }
810
+ }
811
+
812
+ traverse(root, [])
813
+
814
+ return (context) => {
815
+ const clone = root.cloneNode(true)
816
+ for (let i = 0; i < bindingsByPath.length; i++) {
817
+ const entry = bindingsByPath[i]
818
+ let target = clone
819
+ const path = entry.path
820
+ for (let j = 0; j < path.length; j++) {
821
+ target = target.childNodes[path[j]]
822
+ }
823
+
824
+ const binds = entry.binds
825
+ for (let j = 0; j < binds.length; j++) {
826
+ const b = binds[j]
827
+ const val = context[b.prop]
828
+ if (b.type === 'node') {
829
+ target.replaceWith(renderNode(val))
830
+ } else if (b.type === 'attr') {
831
+ setAttribute(b.attr, val, target)
832
+ } else if (b.type === 'style') {
833
+ setStyle(b.attr, val, target)
834
+ }
835
+ }
836
+ }
837
+ return clone
838
+ }
839
+ }