UNPKG

@forter/form-section

Version:
588 lines (516 loc) 14.6 kB
import { decorate as _decorate } from './_virtual/_rollupPluginBabelHelpers.js'; import { LitElement, property, html } from 'lit-element'; import style from './fc-form-section.css'; import bound from 'bound-decorator'; import { FcFormMixin } from '@forter/mixins/src/fc-form-mixin'; import { LifecycleEventsMixin } from '@forter/mixins/src/lifecycle-event-mixin'; import { isEmpty, clear, set, removeItemFromArray } from './fc-form-section-helper.js'; import { ifDefined } from 'lit-html/directives/if-defined'; const isRequired = elm => elm.required; const isFormField = elm => elm.nodeName === 'FC-FORM-FIELD'; const hasValue = value => value || value === 0; const isEmptyArray = arr => (arr || []).length === 0; const hasNoValue = elm => !hasValue(elm.value) && isEmptyArray(elm.values); const ALIGNMENTS = { ROW: 'row', COLUMN: 'column' }; /** * An element by Forter * <!-- Author: oweingart <oweingart@forter.com> --> * * ## Usage * * ```html * <script> * import '@forter/form-section'; * </script> * * <fc-form-section> * <fc-form-field></fc-form-field> * </fc-form-section> * ``` * * @element fc-form-section * @cssprop --fc-form-section-color - section font color. default: "balck" * @cssprop --fc-form-section-padding-left - section padding left. default: "5px" * @cssprop --fc-form-section-margin-right-item - section margin right of each field (row alignment only). default: "20px" * @cssprop --fc-form-section-label-invalid-color - section label color when the section is invalid. default: "var(--fc-red-900)" * @cssprop --fc-form-section-label-font-size - section label font size. default: "12px" * @cssprop --fc-form-section-label-font-weight - section label font weight. default: "normal" * @cssprop --fc-form-section-label-margin-bottom - section label margin bottom. default: "0px" * @cssprop --fc-form-section-arrow-color - section label arrow color (when section is collapsable) . default: "var(--fc-gray-700)" */ let FcFormSection = _decorate(null, function (_initialize, _LifecycleEventsMixin) { class FcFormSection extends _LifecycleEventsMixin { /** * Alignment of the form section (row/column) * @type {string} */ /** * If the section is collapsable * @type {boolean} */ /** * If the section is currently collapsed * @type {boolean} */ /** * Section label */ /** * * Internal Observables * */ /** * If the section is disabled * @type {Boolean} * @attr */ /** * If the section is valid * @type {Boolean} * @attr */ /** * If the section is dirty * @type {Boolean} * @attr */ /** * If the section was touched * @type {Boolean} * @attr */ /** @inheritdoc */ /** @inheritdoc */ constructor() { super(); _initialize(this); this.invalidFields = []; // holds the invalid fields this.missingRequiredFields = []; // holds the fields that are required and has no data this.model = {}; // holds the files path and it's data } /** * If section is valid * @return {Boolean|boolean} */ } return { F: FcFormSection, d: [{ kind: "field", decorators: [property({ type: String })], key: "align", value() { return ALIGNMENTS.ROW; } }, { kind: "field", decorators: [property({ type: Boolean })], key: "collapsable", value() { return false; } }, { kind: "field", decorators: [property({ type: Boolean })], key: "collapse", value() { return false; } }, { kind: "field", decorators: [property({ type: String })], key: "label", value: void 0 }, { kind: "field", decorators: [property({ type: Boolean })], key: "disabled", value() { return false; } }, { kind: "field", decorators: [property({ type: Boolean })], key: "valid", value() { return false; } }, { kind: "field", decorators: [property({ type: Boolean })], key: "dirty", value() { return false; } }, { kind: "field", decorators: [property({ type: Boolean })], key: "touched", value() { return false; } }, { kind: "field", static: true, key: "is", value() { return 'fc-form-section'; } }, { kind: "field", static: true, key: "styles", value() { return [style]; } }, { kind: "get", key: "isValid", value: function isValid() { if (!this.dirty) return true; return this.valid; } }, { kind: "method", decorators: [bound], key: "onPressLabel", value: function onPressLabel(e) { e.preventDefault(); const { key, target } = e; if (key === 'Enter') { target.click(); } } /** * on click collapse */ }, { kind: "method", decorators: [bound], key: "onCollapse", value: function onCollapse() { this.collapse = !this.collapse; } /** * Disable the section if section is hidden after the collapse has ended */ }, { kind: "method", decorators: [bound], key: "onFinishCollapse", value: function onFinishCollapse() { if (this.collapsable) { this.disabled = !this.collapse; } } /** * Fire event to parent when one of the field had change * @param {String} eventName * @param {HTMLElement} field * @param {Object} changedDetails */ }, { kind: "method", decorators: [bound], key: "fireEventOnChange", value: function fireEventOnChange(eventName, field, changedDetails) { const { invalidFields, dirty, model, valid } = this; this.dispatchEvent(new CustomEvent(eventName, { detail: { valid, dirty, invalidFields, model, field, changedDetails } })); } /** * Update section model after change */ }, { kind: "method", decorators: [bound], key: "updateModel", value: function updateModel() { this.childNodes.forEach(field => { const value = field.value || field.values; const { path } = field; if (path) { if (isEmpty(value)) { clear(this.model, path); } else { set(this.model, path, value); } } }); } /** @inheritdoc */ }, { kind: "method", key: "firstUpdated", value: function firstUpdated() { this.updateModel(); } /** * Update invalidFields after change * @param {Boolean} isPathValid * @param {String} changedPath */ }, { kind: "method", decorators: [bound], key: "updateInvalidFields", value: function updateInvalidFields(isPathValid, changedPath) { if (isPathValid) { removeItemFromArray(this.invalidFields, changedPath); } else { changedPath && !this.invalidFields.includes(changedPath) && this.invalidFields.push(changedPath); } } /** * Update missing required fields after change */ }, { kind: "method", key: "updateMissingRequiredFields", value: function updateMissingRequiredFields() { const emptyRequiredFields = this.requiredFields.filter(hasNoValue); if (emptyRequiredFields.length === this.requiredFields.length && !this.required) { this.missingRequiredFields = []; // in case all fields were cleared, so section is valid } else { this.missingRequiredFields = emptyRequiredFields.map(e => e.path); } } }, { kind: "method", decorators: [bound], key: "onFieldRemoved", value: function onFieldRemoved({ target, detail: { path } }) { clear(this.model, path); // remove form model this.requiredFields = this.childNodes.filter(isRequired); this.fieldsToPathMap = this.buildFieldToPathsMap(); this.updateInvalidFields(true, path); // remove path from invalid paths if it's there this.updateMissingRequiredFields(); // re-build list of missing required fields this.updateDirty(); // re-build dirty filed this.updateValid(); // re-build valid field this.fireEventOnChange('change', target, { path }); } /** * All the updates perform when something changes * @param {Boolean} valid * @param {String} path */ }, { kind: "method", decorators: [bound], key: "updateSectionData", value: function updateSectionData(valid, path) { this.updateModel(); // re-build the model this.updateInvalidFields(valid, path); // re-build list of invalid fields this.updateMissingRequiredFields(); // re-build list of missing required fields this.updateDirty(); // re-build dirty filed this.updateValid(); // re-build valid field } /** * On change field with list of values * @param {HTMLElement} target * @param {Object} detail */ }, { kind: "method", decorators: [bound], key: "onChangeFieldWithListOfValues", value: function onChangeFieldWithListOfValues({ target, detail }) { const { valid, path } = detail; this.updateSectionData(valid, path); this.fireEventOnChange('change', target, detail); } /** * On change field single value * @param {HTMLElement} target * @param {Object} detail */ }, { kind: "method", decorators: [bound], key: "onChangeFieldWithSingleValue", value: function onChangeFieldWithSingleValue({ target, detail }) { const { valid, path } = detail; this.updateSectionData(valid, path); this.fireEventOnChange('change', target, detail); } }, { kind: "method", decorators: [bound], key: "onFieldRequiredChange", value: function onFieldRequiredChange({ target, detail }) { const { valid, path } = detail; this.requiredFields = this.childNodes.filter(isRequired); this.updateSectionData(valid, path); this.fireEventOnChange('change', target, detail); } /** * Build a map with the fields path and the field DOM object * @return {{}} */ }, { kind: "method", decorators: [bound], key: "buildFieldToPathsMap", value: function buildFieldToPathsMap() { const map = {}; this.childNodes.filter(isFormField).forEach(fieldElement => set(map, fieldElement.path, fieldElement)); return map; } }, { kind: "method", decorators: [bound], key: "onSlotChange", value: function onSlotChange() { this.passPropsToSlots(); this.childNodes.forEach(child => child.addEventListener('change-values', this.onChangeFieldWithListOfValues)); this.childNodes.forEach(child => child.addEventListener('change-value', this.onChangeFieldWithSingleValue)); this.childNodes.forEach(child => child.addEventListener('field-removed', this.onFieldRemoved)); this.childNodes.forEach(child => child.addEventListener('required-change', this.onFieldRequiredChange)); this.requiredFields = this.childNodes.filter(isRequired); this.fieldsToPathMap = this.buildFieldToPathsMap(); this.dispatchEvent(new CustomEvent('field-added')); } /** * Render the label * @return {*} */ }, { kind: "method", decorators: [bound], key: "renderLabel", value: function renderLabel() { const { collapsable, collapse, isValid, label } = this; const isSlottedLabel = this.querySelector('label'); const labelTemplate = html` ${isSlottedLabel ? html`<slot name="label" ?invalid="${!isValid}"></slot>` : ''} ${label ? html`<label ?invalid="${!isValid}">${label}</label>` : ''}`; if (collapsable) { // Collapsed label const isCollapsed = collapsable && !collapse; const arrowDirection = isCollapsed ? 'right' : 'down'; const invalid = !isValid ? 'invalid' : ''; return html` <div class="collapsable-label-container" @click="${this.onCollapse}" @keyup="${this.onPressLabel}" tabindex="0"> <div class="arrow ${arrowDirection} ${invalid}"></div> ${labelTemplate} </div>`; } else { return html`${labelTemplate}`; } } }, { kind: "method", key: "updated", value: function updated() { const { align } = this; const children = Array.from(this.children); children.forEach(child => { child.setAttribute('align', align); }); } /** @inheritdoc */ }, { kind: "method", key: "render", value: function render() { const { align, collapsable, collapse } = this; let tabIndex = '0'; if (collapsable && !collapse) { tabIndex = '-1'; } return html` ${this.renderLabel()} <section align="${align}" ?collapsed="${collapse}" ?collapsable="${collapsable}" @animationend="${this.onFinishCollapse}" @transitionend="${this.onFinishCollapse}"> <slot @slotchange="${this.onSlotChange}" .tabIndex="${ifDefined(tabIndex)}"> </slot> </section> `; } }] }; }, LifecycleEventsMixin(FcFormMixin(LitElement))); export { ALIGNMENTS, FcFormSection }; //# sourceMappingURL=FcFormSection.js.map