Skip to content

Commit 2870e0d

Browse files
tlouissedaKmoR
authored andcommitted
feat(choice-input): api normalization and cleanup
1 parent 66a1722 commit 2870e0d

File tree

3 files changed

+240
-92
lines changed

3 files changed

+240
-92
lines changed

packages/choice-input/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"devDependencies": {
3939
"@lion/input": "^0.1.22",
4040
"@open-wc/demoing-storybook": "^0.2.0",
41-
"@open-wc/testing": "^0.12.5"
41+
"@open-wc/testing": "^0.12.5",
42+
"sinon": "^7.2.2"
4243
}
4344
}
Lines changed: 119 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,82 @@
11
/* eslint-disable class-methods-use-this */
22

33
import { html, css, nothing } from '@lion/core';
4-
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
54
import { FormatMixin } from '@lion/field';
65

76
export const ChoiceInputMixin = superclass =>
87
// eslint-disable-next-line
9-
class ChoiceInputMixin extends FormatMixin(ObserverMixin(superclass)) {
10-
get delegations() {
8+
class ChoiceInputMixin extends FormatMixin(superclass) {
9+
static get properties() {
1110
return {
12-
...super.delegations,
13-
target: () => this.inputElement,
14-
properties: [...super.delegations.properties, 'checked'],
15-
attributes: [...super.delegations.attributes, 'checked'],
11+
...super.properties,
12+
/**
13+
* Boolean indicating whether or not this element is checked by the end user.
14+
*/
15+
checked: {
16+
type: Boolean,
17+
reflect: true,
18+
},
19+
/**
20+
* Whereas 'normal' `.modelValue`s usually store a complex/typed version
21+
* of a view value, choice inputs have a slightly different approach.
22+
* In order to remain their Single Source of Truth characteristic, choice inputs
23+
* store both the value and 'checkedness', in the format { value: 'x', checked: true }
24+
* Different from the platform, this also allows to serialize the 'non checkedness',
25+
* allowing to restore form state easily and inform the server about unchecked options.
26+
*/
27+
modelValue: {
28+
type: Object,
29+
hasChanged: (nw, old = {}) => nw.value !== old.value || nw.checked !== old.checked,
30+
},
31+
/**
32+
* The value property of the modelValue. It provides an easy inteface for storing
33+
* (complex) values in the modelValue
34+
*/
35+
choiceValue: {
36+
type: Object,
37+
},
1638
};
1739
}
1840

19-
static get syncObservers() {
20-
return {
21-
...super.syncObservers,
22-
_syncModelValueToChecked: ['modelValue'],
23-
};
41+
get choiceValue() {
42+
return this.modelValue.value;
2443
}
2544

26-
static get asyncObservers() {
27-
return {
28-
...super.asyncObservers,
29-
_reflectCheckedToCssClass: ['modelValue'],
30-
};
45+
set choiceValue(value) {
46+
this.requestUpdate('choiceValue', this.choiceValue);
47+
if (this.modelValue.value !== value) {
48+
this.modelValue = { value, checked: this.modelValue.checked };
49+
}
3150
}
3251

33-
get choiceChecked() {
34-
return this.modelValue.checked;
35-
}
52+
_requestUpdate(name, oldValue) {
53+
super._requestUpdate(name, oldValue);
3654

37-
set choiceChecked(checked) {
38-
if (this.modelValue.checked !== checked) {
39-
this.modelValue = { value: this.modelValue.value, checked };
55+
if (name === 'modelValue') {
56+
if (this.modelValue.checked !== this.checked) {
57+
this.__syncModelCheckedToChecked(this.modelValue.checked);
58+
}
59+
} else if (name === 'checked') {
60+
if (this.modelValue.checked !== this.checked) {
61+
this.__syncCheckedToModel(this.checked);
62+
}
4063
}
4164
}
4265

43-
get choiceValue() {
44-
return this.modelValue.value;
66+
firstUpdated(c) {
67+
super.firstUpdated(c);
68+
if (c.has('checked')) {
69+
// Here we set the initial value for our [slot=input] content,
70+
// which has been set by our SlotMixin
71+
this.__syncCheckedToInputElement();
72+
}
4573
}
4674

47-
set choiceValue(value) {
48-
if (this.modelValue.value !== value) {
49-
this.modelValue = { value, checked: this.modelValue.checked };
75+
updated(c) {
76+
super.updated(c);
77+
if (c.has('modelValue')) {
78+
this._reflectCheckedToCssClass({ modelValue: this.modelValue });
79+
this.__syncCheckedToInputElement();
5080
}
5181
}
5282

@@ -56,15 +86,9 @@ export const ChoiceInputMixin = superclass =>
5686
}
5787

5888
/**
59-
* @override
60-
* Override InteractionStateMixin
61-
* 'prefilled' should be false when modelValue is { checked: false }, which would return
62-
* true in original method (since non-empty objects are considered prefilled by default).
89+
* Styles for [input=radio] and [input=checkbox] wrappers.
90+
* For [role=option] extensions, please override completely
6391
*/
64-
static _isPrefilled(modelValue) {
65-
return modelValue.checked;
66-
}
67-
6892
static get styles() {
6993
return [
7094
css`
@@ -79,6 +103,10 @@ export const ChoiceInputMixin = superclass =>
79103
];
80104
}
81105

106+
/**
107+
* Template for [input=radio] and [input=checkbox] wrappers.
108+
* For [role=option] extensions, please override completely
109+
*/
82110
render() {
83111
return html`
84112
<slot name="input"></slot>
@@ -96,22 +124,44 @@ export const ChoiceInputMixin = superclass =>
96124
}
97125

98126
connectedCallback() {
99-
if (super.connectedCallback) super.connectedCallback();
100-
this.addEventListener('user-input-changed', this._toggleChecked);
127+
super.connectedCallback();
128+
this.addEventListener('user-input-changed', this.__toggleChecked);
101129
this._reflectCheckedToCssClass();
102130
}
103131

104132
disconnectedCallback() {
105-
if (super.disconnectedCallback) super.disconnectedCallback();
106-
this.removeEventListener('user-input-changed', this._toggleChecked);
133+
super.disconnectedCallback();
134+
this.removeEventListener('user-input-changed', this.__toggleChecked);
135+
}
136+
137+
__toggleChecked() {
138+
this.checked = !this.checked;
107139
}
108140

109-
_toggleChecked() {
110-
this.choiceChecked = !this.choiceChecked;
141+
__syncModelCheckedToChecked(checked) {
142+
this.checked = checked;
111143
}
112144

113-
_syncModelValueToChecked({ modelValue }) {
114-
this.checked = !!modelValue.checked;
145+
__syncCheckedToModel(checked) {
146+
this.modelValue = { value: this.choiceValue, checked };
147+
}
148+
149+
__syncCheckedToInputElement() {
150+
// .inputElement might not be available yet(slot content)
151+
// or at all (no reliance on platform construct, in case of [role=option])
152+
if (this.inputElement) {
153+
this.inputElement.checked = this.checked;
154+
}
155+
}
156+
157+
/**
158+
* @override
159+
* Override InteractionStateMixin
160+
* 'prefilled' should be false when modelValue is { checked: false }, which would return
161+
* true in original method (since non-empty objects are considered prefilled by default).
162+
*/
163+
static _isPrefilled(modelValue) {
164+
return modelValue.checked;
115165
}
116166

117167
/**
@@ -126,32 +176,15 @@ export const ChoiceInputMixin = superclass =>
126176

127177
/**
128178
* @override
129-
* Override FormatMixin default dispatching of model-value-changed as it only does a simple
130-
* comparision which is not enough in js because
131-
* { value: 'foo', checked: true } !== { value: 'foo', checked: true }
132-
* We do our own "deep" comparision.
133-
*
134-
* @param {object} modelValue
135-
* @param {object} modelValue the old one
179+
* hasChanged is designed for async (updated) callback, also check for sync
180+
* (_requestUpdate) callback
136181
*/
137-
// TODO: consider making a generic option inside FormatMixin for deep object comparisons when
138-
// modelValue is an object
139-
_dispatchModelValueChangedEvent({ modelValue }, { modelValue: old }) {
140-
let changed = true;
141-
if (old) {
142-
changed = modelValue.value !== old.value || modelValue.checked !== old.checked;
143-
}
144-
if (changed) {
145-
this.dispatchEvent(
146-
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
147-
);
182+
_onModelValueChanged({ modelValue }, { modelValue: old }) {
183+
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, old)) {
184+
super._onModelValueChanged({ modelValue });
148185
}
149186
}
150187

151-
_reflectCheckedToCssClass() {
152-
this.classList[this.choiceChecked ? 'add' : 'remove']('state-checked');
153-
}
154-
155188
/**
156189
* @override
157190
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
@@ -171,11 +204,30 @@ export const ChoiceInputMixin = superclass =>
171204

172205
/**
173206
* @override
174-
* Overridden from ValidateMixin, since a different modelValue is used for choice inputs.
207+
* Overridden from Field, since a different modelValue is used for choice inputs.
175208
*/
176209
__isRequired(modelValue) {
177210
return {
178211
required: !!modelValue.checked,
179212
};
180213
}
214+
215+
/**
216+
* @deprecated use .checked
217+
*/
218+
get choiceChecked() {
219+
return this.checked;
220+
}
221+
222+
/**
223+
* @deprecated use .checked
224+
*/
225+
set choiceChecked(c) {
226+
this.checked = c;
227+
}
228+
229+
/** @deprecated for styling purposes, use [checked] attribute */
230+
_reflectCheckedToCssClass() {
231+
this.classList[this.checked ? 'add' : 'remove']('state-checked');
232+
}
181233
};

0 commit comments

Comments
 (0)