UNPKG

@dom-native/ui

Version:

Minimalist, stylable, and extensible DOM UI base components based dom-native

190 lines (154 loc) 6.14 kB
// Making sure the ts helper for decorator is set import { BaseHTMLElement, className, getAttr, on, puller, pusher, setAttr, trigger } from 'dom-native'; export const DX_OPTIONS_NAMES = { PULL_SKIP_UNCHECKED: "pull_skip_unchecked" } /** * Base component for any Field base custom component that provide a `.value` `.name` interface. * This will also automatically set the component as css class `dx` if it has a name, * so that by default they pushed/pulled by `mvdom push/pull` system. * * Attributes: * - `readonly?`: set the component as readonly. * - `disabled?`: set the component as disabled. * - `name?`: reflective of 'name' property. If absent, `.no-name` css class. * - `label?`: if absent, this css `.no-label` will be set. * - `value?`: this is the initial value of the component. TODO: needs to unify when no value (right now .empty for input, .no-value for c-select) * - `placeholder?`: placeholder text. * - `autofocus?`: will do a focus on requestAnimationFrame (later might add timeout when attribute has numeric value) * * Properties: * - `readonly: boolean`: reflective of attribute. * - `disabled: boolean`: reflective of attribute. * - `noValue: boolean`: reflective of css `no-value`. * - `name?: string`: reflective of attribute. * - `placeholder?: string`: reflective of attribute. * - `label?: string`: Manged by subClass. (reflection up to subclasss). * - `value?: any`: Managed by subClass. (reflection up to subclasss). * - `noValue: boolean`: reflective of CSS Attribute `.no-label`. * * CSS: * - `.no-value` when the field has no value (for now, managed by sub class) * - `.no-label` when the field has no label. * - `.dx` will be added when field component has a name. * * Content: * - (subclass dependent) * * Events: * - `CHANGE` Sub Class call `triggerChange()` which will trigger a `evt.detail: {name: string, value: string}`. * * Sub class MUST: * - Sub classes MUST call `super.init()` in their `init()` implementation. * - Manage value property * */ export abstract class BaseFieldElement extends BaseHTMLElement { private _eventReady = false; /** * Determine if this field is event ready (can fire CHANGE and such events) * This is next nextFrame of init so that does not trigger uncessary CHANGE event on DOM setup */ protected get eventReady() { return this._eventReady } static get observedAttributes(): string[] { return ['disabled', 'readonly', 'placeholder', 'ico-lead']; } //// Properties (Attribute Reflective) get readonly(): boolean { return this.hasAttribute('readonly') } set readonly(v: boolean) { setAttr(this, 'readonly', (v) ? '' : null) } get disabled(): boolean { return this.hasAttribute('disabled') } set disabled(v: boolean) { setAttr(this, 'disabled', (v) ? '' : null) } get dFocus(): boolean { return this.classList.contains('d-focus') } set dFocus(v: boolean) { className(this, { 'd-focus': v }) } get name() { return getAttr(this, 'name') } set name(v: string | null) { setAttr(this, 'name', v) } get placeholder() { return getAttr(this, 'placeholder') } set placeholder(v: string | null) { setAttr(this, 'placeholder', v) } get iconLead() { return getAttr(this, 'icon-lead') } get iconTrail() { return getAttr(this, 'icon-trail') } // Note: Making the return object type a little bit more flexible, as might be more data later. get dxOptions(): null | { [key: string]: any } { let [dx, dataDx] = getAttr(this, 'dx', 'data-dx'); let dxStr = dx || dataDx; if (!dxStr) return null; let obj = dxStr.split(',').reduce((acc: any, item) => { acc[item] = true; return acc; }, {}); return obj; } //// Properties (CSS Reflective) get noValue() { return this.classList.contains('no-value') } set noValue(v: boolean) { className(this, { 'no-value': v }) } //// Property (Value) abstract get value(): any abstract set value(val: any) //#region ---------- Lifecycle ---------- init() { super.init(); // best practice, even if it in this case, the parent.init() is blank. this.classList.add('d-field'); // for now until @onEvents supports :host - @onEvent('focusin, focusout', ':host') on(this, 'focusin, focusout', evt => { switch (evt.type) { case 'focusin': this.dFocus = true; break; case 'focusout': this.dFocus = false; break; } }) const [name, label] = getAttr(this, 'name', 'label'); if (!label) { this.classList.add('no-label'); } // by default if we have a 'name' attribute we add // - The '.dx' to allow seamless mvdom push/pull // - The '.c-field' which specifies that this component has name/value so that // we can use the same `mvdom` pusher/puller for all if (name && name.length > 0) { this.classList.add('dx'); } requestAnimationFrame(() => { this._eventReady = true; }); let autofocus = getAttr(this, "autofocus"); if (autofocus != null) { requestAnimationFrame(() => { this.focus(); }); } } // Called when an observed attribute has been added, removed, updated, or replaced attributeChangedCallback(attrName: string, oldVal: any, newVal: any) { switch (attrName) { case 'readonly': break; case 'disabled': break; } } //#endregion ---------- /Lifecycle ---------- triggerChange() { // Will trigger only if the component has been initialized and event ready if (this.initialized && this.eventReady) { const value = this.value; const name = this.name; trigger(this, "CHANGE", { detail: { name, value } }); } } triggerCancel() { // Will trigger only if the component has been initialized and event ready if (this.initialized && this.eventReady) { const value = this.value; const name = this.name; trigger(this, "CANCEL", { detail: { name, value } }); } } } //#region ---------- Register mvdom dx ---------- pusher('.d-field', function (this: BaseFieldElement, val: any) { this.value = val; }); puller('.d-field', function (this: BaseFieldElement) { return this.value; }); //#endregion ---------- /Register mvdom dx ----------