@skhemata/skhemata-form
Version:
Skhemata Form Web Component. This web component can be used as base web component when working with forms and inputs.
249 lines (216 loc) • 7.62 kB
text/typescript
import { html, property, css, CSSResult, unsafeHTML } from '@skhemata/skhemata-base';
import { SkhemataFormInput } from './SkhemataFormInput';
import { SkhemataFormTextbox } from './SkhemataFormTextbox';
import { SkhemataFormTextarea } from './SkhemataFormTextarea';
import { SkhemataFormDropdown } from './SkhemataFormDropdown';
import { SkhemataFormDropzone } from './SkhemataFormDropzone';
import { SkhemataFormButton } from './SkhemataFormButton';
import { SkhemataFormQuill } from './SkhemataFormQuill';
import { SkhemataFormCheckbox } from './SkhemataFormCheckbox';
import { SkhemataFormToggle } from './SkhemataFormToggle';
import { SkhemataFormDatePicker } from './SkhemataFormDatePicker';
/**
* Repeater component that repeats inputs passed in.
*/
export class SkhemataFormRepeat extends SkhemataFormInput {
rowName = "";
addRowButtonText = "Add Row";
removeRowButtonText = "Remove Row";
rowLimit = 10;
repeatedFields = [];
type = "SKHEMATA-FORM-REPEAT";
fieldNodes = [];
rowData = [];
// Pair all component types with appropriate component
allowedComponents = {
textbox: 'skhemata-form-textbox',
textarea: 'skhemata-form-textarea',
dropdown: 'skhemata-form-dropdown',
dropzone: 'skhemata-form-dropzone',
button: 'skhemata-form-button',
quill: 'skhemata-form-quill',
checkbox: 'skhemata-form-checkbox',
toggle: 'skhemata-form-toggle',
datepicker: 'skhemata-form-date-picker'
};
static get scopedElements(){
return {
'skhemata-form-textbox': SkhemataFormTextbox,
'skhemata-form-textarea': SkhemataFormTextarea,
'skhemata-form-dropdown': SkhemataFormDropdown,
'skhemata-form-dropzone': SkhemataFormDropzone,
'skhemata-form-button': SkhemataFormButton,
'skhemata-form-quill': SkhemataFormQuill,
'skhemata-form-checkbox': SkhemataFormCheckbox,
'skhemata-form-toggle': SkhemataFormToggle,
'skhemata-form-date-picker': SkhemataFormDatePicker
}
}
constructor() {
super();
// Event listener that updates local state based on skhemata-form state
this.addEventListener('update-data', (e: any) => {
this.rowData = e.detail.data[this.name];
this.value = this.rowData;
});
}
async firstUpdated() {
await super.firstUpdated();
this.value = this.rowData;
const currentNodes = this.shadowRoot.querySelectorAll('[data-row-num]');
currentNodes.forEach((row, index) => {
const nodes = row.querySelectorAll('[skhemata-input]');
this.dispatchEvent(
new CustomEvent('add-row', {
detail:{
name: this.name,
rowIndex: index,
nodes: nodes
},
composed: true,
bubbles: true,
})
);
this.fieldNodes = Array.from(nodes);
})
}
/**
* Appends a new row of inputs to the end of the component
*
*/
addRow() {
this.rowData.push({});
this.requestUpdate();
}
/**
* Removes row based on the index of the row
*/
removeRow(event: any) {
// remove-row splices the appropriate data from skhemata-form data property
this.dispatchEvent(
new CustomEvent('remove-row', {
detail:{
name: this.name,
rowIndex: event.originalTarget.parentNode.dataset.rowNum
},
composed: true,
bubbles: true,
})
);
this.requestUpdate();
}
static get styles() {
return <CSSResult[]>[
...super.styles,
css`
:host {
width: 100%;
}
h3 {
font-size: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-weight: bold;
}`
];
}
/**
* Renders component based on repeatedFields object
* @param name
* @param attributes
* @param value
* @param content content that is inserted to <slot></slot>
* @returns HtmlTemplate
*/
renderComponent (name: String, attributes: Object, value: any = '', content: String = '') {
// add excception for date-picker value
if(name == 'skhemata-form-date-picker') {
if(value){
value = value.slice(0,10);
}
}
const templateString = `<${name} ${Object.keys(attributes).map(key => {
return `${key}="${attributes[key]}"`;
}).join(' ')} value="${value}" ${value == true ? 'checked="true"' : ''} skhemata-input>${content}</${name}>`;
return html`${unsafeHTML(templateString)}`;
}
/**
* Life cycle method that triggers whenever updated
* if there are new rows added, trigger add row event
*/
updated() {
const currentNodes = this.shadowRoot.querySelectorAll('[skhemata-input]');
// check if last row of rowdata is blank
if(this.rowData != undefined) {
if(this.rowData[this.rowData.length - 1] != undefined) {
if(Object.keys(this.rowData[this.rowData.length - 1]).length == 0) {
if(currentNodes.length > this.fieldNodes.length) {
// Filter out previous nodes from currentNodes
const newNodes = Array.from(currentNodes).filter(node => !this.fieldNodes.includes(node));
/**
* Dispatch the add-row event
* add-row attaches eventlisteners to newly created inputs
*/
this.dispatchEvent(
new CustomEvent('add-row', {
detail:{
name: this.name,
rowIndex: this.rowData?.length - 1,
nodes: newNodes
},
composed: true,
bubbles: true,
})
);
}
}
}
}
// Save the current nodes as previous nodes
this.fieldNodes = Array.from(currentNodes);
}
render() {
const field = html`
<div class="repeater-field field">
${
this.label && !this.horizontal
? html`<label class="label">${this.label}</label>`
: null
}
${
this.description && !this.horizontal
? html`<p>${this.description}</p>`
: null
}
${
this.rowData?.map((data, i) => html`<div data-row-num=${i}>
<h3>${this.rowName} #${i + 1}</h3>
${
this.repeatedFields.map( (field, j) =>
html`${(field.type in this.allowedComponents && (field.hide === undefined || !field.hide)) ? this.renderComponent(this.allowedComponents[field.type], {...field.attributes, 'row-index': i}, data[field.attributes.name], field.content) : ''}`
)
}<button class="button is-danger" =${(e) => this.removeRow(e)}>${this.removeRowButtonText}</button><hr></div>`)
}
${
this.rowData?.length < this.rowLimit ?
html`<button class="button is-success" =${this.addRow}>${this.addRowButtonText}</button>`
: ''
}
</div>
`;
const horizontalFieldLabel = html`
<div class="field-label column is-one-quarter" style="text-align: left">
${this.label ? html`<label class="label">${this.label}</label>` : null}
${this.description ? html`<p>${this.description}</p>` : null}
</div>
`;
const horizontalField = html`
<div class="field is-horizontal">
${this.label || this.description ? horizontalFieldLabel : null}
<div class="field-body column">${field}</div>
</div>
`;
return this.horizontal ? horizontalField : field;
}
}