Skip to content

Commit aea3b59

Browse files
committed
feat: c-input (string): When the input's value changes to empty string, the value is instead emitted as null. This allows a user to return a field to its default, uninitialized state as if they had never typed in the field at all, which alleviates some validation edge cases like [PhoneAttribute] not treating null and "" the same.
1 parent 2ad03be commit aea3b59

File tree

3 files changed

+60
-6
lines changed

3 files changed

+60
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
- `c-admin-audit-log-page`: Added `userProp` prop to allow specifying a custom property name for user identification.
3535
- `c-admin-display`: Binary values now render as links that will download the value as a file, instead of only showing the length in bytes.
3636
- `c-datetime-picker`: Added prop `showTodayButton`
37-
- `c-input`: Added a `filter` prop for enum inputs to restrict the values available for selection.
37+
- `c-input` (enum): Added a `filter` prop for enum inputs to restrict the values available for selection.
38+
- `c-input` (string): When the input's value changes to empty string, the value is instead emitted as `null`. This allows a user to return a field to its default, uninitialized state as if they had never typed in the field at all, which alleviates some validation edge cases like `[PhoneAttribute]` not treating `null` and `""` the same.
3839
- `c-select`: When bound to a `ViewModel` or `ViewModelCollection`, selected items are converted to `ViewModel` instances before being emitted so that event handlers will receive the final object instance, rather than the intermediate plain model instance.
3940
- `c-select`: Now supports binding to a non-many-to-many collection navigation property. Selecting an item will populate the foreign key of the dependent item, and deselecting an item will clear the foreign key. This mechanism is only available when using c-select directly - it is not delegated by c-input.
4041
- `c-select`: The `create` prop now supports a `position` property to control whether the create item appears at the start ('start', default) or end ('end') of the dropdown list.

src/coalesce-vue-vuetify3/src/components/input/c-input.spec.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ describe("CInput", () => {
341341

342342
expect(wrapper.find(".v-input--error").exists()).toBeTruthy();
343343
expect(wrapper.find(".v-messages").text()).toEqual(
344-
"Guid does not match expected format."
344+
"Guid does not match expected format.",
345345
);
346346
});
347347

@@ -366,7 +366,7 @@ describe("CInput", () => {
366366
test("number - rule receives number, not string", async () => {
367367
const model = new ComplexModelViewModel({ intNullable: 7 });
368368
const rule = vitest.fn(
369-
(v: number | null | undefined) => v === 7 || "Custom Rule Failure"
369+
(v: number | null | undefined) => v === 7 || "Custom Rule Failure",
370370
);
371371
const wrapper = mountApp(() => (
372372
<VForm>
@@ -385,6 +385,48 @@ describe("CInput", () => {
385385
expect(rule).not.toHaveBeenCalledWith("7");
386386
expect(rule).not.toHaveBeenCalledWith("42");
387387
});
388+
389+
test("string - empty string becomes null", async () => {
390+
const model = new ComplexModelViewModel({ name: "initial value" });
391+
const wrapper = mount(() => <CInput model={model} for="name" />);
392+
393+
// Verify initial state
394+
expect(model.name).toBe("initial value");
395+
expect(wrapper.find("input").element.value).toBe("initial value");
396+
397+
// Clear the input - this should set the value to null, not empty string
398+
await wrapper.find("input").setValue("");
399+
expect(model.name).toBe(null);
400+
401+
// Type something new
402+
await wrapper.find("input").setValue("new value");
403+
expect(model.name).toBe("new value");
404+
405+
// Clear again - should go back to null
406+
await wrapper.find("input").setValue("");
407+
expect(model.name).toBe(null);
408+
});
409+
410+
test("number - empty string becomes null", async () => {
411+
const model = new ComplexModelViewModel({ intNullable: 42 });
412+
const wrapper = mount(() => <CInput model={model} for="intNullable" />);
413+
414+
// Verify initial state
415+
expect(model.intNullable).toBe(42);
416+
expect(wrapper.find("input").element.value).toBe("42");
417+
418+
// Clear the input - this should set the value to null, not 0
419+
await wrapper.find("input").setValue("");
420+
expect(model.intNullable).toBe(null);
421+
422+
// Type a new number
423+
await wrapper.find("input").setValue("123");
424+
expect(model.intNullable).toBe(123);
425+
426+
// Clear again - should go back to null, not 0
427+
await wrapper.find("input").setValue("");
428+
expect(model.intNullable).toBe(null);
429+
});
388430
});
389431
});
390432

src/coalesce-vue-vuetify3/src/components/input/c-input.vue

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,22 @@ function render() {
342342
data = buildVuetifyAttrs(valueMeta, props.model, data);
343343
344344
const onInput = (value: any) => {
345-
const parsed = parseValue(value, valueMeta);
345+
// For string inputs, convert empty strings to null since they are
346+
// visually indistinct to users and null is the baseline default value.
347+
// This is consistent with how number inputs handle empty values.
348+
// If we don't do this, then a field that starts null, then the user types,
349+
// and then they delete all characters would end back at emptystring instead of the initial null value.
350+
// This then breaks some validation like PhoneAttribute and others that don't allow emptystring.
351+
if (value === "" && valueMeta.type === "string") {
352+
value = null;
353+
} else {
354+
value = parseValue(value, valueMeta);
355+
}
356+
346357
if (valueOwner && valueMeta) {
347-
valueOwner[valueMeta.name] = parsed;
358+
valueOwner[valueMeta.name] = value;
348359
}
349-
emit("update:modelValue", parsed);
360+
emit("update:modelValue", value);
350361
};
351362
352363
// Handle components that delegate immediately to Vuetify

0 commit comments

Comments
 (0)