lion/packages/form-core/src/LionField.js
Joren Broekema 874ff48339 feat(form-core): form-core types
Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
2020-09-02 09:02:47 +02:00

193 lines
5.2 KiB
JavaScript

import { LitElement, SlotMixin } from '@lion/core';
import { ValidateMixin } from './validate/ValidateMixin.js';
import { FocusMixin } from './FocusMixin.js';
import { FormatMixin } from './FormatMixin.js';
import { FormControlMixin } from './FormControlMixin.js';
import { InteractionStateMixin } from './InteractionStateMixin.js'; // applies FocusMixin
/**
* `LionField`: wraps <input>, <textarea>, <select> and other interactable elements.
* Also it would follow a nice hierarchy: lion-form -> lion-fieldset -> lion-field
*
* Note: We don't support placeholders, because we have a helper text and
* placeholders confuse the user with accessibility needs.
*
* Please see the docs for in depth information.
*
* @example
* <lion-field name="myName">
* <label slot="label">My Input</label>
* <input type="text" slot="input">
* </lion-field>
*
* @customElement lion-field
*/
// @ts-expect-error base constructors same return type
export class LionField extends FormControlMixin(
InteractionStateMixin(FocusMixin(FormatMixin(ValidateMixin(SlotMixin(LitElement))))),
) {
static get properties() {
return {
autocomplete: {
type: String,
reflect: true,
},
value: {
type: String,
},
};
}
get _inputNode() {
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
}
/** @type {number} */
get selectionStart() {
const native = this._inputNode;
if (native && native.selectionStart) {
return native.selectionStart;
}
return 0;
}
set selectionStart(value) {
const native = this._inputNode;
if (native && native.selectionStart) {
native.selectionStart = value;
}
}
/** @type {number} */
get selectionEnd() {
const native = this._inputNode;
if (native && native.selectionEnd) {
return native.selectionEnd;
}
return 0;
}
set selectionEnd(value) {
const native = this._inputNode;
if (native && native.selectionEnd) {
native.selectionEnd = value;
}
}
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
/** @type {string} */
set value(value) {
// if not yet connected to dom can't change the value
if (this._inputNode) {
this._setValueAndPreserveCaret(value);
/** @type {string | undefined} */
this.__value = undefined;
} else {
this.__value = value;
}
}
get value() {
return (this._inputNode && this._inputNode.value) || this.__value || '';
}
constructor() {
super();
this.name = '';
/** @type {string | undefined} */
this.autocomplete = undefined;
}
/**
* @param {import('lit-element').PropertyValues } changedProperties
*/
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
/** @type {any} */
this._initialModelValue = this.modelValue;
}
connectedCallback() {
super.connectedCallback();
this._onChange = this._onChange.bind(this);
this._inputNode.addEventListener('change', this._onChange);
this.classList.add('form-field'); // eslint-disable-line
}
disconnectedCallback() {
super.disconnectedCallback();
this._inputNode.removeEventListener('change', this._onChange);
}
/**
* @param {import('lit-element').PropertyValues } changedProperties
*/
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('disabled')) {
this._inputNode.disabled = this.disabled;
this.validate();
}
if (changedProperties.has('name')) {
this._inputNode.name = this.name;
}
if (changedProperties.has('autocomplete')) {
this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete);
}
}
resetInteractionState() {
super.resetInteractionState();
this.submitted = false;
}
reset() {
this.modelValue = this._initialModelValue;
this.resetInteractionState();
}
/**
* Clears modelValue.
* Interaction states are not cleared (use resetInteractionState for this)
*/
clear() {
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
}
_onChange() {
this.dispatchEvent(
new CustomEvent('user-input-changed', {
bubbles: true,
}),
);
}
/**
* Restores the cursor to its original position after updating the value.
* @param {string} newValue The value that should be saved.
*/
_setValueAndPreserveCaret(newValue) {
// Only preserve caret if focused (changing selectionStart will move focus in Safari)
if (this.focused) {
// Not all elements might have selection, and even if they have the
// right properties, accessing them might throw an exception (like for
// <input type=number>)
try {
const start = this._inputNode.selectionStart;
this._inputNode.value = newValue;
// The cursor automatically jumps to the end after re-setting the value,
// so restore it to its original position.
this._inputNode.selectionStart = start;
this._inputNode.selectionEnd = start;
} catch (error) {
// Just set the value and give up on the caret.
this._inputNode.value = newValue;
}
} else {
this._inputNode.value = newValue;
}
}
}