Skip to content

Commit c2ed1c4

Browse files
arthurxavierxmaddie927
authored andcommitted
Parametrize form builders by UI representation (#79)
* Add wrap combinator to form DSL Remove type parameter from Tree and Forest * Add FormDefaults instance to NonEmptyArray * Fix LabeledField label overflowing * Parametrize FormBuilder by UI representation * Allow users to use custom ModifyValidated mappings * Make setModified work on types other than records * Add functions for mapping over form UIs
1 parent 85c3a38 commit c2ed1c4

File tree

7 files changed

+270
-143
lines changed

7 files changed

+270
-143
lines changed

src/Lumi/Components/Details.purs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Prelude
44

55
import Effect.Unsafe (unsafePerformEffect)
66
import JSS (JSS, jss)
7-
import Lumi.Components.Icon (icon_)
87
import Lumi.Components.Text (body_)
98
import React.Basic.DOM as R
109
import React.Basic.Hooks (JSX, component, element)

src/Lumi/Components/Form.purs

Lines changed: 183 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module Lumi.Components.Form
33
, module Internal
44
, module Validation
55
, build
6+
, build'
7+
, defaultRenderForm
68
, static
79
, section
810
, inputBox
@@ -36,7 +38,10 @@ module Lumi.Components.Form
3638
, withProps
3739
, withValue
3840
, mapProps
41+
, mapUI
42+
, mapUI_
3943
, indent
44+
, wrap
4045
, filterWithProps
4146
, withKey
4247
, styles
@@ -66,8 +71,8 @@ import Lumi.Components.Color (colors)
6671
import Lumi.Components.Column (column)
6772
import Lumi.Components.FetchCache as FetchCache
6873
import Lumi.Components.Form.Defaults (formDefaults) as Defaults
69-
import Lumi.Components.Form.Internal (FormBuilder(..), SeqFormBuilder, Tree(..), formBuilder, formBuilder_, invalidate, pruneTree, sequential)
70-
import Lumi.Components.Form.Internal (FormBuilder, SeqFormBuilder, formBuilder, formBuilder_, invalidate, listen, parallel, revalidate, sequential) as Internal
74+
import Lumi.Components.Form.Internal (Forest, FormBuilder'(..), FormBuilder, SeqFormBuilder, Tree(..), formBuilder, formBuilder_, invalidate, pruneTree, sequential)
75+
import Lumi.Components.Form.Internal (Forest, FormBuilder', FormBuilder, SeqFormBuilder', SeqFormBuilder, formBuilder, formBuilder_, invalidate, listen, parallel, revalidate, sequential) as Internal
7176
import Lumi.Components.Form.Validation (Validated(..), Validator, _Validated, fromValidated, mustBe, mustEqual, nonEmpty, nonEmptyArray, nonNull, validNumber, validInt, validDate, optional, setFresh, setModified, validated, warn) as Validation
7277
import Lumi.Components.Input (alignToInput)
7378
import Lumi.Components.Input as Input
@@ -94,68 +99,117 @@ import Unsafe.Coerce (unsafeCoerce)
9499
-- |
95100
-- | _Note_: this function should be fully applied, to avoid remounting
96101
-- | the component on each render.
102+
97103
build
98104
:: forall props unvalidated result
99-
. FormBuilder { readonly :: Boolean | props } unvalidated result
105+
. Union
106+
( forceTopLabels :: Boolean
107+
, inlineTable :: Boolean
108+
)
109+
( readonly :: Boolean
110+
| props
111+
)
112+
( forceTopLabels :: Boolean
113+
, inlineTable :: Boolean
114+
, readonly :: Boolean
115+
| props
116+
)
117+
=> FormBuilder { readonly :: Boolean | props } unvalidated result
100118
-> { value :: unvalidated
101119
, onChange :: (unvalidated -> unvalidated) -> Effect Unit
102-
, inlineTable :: Boolean
103120
, forceTopLabels :: Boolean
121+
, inlineTable :: Boolean
104122
, readonly :: Boolean
105123
| props
106124
}
107125
-> JSX
108-
build editor = makeStateless (createComponent "Form") render where
109-
render props@{ value, onChange, inlineTable, forceTopLabels, readonly } =
110-
111-
let forest = Array.mapMaybe pruneTree $ edit onChange
112-
where
113-
props' = contractProps props
114-
{ edit } = un FormBuilder editor props' value
115-
116-
contractProps
117-
:: { value :: unvalidated
118-
, onChange :: (unvalidated -> unvalidated) -> Effect Unit
119-
, inlineTable :: Boolean
120-
, forceTopLabels :: Boolean
121-
, readonly :: Boolean
122-
| props
123-
}
124-
-> { readonly :: Boolean
125-
| props
126-
}
127-
contractProps = unsafeCoerce
128-
129-
fieldDivider = R.hr { className: "lumi field-divider" }
130-
131-
toRow = case _ of
132-
Child { key, child } ->
133-
maybe identity keyed key $ child
134-
Wrapper { key, children } ->
135-
R.div
136-
{ key: fromMaybe "" key
137-
, children: [ intercalate fieldDivider (map toRow children) ]
138-
}
139-
Node { label, key, required, validationError, children } ->
140-
maybe identity keyed key $ labeledField
141-
{ label: text body
142-
{ children = [ label ]
143-
, className = toNullable (pure "field-label")
144-
}
145-
, value: intercalate fieldDivider (map toRow children)
146-
, validationError: validationError
147-
, required: required
148-
, forceTopLabel: forceTopLabels
149-
, style: R.css {}
150-
}
126+
build = build' defaultRenderForm
151127

152-
in element (R.unsafeCreateDOMComponent "lumi-form")
153-
{ "class": String.joinWith " " $ fold
154-
[ guard inlineTable ["inline-table"]
155-
, guard readonly ["readonly"]
156-
]
157-
, children: surround fieldDivider (map toRow forest)
158-
}
128+
-- | Create a React component for a form from a `FormBuilder'` and a custom
129+
-- | rendering function.
130+
-- |
131+
-- | _Note_: this function should be fully applied, to avoid remounting
132+
-- | the component on each render.
133+
build'
134+
:: forall ui renderProps formProps props unvalidated result
135+
. Union renderProps formProps props
136+
=> ({| props } -> ui -> JSX)
137+
-> FormBuilder' ui {| formProps } unvalidated result
138+
-> { value :: unvalidated
139+
, onChange :: (unvalidated -> unvalidated) -> Effect Unit
140+
| props
141+
}
142+
-> JSX
143+
build' render editor =
144+
makeStateless (createComponent "Form") \props@{ value, onChange } ->
145+
let
146+
{ edit } = un FormBuilder editor (contractFormProps props) value
147+
in
148+
render (contractProps props) (edit onChange)
149+
where
150+
contractFormProps
151+
:: { value :: unvalidated
152+
, onChange :: (unvalidated -> unvalidated) -> Effect Unit
153+
| props
154+
}
155+
-> {| formProps }
156+
contractFormProps = unsafeCoerce
157+
158+
contractProps
159+
:: { value :: unvalidated
160+
, onChange :: (unvalidated -> unvalidated) -> Effect Unit
161+
| props
162+
}
163+
-> {| props }
164+
contractProps = unsafeCoerce
165+
166+
-- | The default Lumi implementation for rendering a forest of JSX
167+
-- | form fields.
168+
defaultRenderForm
169+
:: forall props
170+
. { forceTopLabels :: Boolean
171+
, inlineTable :: Boolean
172+
, readonly :: Boolean
173+
| props
174+
}
175+
-> Forest
176+
-> JSX
177+
defaultRenderForm { inlineTable, forceTopLabels, readonly } forest =
178+
element (R.unsafeCreateDOMComponent "lumi-form")
179+
{ class:
180+
String.joinWith " " $ fold
181+
[ guard inlineTable ["inline-table"]
182+
, guard readonly ["readonly"]
183+
]
184+
, children:
185+
surround fieldDivider (map toRow (Array.mapMaybe pruneTree forest))
186+
}
187+
where
188+
fieldDivider = R.hr { className: "lumi field-divider" }
189+
190+
toRow :: Tree -> JSX
191+
toRow = case _ of
192+
Child { key, child } ->
193+
maybe identity keyed key $ child
194+
Wrapper { key, wrap: f, children } ->
195+
maybe identity keyed key
196+
$ f
197+
$ intercalate [fieldDivider]
198+
$ map (pure <<< toRow)
199+
$ children
200+
Node { label, key, required, validationError, children } ->
201+
maybe identity keyed key $
202+
labeledField
203+
{ label: text body
204+
{ children = [ label ]
205+
, className = toNullable (pure "field-label")
206+
}
207+
, value: intercalate fieldDivider (map toRow children)
208+
, validationError
209+
, required
210+
, forceTopLabel: forceTopLabels
211+
, style: R.css {}
212+
}
159213

160214
-- | Create an always-valid `FormBuilder` that renders the supplied `JSX`.
161215
static :: forall props value. JSX -> FormBuilder props value Unit
@@ -518,7 +572,7 @@ array
518572
-> FormBuilder { readonly :: Boolean | props } (Array u) (Array a)
519573
array { label, addLabel, defaultValue, editor } = FormBuilder \props@{ readonly } xs ->
520574
let editAt i f xs' = fromMaybe xs' (Array.modifyAt i f xs')
521-
wrapper children = Array.singleton $ Wrapper { key: Nothing, children }
575+
wrapper children = Array.singleton $ Wrapper { key: Nothing, wrap: R.div_, children }
522576
in { edit: \onChange ->
523577
wrapper $ xs # Array.mapWithIndex (\i x ->
524578
Node
@@ -620,7 +674,7 @@ arrayModal
620674
-> FormBuilder { readonly :: Boolean | props } (Array a) (Array a)
621675
arrayModal { label, addLabel, defaultValue, summary, component, componentProps } = FormBuilder \props@{ readonly } xs ->
622676
let editAt i f xs' = fromMaybe xs' (Array.modifyAt i f xs')
623-
wrapper children = Array.singleton $ Wrapper { key: Nothing, children }
677+
wrapper children = Array.singleton $ Wrapper { key: Nothing, wrap: R.div_, children }
624678
in { edit : \onChange ->
625679
wrapper $ xs # Array.mapWithIndex (\i x ->
626680
Node
@@ -766,10 +820,10 @@ initializer loader aff =
766820
-- | Caveat emptor, you get what you pay for if you pass in a dodgy
767821
-- | `Iso` here.
768822
via
769-
:: forall props s a result
823+
:: forall ui props s a result
770824
. Iso' s a
771-
-> FormBuilder props a result
772-
-> FormBuilder props s result
825+
-> FormBuilder' ui props a result
826+
-> FormBuilder' ui props s result
773827
via i e = FormBuilder \props s ->
774828
let { edit, validate } = un FormBuilder e props (view i s)
775829
-- TODO: make this point-free
@@ -779,10 +833,10 @@ via i e = FormBuilder \props s ->
779833

780834
-- | Focus a `FormBuilder` on a smaller piece of state, using a `Lens`.
781835
focus
782-
:: forall props s a result
836+
:: forall ui props s a result
783837
. Lens' s a
784-
-> FormBuilder props a result
785-
-> FormBuilder props s result
838+
-> FormBuilder' ui props a result
839+
-> FormBuilder' ui props s result
786840
focus l e = FormBuilder \props s ->
787841
let { edit, validate } = un FormBuilder e props (view l s)
788842
in { edit: \k -> edit (k <<< l)
@@ -792,22 +846,24 @@ focus l e = FormBuilder \props s ->
792846
-- | Focus a `FormBuilder` on a possible type of state, using a `Prism`,
793847
-- | ignoring validation.
794848
match_
795-
:: forall props s a
796-
. Prism' s a
797-
-> FormBuilder props a a
798-
-> FormBuilder props s s
849+
:: forall ui props s a
850+
. Monoid ui
851+
=> Prism' s a
852+
-> FormBuilder' ui props a a
853+
-> FormBuilder' ui props s s
799854
match_ p = match p p
800855

801856
-- | Focus a `FormBuilder` on a possible type of state, using a `Prism`.
802857
-- |
803858
-- | We need two `Prism`s in order to change the result type for
804859
-- | validation purposes.
805860
match
806-
:: forall props result s t a
807-
. Prism s s a a
861+
:: forall ui props result s t a
862+
. Monoid ui
863+
=> Prism s s a a
808864
-> Prism s t a result
809-
-> FormBuilder props a result
810-
-> FormBuilder props s t
865+
-> FormBuilder' ui props a result
866+
-> FormBuilder' ui props s t
811867
match p1 p2 e = FormBuilder \props s ->
812868
case matching p2 s of
813869
Left t -> { edit: mempty, validate: pure t }
@@ -819,24 +875,48 @@ match p1 p2 e = FormBuilder \props s ->
819875

820876
-- | Change the props type.
821877
mapProps
822-
:: forall p q u a
878+
:: forall ui p q u a
823879
. (q -> p)
824-
-> FormBuilder p u a
825-
-> FormBuilder q u a
880+
-> FormBuilder' ui p u a
881+
-> FormBuilder' ui q u a
826882
mapProps f form = FormBuilder (un FormBuilder form <<< f)
827883

884+
-- | Change the UI type of a form.
885+
mapUI_
886+
:: forall ui ui' props value result
887+
. (ui -> ui')
888+
-> FormBuilder' ui props value result
889+
-> FormBuilder' ui' props value result
890+
mapUI_ f = mapUI \_ _ _ -> f
891+
892+
-- | Change the UI type of a form based on the props, the current value and the
893+
-- | validated result.
894+
mapUI
895+
:: forall ui ui' props value result
896+
. (props -> value -> Maybe result -> ui -> ui')
897+
-> FormBuilder' ui props value result
898+
-> FormBuilder' ui' props value result
899+
mapUI f form =
900+
FormBuilder \props value ->
901+
let
902+
{ edit, validate } = un FormBuilder form props value
903+
in
904+
{ edit: f props value validate <<< edit
905+
, validate
906+
}
907+
828908
-- | Make the props available, for convenience.
829909
withProps
830-
:: forall props unvalidated result
831-
. (props -> FormBuilder props unvalidated result)
832-
-> FormBuilder props unvalidated result
910+
:: forall ui props unvalidated result
911+
. (props -> FormBuilder' ui props unvalidated result)
912+
-> FormBuilder' ui props unvalidated result
833913
withProps f = FormBuilder \props value -> un FormBuilder (f props) props value
834914

835915
-- | Make the value available, for convenience.
836916
withValue
837-
:: forall props unvalidated result
838-
. (unvalidated -> FormBuilder props unvalidated result)
839-
-> FormBuilder props unvalidated result
917+
:: forall ui props unvalidated result
918+
. (unvalidated -> FormBuilder' ui props unvalidated result)
919+
-> FormBuilder' ui props unvalidated result
840920
withValue f = FormBuilder \props value -> un FormBuilder (f value) props value
841921

842922
-- | Indent a `Forest` of editors by one level, providing a label.
@@ -859,18 +939,38 @@ indent label required editor = FormBuilder \props val ->
859939
, validate
860940
}
861941

862-
-- | Filter parts of the form based on the current value (and the props).
863-
filterWithProps
942+
wrap
864943
:: forall props u a
865-
. (props -> u -> Boolean)
944+
. (Array JSX -> JSX)
866945
-> FormBuilder props u a
867946
-> FormBuilder props u a
947+
wrap f form =
948+
FormBuilder \props value ->
949+
let
950+
{ edit, validate } = un FormBuilder form props value
951+
in
952+
{ edit: \k ->
953+
pure $ Wrapper
954+
{ key: Nothing
955+
, wrap: f
956+
, children: edit k
957+
}
958+
, validate
959+
}
960+
961+
-- | Filter parts of the form based on the current value (and the props).
962+
filterWithProps
963+
:: forall ui props u a
964+
. Monoid ui
965+
=> (props -> u -> Boolean)
966+
-> FormBuilder' ui props u a
967+
-> FormBuilder' ui props u a
868968
filterWithProps p editor = FormBuilder \props value ->
869969
let { edit, validate } = un FormBuilder editor props value
870970
in { edit: \onChange ->
871971
if p props value
872972
then edit onChange
873-
else []
973+
else mempty
874974
, validate
875975
}
876976

0 commit comments

Comments
 (0)