@zubra/runtime 5.12.0 → 5.13.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/README.md CHANGED
@@ -28,7 +28,7 @@ ships one, this package deletes code instead of components being rewritten.
28
28
  `// @hot-end` in `src/`). Bundlers pick it through the `production` export condition (Vite and
29
29
  webpack do in production builds), and `@zubra/server` serves it when `NODE_ENV` is `production`.
30
30
  It's generated by `npm run build:production` and checked in; `npm run test:tools` fails if it's
31
- out of date, and measures both builds against their size budgets (13.5 KB and 13 KB gzipped).
31
+ out of date, and measures both builds against their size budgets (16.25 KB and 15.25 KB gzipped).
32
32
 
33
33
  ## Signals
34
34
 
@@ -821,7 +821,7 @@ function bind(part, node, hydrating, context) {
821
821
  node[part.property] = $r(part.value());
822
822
  });
823
823
  } else if (part.attribute) {
824
- bindAttribute(part, node);
824
+ bindAttribute(part, node, hydrating);
825
825
  } else if (part.when) {
826
826
  bindWhen(part, node, hydrating, context);
827
827
  } else if (part.await) {
@@ -1130,13 +1130,24 @@ function routeChildren(host, context, slots, tag) {
1130
1130
  // `checked={done}` with clicks.
1131
1131
  const LIVE = new Set(['input.value', 'textarea.value', 'select.value', 'input.checked', 'option.selected']);
1132
1132
 
1133
- function bindAttribute(part, node) {
1133
+ // Changed by someone before the element started: no longer the server's default.
1134
+ const touched = (n, k) => (n.options ? [...n.options].some((o) => o.selected !== o.defaultSelected) : n[k] !== n[`default${k[0].toUpperCase()}${k.slice(1)}`]);
1135
+
1136
+ function bindAttribute(part, node, hydrating = false) {
1134
1137
  const name = part.attribute;
1135
1138
  // For live state: the last value written, so the property is written only when the bound
1136
1139
  // value changes (what the user typed stays until then).
1137
1140
  let written;
1141
+ // Server HTML someone already used keeps what they did until the bound value changes.
1142
+ const live = LIVE.has(`${node.localName}.${name}`);
1143
+ let keep = hydrating && live && touched(node, name);
1138
1144
  effect(() => {
1139
1145
  const value = $r(part.value());
1146
+ if (keep) {
1147
+ keep = false;
1148
+ written = value;
1149
+ return;
1150
+ }
1140
1151
  // Another library's custom element: a property when it has one (React 19's rule).
1141
1152
  if (part.prop && part.prop in node) {
1142
1153
  node[part.prop] = value;
@@ -1149,14 +1160,14 @@ function bindAttribute(part, node) {
1149
1160
  }
1150
1161
  // A control's live state is its property (the attribute is only the default once the
1151
1162
  // user has touched it).
1152
- if (LIVE.has(`${node.localName}.${name}`)) {
1163
+ if (live) {
1153
1164
  if (value !== written) node[name] = written = value;
1154
1165
  return;
1155
1166
  }
1156
1167
  node.toggleAttribute(name, value);
1157
1168
  return;
1158
1169
  }
1159
- if (LIVE.has(`${node.localName}.${name}`)) {
1170
+ if (live) {
1160
1171
  const text = value == null ? '' : String(value);
1161
1172
  if (text !== written) {
1162
1173
  // Unchanged text isn't written: that would move the caret and lose a selection.
@@ -1164,7 +1175,7 @@ function bindAttribute(part, node) {
1164
1175
  written = text;
1165
1176
  // A <select>'s options may come from a list inside it that renders after this part:
1166
1177
  // until the option exists the value doesn't take, so set it again once they're there.
1167
- if (node.localName === 'select' && node.value !== text) {
1178
+ if (node.options && node.value !== text) {
1168
1179
  queueMicrotask(() => {
1169
1180
  if (written === text && node.value !== text) node.value = text;
1170
1181
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zubra/runtime",
3
- "version": "5.12.0",
3
+ "version": "5.13.1",
4
4
  "description": "Browser runtime for Zubra compiled components, built on the TC39 Signals API",
5
5
  "license": "ISC",
6
6
  "repository": {
package/src/element.js CHANGED
@@ -846,7 +846,7 @@ function bind(part, node, hydrating, context) {
846
846
  node[part.property] = $r(part.value());
847
847
  });
848
848
  } else if (part.attribute) {
849
- bindAttribute(part, node);
849
+ bindAttribute(part, node, hydrating);
850
850
  } else if (part.when) {
851
851
  bindWhen(part, node, hydrating, context);
852
852
  } else if (part.await) {
@@ -1158,13 +1158,24 @@ function routeChildren(host, context, slots, tag) {
1158
1158
  // `checked={done}` with clicks.
1159
1159
  const LIVE = new Set(['input.value', 'textarea.value', 'select.value', 'input.checked', 'option.selected']);
1160
1160
 
1161
- function bindAttribute(part, node) {
1161
+ // Changed by someone before the element started: no longer the server's default.
1162
+ const touched = (n, k) => (n.options ? [...n.options].some((o) => o.selected !== o.defaultSelected) : n[k] !== n[`default${k[0].toUpperCase()}${k.slice(1)}`]);
1163
+
1164
+ function bindAttribute(part, node, hydrating = false) {
1162
1165
  const name = part.attribute;
1163
1166
  // For live state: the last value written, so the property is written only when the bound
1164
1167
  // value changes (what the user typed stays until then).
1165
1168
  let written;
1169
+ // Server HTML someone already used keeps what they did until the bound value changes.
1170
+ const live = LIVE.has(`${node.localName}.${name}`);
1171
+ let keep = hydrating && live && touched(node, name);
1166
1172
  effect(() => {
1167
1173
  const value = $r(part.value());
1174
+ if (keep) {
1175
+ keep = false;
1176
+ written = value;
1177
+ return;
1178
+ }
1168
1179
  // Another library's custom element: a property when it has one (React 19's rule).
1169
1180
  if (part.prop && part.prop in node) {
1170
1181
  node[part.prop] = value;
@@ -1177,14 +1188,14 @@ function bindAttribute(part, node) {
1177
1188
  }
1178
1189
  // A control's live state is its property (the attribute is only the default once the
1179
1190
  // user has touched it).
1180
- if (LIVE.has(`${node.localName}.${name}`)) {
1191
+ if (live) {
1181
1192
  if (value !== written) node[name] = written = value;
1182
1193
  return;
1183
1194
  }
1184
1195
  node.toggleAttribute(name, value);
1185
1196
  return;
1186
1197
  }
1187
- if (LIVE.has(`${node.localName}.${name}`)) {
1198
+ if (live) {
1188
1199
  const text = value == null ? '' : String(value);
1189
1200
  if (text !== written) {
1190
1201
  // Unchanged text isn't written: that would move the caret and lose a selection.
@@ -1192,7 +1203,7 @@ function bindAttribute(part, node) {
1192
1203
  written = text;
1193
1204
  // A <select>'s options may come from a list inside it that renders after this part:
1194
1205
  // until the option exists the value doesn't take, so set it again once they're there.
1195
- if (node.localName === 'select' && node.value !== text) {
1206
+ if (node.options && node.value !== text) {
1196
1207
  queueMicrotask(() => {
1197
1208
  if (written === text && node.value !== text) node.value = text;
1198
1209
  });