rasti 3.0.0 → 3.0.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/es/Component.js CHANGED
@@ -3,8 +3,12 @@ import getResult from './utils/getResult.js';
3
3
  import deepFlat from './utils/deepFlat.js';
4
4
  import './Emitter.js';
5
5
 
6
- /*
6
+ /**
7
7
  * Wrapper class for HTML strings marked as safe.
8
+ * @class SafeHTML
9
+ * @param {string} value The HTML string to be marked as safe.
10
+ * @property {string} value The HTML string.
11
+ * @private
8
12
  */
9
13
  class SafeHTML {
10
14
  constructor(value) {
@@ -16,20 +20,22 @@ class SafeHTML {
16
20
  }
17
21
  }
18
22
 
19
- /*
23
+ /**
20
24
  * Same as getResult, but pass context as argument to the expression.
21
25
  * Used to evaluate expressions in the context of a component.
22
26
  * @param {any} expression The expression to be evaluated.
23
27
  * @param {any} context The context to call the expression with.
24
28
  * @return {any} The result of the evaluated expression.
29
+ * @private
25
30
  */
26
31
  const getExpressionResult = (expression, context) => getResult(expression, context, context);
27
32
 
28
- /*
33
+ /**
29
34
  * Generate string with placeholders for interpolated expressions.
30
35
  * @param strings {array} Array of strings.
31
36
  * @param expressions {array} Array of expressions.
32
37
  * @return {string} String with placeholders.
38
+ * @private
33
39
  */
34
40
  const addPlaceholders = (strings, expressions) =>
35
41
  strings.reduce((out, string, i) => {
@@ -42,11 +48,12 @@ const addPlaceholders = (strings, expressions) =>
42
48
  return out;
43
49
  }, []).join('');
44
50
 
45
- /*
51
+ /**
46
52
  * Generate one dimensional array with strings and expressions.
47
53
  * @param main {string} The main template containing placeholders.
48
54
  * @param expressions {array} Array of expressions to replace placeholders.
49
55
  * @return {array} Array containing strings and expressions.
56
+ * @private
50
57
  */
51
58
  const splitPlaceholders = (main, expressions) => {
52
59
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -66,7 +73,7 @@ const splitPlaceholders = (main, expressions) => {
66
73
  return out;
67
74
  };
68
75
 
69
- /*
76
+ /**
70
77
  * Expand attributes.
71
78
  * @param attributes {array} Array of attributes as key, value pairs.
72
79
  * @param getExpressionResult {function} Function to render expressions.
@@ -74,6 +81,7 @@ const splitPlaceholders = (main, expressions) => {
74
81
  * @property {object} all All attributes.
75
82
  * @property {object} events Event listeners.
76
83
  * @property {object} attributes Attributes.
84
+ * @private
77
85
  */
78
86
  const expandAttributes = (attributes, getExpressionResult) => {
79
87
  const out = attributes.reduce((out, pair) => {
@@ -111,7 +119,7 @@ const expandAttributes = (attributes, getExpressionResult) => {
111
119
  return out;
112
120
  };
113
121
 
114
- /*
122
+ /**
115
123
  * Replace component tags with expressions.
116
124
  * `<${Component} />` or `<${Component}></${Component}>` will be replaced
117
125
  * by a function that mounts the component.
@@ -120,6 +128,7 @@ const expandAttributes = (attributes, getExpressionResult) => {
120
128
  * @param main {string} The main template.
121
129
  * @return {string} The template with components tags replaced by expressions
122
130
  * placeholders.
131
+ * @private
123
132
  */
124
133
  const expandComponents = (main, expressions) => {
125
134
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -159,7 +168,7 @@ const expandComponents = (main, expressions) => {
159
168
  );
160
169
  };
161
170
 
162
- /*
171
+ /**
163
172
  * Parse match data to get tag, attributes, inner html and close tag.
164
173
  * @param match {array}
165
174
  * @return {object}
@@ -168,6 +177,7 @@ const expandComponents = (main, expressions) => {
168
177
  * @property {string} close The closing tag.
169
178
  * @property {array} attributes Array of attributes as key, value pairs.
170
179
  * @property {string} raw The whole match.
180
+ * @private
171
181
  */
172
182
  const parseMatch = (match, expressions) => {
173
183
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -220,13 +230,7 @@ const selfClosingTags = {
220
230
  /*
221
231
  * These option keys will be extended on the component instance.
222
232
  */
223
- const componentOptions = {
224
- key : true,
225
- state : true,
226
- onCreate : true,
227
- onChange : true,
228
- onRender : true
229
- };
233
+ const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onRender'];
230
234
 
231
235
  /**
232
236
  * Components are a special kind of `View` that is designed to be easily composable,
@@ -259,9 +263,9 @@ const componentOptions = {
259
263
  class Component extends View {
260
264
  constructor(options = {}) {
261
265
  super(...arguments);
262
- // Extend "this" with options, mapping componentOptions keys.
263
- Object.keys(options).forEach(key => {
264
- if (componentOptions[key]) this[key] = options[key];
266
+ // Extend "this" with options.
267
+ componentOptions.forEach(key => {
268
+ if (key in options) this[key] = options[key];
265
269
  });
266
270
  // Store options by default.
267
271
  this.options = options;
@@ -296,20 +300,22 @@ class Component extends View {
296
300
  return this;
297
301
  }
298
302
 
299
- /*
300
- * Tell if component is a container.
303
+ /**
304
+ * Tell if `Component` is a container.
301
305
  * In which case, it will not have an element by itself.
302
306
  * It will render a single expression which is expected to return a single component as child.
303
307
  * `this.el` will be a reference to that child component's element.
304
308
  * @return {boolean}
309
+ * @private
305
310
  */
306
311
  isContainer() {
307
312
  return !!(!this.tag && this.template);
308
313
  }
309
314
 
310
- /*
311
- * Override. We don't want to ensure an element on instantiation.
315
+ /**
316
+ * Override super method. We don't want to ensure an element on instantiation.
312
317
  * We will provide it later.
318
+ * @private
313
319
  */
314
320
  ensureElement() {
315
321
  // If el is provided, delegate events.
@@ -320,18 +326,25 @@ class Component extends View {
320
326
  }
321
327
  }
322
328
 
323
- /*
324
- * Find view's element on parent node, using its data attribute.
325
- * @param parent {node} The parent node.
326
- * @return {node} The component's element.
329
+ /**
330
+ * Locate the root element of the `Component` within a specified parent node.
331
+ * This is achieved by searching for the element using the unique data attribute assigned to the `Component`.
332
+ * @param {Node} parent - The parent node to search within.
333
+ * @return {Node} The root element of the component, or `null` if not found.
334
+ * @private
327
335
  */
328
336
  findElement(parent) {
329
337
  return (parent || document).querySelector(`[${Component.DATA_ATTRIBUTE_UID}="${this.uid}"]`);
330
338
  }
331
339
 
332
- /*
333
- * Eval attributes expressions.
334
- * @return {object} Object containing add, remove and html properties.
340
+ /**
341
+ * Retrieve the attributes to be applied to the element.
342
+ * This includes attributes to be added, removed, and their HTML representation.
343
+ * @return {object} An object containing the following properties:
344
+ * @property {object} add - Attributes to be added to the element, with their values.
345
+ * @property {object} remove - Attributes to be removed from the element.
346
+ * @property {string} html - A string representation of the attributes for use in HTML.
347
+ * @private
335
348
  */
336
349
  getAttributes() {
337
350
  const add = {};
@@ -370,11 +383,13 @@ class Component extends View {
370
383
  return { add, remove, html : html.join(' ') };
371
384
  }
372
385
 
373
- /*
386
+ /**
374
387
  * Used internally on the render process.
375
- * Attach the view to the dom element.
388
+ * Attach the `Component` to the dom element providing `this.el`, delegate events,
389
+ * subscribe to model changes and call `onRender` lifecycle method with `Component.RENDER_TYPE_HYDRATE` as argument.
376
390
  * @param parent {node} The parent node.
377
391
  * @return {Rasti.Component} The component instance.
392
+ * @private
378
393
  */
379
394
  hydrate(parent) {
380
395
  // Listen to model changes and call onChange.
@@ -382,13 +397,13 @@ class Component extends View {
382
397
  // Listen to state changes and call onChange.
383
398
  if (this.state) this.subscribe(this.state);
384
399
 
385
- if (!this.isContainer()) {
400
+ if (this.isContainer()) {
401
+ this.children[0].hydrate(parent);
402
+ this.el = this.children[0].el;
403
+ } else {
386
404
  this.el = this.findElement(parent);
387
405
  this.delegateEvents();
388
406
  this.children.forEach(child => child.hydrate(this.el));
389
- } else {
390
- this.children[0].hydrate(parent);
391
- this.el = this.children[0].el;
392
407
  }
393
408
  // Call `onRender` lifecycle method.
394
409
  this.onRender.call(this, Component.RENDER_TYPE_HYDRATE);
@@ -396,30 +411,38 @@ class Component extends View {
396
411
  return this;
397
412
  }
398
413
 
399
- /*
400
- * Used internally in the render process.
401
- * Reuse a view that has `key` when its parent is rendered.
414
+ /**
415
+ * Used internally on the render process.
416
+ * Reuse a `Component` that has `key` when its parent is rendered.
417
+ * Call `onRender` lifecycle method with `Component.RENDER_TYPE_RECYCLE` as argument.
402
418
  * @param parent {node} The parent node.
403
419
  * @return {Rasti.Component} The component instance.
420
+ * @private
404
421
  */
405
422
  recycle(parent) {
406
423
  // If component is a container, call recycle on its child.
407
- if (this.isContainer()) return this.children[0].recycle(parent);
408
- // Find placeholder element to be replaced. It has same data attribute as this component.
409
- const toBeReplaced = this.findElement(parent);
410
- // Replace it with this.el.
411
- toBeReplaced.replaceWith(this.el);
424
+ if (this.isContainer()) {
425
+ this.children[0].recycle(parent);
426
+ } else {
427
+ // Find placeholder element to be replaced. It has same data attribute as this component.
428
+ const toBeReplaced = this.findElement(parent);
429
+ // Replace it with this.el.
430
+ toBeReplaced.replaceWith(this.el);
431
+ }
412
432
  // Call `onRender` lifecycle method.
413
433
  this.onRender.call(this, Component.RENDER_TYPE_RECYCLE);
414
434
  // Return `this` for chaining.
415
435
  return this;
416
436
  }
417
437
 
418
- /*
419
- * Override. Add some custom logic to super `destroy` method.
438
+ /**
439
+ * Destroy the `Component`.
440
+ * Destroy children components if any, undelegate events, stop listening to events, call `onDestroy` lifecycle method.
420
441
  * @param {object} options Options object or any arguments passed to `destroy` method will be passed to `onDestroy` method.
442
+ * @return {Rasti.View} Return `this` for chaining.
421
443
  */
422
444
  destroy() {
445
+ // Call super destroy method.
423
446
  super.destroy.apply(this, arguments);
424
447
  // Set destroyed flag to prevent a last render after destroyed.
425
448
  this.destroyed = true;
@@ -448,8 +471,11 @@ class Component extends View {
448
471
  }
449
472
 
450
473
  /**
451
- * Lifecycle method. Called when the view is rendered.
452
- * @param type {string} The render type. Can be `render`, `hydrate` or `recycle`.
474
+ * Lifecycle method. Called after the component is rendered.
475
+ * - When the component is rendered for the first time, this method is called with `Component.RENDER_TYPE_HYDRATE` as the argument.
476
+ * - When the component is updated or re-rendered, this method is called with `Component.RENDER_TYPE_RENDER` as the argument.
477
+ * - When the component is recycled (reused with the same key), this method is called with `Component.RENDER_TYPE_RECYCLE` as the argument.
478
+ * @param {string} type - The render type. Possible values are: `Component.RENDER_TYPE_HYDRATE`, `Component.RENDER_TYPE_RENDER` and `Component.RENDER_TYPE_RECYCLE`.
453
479
  */
454
480
  onRender() {}
455
481
 
@@ -531,22 +557,26 @@ class Component extends View {
531
557
  `<${tag} ${attributes} />`;
532
558
  }
533
559
 
534
- /*
535
- * View render method.
560
+ /**
561
+ * Render the `Component`.
562
+ * - If `this.el` is not present, the `Component` will be rendered as a string inside a `DocumentFragment` and hydrated, making `this.el` available. The `onRender` lifecycle method will be called with `Component.RENDER_TYPE_HYDRATE` as an argument.
563
+ * - If `this.el` is present, the method will update the attributes and inner HTML of the element, or recreate its child component in the case of a container. The `onRender` lifecycle method will be called with `Component.RENDER_TYPE_RENDER` as an argument.
564
+ * - When rendering child components, if the new children have the same key as the previous ones, they will be recycled. A recycled `Component` will call the `onRender` lifecycle method with `Component.RENDER_TYPE_RECYCLE` as an argument.
565
+ * - If the active element is inside the component, it will retain focus after the render.
566
+ * @return {Rasti.Component} The component instance.
536
567
  */
537
568
  render() {
538
569
  // Prevent a last re render if view is already destroyed.
539
570
  if (this.destroyed) return this;
540
-
571
+ // If `this.el` is not present, render the view as a string and hydrate it.
572
+ if (!this.el) {
573
+ const fragment = this.createElement('template');
574
+ fragment.innerHTML = this;
575
+ this.hydrate(fragment.content);
576
+ return this;
577
+ }
578
+ // Update attributes.
541
579
  if (!this.isContainer()) {
542
- // If `this.el` is not present, render the view as a string and hydrate it.
543
- if (!this.el) {
544
- const fragment = this.createElement('template');
545
- fragment.innerHTML = this;
546
- this.hydrate(fragment.content);
547
- return this;
548
- }
549
- // Set `this.el` attributes.
550
580
  const attributes = this.getAttributes();
551
581
  // Remove attributes.
552
582
  Object.keys(attributes.remove).forEach(key => {
@@ -557,7 +587,7 @@ class Component extends View {
557
587
  this.el.setAttribute(key, attributes.add[key]);
558
588
  });
559
589
  }
560
- // Check for `template` to see if view has innerHTML.
590
+ // Check for `template` to see if view has innerHTML or a child component.
561
591
  if (this.template) {
562
592
  // Store active element.
563
593
  const activeElement = document.activeElement;
@@ -598,8 +628,8 @@ class Component extends View {
598
628
  this.addChild(nextChildren[0]).hydrate(fragment.content);
599
629
  // Get next child element.
600
630
  const nextEl = fragment.content.children[0];
601
- // If `this.el` is present, replace it with nextEl.
602
- if (this.el) this.el.replaceWith(nextEl);
631
+ // Replace `this.el` with nextEl.
632
+ this.el.replaceWith(nextEl);
603
633
  // Set `this.el` to nextEl.
604
634
  this.el = nextEl;
605
635
  } else if (recycledChildren[0]) {
@@ -635,10 +665,10 @@ class Component extends View {
635
665
  }
636
666
 
637
667
  /**
638
- * Mark a string as safe HTML to be rendered.
639
- * Normally you don't need to use this method, as Rasti will automatically mark strings
640
- * as safe HTML when the component is @link{#module_component_create created} and when
641
- * using the @link{#module_component__partial Component.partial} method.
668
+ * Mark a string as safe HTML to be rendered.
669
+ * Normally you don't need to use this method, as Rasti will automatically mark string literals
670
+ * as safe HTML when the component is {@link #module_component_create created} and when
671
+ * using the {@link #module_component__partial Component.partial} method.
642
672
  * Be sure that the string is safe to be rendered, as it will be inserted into the DOM without any sanitization.
643
673
  * @static
644
674
  * @param {string} value
@@ -752,8 +782,8 @@ class Component extends View {
752
782
  * // Create a navigation component. Add buttons as children. Iterate over items.
753
783
  * const Navigation = Component.create`
754
784
  * <nav>
755
- * ${self => self.options.items.map(
756
- * item => self.partial`<${Button}>${item.label}</${Button}>`
785
+ * ${({ options, partial }) => options.items.map(
786
+ * item => partial`<${Button}>${item.label}</${Button}>`
757
787
  * )}
758
788
  * </nav>
759
789
  * `;
@@ -772,11 +802,11 @@ class Component extends View {
772
802
  * ${self => self.renderChildren()}
773
803
  * </button>
774
804
  * `;
775
- * // Create a container using the button component
805
+ * // Create a container that renders a Button component.
776
806
  * const ButtonOk = Component.create`
777
807
  * <${Button} className="ok">Ok</${Button}>
778
808
  * `;
779
- * // Create a button component using a function
809
+ * // Create a container that renders a Button component, using a function.
780
810
  * const ButtonCancel = Component.create(() => Button.mount({
781
811
  * className: 'cancel',
782
812
  * renderChildren: () => 'Cancel'
package/es/View.js CHANGED
@@ -4,15 +4,7 @@ import getResult from './utils/getResult.js';
4
4
  /*
5
5
  * These option keys will be extended on the view instance.
6
6
  */
7
- const viewOptions = {
8
- el : true,
9
- tag : true,
10
- attributes : true,
11
- events : true,
12
- model : true,
13
- template : true,
14
- onDestroy : true
15
- };
7
+ const viewOptions = ['el', 'tag', 'attributes', 'events', 'model', 'template', 'onDestroy'];
16
8
 
17
9
  /**
18
10
  * - Listens for changes and renders the UI.
@@ -35,6 +27,7 @@ const viewOptions = {
35
27
  * @property {object|function} events Object in the format `{'event selector' : 'listener'}`. It will be used to bind delegated event listeners to the root element. If it is a function, it will be called to get the events object, bound to the view instance. See {@link module_view_delegateevents View.delegateEvents}.
36
28
  * @property {object} model A model or any object containing data and business logic.
37
29
  * @property {function} template A function that returns a string with the view's inner HTML. See {@link module_view__render View.render}.
30
+ * @property {number} uid Unique identifier for the view instance. This can be used to generate unique IDs for elements within the view. It is automatically generated and should not be set manually.
38
31
  * @example
39
32
  * import { View } from 'rasti';
40
33
  *
@@ -63,7 +56,7 @@ class View extends Emitter {
63
56
  this.preinitialize.apply(this, arguments);
64
57
  // Generate unique id.
65
58
  // Useful to generate element ids.
66
- this.uid = `uid${++View.uid}`;
59
+ this.uid = `rasti-${++View.uid}`;
67
60
  // Store delegated event listeners,
68
61
  // so they can be unbound later.
69
62
  this.delegatedEventListeners = [];
@@ -72,9 +65,9 @@ class View extends Emitter {
72
65
  this.children = [];
73
66
  // Mutable array to store handlers to be called on destroy.
74
67
  this.destroyQueue = [];
75
- // Extend "this" with options, mapping viewOptions keys.
76
- Object.keys(options).forEach(key => {
77
- if (viewOptions[key]) this[key] = options[key];
68
+ // Extend "this" with options.
69
+ viewOptions.forEach(key => {
70
+ if (key in options) this[key] = options[key];
78
71
  });
79
72
  // Ensure that the view has a root element at `this.el`.
80
73
  this.ensureElement();
@@ -303,17 +296,16 @@ class View extends Emitter {
303
296
  }
304
297
 
305
298
  /**
306
- * Renders the view.
299
+ * Renders the view.
307
300
  * This method should be overridden with custom logic.
308
301
  * The only convention is to manipulate the DOM within the scope of `this.el`,
309
- * and to return `this` for chaining.
310
- * If you add any child views, you should call `this.destroyChildren` before re-rendering.
311
- * The default implementation sets the innerHTML of `this.el` with the result
312
- * of calling `this.template`, passing `this.model` as an argument.
313
- * <br><br> &#9888; **Security Notice:** The default implementation utilizes `innerHTML` on the root element
314
- * for rendering, which may introduce Cross-Site Scripting (XSS) risks. Ensure that any user-generated
315
- * content is properly sanitized before inserting it into the DOM. You can use the @link{#module_view_sanitize View.sanitize}
316
- * static method to escape HTML entities in a string.
302
+ * and to return `this` for chaining.
303
+ * If you add any child views, you should call `this.destroyChildren` before re-rendering.
304
+ * The default implementation updates `this.el`'s innerHTML with the result
305
+ * of calling `this.template`, passing `this.model` as the argument.
306
+ * <br><br> &#9888; **Security Notice:** The default implementation utilizes `innerHTML`, which may introduce Cross-Site Scripting (XSS) risks.
307
+ * Ensure that any user-generated content is properly sanitized before inserting it into the DOM.
308
+ * You can use the {@link #module_view_sanitize View.sanitize} static method to escape HTML entities in a string.
317
309
  * For best practices on secure data handling, refer to the
318
310
  * [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
319
311
  * @return {Rasti.View} Returns `this` for chaining.
@@ -344,8 +336,16 @@ class View extends Emitter {
344
336
  }
345
337
  }
346
338
 
347
- /*
348
- * Unique Id
339
+ /**
340
+ * Counter for generating unique IDs for view instances.
341
+ * This is primarily used to assign unique identifiers to each view instance (`this.uid`), which can be helpful for tasks like
342
+ * generating element IDs.
343
+ * {@link #module_component Component}s use `this.uid` to generate data attributes for their elements, to be looked up on hydration.
344
+ * For server-side rendering, this counter should be reset to `0` on every request to ensure that the generated
345
+ * unique IDs match those on the client, enabling seamless hydration of components.
346
+ * @static
347
+ * @type {number}
348
+ * @default 0
349
349
  */
350
350
  View.uid = 0;
351
351