@cosmicdrift/kumiko-headless 0.192.0 → 0.193.0

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": "@cosmicdrift/kumiko-headless",
3
- "version": "0.192.0",
3
+ "version": "0.193.0",
4
4
  "description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-framework": "0.192.0",
39
+ "@cosmicdrift/kumiko-framework": "0.193.0",
40
40
  "temporal-polyfill": "^0.3.2",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -214,6 +214,23 @@ describe("createFormController — submit()", () => {
214
214
  expect(disp.writeSpy).toHaveBeenCalledWith("app:write:task:create", { note: "" });
215
215
  });
216
216
 
217
+ test("payloadMode: 'values' with stripEmptySeeds: false — keeps an untouched empty-string field", async () => {
218
+ // Same setup as the strip test above, but the caller opts out — the
219
+ // untouched "" seed must survive unchanged in the payload.
220
+ const disp = makeDispatcher();
221
+ const form = createFormController({
222
+ initial: { title: "hello", dueDate: "" },
223
+ submit: { dispatcher: disp, type: "app:write:task:create", stripEmptySeeds: false },
224
+ });
225
+
226
+ await form.submit();
227
+
228
+ expect(disp.writeSpy).toHaveBeenCalledWith("app:write:task:create", {
229
+ title: "hello",
230
+ dueDate: "",
231
+ });
232
+ });
233
+
217
234
  test("stale-submit race: edits during the in-flight write stay dirty after success", async () => {
218
235
  // User submits "hello", the network takes 50ms. During those 50ms the
219
236
  // user types "world" into the same field. The server sees "hello"
@@ -325,6 +325,8 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
325
325
  };
326
326
  }
327
327
  payload = submittedSnapshot.changes;
328
+ } else if (submitCfg.stripEmptySeeds === false) {
329
+ payload = submittedValues;
328
330
  } else {
329
331
  payload = stripUntouchedEmptyStrings(submittedValues, submittedSnapshot.initial);
330
332
  }
package/src/form/types.ts CHANGED
@@ -231,8 +231,14 @@ export type FormControllerOptions<TValues extends FormValues, TCtx = unknown> =
231
231
  // objects — but that assumes an update flow. For creates, `changes ===
232
232
  // values` in practice because initial is empty.
233
233
  //
234
- // - "values" — send the full current `values` object. Right for create
234
+ // - "values" — send the current `values` object. Right for create
235
235
  // handlers whose schema expects a full entity payload.
236
+ // Untouched fields that are still "" (the seed
237
+ // buildInitialValues gives controlled optional inputs)
238
+ // are stripped from the payload before it's sent, because
239
+ // `.optional()` server schemas accept `undefined` but not
240
+ // `""`. Set `stripEmptySeeds: false` on SubmitConfig to
241
+ // opt out and send those fields as `""` unchanged.
236
242
  // - "changes" — send only the `changes` delta. Right for update
237
243
  // handlers; noop when the form is un-dirty (submit
238
244
  // short-circuits into a no-network success).
@@ -246,6 +252,12 @@ export type SubmitConfig<TValues extends FormValues = FormValues> = {
246
252
  // Qualified write-handler name (e.g. "orders:write:order:create").
247
253
  readonly type: string;
248
254
  readonly payloadMode?: SubmitPayloadMode;
255
+ /** Only applies to payloadMode "values". Controls whether untouched
256
+ * empty-string fields (still equal to their initial "" seed) are
257
+ * stripped from the payload before dispatch — see SubmitPayloadMode's
258
+ * "values" doc for why the stripping exists. Default `true` (current
259
+ * behavior). Set `false` to send those fields as `""` unchanged. */
260
+ readonly stripEmptySeeds?: boolean;
249
261
  // Optional payload transformer — overrides payloadMode. Used for
250
262
  // nested-writes: the submit path calls buildPayload(snapshot) once at
251
263
  // submit-time and sends the result. The snapshot is the one captured