Skip to content

Commit 4224255

Browse files
Support editing properties of type array without oneOf, selector and selectorAll (#855)
* Support editing a property of type array without oneOf like animation startEvents InputWidget now owns its (de)serialization (mirroring SelectWidget's pattern): it accepts value as string, array, or custom object, and when a schema is provided it stringifies non-string values via schema.stringify for display and parses the input back via schema.parse on change for array-typed properties. This ensures A-Frame always receives an array for array-type properties — avoiding arrayStringify being called on a raw string downstream (which would crash with "t.join is not a function"). The "custom string type like event-set" stringify previously done in PropertyRow is also moved here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PropertyRow: support selector and selectorAll via InputWidget on blur For selector and selectorAll types, read the value from getDOMAttribute (the raw selector string) rather than props.data (the parsed Element/NodeList), and commit on blur instead of on every keystroke — querying the DOM on each character is wasteful and a partial selector is rarely valid. Omit the schema when rendering InputWidget so the typed string is passed through to setAttribute as-is, which A-Frame preserves verbatim in attrValue even if it doesn't resolve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 13795b8 commit 4224255

3 files changed

Lines changed: 59 additions & 22 deletions

File tree

examples/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
<script>
1010
AFRAME.registerComponent('models-array', {
1111
schema: {
12-
models: {type: 'array', oneOf: ['one', 'two', 'three', 'four']}
12+
models: {type: 'array', oneOf: ['one', 'two', 'three', 'four']},
13+
entity: {type: 'selector'},
14+
entities: {type: 'selectorAll'}
1315
}
1416
})
1517

src/components/components/PropertyRow.js

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,29 @@ export default class PropertyRow extends React.Component {
6969
getWidget() {
7070
const props = this.props;
7171
const type = this.getType();
72+
const isSelectorType = type === 'selector' || type === 'selectorAll';
7273

73-
let value =
74-
type === 'selector'
75-
? props.entity.getDOMAttribute(props.componentname)?.[props.name]
76-
: props.data;
74+
const value = isSelectorType
75+
? props.entity.getDOMAttribute(props.componentname)?.[props.name]
76+
: props.data;
7777

78-
if (type === 'string' && value && typeof value !== 'string') {
79-
// Allow editing a custom type like event-set component schema
80-
value = props.schema.stringify(value);
81-
}
78+
const updateProperty = (name, value) => {
79+
updateEntity(
80+
props.entity,
81+
props.componentname,
82+
!props.isSingle ? props.name : '',
83+
value
84+
);
85+
};
8286

87+
// For selector and selectorAll types, commit on blur only (not on each
88+
// keystroke): a partial selector is rarely valid and querying the DOM on
89+
// every character is wasteful.
8390
const widgetProps = {
8491
name: props.name,
85-
onChange: function (name, value) {
86-
updateEntity(
87-
props.entity,
88-
props.componentname,
89-
!props.isSingle ? props.name : '',
90-
value
91-
);
92-
},
92+
...(isSelectorType
93+
? { onBlur: updateProperty }
94+
: { onChange: updateProperty }),
9395
value: value,
9496
id: this.id
9597
};
@@ -136,7 +138,17 @@ export default class PropertyRow extends React.Component {
136138
return <BooleanWidget {...widgetProps} />;
137139
}
138140
default: {
139-
return <InputWidget {...widgetProps} />;
141+
// For selector and selectorAll types, omit the schema so InputWidget
142+
// doesn't parse the string into a DOM element / NodeList. We want the
143+
// raw selector string to reach setAttribute — A-Frame preserves it
144+
// verbatim in attrValue, even when it doesn't resolve, so the UI
145+
// shows what the user typed.
146+
return (
147+
<InputWidget
148+
{...widgetProps}
149+
schema={isSelectorType ? undefined : props.schema}
150+
/>
151+
);
140152
}
141153
}
142154
}

src/components/widgets/InputWidget.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,50 @@ export default class InputWidget extends React.Component {
77
name: PropTypes.string.isRequired,
88
onBlur: PropTypes.func,
99
onChange: PropTypes.func,
10+
schema: PropTypes.object,
1011
value: PropTypes.any
1112
};
1213

1314
constructor(props) {
1415
super(props);
15-
this.state = { value: this.props.value || '' };
16+
this.state = { value: this.stringifyValue(props.value) };
1617
this.input = React.createRef();
1718
}
1819

20+
stringifyValue = (value) => {
21+
// For selector and selectorAll types, getDOMAttribute returns null for
22+
// single-property schema and undefined for multi-property schema when the
23+
// property is not set.
24+
if (value === undefined || value === null) return '';
25+
if (typeof value === 'string') return value;
26+
// Non-string value (array, custom object like event-set): stringify for display
27+
if (this.props.schema) return this.props.schema.stringify(value);
28+
return String(value);
29+
};
30+
31+
parseInput = (value) => {
32+
// The type array doesn't bailout-on-string in its stringify
33+
// (arrayStringify), so we need to parse the input value before calling
34+
// onChange. That could potentially happen for a custom property that
35+
// implements its own parse/stringify functions.
36+
if (this.props.schema) {
37+
return this.props.schema.parse(value);
38+
}
39+
return value;
40+
};
41+
1942
onChange = (event) => {
2043
const value = event.target.value;
2144
this.setState({ value: value });
2245
if (this.props.onChange) {
23-
this.props.onChange(this.props.name, value);
46+
this.props.onChange(this.props.name, this.parseInput(value));
2447
}
2548
};
2649

2750
onBlur = (event) => {
2851
if (this.props.onBlur) {
2952
const value = event.target.value;
30-
this.props.onBlur(this.props.name, value);
53+
this.props.onBlur(this.props.name, this.parseInput(value));
3154
}
3255
};
3356

@@ -43,7 +66,7 @@ export default class InputWidget extends React.Component {
4366

4467
componentDidUpdate(prevProps) {
4568
if (this.props.value !== prevProps.value) {
46-
this.setState({ value: this.props.value || '' });
69+
this.setState({ value: this.stringifyValue(this.props.value) });
4770
}
4871
}
4972

0 commit comments

Comments
 (0)