leaner 0.1.4 → 0.2.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/dist/core.js CHANGED
@@ -540,8 +540,9 @@ function watch( getter, callback ) {
540
540
  updateWatcher( watcher, newDeps );
541
541
 
542
542
  if ( newValue !== value ) {
543
- callback( newValue, value );
543
+ const oldValue = value;
544
544
  value = newValue;
545
+ callback( newValue, oldValue );
545
546
  }
546
547
  }
547
548
  }
package/dist/web.js CHANGED
@@ -1,4 +1,4 @@
1
- import { withScope, destroyScope, reactive, constant } from 'leaner';
1
+ import { withScope, destroyScope, reactive, constant, state } from 'leaner';
2
2
 
3
3
  function appendToArray( item, target ) {
4
4
  if ( Array.isArray( item ) )
@@ -111,31 +111,35 @@ function mountContext( context ) {
111
111
  mountContext( child );
112
112
  }
113
113
 
114
- if ( context.refs != null ) {
115
- for ( const ref of context.refs )
116
- ref.callback( ref.element );
117
- }
114
+ withContext( context, () => {
115
+ if ( context.refs != null ) {
116
+ for ( const ref of context.refs )
117
+ ref.callback( ref.element );
118
+ }
118
119
 
119
- if ( context.mount != null ) {
120
- for ( const callback of context.mount )
121
- callback();
122
- }
120
+ if ( context.mount != null ) {
121
+ for ( const callback of context.mount )
122
+ callback();
123
+ }
124
+ } );
123
125
  }
124
126
 
