Skip to content

Commit 18b5b7d

Browse files
committed
fix(frontend): keep integer property fields editable by replacing the crashing Formly number parser
@ngx-formly/core's json-schema integer/number parser reads document.querySelector('#'+field.id).validity.badInput, assuming the id is on the native <input>. ng-zorro's nz-input-number puts the id on its host element, which has no .validity, so the parser throws 'Cannot read properties of undefined (reading badInput)' whenever the parsed value is null (an intermediate state while typing). Typed edits then never commit and the field is stuck (only the +/- steppers work). Replace the numeric fields' parser in jsonSchemaMapIntercept with a null-safe parser that does no DOM access.
1 parent 878eb8a commit 18b5b7d

3 files changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { FormlyFieldConfig } from "@ngx-formly/core";
21+
import { applySafeNumericParser, parseNumericInput } from "./numeric-input-parser.util";
22+
23+
describe("parseNumericInput", () => {
24+
it("returns the number for a numeric value", () => {
25+
expect(parseNumericInput(10)).toEqual(10);
26+
expect(parseNumericInput(0)).toEqual(0);
27+
expect(parseNumericInput(-5)).toEqual(-5);
28+
});
29+
30+
it("parses numeric strings", () => {
31+
expect(parseNumericInput("10")).toEqual(10);
32+
expect(parseNumericInput("-5")).toEqual(-5);
33+
expect(parseNumericInput("0")).toEqual(0);
34+
expect(parseNumericInput("3.5")).toEqual(3.5);
35+
});
36+
37+
it("treats empty / null / undefined as null", () => {
38+
expect(parseNumericInput("")).toBeNull();
39+
expect(parseNumericInput(null)).toBeNull();
40+
expect(parseNumericInput(undefined)).toBeNull();
41+
});
42+
43+
it("returns null for non-numeric input instead of throwing", () => {
44+
expect(parseNumericInput("abc")).toBeNull();
45+
expect(parseNumericInput("1a")).toBeNull();
46+
expect(parseNumericInput({})).toBeNull();
47+
});
48+
49+
it("preserves negative and zero values (does not clamp to 1)", () => {
50+
// Guards the reported bug: a negative limit must pass through unchanged here,
51+
// not be turned into 1 or dropped.
52+
expect(parseNumericInput(-100)).toEqual(-100);
53+
expect(parseNumericInput("-1")).toEqual(-1);
54+
});
55+
56+
it("parses negative decimals and whitespace-padded numbers", () => {
57+
expect(parseNumericInput("-3.5")).toEqual(-3.5);
58+
expect(parseNumericInput(" 10 ")).toEqual(10);
59+
});
60+
61+
it("treats whitespace-only strings as null", () => {
62+
expect(parseNumericInput(" ")).toBeNull();
63+
expect(parseNumericInput(" ")).toBeNull();
64+
expect(parseNumericInput("\t")).toBeNull();
65+
});
66+
67+
it("rejects NaN and infinities as null", () => {
68+
expect(parseNumericInput(NaN)).toBeNull();
69+
expect(parseNumericInput(Infinity)).toBeNull();
70+
expect(parseNumericInput(-Infinity)).toBeNull();
71+
expect(parseNumericInput("Infinity")).toBeNull();
72+
});
73+
74+
it("rejects non-number/non-string values (boolean, array, object) as null", () => {
75+
expect(parseNumericInput(true)).toBeNull();
76+
expect(parseNumericInput(false)).toBeNull();
77+
expect(parseNumericInput([5])).toBeNull();
78+
expect(parseNumericInput([1, 2])).toBeNull();
79+
});
80+
81+
it("preserves large finite numbers", () => {
82+
expect(parseNumericInput(1_000_000_000)).toEqual(1_000_000_000);
83+
expect(parseNumericInput("2000000")).toEqual(2000000);
84+
});
85+
86+
it("never touches the DOM (safe to call without an element)", () => {
87+
// The whole point of the replacement: no document.querySelector / .validity access.
88+
expect(() => parseNumericInput("42")).not.toThrow();
89+
expect(parseNumericInput("42")).toEqual(42);
90+
});
91+
});
92+
93+
describe("applySafeNumericParser", () => {
94+
it("replaces a numeric field's existing (crash-prone) parser with the safe one", () => {
95+
// Mimics @ngx-formly's parser that throws on nz-input-number when the value is null.
96+
const crashingParser = () => {
97+
throw new TypeError("Cannot read properties of undefined (reading 'badInput')");
98+
};
99+
const field: FormlyFieldConfig = { type: "integer", parsers: [crashingParser] };
100+
101+
applySafeNumericParser(field);
102+
103+
// the crash-prone parser is gone
104+
expect(field.parsers).toHaveLength(1);
105+
expect(field.parsers?.[0]).not.toBe(crashingParser);
106+
// the null intermediate state that used to crash is now handled safely
107+
expect(() => (field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)(null, field)).not.toThrow();
108+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)(null, field)).toBeNull();
109+
// and it still converts real values
110+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("10", field)).toEqual(10);
111+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("-5", field)).toEqual(-5);
112+
});
113+
114+
it("replaces the parser for type 'number' too (not just 'integer')", () => {
115+
const field: FormlyFieldConfig = {
116+
type: "number",
117+
parsers: [
118+
() => {
119+
throw new Error("original numeric parser should not run");
120+
},
121+
],
122+
};
123+
124+
applySafeNumericParser(field);
125+
126+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("3.5", field)).toEqual(3.5);
127+
});
128+
129+
it("does NOT touch an enum (select) field's parser", () => {
130+
// Attribute selectors are 'enum' fields; they carry a (string) parser but render as a
131+
// select, not nz-input-number, so they must be left alone.
132+
const enumParser = (v: unknown) => v;
133+
const field: FormlyFieldConfig = { type: "enum", parsers: [enumParser] };
134+
135+
applySafeNumericParser(field);
136+
137+
expect(field.parsers?.[0]).toBe(enumParser);
138+
});
139+
140+
it("does NOT touch a string field's parser (regression: text inputs must keep working)", () => {
141+
// FormlyJsonschema also sets a parser on string fields. Replacing it with the numeric
142+
// parser would turn typed text into null and break every text property across all
143+
// operators, so string fields must be left completely alone.
144+
const stringParser = (v: unknown) => v;
145+
const field: FormlyFieldConfig = { type: "string", parsers: [stringParser] };
146+
147+
applySafeNumericParser(field);
148+
149+
expect(field.parsers).toHaveLength(1);
150+
expect(field.parsers?.[0]).toBe(stringParser); // exact same parser, untouched
151+
// typed text still passes through unchanged
152+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("hello", field)).toEqual("hello");
153+
});
154+
155+
it("leaves non-numeric fields (without parsers) untouched", () => {
156+
const field: FormlyFieldConfig = { type: "string" };
157+
158+
applySafeNumericParser(field);
159+
160+
expect(field.parsers).toBeUndefined();
161+
});
162+
163+
it("does not add parsers to a field that had none", () => {
164+
const field: FormlyFieldConfig = { key: "someBoolean", type: "boolean" };
165+
166+
applySafeNumericParser(field);
167+
168+
expect(field.parsers).toBeUndefined();
169+
});
170+
171+
it("the replacement parser treats the clear-field cases ('' and null) as null", () => {
172+
const field: FormlyFieldConfig = {
173+
type: "integer",
174+
parsers: [
175+
() => {
176+
throw new Error("original parser should not run");
177+
},
178+
],
179+
};
180+
181+
applySafeNumericParser(field);
182+
const parser = field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown;
183+
184+
expect(parser("", field)).toBeNull();
185+
expect(parser(null, field)).toBeNull();
186+
});
187+
188+
it("collapses multiple existing parsers into a single safe one", () => {
189+
const field: FormlyFieldConfig = { type: "integer", parsers: [() => 1, () => 2] };
190+
191+
applySafeNumericParser(field);
192+
193+
expect(field.parsers).toHaveLength(1);
194+
expect((field.parsers?.[0] as (v: unknown, f: FormlyFieldConfig) => unknown)("7", field)).toEqual(7);
195+
});
196+
197+
it("leaves an empty parsers array untouched (nothing to replace)", () => {
198+
const field: FormlyFieldConfig = { type: "integer", parsers: [] };
199+
200+
applySafeNumericParser(field);
201+
202+
expect(field.parsers).toEqual([]);
203+
});
204+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { FormlyFieldConfig } from "@ngx-formly/core";
21+
22+
/**
23+
* Null-safe parser for numeric (integer/number) property fields.
24+
*
25+
* It replaces @ngx-formly/core's json-schema integer/number parser, which reads
26+
* `document.querySelector('#' + field.id).validity.badInput`. That code assumes the
27+
* field id is on the native `<input>`, but ng-zorro's `nz-input-number` puts the id
28+
* on its host element (which has no `.validity`), so the parser throws
29+
* "Cannot read properties of undefined (reading 'badInput')" and typed edits never
30+
* commit. This version does no DOM access: it just converts the value to a number, or
31+
* null when it is empty/undefined/not a number.
32+
*/
33+
export function parseNumericInput(value: unknown): number | null {
34+
if (typeof value === "number") {
35+
// Reject NaN and ±Infinity — only real, finite numbers are valid.
36+
return Number.isFinite(value) ? value : null;
37+
}
38+
if (typeof value === "string") {
39+
const trimmed = value.trim();
40+
if (trimmed === "") {
41+
return null;
42+
}
43+
const parsed = Number(trimmed);
44+
return Number.isFinite(parsed) ? parsed : null;
45+
}
46+
// null, undefined, boolean, object, array, etc. are not valid numeric input.
47+
return null;
48+
}
49+
50+
/**
51+
* Replace a numeric field's crash-prone Formly parser with {@link parseNumericInput}.
52+
*
53+
* FormlyJsonschema sets `parsers` on integer/number fields (the crash-prone one) but
54+
* ALSO on string fields (a different, harmless one). Only the numeric parser causes the
55+
* nz-input-number crash, so restrict the swap to number/integer fields — replacing a
56+
* string field's parser with a numeric one would drop all typed text and break every
57+
* text property. Leave every other field untouched.
58+
*/
59+
export function applySafeNumericParser(field: FormlyFieldConfig): void {
60+
const isNumericField = field.type === "number" || field.type === "integer";
61+
if (isNumericField && field.parsers && field.parsers.length > 0) {
62+
field.parsers = [value => parseNumericInput(value)];
63+
}
64+
}

frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import { WorkflowPveService } from "../../../service/virtual-environment/virtual
7474
import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
7575
import { of } from "rxjs";
7676
import { map, switchMap, take } from "rxjs/operators";
77+
import { applySafeNumericParser } from "./numeric-input-parser.util";
7778

7879
Quill.register("modules/cursors", QuillCursors);
7980

@@ -488,6 +489,15 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On
488489
},
489490
};
490491

492+
// @ngx-formly/core's json-schema parser for integer/number fields reads
493+
// document.querySelector('#' + field.id).validity.badInput, assuming the id is on
494+
// the native <input>. ng-zorro's nz-input-number puts the id on its host element
495+
// (which has no `.validity`), so that parser throws
496+
// "Cannot read properties of undefined (reading 'badInput')" and typed edits never
497+
// commit — the field gets stuck (only the +/- steppers work). FormlyJsonschema only
498+
// sets `parsers` for numeric fields, so replace it with a null-safe parser here.
499+
applySafeNumericParser(mappedField);
500+
491501
// Disable dummy operator for user
492502
if (mappedField.key === "dummyOperator") {
493503
mappedField.expressions = {

0 commit comments

Comments
 (0)