import { serializeElement, string2Title } from "@maykin-ui/client-common"; import clsx from "clsx"; import React, { useCallback, useEffect, useState } from "react"; import { Field, FieldErrors, FormValidator, TypedField, Validator, errors2errorsArray, field2FormField, field2TypedField, getErrorFromErrors, getFieldName, validateForm, validateRequired, } from "../../../lib"; import { H3 } from "../../typography"; import { Value, ValueEditableUnion, ValueProps } from "../value"; import "./attributelist.scss"; export type AttributeListProps = Omit< React.ComponentPropsWithoutRef<"div">, "onChange" > & { /** The object to show object attributes of. */ object: T; /** The number of columns to span for each field, 12 means 1 pair per row. */ colSpan?: number; /** Whether to decorate the value, if applicable. */ decorate?: boolean; /** Error messages, applied to automatically rendered field. */ errors?: FieldErrors | Partial>; /** The fields in object to show. */ fields: Array | TypedField>; /** A title for the attribute list. */ title?: string; /** An optional id for the title. */ titleId?: string; /** The number of columns to span for the title. */ titleSpan?: number; /** A validation function. */ validate?: FormValidator; /** If set, use this custom set of `Validator`s. */ validators?: Validator[]; /** Props for Value. */ valueProps?: Partial; /** Gets called when a value is edited. */ onEdit?: (value: T) => void; } & Pick< ValueEditableUnion, // TODO: Drop of TypedField<,F>, test multiple valiators and getErrorFromErrors "editable" | "editing" | "labelEdit" | "onChange" | "formControlProps" >; /** * AttributeList Component * * Shows key/value pairs, optionally grouped by `title`. * * @typeParam T - The shape of a single item. */ export const AttributeList = ({ colSpan, object = {} as T, editable, editing, decorate, errors, fields = Object.keys(object) as Field[], formControlProps, labelEdit, title = "", titleId, titleSpan, onBlur, onChange, onEdit, validate = validateForm, validators = [validateRequired], valueProps, ...props }: AttributeListProps) => { const formFields = fields.map((field) => field2FormField(field, [object])); const [errorsState, setErrorsState] = useState< FieldErrors >({}); useEffect(() => { setErrorsState(errors2errorsArray(errors)); }, [JSON.stringify(errors)]); const renderTitle = title &&

{string2Title(title)}

; const isTitleAbove = titleSpan === 12; const handleChange = useCallback( (e: React.ChangeEvent) => { const name = (e.target as HTMLInputElement).name; const value = serializeElement(e.target, { typed: true }); const data = { ...object, [name]: value }; const errors = validate(data, formFields, validators); setErrorsState(errors); onChange?.(e); }, [object, validate, formFields, validators, onChange], ); return (
{title && isTitleAbove && (
{renderTitle}
)}
{title && !isTitleAbove && renderTitle}
{fields.map((field, i) => { const key = getFieldName(field).toString() + i; const fieldErrors = getErrorFromErrors( formFields, errorsState, formFields[i], ); const message = fieldErrors?.join(", "); // FIXME ERROR return ( key={key} object={object} editable={editable} editing={editing} decorate={decorate} error={message} field={field} formControlProps={formControlProps} labelEdit={labelEdit} valueProps={valueProps} onBlur={onBlur} onChange={handleChange} onEdit={onEdit} /> ); })}
); }; export type AttributePairProps = { object: T; field: Field | TypedField; decorate?: boolean; error?: string; valueProps?: Partial; onEdit?: (value: T) => void; } & Pick< ValueEditableUnion, | "formControlProps" | "editable" | "editing" | "labelEdit" | "onBlur" | "onChange" >; /** * A single attribute in an AttributeList */ export const AttributePair = ({ object, field, formControlProps, editable, editing, decorate, error, labelEdit, valueProps, onBlur, onChange, onEdit, }: AttributePairProps) => { const typedField = field2TypedField(field, [object]); /** * Gets called when the 's is blurred. */ const handleBlur = useCallback( (e: React.FocusEvent) => { const data = { ...object, [typedField.name]: serializeElement(e.target), }; onEdit?.(data); onBlur?.(e); }, [object, typedField, onEdit, onBlur], ); const label = string2Title(getFieldName(field).toString()); return (
{!editing &&
{label}
}
); };