diff --git a/CHANGELOG.md b/CHANGELOG.md index 5749e02..dacc621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # v0.0.9(unreleased) - +- Add `dirtyFields` field to `formState` +- Add `valueAsNumber` field to the option argument of `register` # v0.0.8 diff --git a/doc/transformation.md b/doc/transformation.md index f21a5e9..cb76a23 100644 --- a/doc/transformation.md +++ b/doc/transformation.md @@ -39,6 +39,12 @@ type rec valuesOfInputs = | Object(Js.Dict.t) | Array(array) +type fieldDirtyOfInputs = { + example?: bool, + exampleRequired?: bool, + cart?: array +} + type rec useFormReturnOfInputs<'setValueAs> = { control: controlOfInputs, register: (variantOfInputs, ~options: registerOptionsOfInputs<'setValueAs>=?) => JsxDOM.domProps, @@ -52,8 +58,13 @@ type rec useFormReturnOfInputs<'setValueAs> = { } and controlOfInputs and variantOfInputs = | @as("example") Example | @as("exampleRequired") ExampleRequired | @as("cart") Cart -and registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs: 'setValueAs} -and formStateOfInputs = {isDirty: bool, isValid: bool, errors: fieldErrorsOfInputs} +and registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs?: 'setValueAs, valueAsNumber?: bool} +and formStateOfInputs = { + isDirty: bool, + isValid: bool, + errors: fieldErrorsOfInputs, + dirtyFields: fieldDirtyOfInputs +} and fieldErrorsOfInputs = { example?: fieldErrorOfInputs, exampleRequired?: fieldErrorOfInputs, diff --git a/src/ppx/signature.ml b/src/ppx/signature.ml index 768228a..6551a9d 100644 --- a/src/ppx/signature.ml +++ b/src/ppx/signature.ml @@ -169,6 +169,52 @@ let map_type_decl ] in let type_decls3 = + Sig.type_ Recursive + [ + (* type fieldDirtyOfInputs = { example: bool, exampleRequired: bool, cart?: array } *) + Type.mk + (mkloc ("fieldDirtyOf" ^ capitalize record_name) ptype_loc) + ~priv:Public + ~kind: + (Ptype_record + (lds + |> List.map + (fun (({ pld_type } : label_declaration) as ld) -> + match pld_type with + (* type fieldDirtyOfInputs = {cart?: array} *) + | { + ptyp_desc = + Ptyp_constr + ( { txt = Lident "array" }, + [ + { + ptyp_desc = + Ptyp_constr ({ txt = Lident l }, []); + }; + ] ); + } -> + { + ld with + pld_type = + Typ.constr (lid "array") + [ + Typ.constr + (lid @@ "fieldDirtyOf" ^ capitalize l) + []; + ]; + pld_attributes = + add_optional_attribute ld.pld_attributes; + } + | _ -> + { + ld with + pld_type = Typ.constr (lid "bool") []; + pld_attributes = + add_optional_attribute ld.pld_attributes; + }))); + ] + in + let type_decls4 = Sig.type_ Recursive [ (* type useFormReturnOfInputs<'setValueAs> = { @@ -332,7 +378,7 @@ let map_type_decl (mkloc ("variantOf" ^ capitalize record_name) ptype_loc) ~priv:Public ~kind:(Ptype_variant (make_const_decls fields ptype_loc)); - (* type registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs?: 'setValueAs} *) + (* type registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs?: 'setValueAs, valueAsNumber?: bool} *) Type.mk (mkloc ("registerOptionsOf" ^ capitalize record_name) ptype_loc) ~params:[ (Typ.var "setValueAs", (NoVariance, NoInjectivity)) ] @@ -345,8 +391,16 @@ let map_type_decl (Typ.constr (lid "bool") []); Type.field ~attrs:[ attr_optional ] ~mut:Immutable (mknoloc "setValueAs") (Typ.var "setValueAs"); + Type.field ~attrs:[ attr_optional ] ~mut:Immutable + (mknoloc "valueAsNumber") + (Typ.constr (lid "bool") []); ]); - (* type formStateOfInputs = {isDirty: bool, isValid: bool, errors: fieldErrorsOfInputs} *) + (* type formStateOfInputs = { + isDirty: bool, + isValid: bool, + errors: fieldErrorsOfInputs, + dirtyFields: fieldDirtyOfInputs + } *) Type.mk (mkloc ("formStateOf" ^ capitalize record_name) ptype_loc) ~priv:Public @@ -361,6 +415,10 @@ let map_type_decl (Typ.constr (lid @@ "fieldErrorsOf" ^ capitalize record_name) []); + Type.field ~mut:Immutable (mknoloc "dirtyFields") + (Typ.constr + (lid @@ "fieldDirtyOf" ^ capitalize record_name) + []); ]); (* type fieldErrorsOfInputs = { example: fieldErrorOfInputs, exampleRequired: fieldErrorOfInputs } *) Type.mk @@ -606,7 +664,7 @@ let map_type_decl disabled: bool=?, exact: bool=?, } *) - let type_decls4 = + let type_decls5 = Sig.type_ Recursive [ Type.mk @@ -670,7 +728,7 @@ let map_type_decl ])) in - let type_decls5 = + let type_decls6 = lds |> List.filter_map (fun @@ -868,12 +926,13 @@ let map_type_decl type_decls1; type_decls2; type_decls3; + type_decls4; primitive_use_form; module_controller; - type_decls4; + type_decls5; primitive_use_watch; ] - @ type_decls5 @ primitive_use_field_array @ vd_field_array + @ type_decls6 @ primitive_use_field_array @ vd_field_array | _ -> fail ptype_loc "This type is not handled by @ppx_react_hook_form" else [] diff --git a/src/ppx/structure.ml b/src/ppx/structure.ml index d371d30..4ba5617 100644 --- a/src/ppx/structure.ml +++ b/src/ppx/structure.ml @@ -168,6 +168,52 @@ let map_type_decl ] in let type_decls3 = + Str.type_ Recursive + [ + (* type fieldDirtyOfInputs = { example: bool, exampleRequired: bool, cart?: array } *) + Type.mk + (mkloc ("fieldDirtyOf" ^ capitalize record_name) ptype_loc) + ~priv:Public + ~kind: + (Ptype_record + (lds + |> List.map + (fun (({ pld_type } : label_declaration) as ld) -> + match pld_type with + (* type fieldDirtyOfInputs = {cart?: array} *) + | { + ptyp_desc = + Ptyp_constr + ( { txt = Lident "array" }, + [ + { + ptyp_desc = + Ptyp_constr ({ txt = Lident l }, []); + }; + ] ); + } -> + { + ld with + pld_type = + Typ.constr (lid "array") + [ + Typ.constr + (lid @@ "fieldDirtyOf" ^ capitalize l) + []; + ]; + pld_attributes = + add_optional_attribute ld.pld_attributes; + } + | _ -> + { + ld with + pld_type = Typ.constr (lid "bool") []; + pld_attributes = + add_optional_attribute ld.pld_attributes; + }))); + ] + in + let type_decls4 = Str.type_ Recursive [ (* type useFormReturnOfInputs<'setValueAs> = { @@ -331,7 +377,7 @@ let map_type_decl (mkloc ("variantOf" ^ capitalize record_name) ptype_loc) ~priv:Public ~kind:(Ptype_variant (make_const_decls fields ptype_loc)); - (* type registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs?: 'setValueAs} *) + (* type registerOptionsOfInputs<'setValueAs> = {required?: bool, setValueAs?: 'setValueAs, valueAsNumber?: bool} *) Type.mk (mkloc ("registerOptionsOf" ^ capitalize record_name) ptype_loc) ~params:[ (Typ.var "setValueAs", (NoVariance, NoInjectivity)) ] @@ -344,8 +390,16 @@ let map_type_decl (Typ.constr (lid "bool") []); Type.field ~attrs:[ attr_optional ] ~mut:Immutable (mknoloc "setValueAs") (Typ.var "setValueAs"); + Type.field ~attrs:[ attr_optional ] ~mut:Immutable + (mknoloc "valueAsNumber") + (Typ.constr (lid "bool") []); ]); - (* type formStateOfInputs = {isDirty: bool, isValid: bool, errors: fieldErrorsOfInputs} *) + (* type formStateOfInputs = { + isDirty: bool, + isValid: bool, + errors: fieldErrorsOfInputs, + dirtyFields: fieldDirtyOfInputs, + } *) Type.mk (mkloc ("formStateOf" ^ capitalize record_name) ptype_loc) ~priv:Public @@ -360,6 +414,10 @@ let map_type_decl (Typ.constr (lid @@ "fieldErrorsOf" ^ capitalize record_name) []); + Type.field ~mut:Immutable (mknoloc "dirtyFields") + (Typ.constr + (lid @@ "fieldDirtyOf" ^ capitalize record_name) + []); ]); (* type fieldErrorsOfInputs = { example: fieldErrorOfInputs, exampleRequired: fieldErrorOfInputs } *) Type.mk @@ -605,7 +663,7 @@ let map_type_decl disabled: bool=?, exact: bool=?, } *) - let type_decls4 = + let type_decls5 = Str.type_ Recursive [ Type.mk @@ -669,7 +727,7 @@ let map_type_decl ])) in - let type_decls5 = + let type_decls6 = lds |> List.filter_map (fun @@ -1056,12 +1114,13 @@ let map_type_decl type_decls1; type_decls2; type_decls3; + type_decls4; primitive_use_form; module_controller; - type_decls4; + type_decls5; primitive_use_watch; ] - @ type_decls5 @ primitive_use_field_array @ vb_field_array + @ type_decls6 @ primitive_use_field_array @ vb_field_array | _ -> fail ptype_loc "This type is not handled by @ppx_react_hook_form" else [] diff --git a/test/src/pages/basic/Basic.tsx b/test/src/pages/basic/Basic.tsx index bf9d8b6..26427f3 100644 --- a/test/src/pages/basic/Basic.tsx +++ b/test/src/pages/basic/Basic.tsx @@ -10,12 +10,14 @@ export default function Basic() { register, handleSubmit, watch, - formState: { errors }, + formState: { errors, dirtyFields }, } = useForm() const onSubmit: SubmitHandler = (data) => console.log(data) console.log(watch("example")) // watch input value by passing the name of it + console.log(dirtyFields) // { example: true, exampleRequired: true } + return ( /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
diff --git a/test/src/pages/basic_res/Basic.res b/test/src/pages/basic_res/Basic.res index 09baba1..020f58f 100644 --- a/test/src/pages/basic_res/Basic.res +++ b/test/src/pages/basic_res/Basic.res @@ -39,6 +39,8 @@ let default = () => { let exampleFieldState = getFieldState(Example, formState) Js.log(exampleFieldState) + let isExampleFieldDirty = formState.dirtyFields.exampleRequired + Js.log(isExampleFieldDirty) Js.log(setValue) diff --git a/test/src/pages/field_array/FieldArray.tsx b/test/src/pages/field_array/FieldArray.tsx index 26e07b6..012e4a6 100644 --- a/test/src/pages/field_array/FieldArray.tsx +++ b/test/src/pages/field_array/FieldArray.tsx @@ -26,7 +26,7 @@ export default function App() { register, control, handleSubmit, - formState: { errors } + formState: { errors, dirtyFields } } = useForm({ defaultValues: { cart: [{ name: "test", quantity: 1, price: 23 }] @@ -41,6 +41,12 @@ export default function App() { console.log("!!", errors) + const isCartPriceFieldDirty = (index: number) => + (dirtyFields.cart?.[index]?.price) ? + dirtyFields.cart?.[index]?.price + : + false + return (
@@ -73,10 +79,13 @@ export default function App() { })} className={errors?.cart?.[index]?.price ? "error" : ""} /> - + + {(isCartPriceFieldDirty(index) ? "dirty" : "clean")} + +
); })} diff --git a/test/src/pages/field_array_res/FieldArray.res b/test/src/pages/field_array_res/FieldArray.res index e318333..48efb35 100644 --- a/test/src/pages/field_array_res/FieldArray.res +++ b/test/src/pages/field_array_res/FieldArray.res @@ -10,7 +10,7 @@ type inputs = {cart: array} @react.component let default = () => { - let {register, control, handleSubmit, formState: {errors}} = useFormOfInputs( + let {register, control, handleSubmit, formState: {errors, dirtyFields}} = useFormOfInputs( ~options={ defaultValues: { cart: [{quantity: 1., price: 23.}], @@ -20,6 +20,14 @@ let default = () => { ) let {fields, append, remove} = useFieldArrayOfInputsCart({name: Cart, control}) + let isCartPriceFieldDirty = (index: int) => + switch dirtyFields.cart->Option.flatMap(Belt.Array.get(_, index)) { + | Some({price}) => price + | _ => false + } + + Js.log(isCartPriceFieldDirty(0)) + let onSubmit = (data: inputs) => Js.log(data) @@ -40,7 +48,15 @@ let default = () => { | Some({quantity: ?Some({message})}) => message->React.string | _ => React.null }} - fieldArrayOfCart)} placeholder="price" /> + fieldArrayOfCart, + ~options={required: true, valueAsNumber: true}, + )} + placeholder="price" + className={isCartPriceFieldDirty(index) ? "error" : ""} + /> + {(isCartPriceFieldDirty(index) ? "dirty" : "clean")->React.string} {switch errors.cart->ReactHookForm.getFieldError(index) { | Some({price: ?Some({message})}) => message->React.string | _ => React.null