125
127
  function destroyContext( context ) {
126
- if ( context.destroy != null ) {
127
- for ( const callback of context.destroy )
128
- callback();
129
- context.destroy = null;
130
- }
128
+ withContext( context, () => {
129
+ if ( context.destroy != null ) {
130
+ for ( const callback of context.destroy )
131
+ callback();
132
+ context.destroy = null;
133
+ }
131
134
 
132
- context.mount = null;
135
+ if ( context.refs != null ) {
136
+ for ( const ref of context.refs )
137
+ ref.callback( null );
138
+ context.refs = null;
139
+ }
140
+ } );
133
141
 
134
- if ( context.refs != null ) {
135
- for ( const ref of context.refs )
136
- ref.callback( null );
137
- context.refs = null;
138
- }
142
+ context.mount = null;
139
143
 
140
144
  if ( context.scope.length > 0 ) {
141
145
  destroyScope( context.scope );
@@ -162,6 +166,9 @@ function provide( key, value ) {
162
166
  }
163
167
 
164
168
  function inject( key ) {
169
+ if ( current == null )
170
+ throw new Error( 'inject() cannot be called outside of a component' );
171
+
165
172
  for ( let context = current; context != null; context = context.parent ) {
166
173
  if ( context.services != null && context.services.has( key ) )
167
174
  return context.services.get( key );
@@ -295,12 +302,18 @@ function removeNode( node ) {
295
302
  }
296
303
 
297
304
  function replaceNode( newNode, node ) {
305
+ // this is safe because the parent of a DynamicNode is always a true DOM node
306
+ const parent = Array.isArray( node ) ? node[ 0 ].parentNode : node.parentNode;
307
+
308
+ // remember the value of a <select> element so that it can be restored later
309
+ const currentValue = parent instanceof HTMLSelectElement ? parent.value : null;
310
+
298
311
  if ( newNode instanceof Node && node instanceof Node ) {
299
- node.parentNode.replaceChild( newNode, node );
312
+ parent.replaceChild( newNode, node );
300
313
  } else if ( Array.isArray( newNode ) && Array.isArray( node ) ) {
301
314
  let newStart = 0, start = 0;
302
315
  let newEnd = newNode.length, end = node.length;
303
- let parent = null, before = null;
316
+ let before;
304
317
 
305
318
  while ( newStart < newEnd || start < end ) {
306
319
  // skip equal nodes at the start
@@ -324,13 +337,15 @@ function replaceNode( newNode, node ) {
324
337
  before = node[ start ];
325
338
  if ( before instanceof DynamicNode )
326
339
  before = findFirstChild( before.content );
327
- parent = before.parentNode;
328
- } else {
340
+ } else if ( newStart > 0 ) {
329
341
  let after = newNode[ newStart - 1 ];
330
342
  if ( after instanceof DynamicNode )
331
343
  after = findLastChild( after.content );
332
344
  before = after.nextSibling;
333
- parent = after.parentNode;
345
+ } else {
346
+ before = newNode[ newEnd ];
347
+ if ( before instanceof DynamicNode )
348
+ before = findFirstChild( before.content );
334
349
  }
335
350
  }
336
351
  insertBefore( newNode[ newStart ], parent, before );
@@ -352,7 +367,6 @@ function replaceNode( newNode, node ) {
352
367
  before = node[ start ];
353
368
  if ( before instanceof DynamicNode )
354
369
  before = findFirstChild( before.content );
355
- parent = before.parentNode;
356
370
  }
357
371
  insertBefore( node[ end - 1 ], parent, before );
358
372
 
@@ -375,10 +389,12 @@ function replaceNode( newNode, node ) {
375
389
  } else {
376
390
  const after = findLastChild( node );
377
391
  const before = after.nextSibling;
378
- const parent = after.parentNode;
379
392
  removeNode( node );
380
393
  insertBefore( newNode, parent, before );
381
394
  }
395
+
396
+ if ( currentValue != null )
397
+ parent.value = currentValue;
382
398
  }
383
399
 
384
400
  function findFirstChild( node ) {
@@ -471,12 +487,16 @@ function createForDirective( template ) {
471
487
  throw new TypeError( 'Invalid for template' );
472
488
 
473
489
  const itemsGetter = template[ 1 ];
490
+ const callback = template[ 2 ];
491
+ const useIndexes = callback.length > 1;
474
492
 
475
493
  let length = 0;
476
494
  let items = null;
477
495
 
478
496
  let watchingContent = false;
479
497
 
498
+ let indexStates = null;
499
+
480
500
  const result = new DynamicNode( null );
481
501
 
482
502
  const context = createChildContext();
@@ -516,6 +536,8 @@ function createForDirective( template ) {
516
536
  oldIndexMap.set( items[ i ], i );
517
537
  }
518
538
 
539
+ let newIndexStates = null;
540
+
519
541
  for ( let index = 0; index < newItems.length; index++ ) {
520
542
  const value = newItems[ index ];
521
543
 
@@ -545,6 +567,16 @@ function createForDirective( template ) {
545
567
 
546
568
  if ( oldItemContexts != null )
547
569
  oldItemContexts.delete( itemContext );
570
+
571
+ if ( useIndexes && isMatchedByValue ) {
572
+ // update the index passed to the child if it's reordered
573
+ const indexState = indexStates.get( oldIndex );
574
+ if ( oldIndex != index )
575
+ indexState[ 1 ]( index );
576
+ if ( newIndexStates == null )
577
+ newIndexStates = new Map();
578
+ newIndexStates.set( index, indexState );
579
+ }
548
580
  } else {
549
581
  const itemContext = createContext( context );
550
582
  itemContexts.push( itemContext );
@@ -555,7 +587,26 @@ function createForDirective( template ) {
555
587
 
556
588
  const item = isMatchedByValue ? constant( value ) : itemsGetter[ index ];
557
589
 
558
- const itemTemplate = template[ 2 ]( item, index );
590
+ let itemTemplate;
591
+
592
+ if ( useIndexes ) {
593
+ let indexGetter;
594
+
595
+ if ( isMatchedByValue ) {
596
+ // index can change in the future - use reactive state
597
+ const indexState = state( index );
598
+ if ( newIndexStates == null )
599
+ newIndexStates = new Map();
600
+ newIndexStates.set( index, indexState );
601
+ indexGetter = indexState[ 0 ];
602
+ } else {
603
+ indexGetter = constant( index );
604
+ }
605
+
606
+ itemTemplate = callback( item, indexGetter );
607
+ } else {
608
+ itemTemplate = callback( item );
609
+ }
559
610
 
560
611
  const node = withContext( itemContext, () => make( itemTemplate ) );
561
612
 
@@ -588,6 +639,8 @@ function createForDirective( template ) {
588
639
  else
589
640
  context.children = null;
590
641
 
642
+ indexStates = newIndexStates;
643
+
591
644
  if ( containsMatchedByValue && !watchingContent ) {
592
645
  destroyScope( context.scope );
593
646
  context.scope = [];
@@ -601,20 +654,8 @@ function createForDirective( template ) {
601
654
  }
602
655
 
603
656
  function createIfDirective( template ) {
604
- let numberOfConditions = 0;
605
- let hasElse = false;
606
-
607
- for ( let i = 1; i < template.length; i += 2 ) {
608
- if ( i == template.length - 1 )
609
- hasElse = true;
610
- else if ( typeof template[ i ] == 'function' )
611
- numberOfConditions++;
612
- else
613
- throw new TypeError( 'Invalid if template' );
614
- }
615
-
616
- if ( numberOfConditions == 0 )
617
- throw new TypeError( 'Invalid if template' );
657
+ if ( template.length < 2 || typeof template[ 1 ] != 'function' )
658
+ throw new TypeError( 'Invalid if template' );
618
659
 
619
660
  const result = new DynamicNode( null );
620
661
 
@@ -625,23 +666,88 @@ function createIfDirective( template ) {
625
666
  return result;
626
667
 
627
668
  function watchState() {
628
- let state = null;
669
+ return template[ 1 ]();
670
+ }
629
671
 
630
- for ( let i = 0; i < numberOfConditions; i++ ) {
631
- if ( template[ 2 * i + 1 ]() ) {
632
- state = 2 * i + 2;
633
- break;
634
- }
672
+ function update( state ) {
673
+ let childTemplate = null;
674
+ if ( state ) {
675
+ if ( template.length == 3 )
676
+ childTemplate = template[ 2 ];
677
+ else
678
+ childTemplate = [ template.slice( 2 ) ];
635
679
  }
680
+ updateDynamicNode( result, context, childTemplate );
681
+ }
682
+ }
636
683
 
637
- if ( state == null && hasElse )
638
- state = template.length - 1;
684
+ function createRepeatDirective( template ) {
685
+ if ( template.length != 3 || typeof template[ 2 ] != 'function' )
686
+ throw new TypeError( 'Invalid repeat template' );
639
687
 
640
- return state;
688
+ let count = 0;
689
+
690
+ const result = new DynamicNode( null );
691
+
692
+ const context = createChildContext();
693
+
694
+ if ( typeof template[ 1 ] == 'function' ) {
695
+ withScope( context.scope, () => {
696
+ reactive( template[ 1 ], updateCount );
697
+ } );
698
+ } else {
699
+ updateCount( template[ 1 ] );
641
700
  }
642
701
 
643
- function update( state ) {
644
- updateDynamicNode( result, context, state != null ? template[ state ] : null );
702
+ return result;
703
+
704
+ function updateCount( newCount ) {
705
+ if ( newCount > count ) {
706
+ const nodes = result.content != null ? result.content.slice( 0, count ) : [];
707
+ const contexts = context.children != null ? context.children.slice( 0, count ) : [];
708
+
709
+ for ( let i = count; i < newCount; i++ ) {
710
+ const itemContext = createContext( context );
711
+ contexts.push( itemContext );
712
+
713
+ const itemTemplate = template[ 2 ]( i );
714
+
715
+ const node = withContext( itemContext, () => make( itemTemplate ) );
716
+
717
+ if ( Array.isArray( node ) )
718
+ nodes.push( new DynamicNode( node ) );
719
+ else
720
+ nodes.push( node );
721
+ }
722
+
723
+ if ( result.content != null ) {
724
+ replaceNode( nodes, result.content );
725
+
726
+ for ( let i = count; i < newCount; i++ )
727
+ mountContext( contexts[ i ] );
728
+ }
729
+
730
+ result.content = nodes;
731
+ context.children = contexts;
732
+ } else {
733
+ for ( let i = newCount; i < count; i++ )
734
+ destroyContext( context.children[ i ] );
735
+
736
+ const nodes = result.content != null ? result.content.slice( 0, newCount ) : [];
737
+ ensureNotEmpty( nodes );
738
+
739
+ if ( result.content != null )
740
+ replaceNode( nodes, result.content );
741
+
742
+ result.content = nodes;
743
+
744
+ if ( newCount == 0 )
745
+ context.children = null;
746
+ else if ( context.children != null )
747
+ context.children = context.children.slice( 0, newCount );
748
+ }
749
+
750
+ count = newCount;
645
751
  }
646
752
  }
647
753
 
@@ -683,10 +789,68 @@ function setStyleProperty( element, key, value ) {
683
789
  element.style[ key ] = value;
684
790
  }
685
791
 
792
+ function createSwitchDirective( template ) {
793
+ let numberOfConditions = 0;
794
+ let hasElse = false;
795
+
796
+ for ( let i = 1; i < template.length; i++ ) {
797
+ const condition = template[ i ];
798
+ if ( Array.isArray( condition ) && condition.length > 1 && condition[ 0 ] === 'if' && typeof condition[ 1 ] == 'function' )
799
+ numberOfConditions++;
800
+ else if ( i == template.length - 1 && Array.isArray( condition ) && condition.length > 0 && condition[ 0 ] === 'else' )
801
+ hasElse = true;
802
+ else
803
+ throw new TypeError( 'Invalid switch template' );
804
+ }
805
+
806
+ if ( numberOfConditions == 0 )
807
+ throw new TypeError( 'Invalid switch template' );
808
+
809
+ const result = new DynamicNode( null );
810
+
811
+ const context = createChildContext();
812
+
813
+ reactive( watchState, update );
814
+
815
+ return result;
816
+
817
+ function watchState() {
818
+ let state = null;
819
+
820
+ for ( let i = 0; i < numberOfConditions; i++ ) {
821
+ if ( template[ i + 1 ][ 1 ]() ) {
822
+ state = i + 1;
823
+ break;
824
+ }
825
+ }
826
+
827
+ if ( state == null && hasElse )
828
+ state = template.length - 1;
829
+
830
+ return state;
831
+ }
832
+
833
+ function update( state ) {
834
+ let childTemplate = null;
835
+ if ( state != null ) {
836
+ const condition = template[ state ];
837
+ const index = condition[ 0 ] == 'else' ? 1 : 2;
838
+ if ( condition.length == index + 1 )
839
+ childTemplate = condition[ index ];
840
+ else
841
+ childTemplate = [ condition.slice( index ) ];
842
+ }
843
+ updateDynamicNode( result, context, childTemplate );
844
+ }
845
+ }
846
+
686
847
  const Directives = {
687
- if: createIfDirective,
688
- for: createForDirective,
689
848
  dynamic: createDynamicDirective,
849
+ else: unexpectedDirective,
850
+ for: createForDirective,
851
+ if: createIfDirective,
852
+ repeat: createRepeatDirective,
853
+ switch: createSwitchDirective,
690
854
  };
691
855
 
692
856
  const Properties = new Set( [ 'value', 'checked', 'textContent', 'innerHTML' ] );
@@ -807,6 +971,10 @@ function setElementAttribute( element, name, value ) {
807
971
  }
808
972
  }
809
973
 
974
+ function unexpectedDirective( template ) {
975
+ throw new TypeError( `Unexpected ${template[ 0 ]} directive` );
976
+ }
977
+
810
978
  function createApp( component ) {
811
979
  const context = createContext( null );
812
980
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaner",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Thin, reactive wrapper for DOM",
5
5
  "type": "module",
6
6
  "files": [
package/types/core.d.ts CHANGED
@@ -14,7 +14,7 @@ type ArrayGetter<T> = {
14
14
  readonly [ index: number ]: Getter<T>;
15
15
  get( index: number | ( () => number ) ): Getter<T>;
16
16
  length(): number;
17
- }
17
+ };
18
18
 
19
19
  declare class Mutator<T> {
20
20
  private callback: ( value: T ) => void;