Skip to content

Commit de10db0

Browse files
author
Ryan A. Johnson
committed
feat(HXFormControLElement): centralize custom form control logic
- handles adding 'hx-changed' and 'hx-touched' attrs to communicate state
1 parent 35b4582 commit de10db0

11 files changed

+244
-115
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { HXFormControlElement } from './HXFormControlElement';
2+
3+
/**
4+
* Defines behavior for the `<hx-select-control>` element.
5+
*
6+
* @extends HXFormControlElement
7+
* @hideconstructor
8+
* @since 0.16.0
9+
*/
10+
export class HXCheckboxControlElement extends HXFormControlElement {
11+
/** @override */
12+
static get is () {
13+
return 'hx-checkbox-control';
14+
}
15+
16+
/**
17+
* Fetch the first `<input type="checkbox">` descendant
18+
*
19+
* @override
20+
* @readonly
21+
* @type {?HTMLInputElement}
22+
*/
23+
get controlElement () {
24+
return this.querySelector('input[type="checkbox"]');
25+
}
26+
}

src/helix-ui/elements/HXCheckboxElement.js

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import shadowMarkup from './HXCheckboxElement.html';
33
import shadowStyles from './HXCheckboxElement.less';
44

55
/**
6-
* Defines behavior for the `<hx-checkbox>` element.
6+
* Applies Shadow DOM to the `<hx-checkbox>` facade element.
77
*
88
* @extends HXElement
99
* @hideconstructor
@@ -18,42 +18,4 @@ export class HXCheckboxElement extends HXElement {
1818
static get template () {
1919
return `<style>${shadowStyles}</style>${shadowMarkup}`;
2020
}
21-
22-
/** @override */
23-
$onConnect () {
24-
this.addEventListener('click', this._onClick);
25-
}
26-
27-
/** @override */
28-
$onDisconnect () {
29-
this.removeEventListener('click', this._onClick);
30-
}
31-
32-
/**
33-
* @readonly
34-
* @type {HTMLElement}
35-
*/
36-
get controlElement () {
37-
return this.getRootNode().querySelector(`[id="${this.htmlFor}"]`);
38-
}
39-
40-
/**
41-
* ID of associated checkbox control.
42-
*
43-
* @type {string}
44-
*/
45-
get htmlFor () {
46-
return this.getAttribute('for') || '';
47-
}
48-
set htmlFor (value) {
49-
this.setAttribute('for', value);
50-
}
51-
52-
/** @private */
53-
_onClick () {
54-
let ctrl = this.controlElement;
55-
if (ctrl) {
56-
ctrl.click();
57-
}
58-
}
5921
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { HXElement } from './HXElement';
2+
3+
const STATE = {
4+
changed: 'hx-changed',
5+
touched: 'hx-touched',
6+
};
7+
8+
/**
9+
* Abstract class which defines shared behavior among all
10+
* form control custom elements (e.g., HXSelectControlElement,
11+
* HXCheckboxControlElement, etc.).
12+
*
13+
* ## States
14+
* States are applied as events occur on the `controlElement`.
15+
*
16+
* ### Changed
17+
* Applies the `hx-changed` content attribute when controlElement
18+
* emits a `change` event. This typically occurs after the value
19+
* has been modified and the user moves away (blurs) the text control.
20+
*
21+
* ### Touched
22+
* Applies the `hx-touched` content attribute when controlElement
23+
* emits a `blur` event (meaning that the user has "visited" the
24+
* text control and moved on).
25+
*
26+
* @abstract
27+
* @hideconstructor
28+
* @since 0.16.0
29+
*/
30+
export class HXFormControlElement extends HXElement {
31+
/** @override */
32+
constructor () {
33+
super();
34+
35+
this._onCtrlBlur = this._onCtrlBlur.bind(this);
36+
this._onCtrlChange = this._onCtrlChange.bind(this);
37+
}
38+
39+
/**
40+
* Adds `change` and `blur` event listeners to apply
41+
* "changed" and "touched" states to the custom control
42+
* element, in addition to superclass behavior.
43+
*
44+
* - preserves `$onConnect()` hook for subclasses
45+
*
46+
* @override
47+
*/
48+
connectedCallback () {
49+
super.connectedCallback();
50+
51+
let ctrl = this.controlElement;
52+
if (ctrl) {
53+
ctrl.addEventListener('change', this._onCtrlChange);
54+
ctrl.addEventListener('blur', this._onCtrlBlur);
55+
}
56+
}
57+
58+
/**
59+
* Removes event listeners added in connectedCallback,
60+
* in addition to superclass behavior.
61+
*
62+
* - preserves `$onDisconnect()` hook for subclasses
63+
*
64+
* @override
65+
*/
66+
disconnectedCallback () {
67+
super.disconnectedCallback();
68+
69+
let ctrl = this.controlElement;
70+
if (ctrl) {
71+
ctrl.removeEventListener('change', this._onCtrlChange);
72+
ctrl.removeEventListener('blur', this._onCtrlBlur);
73+
}
74+
}
75+
76+
/**
77+
* This should be overridden by subclasses.
78+
*
79+
* Logic should make a best effort to return an HTML
80+
* form control element (e.g., `<input>`, `<select>`,
81+
* `<textarea>`, etc.).
82+
*
83+
* @abstract
84+
* @default undefined
85+
* @type {?HTMLElement}
86+
*/
87+
get controlElement () {}
88+
89+
/**
90+
* @readonly
91+
* @type {Boolean} [false]
92+
*/
93+
get wasChanged () {
94+
return this.hasAttribute(STATE.changed);
95+
}
96+
97+
/**
98+
* @readonly
99+
* @type {Boolean} [false]
100+
*/
101+
get wasTouched () {
102+
return this.hasAttribute(STATE.touched);
103+
}
104+
105+
/** @private */
106+
_onCtrlBlur () {
107+
// communicate state via read-only, boolean content attribute
108+
this.$defaultAttribute(STATE.touched, '');
109+
}
110+
111+
/** @private */
112+
_onCtrlChange () {
113+
// communicate state via read-only, boolean content attribute
114+
this.$defaultAttribute(STATE.changed, '');
115+
}
116+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { HXFormControlElement } from './HXFormControlElement';
2+
3+
/**
4+
* Defines behavior for the `<hx-radio-control>` element.
5+
*
6+
* @extends HXFormControlElement
7+
* @hideconstructor
8+
* @since 0.16.0
9+
*/
10+
export class HXRadioControlElement extends HXFormControlElement {
11+
/** @override */
12+
static get is () {
13+
return 'hx-radio-control';
14+
}
15+
16+
/**
17+
* Fetch the first `<input type="radio">` descendant
18+
*
19+
* @override
20+
* @readonly
21+
* @type {?HTMLInputElement}
22+
*/
23+
get controlElement () {
24+
return this.querySelector('input[type="radio"]');
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!--
2+
TODO: update with SVG to render the radio button
3+
See: https://codepen.io/CITguy/pen/KEoBNZ for a prototype.
4+
-->
5+
<slot></slot>
Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { HXElement } from './HXElement';
2+
import shadowMarkup from './HXRadioElement.html';
3+
import shadowStyles from './HXRadioElement.less';
24

35
/**
4-
* Defines behavior for the `<hx-radio>` element.
6+
* Applies Shadow DOM to the `<hx-radio>` facade element.
57
*
68
* @extends HXElement
79
* @hideconstructor
@@ -13,40 +15,7 @@ export class HXRadioElement extends HXElement {
1315
}
1416

1517
/** @override */
16-
$onConnect () {
17-
this.addEventListener('click', this._onClick);
18-
}
19-
20-
/** @override */
21-
$onDisconnect () {
22-
this.removeEventListener('click', this._onClick);
23-
}
24-
25-
/**
26-
* @readonly
27-
* @type {HTMLElement}
28-
*/
29-
get controlElement () {
30-
return this.getRootNode().querySelector(`[id="${this.htmlFor}"]`);
31-
}
32-
33-
/**
34-
* ID of associated radio control.
35-
*
36-
* @type {string}
37-
*/
38-
get htmlFor () {
39-
return this.getAttribute('for') || '';
40-
}
41-
set htmlFor (value) {
42-
this.setAttribute('for', value);
43-
}
44-
45-
/** @private */
46-
_onClick () {
47-
let ctrl = this.controlElement;
48-
if (ctrl) {
49-
ctrl.click();
50-
}
18+
static get template () {
19+
return `<style>${shadowStyles}</style>${shadowMarkup}`;
5120
}
5221
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// TBD
Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,26 @@
1-
import { HXElement } from './HXElement';
1+
import { HXFormControlElement } from './HXFormControlElement';
22

33
/**
44
* Defines behavior for the `<hx-select-control>` element.
55
*
6-
* @extends HXElement
6+
* @extends HXFormControlElement
77
* @hideconstructor
88
* @since 0.16.0
99
*/
10-
export class HXSelectControlElement extends HXElement {
10+
export class HXSelectControlElement extends HXFormControlElement {
1111
/** @override */
1212
static get is () {
1313
return 'hx-select-control';
1414
}
1515

16-
/** @override */
17-
$onCreate () {
18-
this._onCtrlBlur = this._onCtrlBlur.bind(this);
19-
this._onCtrlChange = this._onCtrlChange.bind(this);
20-
}
21-
22-
/** @override */
23-
$onConnect () {
24-
let ctrl = this.controlElement;
25-
if (ctrl) {
26-
ctrl.addEventListener('change', this._onCtrlChange);
27-
ctrl.addEventListener('blur', this._onCtrlBlur);
28-
}
29-
}
30-
31-
/** @override */
32-
$onDisconnect () {
33-
let ctrl = this.controlElement;
34-
if (ctrl) {
35-
ctrl.removeEventListener('change', this._onCtrlChange);
36-
ctrl.removeEventListener('blur', this._onCtrlBlur);
37-
}
38-
}
39-
4016
/**
4117
* Fetch the first `<select>` descendant
4218
*
19+
* @override
4320
* @readonly
44-
* @type {?HTMLElement}
21+
* @type {?HTMLSelectElement}
4522
*/
4623
get controlElement () {
4724
return this.querySelector('select');
4825
}
49-
50-
/** @private */
51-
_onCtrlBlur () {
52-
// communicate state via attribute
53-
this.$defaultAttribute('hx-touched', '');
54-
}
55-
56-
/** @private */
57-
_onCtrlChange () {
58-
// communicate state via attribute
59-
this.$defaultAttribute('hx-changed', '');
60-
}
6126
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { HXFormControlElement } from './HXFormControlElement';
2+
3+
/**
4+
* Defines behavior for the `<hx-text-control>` element.
5+
*
6+
* @extends HXFormControlElement
7+
* @hideconstructor
8+
* @since 0.16.0
9+
*/
10+
export class HXTextControlElement extends HXFormControlElement {
11+
/** @override */
12+
static get is () {
13+
return 'hx-text-control';
14+
}
15+
16+
/**
17+
* Fetch the first text `<input>` descendant,
18+
* whether implicit (`<input />`) or explicit
19+
* (`<input type="text" />`).
20+
*
21+
* @override
22+
* @readonly
23+
* @type {?HTMLInputElement}
24+
*/
25+
get controlElement () {
26+
return this.querySelector('input:not([type]), input[type="text"]');
27+
}
28+
}

0 commit comments

Comments
 (0)