Skip to content

Commit ae52a91

Browse files
author
Elson Correia
committed
use both element property and attribute to set the value on the element
1 parent 484631b commit ae52a91

File tree

4 files changed

+76
-41
lines changed

4 files changed

+76
-41
lines changed

docs/documentation/capabilities/form-controls.md

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class TextField extends WebComponent {
2121
'value',
2222
'placeholder',
2323
'pattern',
24+
'disabled',
2425
'required',
2526
'error',
2627
]
@@ -30,26 +31,26 @@ class TextField extends WebComponent {
3031
value = ''
3132
pattern = ''
3233
required = false
34+
disabled = false
3335
error = 'Invalid field value.'
3436

3537
handleChange = (value) => {
38+
this.value = value
3639
// dispatch a change event with the input field value
3740
this.dispatch('change', { value })
3841
}
3942

4043
// render the component content
4144
render() {
45+
const { error, ...inputAttrs } = this.props
46+
4247
return html`
4348
<input
49+
${inputAttrs}
4450
part="text-input"
4551
type="text"
4652
ref="input"
4753
onchange="${(event) => this.handleChange(event.target.value)}"
48-
placeholder="${this.props.placeholder}"
49-
disabled="${this.props.disabled}"
50-
pattern="${this.props.pattern}"
51-
required="${this.props.required}"
52-
value="${this.props.value}"
5354
/>
5455
`
5556
}
@@ -148,7 +149,7 @@ class TextField extends WebComponent {
148149

149150
formAssociatedCallback(form) {
150151
// register value received from props
151-
this.internals.setFormValue(this.props.value());
152+
this.internals.setFormValue(this.props.value(), false);
152153
}
153154
}
154155
```
@@ -173,6 +174,7 @@ class TextField extends WebComponent {
173174

174175
handleChange = (value, report = true) => {
175176
this.internals.setFormValue(value);
177+
this.value = value;
176178

177179
const [inputField] = this.refs['input'];
178180

@@ -206,7 +208,7 @@ class TextField extends WebComponent {
206208
...
207209

208210
formAssociatedCallback(form) {
209-
this.internals.setFormValue(this.props.value());
211+
this.handleChange(this.props.value(), false);
210212
}
211213

212214
...
@@ -227,7 +229,7 @@ class TextField extends WebComponent {
227229
...
228230

229231
formDisabledCallback(disabled) {
230-
this.refs['input'][0].disabled = disabled;
232+
this.disabled = disabled;
231233
}
232234

233235
...
@@ -245,8 +247,7 @@ class TextField extends WebComponent {
245247
...
246248

247249
formResetCallback(form) {
248-
this.handleChange('')
249-
this.refs['input'][0].value = ''
250+
this.handleChange('', false)
250251
}
251252

252253
...
@@ -270,8 +271,7 @@ class TextField extends WebComponent {
270271
if (mode == 'restore') {
271272
// expects a state parameter in the form 'controlMode/value'
272273
const [controlMode, value] = state.split('/');
273-
this.handleChange(value)
274-
this.refs['input'][0].value = value
274+
this.handleChange(value, false)
275275
}
276276
}
277277

@@ -290,6 +290,7 @@ class TextField extends WebComponent {
290290
static observedAttributes = [
291291
'value',
292292
'placeholder',
293+
'disabled',
293294
'pattern',
294295
'error',
295296
'required',
@@ -316,6 +317,7 @@ class TextField extends WebComponent {
316317
placeholder = ''
317318
value = ''
318319
pattern = ''
320+
disabled = false
319321
required = false
320322
error = 'Invalid field value'
321323

@@ -324,24 +326,23 @@ class TextField extends WebComponent {
324326
}
325327

326328
formDisabledCallback(disabled) {
327-
this.refs['input'][0].disabled = disabled
329+
this.disabled = disabled
328330
}
329331

330332
formResetCallback() {
331-
this.handleChange('')
332-
this.refs['input'][0].value = ''
333+
this.handleChange('', false)
333334
}
334335

335336
formStateRestoreCallback(state, mode) {
336337
if (mode == 'restore') {
337338
const [controlMode, value] = state.split('/')
338-
this.handleChange(value)
339-
this.refs['input'][0].value = value
339+
this.handleChange(value, false)
340340
}
341341
}
342342

343343
handleChange = (value, report = true) => {
344344
this.internals.setFormValue(value)
345+
this.value = value
345346

346347
const [inputField] = this.refs['input']
347348

@@ -357,17 +358,15 @@ class TextField extends WebComponent {
357358
}
358359

359360
render() {
361+
const { error, ...inputAttrs } = this.props
362+
360363
return html`
361364
<input
365+
${inputAttrs}
362366
part="text-input"
363367
type="text"
364368
ref="input"
365369
onchange="${(event) => this.handleChange(event.target.value)}"
366-
placeholder="${this.props.placeholder}"
367-
disabled="${this.props.disabled}"
368-
pattern="${this.props.pattern}"
369-
required="${this.props.required}"
370-
value="${this.props.value}"
371370
/>
372371
`
373372
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@beforesemicolon/markup",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "Reactive HTML Templating System",
55
"engines": {
66
"node": ">=18.16.0"

src/html.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,35 @@ describe('html', () => {
828828
expect(countUpSpy).toHaveBeenCalledTimes(1);
829829
expect(countUpSpy).toHaveBeenCalledWith(expect.any(Event));
830830
})
831+
832+
it('should handle input value', () => {
833+
const [value, setValue] = state("");
834+
835+
const temp = html`
836+
<input type="text" value="${value}" ref="input" />
837+
`.render(document.body);
838+
839+
expect(document.body.innerHTML).toBe('<input type="text" value="">');
840+
841+
const [inputField] = temp.refs['input'] as HTMLInputElement[];
842+
843+
expect(inputField.getAttribute('value')).toBe('')
844+
expect(inputField.value).toBe('')
845+
846+
setValue('works')
847+
848+
jest.advanceTimersToNextTimer(5)
849+
850+
expect(inputField.getAttribute('value')).toBe('works')
851+
expect(inputField.value).toBe('works')
852+
853+
setValue('')
854+
855+
jest.advanceTimersToNextTimer(5)
856+
857+
expect(inputField.getAttribute('value')).toBe('')
858+
expect(inputField.value).toBe('')
859+
})
831860
})
832861

833862
it('should handle primitive attribute value', () => {
@@ -854,7 +883,7 @@ describe('html', () => {
854883
el.render(document.body)
855884

856885
expect(document.body.innerHTML).toBe(
857-
'<div data-test-map="[object Map]" data-test-val="[object Object]"></div>'
886+
'<div data-test-map="{}" data-test-val="{&quot;val&quot;:12}"></div>'
858887
)
859888
})
860889

src/utils/set-element-attribute.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,33 @@ export const setElementAttribute = (
1313
value: unknown
1414
) => {
1515
if (value !== undefined && value !== null) {
16-
// take care of web component elements with prop setters
17-
if (el.nodeName.includes('-') && !isPrimitive(value)) {
18-
const descriptor =
19-
// describe the property directly on the object
20-
Object.getOwnPropertyDescriptor(el, key) ??
21-
// describe properties defined as setter/getter by checking the prototype
22-
Object.getOwnPropertyDescriptors(Object.getPrototypeOf(el))[key]
16+
const descriptor =
17+
Object.getOwnPropertyDescriptor(el, key) ??
18+
// describe properties defined as setter/getter by checking the prototype
19+
Object.getOwnPropertyDescriptors(Object.getPrototypeOf(el))[key]
20+
const isWritable =
21+
descriptor?.writable || typeof descriptor?.set === 'function'
22+
// Using setAttribute() to modify certain attributes, most notably value in XUL, works inconsistently,
23+
// as the attribute specifies the default value. To access or modify the current values, you should use
24+
// the properties. For example, use elt.value instead of elt.setAttribute('value', val).
25+
// https://stackoverflow.com/questions/29929797/setattribute-doesnt-work-the-way-i-expect-it-to
26+
if (isWritable) {
27+
// @ts-expect-error Cannot assign to X because it is a read-only property.
28+
el[key] = value
29+
}
2330

24-
if (descriptor?.writable || typeof descriptor?.set === 'function') {
25-
// @ts-expect-error Cannot assign to X because it is a read-only property.
26-
if (el[key] !== value) el[key] = value
27-
} else {
28-
const v = jsonStringify(value)
29-
if (v !== el.getAttribute(key)) el.setAttribute(key, v)
30-
}
31+
const strValue = jsonStringify(value)
3132

32-
return
33+
if (
34+
// in case !isWritable or setting the property did not also update the attribute
35+
strValue !== el.getAttribute(key) &&
36+
// only set primitive values
37+
// only set non-primitive values on non-components
38+
// only set non-primitive values on web components if the property is !isWritable
39+
(isPrimitive(value) || !el.nodeName.includes('-') || !isWritable)
40+
) {
41+
el.setAttribute(key, strValue)
3342
}
34-
35-
if (value !== el.getAttribute(key)) el.setAttribute(key, String(value))
3643
} else {
3744
el.removeAttribute(key)
3845
}

0 commit comments

Comments
 (0)