leaner 0.1.4 → 0.3.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
@@ -92,6 +92,9 @@ function withScope( scope, callback ) {
92
92
  }
93
93
 
94
94
  function createWatcher( deps, callback, async ) {
95
+ if ( async )
96
+ callback = createSafeCallback( callback );
97
+
95
98
  const watcher = {
96
99
  deps,
97
100
  callback,
@@ -99,7 +102,7 @@ function createWatcher( deps, callback, async ) {
99
102
  };
100
103
 
101
104
  if ( current != null )
102
- current.push( watcher );
105
+ current.watchers.push( watcher );
103
106
 
104
107
  if ( deps != null ) {
105
108
  for ( const dep of deps ) {
@@ -110,6 +113,24 @@ function createWatcher( deps, callback, async ) {
110
113
  return watcher;
111
114
  }
112
115
 
116
+ function createSafeCallback( callback ) {
117
+ if ( current != null && current.errorHandler != null ) {
118
+ const handler = current.errorHandler;
119
+
120
+ return safeCallback;
121
+
122
+ function safeCallback() {
123
+ try {
124
+ callback();
125
+ } catch ( err ) {
126
+ handler( err );
127
+ }
128
+ }
129
+ }
130
+
131
+ return callback;
132
+ }
133
+
113
134
  function updateWatcher( watcher, deps ) {
114
135
  for ( const dep of deps ) {
115
136
  dep.add( watcher );
@@ -126,7 +147,7 @@ function updateWatcher( watcher, deps ) {
126
147
  }
127
148
 
128
149
  function destroyScope( scope ) {
129
- for ( const watcher of scope ) {
150
+ for ( const watcher of scope.watchers ) {
130
151
  watcher.callback = null;
131
152
 
132
153
  if ( watcher.deps != null ) {
@@ -540,8 +561,9 @@ function watch( getter, callback ) {
540
561
  updateWatcher( watcher, newDeps );
541
562
 
542
563
  if ( newValue !== value ) {
543
- callback( newValue, value );
564
+ const oldValue = value;
544
565
  value = newValue;
566
+ callback( newValue, oldValue );
545
567
  }
546
568
  }
547
569
  }
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 ) )
@@ -37,15 +37,21 @@ function createComponent( template ) {
37
37
  }
38
38
 
39
39
  function createContext( parent ) {
40
- return {
40
+ const context = {
41
41
  parent,
42
42
  children: null,
43
- scope: [],
43
+ scope: {
44
+ watchers: [],
45
+ errorHandler: err => handleError( context, err ),
46
+ },
44
47
  mount: null,
45
48
  destroy: null,
46
49
  refs: null,
47
50
  services: null,
51
+ errorHandler: null,
48
52
  };
53
+
54
+ return context;
49
55
  }
50
56
 
51
57
  function createChildContext() {
@@ -111,40 +117,63 @@ function mountContext( context ) {
111
117
  mountContext( child );
112
118
  }
113
119
 
114
- if ( context.refs != null ) {
115
- for ( const ref of context.refs )
116
- ref.callback( ref.element );
117
- }
120
+ withContext( context, () => {
121
+ if ( context.refs != null ) {
122
+ for ( const ref of context.refs )
123
+ ref.callback( ref.element );
124
+ }
118
125
 
119
- if ( context.mount != null ) {
120
- for ( const callback of context.mount )
121
- callback();
122
- }
126
+ if ( context.mount != null ) {
127
+ for ( const callback of context.mount )
128
+ callback();
129
+ }
130
+ } );
123
131
  }
124
132
 
125
133
  function destroyContext( context ) {
126
- if ( context.destroy != null ) {
127
- for ( const callback of context.destroy )
128
- callback();
129
- context.destroy = null;
130
- }
134
+ withContext( context, () => {
135
+ if ( context.destroy != null ) {
136
+ for ( const callback of context.destroy )
137
+ callback();
138
+ context.destroy = null;
139
+ }
140
+
141
+ if ( context.refs != null ) {
142
+ for ( const ref of context.refs )
143
+ ref.callback( null );
144
+ context.refs = null;
145
+ }
146
+ } );
131
147
 
132
148
  context.mount = null;
133
149
 
134
- if ( context.refs != null ) {
135
- for ( const ref of context.refs )
136
- ref.callback( null );
137
- context.refs = null;
150
+ if ( context.scope.watchers.length > 0 ) {
151
+ destroyScope( context.scope );
152
+ context.scope.watchers = [];
138
153
  }
139
154
 
140
- if ( context.scope.length > 0 ) {
155
+ if ( context.children != null ) {
156
+ for ( const child of context.children )
157
+ destroyContext( child );
158
+ context.children = null;
159
+ }
160
+
161
+ context.services = null;
162
+ }
163
+
164
+ function cleanUpContext( context ) {
165
+ context.destroy = null;
166
+ context.refs = null;
167
+ context.mount = null;
168
+
169
+ if ( context.scope.watchers.length > 0 ) {
141
170
  destroyScope( context.scope );
142
- context.scope = [];
171
+ context.scope.watchers = [];
143
172
  }
144
173
 
145
174
  if ( context.children != null ) {
146
175
  for ( const child of context.children )
147
- destroyContext( child );
176
+ cleanUpContext( child );
148
177
  context.children = null;
149
178
  }
150
179
 
@@ -162,12 +191,44 @@ function provide( key, value ) {
162
191
  }
163
192
 
164
193
  function inject( key ) {
194
+ if ( current == null )
195
+ throw new Error( 'inject() cannot be called outside of a component' );
196
+
165
197
  for ( let context = current; context != null; context = context.parent ) {
166
198
  if ( context.services != null && context.services.has( key ) )
167
199
  return context.services.get( key );
168
200
  }
169
201
  }
170
202
 
203
+ function handleError( context, err ) {
204
+ while ( context != null) {
205
+ if ( context.errorHandler != null ) {
206
+ context.errorHandler( err );
207
+ return;
208
+ }
209
+ context = context.parent;
210
+ }
211
+ throw err;
212
+ }
213
+
214
+ function createSafeHandler( callback ) {
215
+ if ( current != null ) {
216
+ const context = current;
217
+
218
+ return safeHandler;
219
+
220
+ function safeHandler( e ) {
221
+ try {
222
+ callback( e );
223
+ } catch ( err ) {
224
+ handleError( context, err );
225
+ }
226
+ }
227
+ }
228
+
229
+ return callback;
230
+ }
231
+
171
232
  function setClasses( element, classes ) {
172
233
  if ( typeof classes == 'function' ) {
173
234
  reactive( classes, value => {
@@ -295,12 +356,18 @@ function removeNode( node ) {
295
356
  }
296
357
 
297
358
  function replaceNode( newNode, node ) {
359
+ // this is safe because the parent of a DynamicNode is always a true DOM node
360
+ const parent = Array.isArray( node ) ? node[ 0 ].parentNode : node.parentNode;
361
+
362
+ // remember the value of a <select> element so that it can be restored later
363
+ const currentValue = parent instanceof HTMLSelectElement ? parent.value : null;
364
+
298
365
  if ( newNode instanceof Node && node instanceof Node ) {
299
- node.parentNode.replaceChild( newNode, node );
366
+ parent.replaceChild( newNode, node );
300
367
  } else if ( Array.isArray( newNode ) && Array.isArray( node ) ) {
301
368
  let newStart = 0, start = 0;
302
369
  let newEnd = newNode.length, end = node.length;
303
- let parent = null, before = null;
370
+ let before;
304
371
 
305
372
  while ( newStart < newEnd || start < end ) {
306
373
  // skip equal nodes at the start
@@ -324,13 +391,15 @@ function replaceNode( newNode, node ) {
324
391
  before = node[ start ];
325
392
  if ( before instanceof DynamicNode )
326
393
  before = findFirstChild( before.content );
327
- parent = before.parentNode;
328
- } else {
394
+ } else if ( newStart > 0 ) {
329
395
  let after = newNode[ newStart - 1 ];
330
396
  if ( after instanceof DynamicNode )
331
397
  after = findLastChild( after.content );
332
398
  before = after.nextSibling;
333
- parent = after.parentNode;
399
+ } else {
400
+ before = newNode[ newEnd ];
401
+ if ( before instanceof DynamicNode )
402
+ before = findFirstChild( before.content );
334
403
  }
335
404
  }
336
405
  insertBefore( newNode[ newStart ], parent, before );
@@ -352,7 +421,6 @@ function replaceNode( newNode, node ) {
352
421
  before = node[ start ];
353
422
  if ( before instanceof DynamicNode )
354
423
  before = findFirstChild( before.content );
355
- parent = before.parentNode;
356
424
  }
357
425
  insertBefore( node[ end - 1 ], parent, before );
358
426
 
@@ -375,10 +443,12 @@ function replaceNode( newNode, node ) {
375
443
  } else {
376
444
  const after = findLastChild( node );
377
445
  const before = after.nextSibling;
378
- const parent = after.parentNode;
379
446
  removeNode( node );
380
447
  insertBefore( newNode, parent, before );
381
448
  }
449
+
450
+ if ( currentValue != null )
451
+ parent.value = currentValue;
382
452
  }
383
453
 
384
454
  function findFirstChild( node ) {
@@ -471,12 +541,16 @@ function createForDirective( template ) {
471
541
  throw new TypeError( 'Invalid for template' );
472
542
 
473
543
  const itemsGetter = template[ 1 ];
544
+ const callback = template[ 2 ];
545
+ const useIndexes = callback.length > 1;
474
546
 
475
547
  let length = 0;
476
548
  let items = null;
477
549
 
478
550
  let watchingContent = false;
479
551
 
552
+ let indexStates = null;
553
+
480
554
  const result = new DynamicNode( null );
481
555
 
482
556
  const context = createChildContext();
@@ -516,6 +590,8 @@ function createForDirective( template ) {
516
590
  oldIndexMap.set( items[ i ], i );
517
591
  }
518
592
 
593
+ let newIndexStates = null;
594
+
519
595
  for ( let index = 0; index < newItems.length; index++ ) {
520
596
  const value = newItems[ index ];
521
597
 
@@ -545,6 +621,16 @@ function createForDirective( template ) {
545
621
 
546
622
  if ( oldItemContexts != null )
547
623
  oldItemContexts.delete( itemContext );
624
+
625
+ if ( useIndexes && isMatchedByValue ) {
626
+ // update the index passed to the child if it's reordered
627
+ const indexState = indexStates.get( oldIndex );
628
+ if ( oldIndex != index )
629
+ indexState[ 1 ]( index );
630
+ if ( newIndexStates == null )
631
+ newIndexStates = new Map();
632
+ newIndexStates.set( index, indexState );
633
+ }
548
634
  } else {
549
635
  const itemContext = createContext( context );
550
636
  itemContexts.push( itemContext );
@@ -555,7 +641,26 @@ function createForDirective( template ) {
555
641
 
556
642
  const item = isMatchedByValue ? constant( value ) : itemsGetter[ index ];
557
643
 
558
- const itemTemplate = template[ 2 ]( item, index );
644
+ let itemTemplate;
645
+
646
+ if ( useIndexes ) {
647
+ let indexGetter;
648
+
649
+ if ( isMatchedByValue ) {
650
+ // index can change in the future - use reactive state
651
+ const indexState = state( index );
652
+ if ( newIndexStates == null )
653
+ newIndexStates = new Map();
654
+ newIndexStates.set( index, indexState );
655
+ indexGetter = indexState[ 0 ];
656
+ } else {
657
+ indexGetter = constant( index );
658
+ }
659
+
660
+ itemTemplate = callback( item, indexGetter );
661
+ } else {
662
+ itemTemplate = callback( item );
663
+ }
559
664
 
560
665
  const node = withContext( itemContext, () => make( itemTemplate ) );
561
666
 
@@ -588,9 +693,11 @@ function createForDirective( template ) {
588
693
  else
589
694
  context.children = null;
590
695
 
696
+ indexStates = newIndexStates;
697
+
591
698
  if ( containsMatchedByValue && !watchingContent ) {
592
699
  destroyScope( context.scope );
593
- context.scope = [];
700
+ context.scope.watchers = [];
594
701
  withScope( context.scope, () => {
595
702
  // clone the array to watch all elements
596
703
  reactive( () => [ ...itemsGetter() ], updateContent );
@@ -601,20 +708,8 @@ function createForDirective( template ) {
601
708
  }
602
709
 
603
710
  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' );
711
+ if ( template.length < 2 || typeof template[ 1 ] != 'function' )
712
+ throw new TypeError( 'Invalid if template' );
618
713
 
619
714
  const result = new DynamicNode( null );
620
715
 
@@ -625,23 +720,88 @@ function createIfDirective( template ) {
625
720
  return result;
626
721
 
627
722
  function watchState() {
628
- let state = null;
723
+ return template[ 1 ]();
724
+ }
629
725
 
630
- for ( let i = 0; i < numberOfConditions; i++ ) {
631
- if ( template[ 2 * i + 1 ]() ) {
632
- state = 2 * i + 2;
633
- break;
634
- }
726
+ function update( state ) {
727
+ let childTemplate = null;
728
+ if ( state ) {
729
+ if ( template.length == 3 )
730
+ childTemplate = template[ 2 ];
731
+ else
732
+ childTemplate = [ template.slice( 2 ) ];
635
733
  }
734
+ updateDynamicNode( result, context, childTemplate );
735
+ }
736
+ }
636
737
 
637
- if ( state == null && hasElse )
638
- state = template.length - 1;
738
+ function createRepeatDirective( template ) {
739
+ if ( template.length != 3 || typeof template[ 2 ] != 'function' )
740
+ throw new TypeError( 'Invalid repeat template' );
639
741
 
640
- return state;
742
+ let count = 0;
743
+
744
+ const result = new DynamicNode( null );
745
+
746
+ const context = createChildContext();
747
+
748
+ if ( typeof template[ 1 ] == 'function' ) {
749
+ withScope( context.scope, () => {
750
+ reactive( template[ 1 ], updateCount );
751
+ } );
752
+ } else {
753
+ updateCount( template[ 1 ] );
641
754
  }
642
755
 
643
- function update( state ) {
644
- updateDynamicNode( result, context, state != null ? template[ state ] : null );
756
+ return result;
757
+
758
+ function updateCount( newCount ) {
759
+ if ( newCount > count ) {
760
+ const nodes = result.content != null ? result.content.slice( 0, count ) : [];
761
+ const contexts = context.children != null ? context.children.slice( 0, count ) : [];
762
+
763
+ for ( let i = count; i < newCount; i++ ) {
764
+ const itemContext = createContext( context );
765
+ contexts.push( itemContext );
766
+
767
+ const itemTemplate = template[ 2 ]( i );
768
+
769
+ const node = withContext( itemContext, () => make( itemTemplate ) );
770
+
771
+ if ( Array.isArray( node ) )
772
+ nodes.push( new DynamicNode( node ) );
773
+ else
774
+ nodes.push( node );
775
+ }
776
+
777
+ if ( result.content != null ) {
778
+ replaceNode( nodes, result.content );
779
+
780
+ for ( let i = count; i < newCount; i++ )
781
+ mountContext( contexts[ i ] );
782
+ }
783
+
784
+ result.content = nodes;
785
+ context.children = contexts;
786
+ } else {
787
+ for ( let i = newCount; i < count; i++ )
788
+ destroyContext( context.children[ i ] );
789
+
790
+ const nodes = result.content != null ? result.content.slice( 0, newCount ) : [];
791
+ ensureNotEmpty( nodes );
792
+
793
+ if ( result.content != null )
794
+ replaceNode( nodes, result.content );
795
+
796
+ result.content = nodes;
797
+
798
+ if ( newCount == 0 )
799
+ context.children = null;
800
+ else if ( context.children != null )
801
+ context.children = context.children.slice( 0, newCount );
802
+ }
803
+
804
+ count = newCount;
645
805
  }
646
806
  }
647
807
 
@@ -683,10 +843,104 @@ function setStyleProperty( element, key, value ) {
683
843
  element.style[ key ] = value;
684
844
  }
685
845
 
846
+ function createSwitchDirective( template ) {
847
+ let numberOfConditions = 0;
848
+ let hasElse = false;
849
+
850
+ for ( let i = 1; i < template.length; i++ ) {
851
+ const condition = template[ i ];
852
+ if ( Array.isArray( condition ) && condition.length > 1 && condition[ 0 ] === 'if' && typeof condition[ 1 ] == 'function' )
853
+ numberOfConditions++;
854
+ else if ( i == template.length - 1 && Array.isArray( condition ) && condition.length > 0 && condition[ 0 ] === 'else' )
855
+ hasElse = true;
856
+ else
857
+ throw new TypeError( 'Invalid switch template' );
858
+ }
859
+
860
+ if ( numberOfConditions == 0 )
861
+ throw new TypeError( 'Invalid switch template' );
862
+
863
+ const result = new DynamicNode( null );
864
+
865
+ const context = createChildContext();
866
+
867
+ reactive( watchState, update );
868
+
869
+ return result;
870
+
871
+ function watchState() {
872
+ let state = null;
873
+
874
+ for ( let i = 0; i < numberOfConditions; i++ ) {
875
+ if ( template[ i + 1 ][ 1 ]() ) {
876
+ state = i + 1;
877
+ break;
878
+ }
879
+ }
880
+
881
+ if ( state == null && hasElse )
882
+ state = template.length - 1;
883
+
884
+ return state;
885
+ }
886
+
887
+ function update( state ) {
888
+ let childTemplate = null;
889
+ if ( state != null ) {
890
+ const condition = template[ state ];
891
+ const index = condition[ 0 ] == 'else' ? 1 : 2;
892
+ if ( condition.length == index + 1 )
893
+ childTemplate = condition[ index ];
894
+ else
895
+ childTemplate = [ condition.slice( index ) ];
896
+ }
897
+ updateDynamicNode( result, context, childTemplate );
898
+ }
899
+ }
900
+
901
+ function createTryDirective( template ) {
902
+ if ( template.length != 3 || typeof template[ 2 ] != 'function' )
903
+ throw new TypeError( 'Invalid try template' );
904
+
905
+ const result = new DynamicNode( null );
906
+
907
+ const context = createChildContext();
908
+
909
+ try {
910
+ updateDynamicNode( result, context, template[ 1 ] );
911
+ } catch ( err ) {
912
+ updateErrorState( err );
913
+ return result;
914
+ }
915
+
916
+ context.errorHandler = asyncErrorHandler;
917
+
918
+ return result;
919
+
920
+ function asyncErrorHandler( err ) {
921
+ try {
922
+ updateErrorState( err );
923
+ } catch ( err ) {
924
+ handleError( context, err );
925
+ }
926
+ }
927
+
928
+ function updateErrorState( err ) {
929
+ context.errorHandler = null;
930
+ if ( result.content == null )
931
+ cleanUpContext( context );
932
+ updateDynamicNode( result, context, template[ 2 ]( err ) );
933
+ }
934
+ }
935
+
686
936
  const Directives = {
687
- if: createIfDirective,
688
- for: createForDirective,
689
937
  dynamic: createDynamicDirective,
938
+ else: unexpectedDirective,
939
+ for: createForDirective,
940
+ if: createIfDirective,
941
+ repeat: createRepeatDirective,
942
+ switch: createSwitchDirective,
943
+ try: createTryDirective,
690
944
  };
691
945
 
692
946
  const Properties = new Set( [ 'value', 'checked', 'textContent', 'innerHTML' ] );
@@ -779,7 +1033,7 @@ function setElementProperties( element, properties ) {
779
1033
 
780
1034
  function setElementProperty( element, key, value ) {
781
1035
  if ( key.startsWith( 'on' ) ) {
782
- element.addEventListener( key.substring( 2 ), value );
1036
+ element.addEventListener( key.substring( 2 ), createSafeHandler( value ) );
783
1037
  } else if ( Properties.has( key ) ) {
784
1038
  if ( typeof value == 'function' )
785
1039
  reactive( value, value => element[ key ] = value != null ? value : '' );
@@ -807,6 +1061,10 @@ function setElementAttribute( element, name, value ) {
807
1061
  }
808
1062
  }
809
1063
 
1064
+ function unexpectedDirective( template ) {
1065
+ throw new TypeError( `Unexpected ${template[ 0 ]} directive` );
1066
+ }
1067
+
810
1068
  function createApp( component ) {
811
1069
  const context = createContext( null );
812
1070
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaner",
3
- "version": "0.1.4",
3
+ "version": "0.3.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;