rasti 4.0.0-alpha.0 → 4.0.0-alpha.2

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/es/Component.js CHANGED
@@ -34,7 +34,7 @@ const getExpressionResult = (expression, context) => getResult(expression, conte
34
34
  * @return {boolean} True if the element is a component root element.
35
35
  * @private
36
36
  */
37
- const isComponent = (el) => el.hasAttribute(Component.ATTRIBUTE_ELEMENT) && el.getAttribute(Component.ATTRIBUTE_ELEMENT).endsWith('-1');
37
+ const isComponent = (el) => !!el.dataset[Component.DATASET_ELEMENT] && el.dataset[Component.DATASET_ELEMENT].endsWith('-1');
38
38
 
39
39
  /**
40
40
  * Check if an element has the Rasti data attribute.
@@ -43,7 +43,7 @@ const isComponent = (el) => el.hasAttribute(Component.ATTRIBUTE_ELEMENT) && el.g
43
43
  * @return {boolean} True if the element has the data attribute.
44
44
  * @private
45
45
  */
46
- const isElement = (el) => el.hasAttribute(Component.ATTRIBUTE_ELEMENT);
46
+ const isElement = (el) => !!el.dataset[Component.DATASET_ELEMENT];
47
47
 
48
48
  /**
49
49
  * Generate string with placeholders for interpolated expressions.
@@ -155,21 +155,39 @@ const expandEvents = (attributes, eventsManager) => {
155
155
  * modifies the expressions array adding the mount functions.
156
156
  * @param main {string} The main template.
157
157
  * @param {Array<any>} expressions Array of expressions.
158
+ * @param {boolean} skipNormalization Skip placeholder normalization (for recursive calls).
158
159
  * @return {string} The template with components tags replaced by expressions
159
160
  * placeholders.
160
161
  * @private
161
162
  */
162
- const expandComponents = (main, expressions) => {
163
+ const expandComponents = (main, expressions, skipNormalization = false) => {
163
164
  const PH = Component.PLACEHOLDER('(\\d+)');
164
- // Match component tags.
165
+ const componentRefMap = new Map();
166
+ // Normalize component references to use first placeholder index.
167
+ // Only on first call, not on recursive calls.
168
+ if (!skipNormalization) {
169
+ main = main.replace(
170
+ new RegExp(PH, 'g'),
171
+ (match, idx) => {
172
+ const expression = expressions[idx];
173
+ if (expression && expression.prototype instanceof Component) {
174
+ if (componentRefMap.has(expression)) {
175
+ return componentRefMap.get(expression);
176
+ }
177
+ componentRefMap.set(expression, match);
178
+ }
179
+ return match;
180
+ }
181
+ );
182
+ }
183
+ // Match component tags with backreference to ensure correct pairing.
165
184
  return main.replace(
166
- new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</(${PH})>|<(${PH})([^>]*)/>`,'g'),
167
- (match, openTag, openIdx, nonVoidAttrs, inner, closeTag, closeIdx, selfClosingTag, selfClosingIdx, selfClosingAttrs) => {
168
- let tag, close, attributesStr;
185
+ new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'),
186
+ (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => {
187
+ let tag, attributesStr, innerList;
169
188
 
170
189
  if (openTag) {
171
- tag = typeof openIdx !== 'undefined' ? expressions[openIdx] : openTag;
172
- close = typeof closeIdx !== 'undefined' ? expressions[closeIdx] : closeTag;
190
+ tag = expressions[openIdx];
173
191
  attributesStr = nonVoidAttrs;
174
192
  } else {
175
193
  tag = typeof selfClosingIdx !== 'undefined' ? expressions[selfClosingIdx] : selfClosingTag;
@@ -177,15 +195,11 @@ const expandComponents = (main, expressions) => {
177
195
  }
178
196
  // No component found.
179
197
  if (!(tag.prototype instanceof Component)) return match;
180
-
181
- let innerList;
182
198
  // Non void component.
183
- if (close) {
184
- // Close component tag must match open component tag.
185
- if (tag !== close) return match;
199
+ if (openTag) {
186
200
  // Process inner content same way as partial().
187
201
  // Recursively expand inner components.
188
- const innerTemplate = expandComponents(inner, expressions);
202
+ const innerTemplate = expandComponents(inner, expressions, true);
189
203
  // Parse partial elements to handle dynamic attributes and events.
190
204
  const parsedInner = parsePartialElements(innerTemplate, expressions);
191
205
  // Split into items.
@@ -418,7 +432,7 @@ const parseAttributes = (attributesStr, expressions) => {
418
432
  /*
419
433
  * These option keys will be extended on the component instance.
420
434
  */
421
- const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate'];
435
+ const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onMount', 'onRecycle', 'onUpdate'];
422
436
 
423
437
  /**
424
438
  * @lends module:Component
@@ -486,34 +500,6 @@ class Component extends View {
486
500
  return events;
487
501
  }
488
502
 
489
- /**
490
- * Subscribes to a `change` event on a model or emitter object and invokes the `onChange` lifecycle method.
491
- * The subscription is automatically cleaned up when the component is destroyed.
492
- * By default, the component subscribes to changes on `this.model`, `this.state`, and `this.props`.
493
- *
494
- * @param {Object} model - The model or emitter object to listen to.
495
- * @param {string} [type='change'] - The event type to listen for.
496
- * @param {Function} [listener=this.onChange] - The callback to invoke when the event is emitted.
497
- * @returns {Component} The current component instance for chaining.
498
- */
499
- subscribe(model, type = 'change', listener = this.onChange) {
500
- // Check if model has `on` method.
501
- if (model.on) this.listenTo(model, type, listener);
502
- return this;
503
- }
504
-
505
- /**
506
- * Tell if `Component` is a container.
507
- * In which case, it will not have an element by itself.
508
- * It will render a single expression which is expected to return a single component as child.
509
- * `this.el` will be a reference to that child component's element.
510
- * @return {boolean}
511
- * @private
512
- */
513
- isContainer() {
514
- return this.template.elements.length === 0 && this.template.interpolations.length === 1;
515
- }
516
-
517
503
  /**
518
504
  * Override super method. We don't want to ensure an element on instantiation.
519
505
  * We will provide it later.
@@ -537,6 +523,34 @@ class Component extends View {
537
523
  }
538
524
  }
539
525
 
526
+ /**
527
+ * Tell if `Component` is a container.
528
+ * In which case, it will not have an element by itself.
529
+ * It will render a single expression which is expected to return a single component as child.
530
+ * `this.el` will be a reference to that child component's element.
531
+ * @return {boolean}
532
+ * @private
533
+ */
534
+ isContainer() {
535
+ return this.template.elements.length === 0 && this.template.interpolations.length === 1;
536
+ }
537
+
538
+ /**
539
+ * Subscribes to a `change` event on a model or emitter object and invokes the `onChange` lifecycle method.
540
+ * The subscription is automatically cleaned up when the component is destroyed.
541
+ * By default, the component subscribes to changes on `this.model`, `this.state`, and `this.props`.
542
+ *
543
+ * @param {Object} model - The model or emitter object to listen to.
544
+ * @param {string} [type='change'] - The event type to listen for.
545
+ * @param {Function} [listener=this.onChange] - The callback to invoke when the event is emitted.
546
+ * @returns {Component} The current component instance for chaining.
547
+ */
548
+ subscribe(model, type = 'change', listener = this.onChange) {
549
+ // Check if model has `on` method.
550
+ if (model.on) this.listenTo(model, type, listener);
551
+ return this;
552
+ }
553
+
540
554
  /**
541
555
  * Used internally on the render process.
542
556
  * Attach the `Component` to the dom element providing `this.el`, delegate events,
@@ -581,6 +595,25 @@ class Component extends View {
581
595
  return this;
582
596
  }
583
597
 
598
+ /**
599
+ * Used internally on the render process.
600
+ * Reuse a `Component` by replacing the placeholder comment with the real nodes.
601
+ * Call `onRecycle` lifecycle method.
602
+ * @param parent {node} The parent node.
603
+ * @return {Component} The component instance.
604
+ * @private
605
+ */
606
+ recycle(parent) {
607
+ // Locate the placeholder comment and replace it with the real nodes
608
+ const toBeReplaced = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
609
+ // Replace it with this.el.
610
+ toBeReplaced.replaceWith(...this.getNodes());
611
+ // Call `onRecycle` lifecycle method.
612
+ this.onRecycle.call(this);
613
+ // Return `this` for chaining.
614
+ return this;
615
+ }
616
+
584
617
  /**
585
618
  * Get a `comment` marker with same data attribute as this component.
586
619
  * Used to replace the component when it is recycled.
@@ -607,65 +640,19 @@ class Component extends View {
607
640
  }
608
641
 
609
642
  /**
610
- * Used internally on the render process.
611
- * Reuse a `Component` by replacing the placeholder comment with the real nodes.
612
- * Call `onRecycle` lifecycle method.
613
- * @param parent {node} The parent node.
614
- * @return {Component} The component instance.
643
+ * Call onMount lifecycle method on children and self.
644
+ * Used internally during the mount and render process.
615
645
  * @private
616
646
  */
617
- recycle(parent) {
618
- // Locate the placeholder comment and replace it with the real nodes
619
- const toBeReplaced = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
620
- // Replace it with this.el.
621
- toBeReplaced.replaceWith(...this.getNodes());
622
- // Call `onRecycle` lifecycle method.
623
- this.onRecycle.call(this);
647
+ callOnMount() {
648
+ // Call onMount on children first.
649
+ this.children.forEach(child => child.callOnMount());
650
+ // Call onMount on self.
651
+ this.onMount.call(this);
624
652
  // Return `this` for chaining.
625
653
  return this;
626
654
  }
627
655
 
628
- /**
629
- * Lifecycle method. Called when the view is created, at the end of the `constructor`.
630
- * @param options {object} The view options.
631
- */
632
- onCreate() {}
633
-
634
- /**
635
- * Lifecycle method. Called when model emits `change` event.
636
- * By default calls `render` method.
637
- * This method can be extended with custom logic.
638
- * Maybe comparing new attributes with previous ones and calling
639
- * render when needed.
640
- * @param model {Rasti.Model} The model that emitted the event.
641
- * @param changed {object} Object containing keys and values that has changed.
642
- * @param [...args] {any} Any extra arguments passed to set method.
643
- */
644
- onChange() {
645
- this.render();
646
- }
647
-
648
- /**
649
- * Lifecycle method. Called when the component is rendered for the first time and hydrated.
650
- */
651
- onHydrate() {}
652
-
653
- /**
654
- * Lifecycle method. Called when the component is recycled (reused with the same key) and added to the DOM again.
655
- */
656
- onRecycle() {}
657
-
658
- /**
659
- * Lifecycle method. Called when the component is updated or re-rendered.
660
- */
661
- onUpdate() {}
662
-
663
- /**
664
- * Lifecycle method. Called when the component is destroyed.
665
- * @param {object} options Options object or any arguments passed to `destroy` method.
666
- */
667
- onDestroy() {}
668
-
669
656
  /**
670
657
  * Tagged template helper method.
671
658
  * Used to create a partial template.
@@ -791,6 +778,7 @@ class Component extends View {
791
778
  * - Components with a `key` are recycled if a previous child with the same key exists.
792
779
  * - Unkeyed components are recycled if they have the same type and position in the template or partial.
793
780
  * A recycled `Component` will call the `onRecycle` lifecycle method.
781
+ * - All child components will have their `onMount` lifecycle method called after they are added to the DOM. New children will be hydrated first (calling their `onHydrate` lifecycle method).
794
782
  * - If the active element is inside the component, it will retain focus after the render.
795
783
  * @return {Component} The component instance.
796
784
  */
@@ -860,12 +848,12 @@ class Component extends View {
860
848
  discarded.destroy();
861
849
  });
862
850
  // Add new children. Hydrate them.
863
- nextChildren.forEach(child => {
864
- this.addChild(child).hydrate(fragment);
865
- });
851
+ nextChildren.forEach(child => this.addChild(child).hydrate(fragment));
866
852
 
867
853
  interpolation.update(fragment);
868
854
  });
855
+ // Call onMount lifecycle method on all children after interpolations update.
856
+ this.children.forEach(child => child.callOnMount());
869
857
  // Destroy unused children.
870
858
  previousChildren.forEach(prev => {
871
859
  if (this.children.indexOf(prev) < 0) prev.destroy();
@@ -889,6 +877,55 @@ class Component extends View {
889
877
  return this;
890
878
  }
891
879
 
880
+ /**
881
+ * Lifecycle method. Called when the view is created, at the end of the `constructor`.
882
+ * @param options {object} The view options.
883
+ */
884
+ onCreate() {}
885
+
886
+ /**
887
+ * Lifecycle method. Called when model emits `change` event.
888
+ * By default calls `render` method.
889
+ * This method can be extended with custom logic.
890
+ * Maybe comparing new attributes with previous ones and calling
891
+ * render when needed.
892
+ * @param model {Rasti.Model} The model that emitted the event.
893
+ * @param changed {object} Object containing keys and values that has changed.
894
+ * @param [...args] {any} Any extra arguments passed to set method.
895
+ */
896
+ onChange() {
897
+ this.render();
898
+ }
899
+
900
+ /**
901
+ * Lifecycle method. Called when the component is rendered for the first time and hydrated.
902
+ */
903
+ onHydrate() {}
904
+
905
+ /**
906
+ * Lifecycle method. Called when the component is mounted to the DOM.
907
+ * This method is called after `onHydrate` and only when the component's element is actually present in the document.
908
+ * It is called during the initial mount process (when using `Component.mount()`) and when new child components are added during render.
909
+ * Use this method to perform operations that require the element to be in the DOM, such as measuring dimensions or focusing elements.
910
+ */
911
+ onMount() {}
912
+
913
+ /**
914
+ * Lifecycle method. Called when the component is recycled (reused with the same key) and added to the DOM again.
915
+ */
916
+ onRecycle() {}
917
+
918
+ /**
919
+ * Lifecycle method. Called when the component is updated or re-rendered.
920
+ */
921
+ onUpdate() {}
922
+
923
+ /**
924
+ * Lifecycle method. Called when the component is destroyed.
925
+ * @param {object} options Options object or any arguments passed to `destroy` method.
926
+ */
927
+ onDestroy() {}
928
+
892
929
  /**
893
930
  * Mark a string as safe HTML to be rendered.
894
931
  * Normally you don't need to use this method, as Rasti will automatically mark string literals
@@ -925,6 +962,7 @@ class Component extends View {
925
962
  * Mount the component into the dom.
926
963
  * It instantiate the Component view using options,
927
964
  * appends its element into the DOM (if `el` is provided).
965
+ * The `onMount` lifecycle method will be called after the component is added to the DOM.
928
966
  * And returns the view instance.
929
967
  * @static
930
968
  * @param {object} [options] The view options.
@@ -946,6 +984,8 @@ class Component extends View {
946
984
  // Append element to the DOM.
947
985
  el.append(...component.render().getNodes());
948
986
  }
987
+ // Call onMount lifecycle method after nodes are in the DOM.
988
+ component.callOnMount();
949
989
  }
950
990
  // Return component instance.
951
991
  return component;
@@ -1126,6 +1166,11 @@ class Component extends View {
1126
1166
  Component.ATTRIBUTE_ELEMENT = 'data-rasti-el';
1127
1167
  Component.ATTRIBUTE_EVENT = (type) => `data-rasti-on-${type}`;
1128
1168
 
1169
+ /*
1170
+ * Dataset attribute used to identify elements.
1171
+ */
1172
+ Component.DATASET_ELEMENT = 'rastiEl';
1173
+
1129
1174
  /*
1130
1175
  * Placeholders used to temporarily replace expressions in the template.
1131
1176
  */
@@ -12,12 +12,14 @@ function getAttributesDiff(attributes, previous = {}) {
12
12
  // Find attributes to add/update.
13
13
  Object.keys(attributes).forEach(key => {
14
14
  let value = attributes[key];
15
-
16
- if (value === true) {
17
- add[key] = '';
18
- } else if (value !== false) {
19
- if (value === null || typeof value === 'undefined') value = '';
20
- add[key] = value;
15
+ // Only add if the value is different from previous.
16
+ if (value !== previous[key]) {
17
+ if (value === true) {
18
+ add[key] = '';
19
+ } else if (value !== false) {
20
+ if (value === null || typeof value === 'undefined') value = '';
21
+ add[key] = value;
22
+ }
21
23
  }
22
24
  });
23
25
  // Find attributes to remove.