ripple 0.2.89 → 0.2.90

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.89",
6
+ "version": "0.2.90",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -455,10 +455,25 @@ function RipplePlugin(config) {
455
455
  jsx_parseExpressionContainer() {
456
456
  let node = this.startNode();
457
457
  this.next();
458
+ let tracked = false;
459
+
460
+ if (this.value === 'html') {
461
+ node.html = true;
462
+ this.next();
463
+ if (this.type.label === '@') {
464
+ this.next(); // consume @
465
+ tracked = true;
466
+ }
467
+ }
458
468
 
459
469
  node.expression =
460
470
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
461
471
  this.expect(tt.braceR);
472
+
473
+ if (tracked && node.expression.type === 'Identifier') {
474
+ node.expression.tracked = true;
475
+ }
476
+
462
477
  return this.finishNode(node, 'JSXExpressionContainer');
463
478
  }
464
479
 
@@ -954,7 +969,8 @@ function RipplePlugin(config) {
954
969
 
955
970
  if (this.type.label === '{') {
956
971
  const node = this.jsx_parseExpressionContainer();
957
- node.type = 'Text';
972
+ node.type = node.html ? 'Html' : 'Text';
973
+ delete node.html;
958
974
  body.push(node);
959
975
  } else if (this.type.label === '}') {
960
976
  return;
@@ -133,9 +133,7 @@ function visit_title_element(node, context) {
133
133
  ),
134
134
  );
135
135
  } else {
136
- context.state.init.push(
137
- b.stmt(b.assignment('=', b.id('_$_.document.title'), result)),
138
- );
136
+ context.state.init.push(b.stmt(b.assignment('=', b.id('_$_.document.title'), result)));
139
137
  }
140
138
  }
141
139
 
@@ -346,7 +344,7 @@ const visitors = {
346
344
 
347
345
  return b.new(
348
346
  b.id('TrackedObject'),
349
- b.object(node.properties.map((prop) => context.visit(prop)))
347
+ b.object(node.properties.map((prop) => context.visit(prop))),
350
348
  );
351
349
  }
352
350
 
