@forter/form-section
Version:
form-section from Forter Components
588 lines (516 loc) • 14.6 kB
JavaScript
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"
="${this.onCollapse}"
="${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}"
="${this.onFinishCollapse}"
="${this.onFinishCollapse}">
<slot ="${this.onSlotChange}"
.tabIndex="${ifDefined(tabIndex)}">
</slot>
</section>
`;
}
}]
};
}, LifecycleEventsMixin(FcFormMixin(LitElement)));
export { ALIGNMENTS, FcFormSection };
//# sourceMappingURL=FcFormSection.js.map