leaner 0.0.1 → 0.1.1

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/dist/web.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useReactive, useReactiveWatch, useConstant } from 'leaner';
1
+ import { withScope, destroyScope, reactive, constant } from 'leaner';
2
2
 
3
3
  function appendToArray( item, target ) {
4
4
  if ( Array.isArray( item ) )
@@ -40,8 +40,10 @@ function createContext( parent ) {
40
40
  return {
41
41
  parent,
42
42
  children: null,
43
+ scope: [],
43
44
  mount: null,
44
45
  destroy: null,
46
+ services: null,
45
47
  };
46
48
  }
47
49
 
@@ -61,7 +63,7 @@ function withContext( context, callback ) {
61
63
  const last = current;
62
64
  current = context;
63
65
  try {
64
- return callback();
66
+ return withScope( current.scope, callback );
65
67
  } finally {
66
68
  current = last;
67
69
  }
@@ -70,7 +72,7 @@ function withContext( context, callback ) {
70
72
  function withChildContext( callback ) {
71
73
  current = createChildContext();
72
74
  try {
73
- return callback();
75
+ return withScope( current.scope, callback );
74
76
  } finally {
75
77
  current = current.parent;
76
78
  }
@@ -108,20 +110,43 @@ function destroyContext( context ) {
108
110
  if ( context.destroy != null ) {
109
111
  for ( const callback of context.destroy )
110
112
  callback();
113
+ context.destroy = null;
114
+ }
115
+
116
+ context.mounted = null;
117
+
118
+ if ( context.scope.length > 0 ) {
119
+ destroyScope( context.scope );
120
+ context.scope = [];
111
121
  }
112
122
 
113
123
  if ( context.children != null ) {
114
124
  for ( const child of context.children )
115
125
  destroyContext( child );
116
-
117
126
  context.children = null;
118
127
  }
119
128
  }
120
129
 
130
+ function provide( key, value ) {
131
+ if ( current == null )
132
+ throw new Error( 'provide() cannot be called outside of a component' );
133
+
134
+ if ( current.services == null )
135
+ current.services = new Map();
136
+
137
+ current.services.set( key, value );
138
+ }
139
+
140
+ function inject( key ) {
141
+ for ( let context = current; context != null; context = context.parent ) {
142
+ if ( context.services != null && context.services.has( key ) )
143
+ return context.services.get( key );
144
+ }
145
+ }
146
+
121
147
  function setClasses( element, classes ) {
122
148
  if ( typeof classes == 'function' ) {
123
- useReactive( () => {
124
- const value = classes();
149
+ reactive( classes, value => {
125
150
  if ( Array.isArray( value ) ) {
126
151
  element.className = '';
127
152
  element.classList.add( ...value );
@@ -141,11 +166,14 @@ function setClasses( element, classes ) {
141
166
  function setClassesObject( element, classes ) {
142
167
  for ( const [ key, value ] of Object.entries( classes ) ) {
143
168
  if ( typeof value == 'function' ) {
144
- useReactive( () => {
145
- if ( value() )
146
- element.classList.add( key );
147
- else
148
- element.classList.remove( key );
169
+ reactive( value, ( newValue, value ) => {
170
+ if ( newValue ) {
171
+ if ( !value )
172
+ element.classList.add( key );
173
+ } else {
174
+ if ( value )
175
+ element.classList.remove( key );
176
+ }
149
177
  } );
150
178
  } else if ( value ) {
151
179
  element.classList.add( key );
@@ -156,18 +184,16 @@ function setClassesObject( element, classes ) {
156
184
  function setClassesArray( element, classes ) {
157
185
  for ( const item of classes ) {
158
186
  if ( typeof item == 'function' ) {
159
- let value = '';
160
- useReactive( () => {
161
- const newValue = item();
162
- if ( newValue === value )
163
- return;
164
- if ( value === '' )
165
- element.classList.add( newValue );
166
- else if ( newValue === '' )
167
- element.classList.remove( value );
168
- else
169
- element.classList.replace( value, newValue );
170
- value = newValue;
187
+ reactive( item, ( newValue, value ) => {
188
+ if ( newValue ) {
189
+ if ( value )
190
+ element.classList.replace( value, newValue );
191
+ else
192
+ element.classList.add( newValue );
193
+ } else {
194
+ if ( value )
195
+ element.classList.remove( value );
196
+ }
171
197
  } );
172
198
  } else if ( isPlainObject( item ) ) {
173
199
  setClassesObject( element, item );
@@ -180,38 +206,45 @@ function setClassesArray( element, classes ) {
180
206
  class DynamicNode {
181
207
  constructor( content ) {
182
208
  this.content = content;
209
+ this.parentNode = null;
183
210
  }
184
211
  }
185
212
 
186
- function appendNode( node, target ) {
213
+ function appendNode( node, parent ) {
187
214
  if ( Array.isArray( node ) ) {
188
215
  if ( node.some( item => item instanceof DynamicNode ) ) {
189
216
  for ( const item of node ) {
190
- if ( item instanceof DynamicNode )
191
- appendNode( item.content, target );
192
- else
193
- target.append( item );
217
+ if ( item instanceof DynamicNode ) {
218
+ item.parentNode = parent;
219
+ appendNode( item.content, parent );
220
+ } else {
221
+ parent.append( item );
222
+ }
194
223
  }
195
224
  } else {
196
- target.append( ...node );
225
+ parent.append( ...node );
197
226
  }
198
227
  } else if ( node instanceof DynamicNode ) {
199
- appendNode( node.content, target );
228
+ node.parentNode = parent;
229
+ appendNode( node.content, parent );
200
230
  } else {
201
- target.append( node );
231
+ parent.append( node );
202
232
  }
203
233
  }
204
234
 
205
235
  function insertBefore( node, parent, before ) {
206
236
  if ( Array.isArray( node ) ) {
207
237
  for ( const item of node ) {
208
- if ( item instanceof DynamicNode )
238
+ if ( item instanceof DynamicNode ) {
239
+ item.parentNode = parent;
209
240
  insertBefore( item.content, parent, before );
210
- else
241
+ } else {
211
242
  parent.insertBefore( item, before );
243
+ }
212
244
  }
213
245
  } else if ( node instanceof DynamicNode ) {
214
- insertBefore( node.content, before );
246
+ node.parentNode = parent;
247
+ insertBefore( node.content, parent, before );
215
248
  } else {
216
249
  parent.insertBefore( node, before );
217
250
  }
@@ -219,40 +252,133 @@ function insertBefore( node, parent, before ) {
219
252
 
220
253
  function removeNode( node ) {
221
254
  if ( Array.isArray( node ) ) {
222
- let parent = null, before = null;
223
255
  for ( const item of node ) {
224
256
  if ( item instanceof DynamicNode ) {
225
- [ parent, before ] = removeNode( item.content );
257
+ removeNode( item.content );
258
+ item.parentNode = null;
226
259
  } else {
227
- parent = item.parentNode;
228
- before = item.nextSibling;
229
- parent.removeChild( item );
260
+ item.parentNode.removeChild( item );
230
261
  }
231
262
  }
232
- return [ parent, before ];
263
+ } else if ( node instanceof DynamicNode ) {
264
+ removeNode( node.content );
265
+ node.parentNode = null;
266
+ } else {
267
+ node.parentNode.removeChild( node );
233
268
  }
234
-
235
- if ( node instanceof DynamicNode )
236
- return removeNode( node.content );
237
-
238
- const parent = node.parentNode;
239
- const before = node.nextSibling;
240
-
241
- parent.removeChild( node );
242
-
243
- return [ parent, before ];
244
269
  }
245
270
 
246
271
  function replaceNode( newNode, node ) {
247
272
  if ( newNode instanceof Node && node instanceof Node ) {
248
273
  node.parentNode.replaceChild( newNode, node );
249
- } else {
250
- const [ parent, before ] = removeNode( node );
274
+ } else if ( Array.isArray( newNode ) && Array.isArray( node ) ) {
275
+ let newStart = 0, start = 0;
276
+ let newEnd = newNode.length, end = node.length;
277
+ let parent = null, before = null;
278
+
279
+ while ( newStart < newEnd || start < end ) {
280
+ // skip equal nodes at the start
281
+ if ( newNode[ newStart ] == node[ start ] ) {
282
+ newStart++;
283
+ start++;
284
+ before = null;
285
+ continue;
286
+ }
287
+
288
+ // skip equal nodes at the end
289
+ while ( newNode[ newEnd - 1 ] == node[ end - 1 ] ) {
290
+ newEnd--;
291
+ end--;
292
+ }
293
+
294
+ // insert new node
295
+ if ( newStart < newEnd && newNode[ newStart ].parentNode == null ) {
296
+ if ( before == null ) {
297
+ if ( start < end ) {
298
+ before = node[ start ];
299
+ if ( before instanceof DynamicNode )
300
+ before = findFirstChild( before.content );
301
+ parent = before.parentNode;
302
+ } else {
303
+ let after = newNode[ newStart - 1 ];
304
+ if ( after instanceof DynamicNode )
305
+ after = findLastChild( after.content );
306
+ before = after.nextSibling;
307
+ parent = after.parentNode;
308
+ }
309
+ }
310
+ insertBefore( newNode[ newStart ], parent, before );
311
+ newStart++;
312
+ continue;
313
+ }
314
+
315
+ // swap nodes
316
+ if ( newNode[ newStart ] == node[ end - 1 ] && newNode[ newEnd - 1 ] == node[ start ] ) {
317
+ let after, before2;
318
+ if ( end - start > 2 ) {
319
+ after = node[ end - 1 ];
320
+ if ( after instanceof DynamicNode )
321
+ after = findLastChild( after.content );
322
+ before2 = after.nextSibling;
323
+ }
324
+
325
+ if ( before == null ) {
326
+ before = node[ start ];
327
+ if ( before instanceof DynamicNode )
328
+ before = findFirstChild( before.content );
329
+ parent = before.parentNode;
330
+ }
331
+ insertBefore( node[ end - 1 ], parent, before );
332
+
333
+ if ( after != null )
334
+ insertBefore( node[ start ], parent, before2 );
335
+
336
+ start++;
337
+ newStart++;
338
+ end--;
339
+ newEnd--;
251
340
 
341
+ continue;
342
+ }
343
+
344
+ // remove node
345
+ removeNode( node[ start ] );
346
+ start++;
347
+ before = null;
348
+ }
349
+ } else {
350
+ const after = findLastChild( node );
351
+ const before = after.nextSibling;
352
+ const parent = after.parentNode;
353
+ removeNode( node );
252
354
  insertBefore( newNode, parent, before );
253
355
  }
254
356
  }
255
357
 
358
+ function findFirstChild( node ) {
359
+ if ( Array.isArray( node ) ) {
360
+ const item = node[ 0 ];
361
+ if ( item instanceof DynamicNode )
362
+ return findFirstChild( item.content );
363
+ return item;
364
+ }
365
+ if ( node instanceof DynamicNode )
366
+ return findFirstChild( node.content );
367
+ return node;
368
+ }
369
+
370
+ function findLastChild( node ) {
371
+ if ( Array.isArray( node ) ) {
372
+ const item = node[ node.length - 1 ];
373
+ if ( item instanceof DynamicNode )
374
+ return findLastChild( item.content );
375
+ return item;
376
+ }
377
+ if ( node instanceof DynamicNode )
378
+ return findLastChild( node.content );
379
+ return node;
380
+ }
381
+
256
382
  function ensureNotEmpty( node ) {
257
383
  if ( isEmpty( node ) ) {
258
384
  const comment = document.createComment( '' );
@@ -305,7 +431,7 @@ function createDynamicDirective( template ) {
305
431
 
306
432
  const context = createChildContext();
307
433
 
308
- useReactiveWatch( type, update );
434
+ reactive( type, update );
309
435
 
310
436
  return result;
311
437
 
@@ -318,6 +444,8 @@ function createForDirective( template ) {
318
444
  if ( template.length != 3 || typeof template[ 1 ] != 'function' || typeof template[ 2 ] != 'function' )
319
445
  throw new TypeError( 'Invalid for template' );
320
446
 
447
+ const itemsGetter = template[ 1 ];
448
+
321
449
  let length = 0;
322
450
  let items = null;
323
451
 
@@ -327,25 +455,16 @@ function createForDirective( template ) {
327
455
 
328
456
  const context = createChildContext();
329
457
 
330
- useReactiveWatch( watchLength, updateLength );
458
+ withScope( context.scope, () => {
459
+ // initially only watch the length of the items array
460
+ reactive( itemsGetter.length, updateLength );
461
+ } );
331
462
 
332
463
  return result;
333
464
 
334
- function watchLength() {
335
- if ( !watchingContent )
336
- return template[ 1 ].length();
337
- }
338
-
339
465
  function updateLength( newLength ) {
340
- if ( !watchingContent ) {
341
- update( template[ 1 ]() );
342
- length = newLength;
343
- }
344
- }
345
-
346
- function watchContent() {
347
- // clone the array to watch all elements
348
- return [ ...template[ 1 ]() ];
466
+ update( itemsGetter() );
467
+ length = newLength;
349
468
  }
350
469
 
351
470
  function updateContent( newItems ) {
@@ -364,6 +483,13 @@ function createForDirective( template ) {
364
483
 
365
484
  let containsMatchedByValue = false;
366
485
 
486
+ let oldIndexMap = null;
487
+ if ( watchingContent && items.length != 0 && newItems.length != 0 ) {
488
+ oldIndexMap = new Map();
489
+ for ( let i = 0; i < items.length; i++ )
490
+ oldIndexMap.set( items[ i ], i );
491
+ }
492
+
367
493
  for ( let index = 0; index < newItems.length; index++ ) {
368
494
  const value = newItems[ index ];
369
495
 
@@ -372,20 +498,20 @@ function createForDirective( template ) {
372
498
  if ( isMatchedByValue )
373
499
  containsMatchedByValue = true;
374
500
 
375
- let oldIndex = -1;
501
+ let oldIndex = null;
376
502
 
377
503
  // reuse existing items - objects and arrays are matched by value, other items by index
378
504
  if ( result.content != null ) {
379
505
  if ( isMatchedByValue ) {
380
- if ( items != null )
381
- oldIndex = items.indexOf( value );
506
+ if ( oldIndexMap != null )
507
+ oldIndex = oldIndexMap.get( value );
382
508
  } else if ( index < length ) {
383
509
  if ( items == null || !isPlainObjectOrArray( items[ index ] ) )
384
510
  oldIndex = index;
385
511
  }
386
512
  }
387
513
 
388
- if ( oldIndex >= 0 ) {
514
+ if ( oldIndex != null ) {
389
515
  nodes.push( result.content[ oldIndex ] );
390
516
 
391
517
  const itemContext = context.children[ oldIndex ];
@@ -401,7 +527,7 @@ function createForDirective( template ) {
401
527
  newItemContexts = [];
402
528
  newItemContexts.push( itemContext );
403
529
 
404
- const item = isMatchedByValue ? useConstant( value ) : template[ 1 ][ index ];
530
+ const item = isMatchedByValue ? constant( value ) : itemsGetter[ index ];
405
531
 
406
532
  const itemTemplate = template[ 2 ]( item, index );
407
533
 
@@ -437,7 +563,12 @@ function createForDirective( template ) {
437
563
  context.children = null;
438
564
 
439
565
  if ( containsMatchedByValue && !watchingContent ) {
440
- useReactiveWatch( watchContent, updateContent );
566
+ destroyScope( context.scope );
567
+ context.scope = [];
568
+ withScope( context.scope, () => {
569
+ // clone the array to watch all elements
570
+ reactive( () => [ ...itemsGetter() ], updateContent );
571
+ } );
441
572
  watchingContent = true;
442
573
  }
443
574
  }
@@ -463,7 +594,7 @@ function createIfDirective( template ) {
463
594
 
464
595
  const context = createChildContext();
465
596
 
466
- useReactiveWatch( watchState, update );
597
+ reactive( watchState, update );
467
598
 
468
599
  return result;
469
600
 
@@ -490,8 +621,7 @@ function createIfDirective( template ) {
490
621
 
491
622
  function setStyles( element, styles ) {
492
623
  if ( typeof styles == 'function' ) {
493
- useReactive( () => {
494
- const value = styles();
624
+ reactive( styles, value => {
495
625
  if ( isPlainObject( value ) ) {
496
626
  element.style = '';
497
627
  for ( const [ key, value ] of Object.entries( value ) )
@@ -510,7 +640,7 @@ function setStyles( element, styles ) {
510
640
  function setStylesObject( element, styles ) {
511
641
  for ( const [ key, value ] of Object.entries( styles ) ) {
512
642
  if ( typeof value == 'function' )
513
- useReactive( () => element.style[ key ] = value() );
643
+ reactive( value, value => element.style[ key ] = value );
514
644
  else
515
645
  setStyleProperty( element, key, value );
516
646
  }
@@ -523,12 +653,20 @@ function setStyleProperty( element, key, value ) {
523
653
  element.style[ key ] = value;
524
654
  }
525
655
 
526
- const directives = {
656
+ const Directives = {
527
657
  if: createIfDirective,
528
658
  for: createForDirective,
529
659
  dynamic: createDynamicDirective,
530
660
  };
531
661
 
662
+ const Properties = new Set( [ 'value', 'checked', 'textContent', 'innerHTML' ] );
663
+
664
+ const BooleanAttributes = new Set( [
665
+ 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden',
666
+ 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly',
667
+ 'required', 'reversed', 'seamless', 'scoped', 'selected',
668
+ ] );
669
+
532
670
  function make( template ) {
533
671
  if ( template == null )
534
672
  return [];
@@ -547,8 +685,8 @@ function make( template ) {
547
685
  if ( typeof template[ 0 ] == 'string' ) {
548
686
  const tag = template[ 0 ];
549
687
 
550
- if ( directives.hasOwnProperty( tag ) )
551
- return directives[ tag ]( template );
688
+ if ( Directives.hasOwnProperty( tag ) )
689
+ return Directives[ tag ]( template );
552
690
 
553
691
  const element = document.createElement( tag );
554
692
 
@@ -570,7 +708,7 @@ function make( template ) {
570
708
 
571
709
  if ( typeof template == 'function' ) {
572
710
  const textNode = document.createTextNode( '' );
573
- useReactive( () => textNode.textContent = template() );
711
+ reactive( template, value => textNode.textContent = value );
574
712
  return textNode;
575
713
  }
576
714
 
@@ -600,39 +738,61 @@ function setElementProperties( element, properties ) {
600
738
  }
601
739
 
602
740
  function setElementProperty( element, key, value ) {
603
- if ( key in element ) {
604
- if ( typeof value == 'function' && !key.startsWith( 'on' ) )
605
- useReactive( () => element[ key ] = value() );
741
+ if ( key.startsWith( 'on' ) ) {
742
+ element.addEventListener( key.substring( 2 ), value );
743
+ } else if ( Properties.has( key ) ) {
744
+ if ( typeof value == 'function' )
745
+ reactive( value, value => element[ key ] = value );
606
746
  else
607
747
  element[ key ] = value;
608
748
  } else {
609
749
  if ( typeof value == 'function' )
610
- useReactive( () => element.setAttribute( key, value() ) );
750
+ reactive( value, value => setElementAttribute( element, key, value ) );
611
751
  else
612
- element.setAttribute( key, value );
752
+ setElementAttribute( element, key, value );
613
753
  }
614
754
  }
615
755
 
616
- function mount( component, target ) {
617
- const context = {
618
- parent: null,
619
- children: [],
620
- };
621
-
622
- const root = withContext( context, () => make( component( {}, [] ) ) );
756
+ function setElementAttribute( element, name, value ) {
757
+ if ( BooleanAttributes.has( name ) ) {
758
+ if ( value || value === '' )
759
+ element.setAttribute( name, '' );
760
+ else
761
+ element.removeAttribute( name );
762
+ } else {
763
+ if ( value != null )
764
+ element.setAttribute( name, value );
765
+ else
766
+ element.removeAttribute( name );
767
+ }
768
+ }
623
769
 
624
- appendNode( root, target );
770
+ function createApp( component ) {
771
+ const context = createContext( null );
625
772
 
626
- mountContext( context );
773
+ let root;
627
774
 
628
775
  return {
629
- destroy,
776
+ mount( target ) {
777
+ root = withContext( context, () => make( component( {}, [] ) ) );
778
+ appendNode( root, target );
779
+ mountContext( context );
780
+ },
781
+
782
+ destroy() {
783
+ destroyContext( context );
784
+ removeNode( root );
785
+ root = null;
786
+ },
787
+
788
+ provide( key, value ) {
789
+ withContext( context, () => provide( key, value ) );
790
+ },
791
+
792
+ use( callback ) {
793
+ withContext( context, callback );
794
+ },
630
795
  };
631
-
632
- function destroy() {
633
- destroyContext( context );
634
- removeNode( root );
635
- }
636
796
  }
637
797
 
638
- export { mount, onDestroy, onMount };
798
+ export { createApp, inject, onDestroy, onMount, provide };
package/package.json CHANGED
@@ -1,23 +1,41 @@
1
1
  {
2
2
  "name": "leaner",
3
- "version": "0.0.1",
4
- "description": "Leaner.js",
3
+ "version": "0.1.1",
4
+ "description": "Thin, reactive wrapper for DOM",
5
5
  "type": "module",
6
6
  "files": [
7
- "dist"
7
+ "dist",
8
+ "types"
8
9
  ],
10
+ "module": "./dist/core.js",
11
+ "types": "./types/core.d.ts",
12
+ "typesVersions": {
13
+ "*": {
14
+ "web": [
15
+ "./types/web.d.ts"
16
+ ]
17
+ }
18
+ },
9
19
  "exports": {
10
- ".": "./dist/core.js",
11
- "./web": "./dist/web.js",
20
+ ".": {
21
+ "import": "./dist/core.js",
22
+ "types": "./types/core.d.ts"
23
+ },
24
+ "./web": {
25
+ "import": "./dist/web.js",
26
+ "types": "./types/web.d.ts"
27
+ },
12
28
  "./dist/*": "./dist/*",
13
29
  "./package.json": "./package.json"
14
30
  },
15
31
  "scripts": {
16
- "build": "rollup -c"
32
+ "build": "rollup -c",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest watch"
17
35
  },
18
36
  "repository": {
19
37
  "type": "git",
20
- "url": "git+https://github.com/mimecorg/leaner.git"
38
+ "url": "git+https://github.com/leaner-js/leaner.git"
21
39
  },
22
40
  "keywords": [
23
41
  "leaner",
@@ -27,10 +45,12 @@
27
45
  "author": "Michał Męciński",
28
46
  "license": "MIT",
29
47
  "bugs": {
30
- "url": "https://github.com/mimecorg/leaner/issues"
48
+ "url": "https://github.com/leaner-js/leaner/issues"
31
49
  },
32
- "homepage": "https://github.com/mimecorg/leaner",
50
+ "homepage": "https://leanerjs.org/",
33
51
  "devDependencies": {
34
- "rollup": "^4.17.2"
52
+ "jsdom": "^24.0.0",
53
+ "rollup": "^4.17.2",
54
+ "vitest": "^1.6.0"
35
55
  }
36
56
  }