@@ -723,9 +721,10 @@ const visitors = {
723
721
  const metadata = { tracking: false, await: false };
724
722
  let expression = visit(class_attribute.value, { ...state, metadata });
725
723
 
726
- const hash_arg = node.metadata.scoped && state.component.css
727
- ? b.literal(state.component.css.hash)
728
- : undefined;
724
+ const hash_arg =
725
+ node.metadata.scoped && state.component.css
726
+ ? b.literal(state.component.css.hash)
727
+ : undefined;
729
728
  const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg';
730
729
 
731
730
  if (metadata.tracking) {
@@ -1491,6 +1490,7 @@ function transform_children(children, context) {
1491
1490
  node.type === 'IfStatement' ||
1492
1491
  node.type === 'TryStatement' ||
1493
1492
  node.type === 'ForOfStatement' ||
1493
+ node.type === 'Html' ||
1494
1494
  (node.type === 'Element' &&
1495
1495
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
1496
1496
  ) ||
@@ -1585,6 +1585,14 @@ function transform_children(children, context) {
1585
1585
  visit(node, { ...state, flush_node, namespace: state.namespace });
1586
1586
  } else if (node.type === 'HeadElement') {
1587
1587
  visit(node, { ...state, flush_node, namespace: state.namespace });
1588
+ } else if (node.type === 'Html') {
1589
+ const metadata = { tracking: false, await: false };
1590
+ const expression = visit(node.expression, { ...state, metadata });
1591
+
1592
+ context.state.template.push('<!>');
1593
+
1594
+ const id = flush_node();
1595
+ state.update.push(b.stmt(b.call('_$_.html', id, b.thunk(expression))));
1588
1596
  } else if (node.type === 'Text') {
1589
1597
  const metadata = { tracking: false, await: false };
1590
1598
  const expression = visit(node.expression, { ...state, metadata });
@@ -1633,7 +1641,7 @@ function transform_children(children, context) {
1633
1641
  visit_head_element(head_element, context);
1634
1642
  }
1635
1643
 
1636
- if (context.state.inside_head) {
1644
+ if (context.state.inside_head) {
1637
1645
  const title_element = children.find(
1638
1646
  (node) =>
1639
1647
  node.type === 'Element' && node.id.type === 'Identifier' && node.id.name === 'title',
@@ -318,6 +318,20 @@ export function is_destroyed(target_block) {
318
318
  return true;
319
319
  }
320
320
 
321
+ /**
322
+ * @param {Node | null} node
323
+ * @param {Node} end
324
+ */
325
+ export function remove_block_dom(node, end) {
326
+ while (node !== null) {
327
+ /** @type {Node | null} */
328
+ var next = node === end ? null : next_sibling(node);
329
+
330
+ /** @type {Element | Text | Comment} */ (node).remove();
331
+ node = next;
332
+ }
333
+ }
334
+
321
335
  /**
322
336
  * @param {Block} block
323
337
  * @param {boolean} [remove_dom]
@@ -330,16 +344,7 @@ export function destroy_block(block, remove_dom = true) {
330
344
 
331
345
  if ((remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0) || (f & HEAD_BLOCK) !== 0) {
332
346
  var s = block.s;
333
- var node = s.start;
334
- var end = s.end;
335
-
336
- while (node !== null) {
337
- var next = node === end ? null : next_sibling(node);
338
-
339
- node.remove();
340
- node = next;
341
- }
342
-
347
+ remove_block_dom(s.start, s.end);
343
348
  removed = true;
344
349
  }
345
350
 
@@ -0,0 +1,41 @@
1
+ /** @import { Block } from '#client' */
2
+
3
+ import { remove_block_dom, render } from './blocks.js';
4
+ import { first_child } from './operations.js';
5
+ import { active_block } from './runtime.js';
6
+ import { assign_nodes, create_fragment_from_html } from './template.js';
7
+
8
+ /**
9
+ * Renders dynamic HTML content into the DOM by inserting it before the anchor node.
10
+ * Manages the lifecycle of HTML blocks, removing old content and inserting new content.
11
+ *
12
+ * TODO handle SVG/MathML
13
+ *
14
+ * @param {ChildNode} node
15
+ * @param {() => string} get_html
16
+ * @returns {void}
17
+ */
18
+ export function html(node, get_html) {
19
+ /** @type {ChildNode} */
20
+ var anchor = node;
21
+ /** @type {string} */
22
+ var html = '';
23
+
24
+ render(() => {
25
+ var block = /** @type {Block} */ (active_block);
26
+ html = get_html() + '';
27
+
28
+ if (block.s !== null && block.s.start !== null) {
29
+ remove_block_dom(block.s.start, /** @type {Node} */ (block.s.end));
30
+ block.s.start = block.s.end = null;
31
+ }
32
+
33
+ if (html === '') return;
34
+ /** @type {DocumentFragment} */
35
+ var node = create_fragment_from_html(html);
36
+
37
+ assign_nodes(/** @type {Node } */ (first_child(node)), /** @type {Node} */ (node.lastChild));
38
+
39
+ anchor.before(node);
40
+ });
41
+ }
@@ -64,4 +64,6 @@ export { tracked_object } from '../../object.js';
64
64
 
65
65
  export { head } from './head.js';
66
66
 
67
- export { script } from './script.js';
67
+ export { script } from './script.js';
68
+
69
+ export { html } from './html.js';
@@ -35,7 +35,7 @@ export function assign_nodes(start, end) {
35
35
  * @param {boolean} use_mathml_namespace - Whether to use MathML namespace.
36
36
  * @returns {DocumentFragment}
37
37
  */
38
- function create_fragment_from_html(html, use_svg_namespace = false, use_mathml_namespace = false) {
38
+ export function create_fragment_from_html(html, use_svg_namespace = false, use_mathml_namespace = false) {
39
39
  if (use_svg_namespace) {
40
40
  return from_namespace(html, 'svg');
41
41
  }
@@ -0,0 +1,40 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`html directive > renders dynamic html 1`] = `
4
+ <div>
5
+ <!---->
6
+ <div>
7
+ Test
8
+ </div>
9
+ <!---->
10
+ <button>
11
+ Update
12
+ </button>
13
+
14
+ </div>
15
+ `;
16
+
17
+ exports[`html directive > renders dynamic html 2`] = `
18
+ <div>
19
+ <!---->
20
+ <div>
21
+ Updated
22
+ </div>
23
+ <!---->
24
+ <button>
25
+ Update
26
+ </button>
27
+
28
+ </div>
29
+ `;
30
+
31
+ exports[`html directive > renders static html 1`] = `
32
+ <div>
33
+ <!---->
34
+ <div>
35
+ Test
36
+ </div>
37
+ <!---->
38
+
39
+ </div>
40
+ `;
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track } from 'ripple';
3
+
4
+ describe('html directive', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('renders static html', () => {
24
+ component App() {
25
+ let str = '<div>Test</div>';
26
+
27
+ {html str}
28
+ }
29
+
30
+ render(App);
31
+ expect(container).toMatchSnapshot();
32
+ });
33
+
34
+ it('renders dynamic html', () => {
35
+ component App() {
36
+ let str = track('<div>Test</div>');
37
+
38
+ {html @str}
39
+
40
+ <button onClick={() => { @str = '<div>Updated</div>'; }}>{'Update'}</button>
41
+ }
42
+
43
+ render(App);
44
+ expect(container).toMatchSnapshot();
45
+
46
+ const button = container.querySelector('button');
47
+ button.click();
48
+ flushSync();
49
+
50
+ expect(container).toMatchSnapshot();
51
+ });
52
+ });