import { html, nothing, PropertyValueMap, unsafeCSS } from "lit";
import { customElement, property, queryAll } from "lit/decorators.js";
import { FDiv, FRoot, injectCss } from "@nonfx/flow-core";
import eleStyle from "./f-form-array.scss?inline";
import {
CanValidateFields,
FFormInputElements,
FormBuilderArrayField,
FormBuilderBaseField,
FormBuilderValidationPromise,
FormBuilderValues
} from "../../types";
import fieldRenderer from "../f-form-builder/fields";
import { createRef, Ref } from "lit/directives/ref.js";
import { isEmptyArray } from "../../modules/utils";
import { validateField } from "../../modules/validation/validator";
import { SimpleSubject } from "@nonfx/flow-core-config";
import { getEssentialFlowCoreStyles, propogateProperties } from "../../modules/helpers";
import { FFormObject } from "../f-form-object/f-form-object";
import { FIconButton } from "@nonfx/flow-core";
import { ifDefined } from "lit/directives/if-defined.js";
import globalStyle from "./f-form-array-global.scss?inline";
injectCss("f-form-array", globalStyle);
export type ArrayValueType = (
| string
| string[]
| number
| number[]
| unknown
| unknown[]
| undefined
)[];
@customElement("f-form-array")
export class FFormArray extends FRoot {
/**
* css loaded from scss file
*/
static styles = [unsafeCSS(eleStyle), unsafeCSS(globalStyle), ...getEssentialFlowCoreStyles()];
/**
* @attribute comments baout title
*/
@property({ type: Object })
config!: FormBuilderArrayField;
/**
* @attribute value
*/
@property({
type: Object,
hasChanged(newVal: ArrayValueType, oldVal: ArrayValueType) {
return JSON.stringify(newVal) !== JSON.stringify(oldVal);
}
})
value!: ArrayValueType;
@property({ reflect: true, type: String })
state?: "primary" | "default" | "success" | "warning" | "danger" = "default";
/**
* @attribute Gap is used to define the gap between the elements
*/
@property({ reflect: true, type: String })
gap?: "large" | "medium" | "small" | "x-small" = "medium";
@queryAll("f-icon-button.f-form-array-action")
actions?: NodeListOf;
fieldRefs: Ref[] = [];
showWhenSubject!: SimpleSubject;
get isRequired() {
return !this.config.allowEmpty;
}
render() {
this.fieldRefs = [];
if (this.isRequired) {
let valueCount = 1;
if (this.value && !isEmptyArray(this.value)) {
valueCount = this.value.length;
} else {
this.value = [null];
}
return html`${this.buildFields(valueCount)}`;
} else {
let valueCount = 0;
if (this.value && !isEmptyArray(this.value)) {
valueCount = this.value.length;
} else {
this.value = [];
}
return html`${this.buildFields(valueCount)}`;
}
}
getFieldValue(index: number) {
return this.value ? this.value[index] : undefined;
}
buildFields(valueCount: number) {
const fieldTemplates = [];
for (let i = 0; i < valueCount; i++) {
const fieldRef: Ref = createRef();
this.fieldRefs.push(fieldRef);
fieldTemplates.push(
html` ${fieldRenderer[this.config.field.type](
``,
this.config.field,
fieldRef,
this.getFieldValue(i)
)}
${i === 0 && this.isRequired
? html` `
: html` {
this.removeField(i);
}}
/>`}
`
);
}
return html`
${this.config.label
? html`
${this.config.label?.title}
${this.config.label?.iconTooltip
? html` `
: ""}
${!this.isRequired
? html``
: ``}
${this.config.label?.description
? html` ${this.config.label?.description}`
: ""}
`
: ``}
${fieldTemplates.length > 0
? html` ${fieldTemplates} `
: ``}
${this.config.helperText
? html`${this.config?.helperText}`
: nothing}
`;
}
async validate(silent = false) {
await this.updateComplete;
const fieldConfig = this.config.field;
const allValidations: FormBuilderValidationPromise[] = [];
this.fieldRefs.forEach(fieldRef => {
if ((fieldConfig.type === "object" || fieldConfig.type === "array") && fieldRef.value) {
allValidations.push(fieldRef.value.validate(silent));
allValidations.push(validateField(fieldConfig, fieldRef.value, silent));
} else {
allValidations.push(
validateField(
this.config.field as CanValidateFields,
fieldRef.value as FFormInputElements,
silent
)
);
}
});
return Promise.all(allValidations);
}
applyLabelOffSet(element: HTMLElement) {
let totalHeight = 0;
if (this.config.field.type === "object") {
let innerLabelTotal = (element as FFormObject).getLabelOffSet();
if (innerLabelTotal > 0) {
innerLabelTotal += 4;
}
totalHeight += innerLabelTotal;
} else {
const labelHeight: number =
(element.querySelector("[slot='label']") as HTMLElement)?.offsetHeight ?? 0;
const descriptionHeight: number =
(element.querySelector("[slot='description']") as HTMLElement)?.offsetHeight ?? 0;
totalHeight = labelHeight + descriptionHeight;
if (totalHeight > 0) {
totalHeight += 4;
}
}
if (this.actions) {
this.actions.forEach(el => {
el.style.marginTop = `${totalHeight + 8}px`;
el.style.visibility = "visible";
});
}
}
/**
* updated hook of lit element
* @param _changedProperties
*/
protected async updated(
_changedProperties: PropertyValueMap | Map
): Promise {
super.updated(_changedProperties);
await this.updateComplete;
this.fieldRefs.forEach((ref, idx) => {
if (ref.value) {
if (idx === 0) {
this.applyLabelOffSet(ref.value);
}
ref.value.showWhenSubject = this.showWhenSubject;
const fieldValidation = async (event: Event) => {
event.stopPropagation();
this.value[idx] = ref.value?.value;
/**
* FLOW-903 moving up to avoid race condition
*/
if (event.type !== "blur") {
this.dispatchInputEvent();
}
await validateField(
this.config.field as CanValidateFields,
ref.value as FFormInputElements,
false
);
};
ref.value.oninput = fieldValidation;
ref.value.onblur = fieldValidation;
const fieldConfig = this.config.field;
if (fieldConfig.showWhen) {
/**
* subsscribe to show when subject, whenever new values are there in formbuilder then show when will execute
*/
this.showWhenSubject.subscribe(values => {
if (fieldConfig.showWhen && ref.value) {
const showField = fieldConfig.showWhen(values);
if (!showField) {
ref.value.dataset.hidden = "true";
if ((fieldConfig as FormBuilderBaseField).layout === "label-left") {
const wrapper = ref.value.closest(".label-left-layout");
if (wrapper) {
wrapper.dataset.hidden = "true";
}
}
} else {
ref.value.dataset.hidden = "false";
if ((fieldConfig as FormBuilderBaseField).layout === "label-left") {
const wrapper = ref.value.closest(".label-left-layout");
if (wrapper) {
wrapper.dataset.hidden = "false";
}
}
}
this.dispatchShowWhenExeEvent();
}
});
this.dispatchShowWhenEvent();
}
}
});
await propogateProperties(this);
}
addField() {
this.value.push(null);
this.dispatchInputEvent();
this.requestUpdate();
}
removeField(idx: number) {
this.value.splice(idx, 1);
this.dispatchInputEvent();
this.requestUpdate();
}
/**
* dispatch showWhen event so that root will publish new form values
*/
dispatchShowWhenEvent() {
const showWhen = new CustomEvent("show-when", {
detail: true,
bubbles: true,
composed: true
});
this.dispatchEvent(showWhen);
}
dispatchInputEvent() {
const input = new CustomEvent("input", {
detail: this.value,
bubbles: true,
composed: true
});
this.dispatchEvent(input);
}
/**
* dispatch showWhen event so that root will publish new form values
*/
dispatchShowWhenExeEvent() {
const showWhen = new CustomEvent("show-when-exe", {
detail: true,
bubbles: true,
composed: true
});
this.dispatchEvent(showWhen);
}
}