@dom-native/ui
Version:
Minimalist, stylable, and extensible DOM UI base components based dom-native
192 lines (164 loc) • 5.41 kB
text/typescript
import { elem, getAttr, html, on, setAttr } from 'dom-native';
import { BaseFieldElement } from './d-base-field.js';
import { svgSymbolEl } from './d-ico-symbol.js';
const SHADOW_CONTENT = html`
<slot name="icon-lead"></slot>
<slot name="icon-trail"></slot>
<slot name="label"></slot>
<slot name="label-trail"></slot>
<slot name="text-trail"></slot>
<div class="box" part="box"></div>
`;
/**
* Base field element for d-input and d-text (text area)
*
* Attributes:
* - See BaseFieldElement.
* - `password?`: set input as password
*
* Properties:
* - See BaseFieldElement.
* - `password: boolean`: reflective of attribute.
*
* CSS:
* - See BaseFieldElement.
*
* Content:
* - none
*
* Events:
* - `CHANGE` see BaseFieldElement.
*
*/
export abstract class BaseInputElement extends BaseFieldElement {
// --- Component Key Children (on demand for more DOM mutation resiliency)
protected ctrlEl!: HTMLElement | HTMLInputElement | HTMLTextAreaElement;
protected labelEl!: HTMLElement | null;
protected labelTrailEl?: HTMLElement;
// --- Input value are always text
abstract get value(): string | null;
abstract set value(val: string | null)
constructor() {
super();
// Base shadow content
const content = SHADOW_CONTENT.cloneNode(true);
// -- Add the input control to the shadow dom
const ctrlEl = this.createCtrlEl();
setAttr(ctrlEl, { part: 'ctrl' });
ctrlEl.classList.add('ctrl');
// Carry over the attributes
let [spellcheck, autocorrect, autocomplete] = getAttr(ctrlEl, "spellcheck", "autocorrect", "autocomplete")
setAttr(ctrlEl, { spellcheck, autocorrect, autocomplete });
this.ctrlEl = ctrlEl;
content.appendChild(this.ctrlEl);
// Forward d-input attributes to HTMLInputElement ctrl element
const [readonly, disabled, placeholder] = getAttr(this, 'readonly', 'disabled', 'placeholder');
if (isValueElement(this.ctrlEl)) {
setAttr(this.ctrlEl, { readonly, disabled, placeholder });
}
// Build the shadow and append
const shadow = this.attachShadow({ mode: 'open' });
shadow.append(content);
}
//#region ---------- Lifecycle ----------
// Component initialization (will be called once by BaseHTMLElement on first connectedCallback)
init() {
super.init();
// --- Add icon-lead if needed
const icoLead = this.iconLead;
if (icoLead) {
this.classList.add("has-icon-lead");
this.appendChild(svgSymbolEl(icoLead, { slot: "icon-lead" }));
}
// --- Add the icon-trail if needed
const icoTrail = this.iconTrail;
if (icoTrail) {
this.classList.add("has-icon-trail");
this.appendChild(svgSymbolEl(icoTrail, { slot: "icon-trail" }));
}
const [label, labelTrail, textTrail] = getAttr(this, 'label', 'label-trail', 'text-trail');
// add the label-trail
if (labelTrail) {
this.appendChild(elem('label', { slot: 'label-trail', $: { textContent: labelTrail } }));
}
// add the label
if (label) {
this.appendChild(elem('label', { slot: 'label', $: { textContent: label } }));
}
// add the text-trail
if (textTrail) {
this.classList.add("has-text-trail");
this.appendChild(elem('div', { slot: 'text-trail', $: { textContent: textTrail } }));
}
// --- Set the states
const value = this.getInitialValue();
this.noValue = (!value);
this.value = value;
// --- Bind internal component events
on(this.ctrlEl, 'keyup, change, focusin, focusout', (evt) => {
const m_input = this;
switch (evt.type) {
case 'keyup':
// if exec, triggerCancel
if (evt.key === 'Escape') {
m_input.triggerCancel();
}
break;
case 'focusin':
this.dFocus = true;
break;
case 'focusout':
this.dFocus = false;
break;
case 'change':
// here we forward the value from the input to this component state value to make srue all get changed.
if (isValueElement(this.ctrlEl)) {
this.value = this.ctrlEl.value;
} else {
this.value = this.textContent;
}
break;
}
});
// TODO: minor bug when user re-click on label when input is empty, it toggle focus off.
on(this.shadowRoot, 'click', 'label', (evt) => {
this.ctrlEl.focus();
});
}
attributeChangedCallback(name: string, oldVal: any, newVal: any) {
super.attributeChangedCallback(name, oldVal, newVal); // always
if (this.initialized) {
switch (name) {
case 'readonly':
if (isValueElement(this.ctrlEl)) {
setAttr(this.ctrlEl, { readonly: newVal });
}
break;
case 'disabled':
if (isValueElement(this.ctrlEl)) {
setAttr(this.ctrlEl, { disabled: newVal });
}
break;
case 'placeholder':
if (isValueElement(this.ctrlEl)) {
setAttr(this.ctrlEl, { placeholder: newVal });
} else {
// TODO - check if
this.value = this.value;
}
break;
}
}
}
//#endregion ---------- /Lifecycle ----------
//#region ---------- HTML Element Overrides ----------
focus() {
this.ctrlEl?.focus();
}
//#endregion ---------- /HTML Element Overrides ----------
abstract createCtrlEl(): HTMLElement | HTMLInputElement | HTMLTextAreaElement
abstract getInitialValue(): string | null
}
export function isValueElement(obj: HTMLElement | HTMLInputElement | HTMLTextAreaElement): obj is HTMLInputElement | HTMLTextAreaElement {
return (obj instanceof HTMLInputElement || obj instanceof HTMLTextAreaElement);
}