import { html, nothing, PropertyValueMap, TemplateResult, unsafeCSS } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { FDiv, FRoot } from "@nonfx/flow-core";
import eleStyle from "./f-form-object.scss?inline";
import globalStyle from "./f-form-object-global.scss?inline";
import fieldRenderer from "../f-form-builder/fields";
import { createRef, Ref } from "lit/directives/ref.js";
import {
CanValidateFields,
FFormInputElements,
FormBuilderBaseField,
FormBuilderObjectField,
FormBuilderValidationPromise,
FormBuilderValues
} from "../../types";
import { validateField } from "../../modules/validation/validator";
import { SimpleSubject } from "@nonfx/flow-core-config";
import { getEssentialFlowCoreStyles, propogateProperties } from "../../modules/helpers";
import { FFormGroup } from "@nonfx/flow-core";
import { FFieldSeparator } from "../f-field-separator/f-field-separator";
import { radioGroupStyles } from "../f-radio-group/f-radio-group";
import { checkboxGroupStyles } from "../f-checkbox-group/f-checkbox-group";
import { ifDefined } from "lit/directives/if-defined.js";
export type ObjectValueType = Record<
string,
string | string[] | number | number[] | unknown | unknown[] | undefined
>;
@customElement("f-form-object")
export class FFormObject extends FRoot {
/**
* css loaded from scss file
*/
static styles = [
unsafeCSS(eleStyle),
...FFieldSeparator.styles,
unsafeCSS(radioGroupStyles),
unsafeCSS(checkboxGroupStyles),
...getEssentialFlowCoreStyles(),
unsafeCSS(globalStyle)
];
/**
* @attribute comments baout title
*/
@property({ type: Object })
config!: FormBuilderObjectField;
/**
* @attribute value
*/
@property({
type: Object,
hasChanged(newVal: ObjectValueType, oldVal: ObjectValueType) {
return JSON.stringify(newVal) !== JSON.stringify(oldVal);
}
})
value!: ObjectValueType;
@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";
@query("f-form-group")
formGroupElement?: FFormGroup;
fieldRefs: Record> = {};
showWhenSubject!: SimpleSubject;
render() {
return html`${this.buildFields()}`;
}
getFieldValue(fieldname: string) {
return this.value ? this.value[fieldname] : undefined;
}
getLabelOffSet() {
const allFields = Object.entries(this.fieldRefs);
let element = allFields[allFields.length - 1][1].value as FFormInputElements;
if (this.config.direction == "vertical") {
element = allFields[0][1].value as FFormInputElements;
}
const labelHeight: number =
(element.querySelector("[slot='label']") as HTMLElement)?.offsetHeight ?? 0;
const descriptionHeight: number =
(element.querySelector("[slot='description']") as HTMLElement)?.offsetHeight ?? 0;
let totalHeight = labelHeight + descriptionHeight;
if (this.formGroupElement) {
const fDiv = this.formGroupElement.shadowRoot?.querySelector(
"f-div.f-form-group-label-wrapper"
);
if (fDiv) {
totalHeight += fDiv.offsetHeight + 12;
}
}
return totalHeight;
}
buildFields() {
const fieldTemplates: TemplateResult[] = [];
Object.entries(this.config.fields).forEach(([fieldname, fieldConfig], idx, fieldArray) => {
const fieldRef: Ref = createRef();
if (fieldConfig.type === "separator") {
fieldConfig.direction = this.config.direction ?? "vertical";
}
this.fieldRefs[fieldname] = fieldRef;
fieldTemplates.push(html`
${fieldRenderer[fieldConfig.type](
fieldname,
fieldConfig,
fieldRef,
this.getFieldValue(fieldname)
)}
${this.config.fieldSeparator && idx < fieldArray.length - 1
? html``
: ""}
`);
});
return html`
${fieldTemplates}
${this.config.helperText
? html`${this.config?.helperText}`
: nothing}
`;
}
async validate(silent = false) {
await this.updateComplete;
const allValidations: FormBuilderValidationPromise[] = [];
Object.entries(this.config.fields).forEach(([fieldname, fieldConfig]) => {
if (
(fieldConfig.type === "object" || fieldConfig.type === "array") &&
this.fieldRefs[fieldname].value
) {
allValidations.push(
(this.fieldRefs[fieldname].value as FFormInputElements).validate(silent)
);
allValidations.push(validateField(fieldConfig, this.fieldRefs[fieldname].value, silent));
} else {
if (this.fieldRefs[fieldname]) {
allValidations.push(
validateField(
fieldConfig as CanValidateFields,
this.fieldRefs[fieldname].value as FFormInputElements,
silent
)
);
}
}
});
return Promise.all(allValidations);
}
/**
* updated hook of lit element
* @param _changedProperties
*/
protected async updated(
_changedProperties: PropertyValueMap | Map
): Promise {
super.updated(_changedProperties);
await this.updateComplete;
Object.entries(this.fieldRefs).forEach(([name, ref]) => {
if (ref.value) {
ref.value.showWhenSubject = this.showWhenSubject;
const fieldValidation = async (event: Event) => {
event.stopPropagation();
if (!this.value) {
this.value = {};
}
this.value[name] = ref.value?.value;
/**
* FLOW-903 moving up to avoid race condition
*/
if (event.type !== "blur") {
this.dispatchInputEvent();
}
await validateField(
this.config.fields[name] as CanValidateFields,
ref.value as FFormInputElements,
false
);
};
ref.value.oninput = fieldValidation;
ref.value.onblur = fieldValidation;
const fieldConfig = this.config.fields[name];
if (fieldConfig.showWhen) {
this.showWhenSubject.subscribe(values => {
if (fieldConfig.showWhen && ref.value) {
const showField = fieldConfig.showWhen(values);
if (!showField) {
ref.value.dataset.hidden = "true";
const divider = this.shadowRoot?.querySelector(
`#${ref.value.getAttribute("name")}-divider`
);
if (divider) {
divider.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";
const divider = this.shadowRoot?.querySelector(
`#${ref.value.getAttribute("name")}-divider`
);
if (divider) {
divider.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);
}
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
*/
dispatchShowWhenEvent() {
const showWhen = new CustomEvent("show-when", {
detail: true,
bubbles: true,
composed: true
});
this.dispatchEvent(showWhen);
}
/**
* 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);
}
}