@torpor/view 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torpor/view",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Torpor's view library, for writing and mounting components",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -35,7 +35,7 @@
35
35
  "@testing-library/user-event": "^14.6.1",
36
36
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
37
37
  "@types/jsdom": "^21.1.7",
38
- "@types/node": "^22.15.21",
38
+ "@types/node": "^22.15.29",
39
39
  "fast-glob": "^3.3.3",
40
40
  "jsdom": "^26.1.0",
41
41
  "sucrase": "^3.35.0",
@@ -43,7 +43,7 @@
43
43
  "tsup": "^8.5.0",
44
44
  "tsx": "^4.19.4",
45
45
  "typescript": "^5.8.3",
46
- "vitest": "^3.1.4"
46
+ "vitest": "^3.2.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc --noEmit && tsup && tsx ./scripts/copyToRepl.ts",
@@ -173,9 +173,23 @@ function buildElementAttributes(
173
173
  // TODO: Flatten this out
174
174
  // TODO: Add an error if any reactive attributes are used non-reactively
175
175
 
176
+ // Do &ref first, just in case any other attributes depend on it being set
177
+ // e.g. if you have a `style` attribute that depends on the element's size
178
+ const refAttribute = node.attributes.find((a) => a.name === "&ref");
179
+ if (refAttribute) {
180
+ let value = refAttribute.value;
181
+ if (value != null && isFullyReactive(value)) {
182
+ // Bind the DOM element to a user-defined variable
183
+ value = value.substring(1, value.length - 1);
184
+ b.append(`${value} = ${varName};`);
185
+ }
186
+ }
187
+
176
188
  for (let { name, value } of node.attributes) {
177
189
  if (name === "self" && node.tagName === ":element") {
178
190
  // Ignore this special attribute
191
+ } else if (name === "&ref") {
192
+ // Ignore this one, it should have been done already, above
179
193
  } else if (name.startsWith("{") && name.endsWith("}")) {
180
194
  // It's a shortcut attribute
181
195
  // It could be e.g. {width} or it could be {$state.width}, but
@@ -191,10 +205,7 @@ function buildElementAttributes(
191
205
  value = `\`${trimQuotes(value).replaceAll("{", "${")}\``;
192
206
  }
193
207
 
194
- if (name === "&ref") {
195
- // Bind the DOM element to a user-defined variable
196
- b.append(`${value} = ${varName};`);
197
- } else if (name === "&group") {
208
+ if (name === "&group") {
198
209
  buildBindGroupAttribute(node, varName, value, status, b);
199
210
  } else if (name === "&value" || name === "&checked") {
200
211
  buildBindAttribute(node, varName, name, value, status, b);
@@ -6,6 +6,7 @@ import { type BuildStatus } from "./BuildStatus";
6
6
  import buildAddFragment from "./buildAddFragment";
7
7
  import buildFragment from "./buildFragment";
8
8
  import buildNode from "./buildNode";
9
+ import replaceForVarNames from "./replaceForVarNames";
9
10
 
10
11
  // TODO: Are there too many branches for ifs etc?
11
12
 
@@ -55,7 +56,7 @@ function buildIfBranch(
55
56
  ) {
56
57
  status.imports.add("t_run_branch");
57
58
 
58
- b.append(`${node.statement} {`);
59
+ b.append(`${replaceForVarNames(node.statement, status)} {`);
59
60
  b.append(`t_run_branch(${rangeName}, ${index}, () => {`);
60
61
 
61
62
  buildFragment(node, status, b, parentName, "t_before");
@@ -5,6 +5,7 @@ import { type BuildStatus } from "./BuildStatus";
5
5
  import buildAddFragment from "./buildAddFragment";
6
6
  import buildFragment from "./buildFragment";
7
7
  import buildNode from "./buildNode";
8
+ import replaceForVarNames from "./replaceForVarNames";
8
9
 
9
10
  export default function buildReplaceNode(node: ControlNode, status: BuildStatus, b: Builder): void {
10
11
  const replaceAnchorName = node.varName!;
@@ -38,7 +39,7 @@ function buildReplaceBranch(
38
39
  ) {
39
40
  status.imports.add("t_run_branch");
40
41
 
41
- b.append(`${node.statement};`);
42
+ b.append(`${replaceForVarNames(node.statement, status)};`);
42
43
  b.append(`t_run_branch(${rangeName}, -1, () => {`);
43
44
 
44
45
  buildFragment(node, status, b, parentName, "t_before");
@@ -6,6 +6,7 @@ import { type BuildStatus } from "./BuildStatus";
6
6
  import buildAddFragment from "./buildAddFragment";
7
7
  import buildFragment from "./buildFragment";
8
8
  import buildNode from "./buildNode";
9
+ import replaceForVarNames from "./replaceForVarNames";
9
10
 
10
11
  export default function buildSwitchNode(node: ControlNode, status: BuildStatus, b: Builder): void {
11
12
  const switchParentName = node.parentName!;
@@ -55,7 +56,7 @@ function buildSwitchBranch(
55
56
  ) {
56
57
  status.imports.add("t_run_branch");
57
58
 
58
- b.append(`${node.statement} {`);
59
+ b.append(`${replaceForVarNames(node.statement, status)} {`);
59
60
  b.append(`t_run_branch(${rangeName}, ${index}, () => {`);
60
61
 
61
62
  buildFragment(node, status, b, parentName, "t_before");
@@ -18,13 +18,13 @@ export default async function animate(
18
18
  // - store the animation
19
19
  // - reverse it if it's running and the range appears/disappears
20
20
  // - transfer the animation to the new element
21
-
22
21
  let animationOptions = Object.assign(
23
22
  {
24
23
  direction: enter ? "normal" : "reverse",
25
- // TODO: Should probably have a global setting for this somewhere
24
+ // TODO: Should probably have a global setting for these somewhere
26
25
  // And respect browser reduce motion settings
27
- duration: 1000,
26
+ duration: 300,
27
+ easing: "ease-in-out",
28
28
  fill: "forwards",
29
29
  },
30
30
  options,
@@ -32,9 +32,13 @@ export default async function animate(
32
32
 
33
33
  const animation = el.animate(keyframes, animationOptions);
34
34
 
35
- // HACK: I'm not entirely sure why this is needed, but without it,
36
- // animations never start
37
- animation.timeline = el.ownerDocument.timeline;
35
+ // HACK: I'm not entirely sure why we need to sent the animation's timeline,
36
+ // but without it, animations never start
37
+ // HACK: We also need to make sure the element has been added to the DOM
38
+ // with a timeout
39
+ setTimeout(() => {
40
+ animation.timeline = el.ownerDocument.timeline;
41
+ }, 1);
38
42
 
39
43
  // Add the animation to the active range, so that the range won't be cleared
40
44
  // until the animation is completed
@@ -41,16 +41,19 @@ export default function addFragment(
41
41
  }
42
42
 
43
43
  // If we're adding this fragment to the DOM, we can now run any $mount
44
- // functions, add our stashed events and play our stashed animations
44
+ // effects, add our stashed events and play our stashed animations
45
45
  if (!parentIsFragment) {
46
- // Set the active range for each event and animation so it will get
47
- // attached to the right one and set it back afterwards
48
-
49
- for (let effect of context.mountEffects) {
50
- $run(effect);
46
+ // Only run $mount effects if not hydrating (if hydrating, they will get
47
+ // run at the end when everything is hooked up)
48
+ if (!context.hydrationNode) {
49
+ for (let effect of context.mountEffects) {
50
+ $run(effect);
51
+ }
52
+ context.mountEffects.length = 0;
51
53
  }
52
- context.mountEffects.length = 0;
53
54
 
55
+ // Set the active range for each event so it will get attached to the
56
+ // right one and set it back afterwards
54
57
  for (let event of context.stashedEvents) {
55
58
  context.activeRange = event.range;
56
59
  $run(function addFragmentEvent() {
@@ -62,6 +65,8 @@ export default function addFragment(
62
65
  }
63
66
  context.stashedEvents.length = 0;
64
67
 
68
+ // Set the active range for each animation so it will get attached to
69
+ // the right one and set it back afterwards
65
70
  for (let animation of context.stashedAnimations) {
66
71
  context.activeRange = animation.range;
67
72
  $run(function addFragmentAnimation() {
@@ -1,5 +1,6 @@
1
1
  import { type Component } from "../types/Component";
2
2
  import { type SlotRender } from "../types/SlotRender";
3
+ import $run from "./$run";
3
4
  import context from "./context";
4
5
 
5
6
  /**
@@ -22,4 +23,10 @@ export default function hydrate(
22
23
  component(parent, null, props, undefined, slots);
23
24
 
24
25
  context.hydrationNode = null;
26
+
27
+ // Now that we've hydrated, we can run $mount effects
28
+ for (let effect of context.mountEffects) {
29
+ $run(effect);
30
+ }
31
+ context.mountEffects.length = 0;
25
32
  }
@@ -28,7 +28,7 @@ function moveChildEffects(
28
28
  newData.propData.set(key, props);
29
29
 
30
30
  let oldPropValue = oldValue[key];
31
- let oldPropData = oldPropValue[proxyDataSymbol];
31
+ let oldPropData = oldPropValue != null ? oldPropValue[proxyDataSymbol] : undefined;
32
32
 
33
33
  // If the old value was a proxy, make the new value one too
34
34
  // TODO: Make sure we aren't accidentally creating proxies for
@@ -2,7 +2,6 @@ import { getByText, queryByText } from "@testing-library/dom";
2
2
  import "@testing-library/jest-dom/vitest";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import { beforeAll, expect, test } from "vitest";
5
- import { $watch } from "../../src";
6
5
  import buildOutputFiles from "../buildOutputFiles";
7
6
  import hydrateComponent from "../hydrateComponent";
8
7
  import importComponent from "../importComponent";
@@ -14,38 +13,28 @@ beforeAll(() => {
14
13
  buildOutputFiles(componentPath);
15
14
  });
16
15
 
17
- interface State {
18
- text: string;
19
- }
20
-
21
16
  test("props reactive -- mounted", async () => {
22
- let $state = $watch({ text: "before" });
23
-
24
17
  const container = document.createElement("div");
25
18
  const component = await importComponent(componentPath, "client");
26
- mountComponent(container, component, $state);
19
+ mountComponent(container, component);
27
20
 
28
- check(container, $state);
21
+ await check(container);
29
22
  });
30
23
 
31
24
  test("props reactive -- hydrated", async () => {
32
- let $state = $watch({ text: "before" });
33
-
34
25
  const container = document.createElement("div");
35
26
  const clientComponent = await importComponent(componentPath, "client");
36
27
  const serverComponent = await importComponent(componentPath, "server");
37
- hydrateComponent(container, clientComponent, serverComponent, $state);
28
+ hydrateComponent(container, clientComponent, serverComponent);
38
29
 
39
- check(container, $state);
30
+ await check(container);
40
31
  });
41
32
 
42
- async function check(container: HTMLElement, $state: State) {
33
+ async function check(container: HTMLElement) {
43
34
  expect(queryByText(container, "before")).toBeInTheDocument();
44
35
 
45
36
  await userEvent.click(getByText(container, "Update text"));
46
37
 
47
- $state.text = "after";
48
-
49
38
  expect(queryByText(container, "before")).not.toBeInTheDocument();
50
39
  expect(queryByText(container, "after")).toBeInTheDocument();
51
40
  }