Skip to content

Commit 6a85dbc

Browse files
committed
fix(field): don't return Unparseable on empty strings
1 parent caedb05 commit 6a85dbc

File tree

4 files changed

+55
-11
lines changed

4 files changed

+55
-11
lines changed

packages/choice-input/src/ChoiceInputMixin.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ export const ChoiceInputMixin = superclass =>
212212
};
213213
}
214214

215+
/**
216+
* @override
217+
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
218+
* Synchronization from user input is already arranged in this Mixin.
219+
*/
220+
_syncValueUpwards() {}
221+
215222
/**
216223
* @deprecated use .checked
217224
*/

packages/choice-input/test/ChoiceInputMixin.test.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ describe('ChoiceInputMixin', () => {
1212
if (super.connectedCallback) super.connectedCallback();
1313
this.type = 'checkbox'; // could also be 'radio', should be tested in integration test
1414
}
15-
16-
_syncValueUpwards() {} // We need to disable the method for the test to pass
1715
}
1816
customElements.define('choice-input', ChoiceInput);
1917
});
@@ -82,7 +80,6 @@ describe('ChoiceInputMixin', () => {
8280
const nativeInput = el.inputElement;
8381
nativeInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); // fired by (at least) Chrome
8482
expect(counter).to.equal(0);
85-
el._syncValueUpwards = () => {}; // We need to disable the method for the test to pass
8683
nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true }));
8784
expect(counter).to.equal(1);
8885
});
@@ -105,8 +102,6 @@ describe('ChoiceInputMixin', () => {
105102
const precheckedElementAttr = await fixture(html`
106103
<choice-input .checked=${true}></choice-input>
107104
`);
108-
el._syncValueUpwards = () => {}; // We need to disable the method for the test to pass
109-
110105
expect(precheckedElementAttr.checked).to.equal(true, 'initially checked via attribute');
111106
});
112107

@@ -122,7 +117,6 @@ describe('ChoiceInputMixin', () => {
122117

123118
it('can be checked and unchecked via user interaction', async () => {
124119
const el = await fixture(`<choice-input></choice-input>`);
125-
el._syncValueUpwards = () => {}; // We need to disable the method for the test to pass
126120
el.inputElement.click();
127121
expect(el.checked).to.be.true;
128122
el.inputElement.click();

packages/field/src/FormatMixin.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,37 @@ export const FormatMixin = dedupeMixin(
171171
}
172172

173173
__callParser(value = this.formattedValue) {
174-
let result;
175-
if (typeof value === 'string') {
176-
result = this.parser(value, this.formatOptions);
174+
// A) check if we need to parse at all
175+
176+
// A.1) The end user had no intention to parse
177+
if (value === '') {
178+
// Ideally, modelValue should be undefined for empty strings.
179+
// For backwards compatibility we return an empty string:
180+
// - it triggers validation for required validators (see ValidateMixin.validate())
181+
// - it can be expected by 3rd parties (for instance unit tests)
182+
// TODO: In a breaking refactor of the Validation System, this behaviot can be corrected.
183+
return '';
184+
}
185+
186+
// A.2) Handle edge cases We might have no view value yet, for instance because
187+
// inputElement.value was not available yet
188+
if (typeof value !== 'string') {
189+
// This means there is nothing to find inside the view that can be of
190+
// interest to the Application Developer or needed to store for future form state
191+
// retrieval.
192+
return undefined;
177193
}
178-
return typeof result !== 'undefined' ? result : new Unparseable(value);
194+
195+
// B) parse the view value
196+
197+
// - if result:
198+
// return the successfully parsed viewValue
199+
// - if no result:
200+
// Apparently, the parser was not able to produce a satisfactory output for the desired
201+
// modelValue type, based on the current viewValue. Unparseable allows to restore all
202+
// states (for instance from a lost user session), since it saves the current viewValue.
203+
const result = this.parser(value, this.formatOptions);
204+
return result !== undefined ? result : new Unparseable(value);
179205
}
180206

181207
__callFormatter() {

packages/field/test/FormatMixin.test.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('FormatMixin', () => {
193193
expect(formatterSpy.args[0][1].decimalSeparator).to.equal('-');
194194
});
195195

196-
it('will only call the parser for string values', async () => {
196+
it('will only call the parser for defined values', async () => {
197197
const parserSpy = sinon.spy();
198198
const el = await fixture(html`
199199
<${elem} .parser="${parserSpy}">
@@ -202,8 +202,25 @@ describe('FormatMixin', () => {
202202
`);
203203
el.modelValue = 'foo';
204204
expect(parserSpy.callCount).to.equal(1);
205+
// This could happen for instance in a reset
205206
el.modelValue = undefined;
206207
expect(parserSpy.callCount).to.equal(1);
208+
// This could happen when the user erases the input value
209+
mimicUserInput(el, '');
210+
expect(parserSpy.callCount).to.equal(1);
211+
});
212+
213+
it('will not return Unparseable when empty strings are inputted', async () => {
214+
const el = await fixture(html`
215+
<${elem}>
216+
<input slot="input" value="string">
217+
</${elem}>
218+
`);
219+
// This could happen when the user erases the input value
220+
mimicUserInput(el, '');
221+
// For backwards compatibility, we keep the modelValue an empty string here.
222+
// Undefined would be more appropriate 'conceptually', however
223+
expect(el.modelValue).to.equal('');
207224
});
208225

209226
it('will only call the formatter for valid values', async () => {

0 commit comments

Comments
 (0)