Skip to content

Commit ba1c0ec

Browse files
authored
Add an alternative Form api using hooks (#39)
* Add an alternative Form api using hooks * Tidy up useForm * PR notes, move code out of Form.Hooks module * 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 * Merge Form updates
1 parent c2ed1c4 commit ba1c0ec

File tree

3 files changed

+230
-159
lines changed

3 files changed

+230
-159
lines changed

bower.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"purescript-profunctor-lenses": ">=4.0.0 <7.0.0",
3131
"purescript-random": "^4.0.0",
3232
"purescript-react-basic": ">= 8.0.0 < 10.0.0",
33+
"purescript-react-basic-hooks": "^0.6.1",
3334
"purescript-react-dnd-basic": "^6.0.0",
3435
"purescript-record": ">= 1.0.0 < 3.0.0",
3536
"purescript-simple-json": ">=4.0.0 <7.0.0",
@@ -42,7 +43,6 @@
4243
"purescript-js-timers": "^4.0.1",
4344
"purescript-heterogeneous": ">=0.3.0 <0.5.0",
4445
"purescript-free": "^5.1.0",
45-
"purescript-react-basic-hooks": "^0.6.1",
4646
"purescript-colors": "^5.0.0"
4747
},
4848
"devDependencies": {

docs/Examples/Form.example.purs

Lines changed: 97 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import Data.Foldable (foldMap)
99
import Data.Int as Int
1010
import Data.Lens (iso)
1111
import Data.Lens.Record (prop)
12-
import Data.Maybe (Maybe(..), fromMaybe, isJust, maybe)
12+
import Data.Maybe (Maybe(..), isJust, maybe)
1313
import Data.Monoid as Monoid
1414
import Data.Newtype (class Newtype, un)
1515
import Data.Nullable as Nullable
1616
import Data.String as String
1717
import Data.String.NonEmpty (NonEmptyString, appendString, length, toString)
1818
import Data.Symbol (SProxy(..))
19-
import Effect (Effect)
2019
import Effect.Aff (Milliseconds(..), delay, error, throwError)
2120
import Effect.Class (liftEffect)
2221
import Effect.Random (randomRange)
22+
import Effect.Unsafe (unsafePerformEffect)
2323
import Lumi.Components.Button as Button
2424
import Lumi.Components.Column (column, column_)
2525
import Lumi.Components.Example (example)
@@ -28,179 +28,135 @@ import Lumi.Components.Form as F
2828
import Lumi.Components.Form.Defaults (formDefaults)
2929
import Lumi.Components.Form.Table as FT
3030
import Lumi.Components.Input as Input
31-
import Lumi.Components.LabeledField (labeledField, RequiredField(..))
31+
import Lumi.Components.LabeledField (RequiredField(..))
3232
import Lumi.Components.Modal (dialog)
3333
import Lumi.Components.Row (row)
3434
import Lumi.Components.Size (Size(..))
35-
import Lumi.Components.Text (h1_)
3635
import Lumi.Components.Upload (FileId(..))
3736
import Lumi.Components.Upload as Upload
38-
import React.Basic (Component, JSX, createComponent, make)
3937
import React.Basic.DOM (css)
4038
import React.Basic.DOM as R
41-
import React.Basic.DOM.Events (targetChecked)
39+
import React.Basic.DOM.Events (preventDefault)
4240
import React.Basic.Events (handler, handler_)
41+
import React.Basic.Hooks (JSX, CreateComponent, component, element, useEffect, useState, (/\))
42+
import React.Basic.Hooks as React
4343
import Web.File.File as File
4444

45-
component :: Component Unit
46-
component = createComponent "FormExample"
47-
48-
data Action formData formResult
49-
= SetInlineTable Boolean
50-
| SetForceTopLabels Boolean
51-
| SetReadonly Boolean
52-
| SetSimulatePauses Boolean
53-
| SetUser (formData -> formData)
54-
| Submit
55-
| Reset
56-
5745
docs :: JSX
58-
docs = unit # make component { initialState, render }
59-
where
60-
initialState =
61-
{ user: (formDefaults :: User)
62-
{ leastFavoriteColors = ["red"]
63-
}
64-
, result: Nothing :: Maybe ValidatedUser
65-
, modalOpen: false
66-
, inlineTable: false
67-
, forceTopLabels: false
68-
, readonly: false
69-
, simulatePauses: true
70-
}
46+
docs = flip element {} $ unsafePerformEffect do
7147

72-
send self = case _ of
73-
SetInlineTable inlineTable ->
74-
self.setState _ { inlineTable = inlineTable }
48+
userFormExample <- mkUserFormExample
7549

76-
SetForceTopLabels forceTopLabels ->
77-
self.setState _ { forceTopLabels = forceTopLabels }
78-
79-
SetReadonly readonly ->
80-
self.setState _ { readonly = readonly }
81-
82-
SetSimulatePauses simulatePauses ->
83-
self.setState _ { simulatePauses = simulatePauses }
84-
85-
SetUser update ->
86-
let
87-
formProps =
88-
{ readonly: self.state.readonly
89-
, simulatePauses: self.state.simulatePauses
90-
}
91-
in
92-
self.setState \s -> s
93-
{ result = F.revalidate userForm formProps (update s.user)
94-
, user = update s.user
95-
}
50+
component "FormExample" \_ -> React.do
51+
{ formData, form } <- F.useForm metaForm
52+
{ initialState: formDefaults
53+
, readonly: false
54+
, inlineTable: false
55+
, forceTopLabels: false
56+
}
9657

97-
Submit ->
98-
self.setState \state -> state { user = F.setModified state.user, modalOpen = isJust state.result }
58+
pure $ column_
59+
[ column
60+
{ style: css { width: "100%", maxWidth: 300, padding: "2rem 0" }
61+
, children: [ form ]
62+
}
9963

100-
Reset ->
101-
self.setState (const initialState)
64+
, example $ element userFormExample formData
65+
]
10266

103-
render self@{ state: { user, result, inlineTable, forceTopLabels, readonly, simulatePauses, modalOpen } } =
104-
column_
105-
[ h1_ "Form"
67+
-- | This form renders the toggles at the top of the example
68+
metaForm
69+
:: forall props
70+
. FormBuilder
71+
{ readonly :: Boolean
72+
| props
73+
}
74+
{ inlineTable :: Boolean
75+
, forceTopLabels :: Boolean
76+
, readonly :: Boolean
77+
, simulatePauses :: Boolean
78+
}
79+
Unit
80+
metaForm = ado
81+
inlineTable <-
82+
F.indent "Inline table" Neither
83+
$ F.focus (prop (SProxy :: SProxy "inlineTable"))
84+
$ F.switch
85+
forceTopLabels <-
86+
F.indent "Force top labels" Neither
87+
$ F.focus (prop (SProxy :: SProxy "forceTopLabels"))
88+
$ F.switch
89+
readonly <-
90+
F.indent "Readonly" Neither
91+
$ F.focus (prop (SProxy :: SProxy "readonly"))
92+
$ F.switch
93+
simulatePauses <-
94+
F.indent "Simulate pauses (pet color picker)" Neither
95+
$ F.focus (prop (SProxy :: SProxy "simulatePauses"))
96+
$ F.switch
97+
in unit
98+
99+
mkUserFormExample
100+
:: CreateComponent
101+
{ inlineTable :: Boolean
102+
, forceTopLabels :: Boolean
103+
, readonly :: Boolean
104+
, simulatePauses :: Boolean
105+
}
106+
mkUserFormExample = do
107+
component "UserFormExample" \props -> React.do
108+
modalOpen /\ setModalOpen <- useState false
109+
110+
{ setModified, reset, validated, form } <- F.useForm userForm
111+
{ initialState: formDefaults
112+
, readonly: props.readonly
113+
, inlineTable: props.inlineTable
114+
, forceTopLabels: props.forceTopLabels && not props.inlineTable
115+
, simulatePauses: props.simulatePauses
116+
}
106117

107-
, column
108-
{ style: css { maxWidth: "50rem", padding: "2rem 0" }
118+
let hasResult = isJust validated
119+
useEffect hasResult do
120+
setModalOpen $ const hasResult
121+
mempty
122+
123+
pure $ R.form -- Forms should be enclosed in a single "<form>" element to enable
124+
-- default browser behavior, such as the enter key. Use "type=submit"
125+
-- on the form's submit button and `preventDefault` to keep the browser
126+
-- from reloading the page on submission.
127+
{ onSubmit: handler preventDefault \_ -> setModified
128+
, style: R.css { alignSelf: "stretch" }
129+
, children:
130+
[ form
131+
, row
132+
{ style: R.css { justifyContent: "flex-end" }
109133
, children:
110-
[ labeledField
111-
{ label: R.text "Inline Table"
112-
, value: Input.input Input.switch
113-
{ checked = if inlineTable then Input.On else Input.Off
114-
, onChange = handler targetChecked $ send self <<< SetInlineTable <<< fromMaybe false
115-
}
116-
, validationError: Nothing
117-
, required: Neither
118-
, forceTopLabel: false
119-
, style: css {}
120-
}
121-
122-
, labeledField
123-
{ label: R.text "Force Top Labels"
124-
, value: Input.input Input.switch
125-
{ checked = if forceTopLabels then Input.On else Input.Off
126-
, disabled = inlineTable
127-
, onChange = handler targetChecked $ send self <<< SetForceTopLabels <<< fromMaybe false
128-
}
129-
, validationError: Nothing
130-
, required: Neither
131-
, forceTopLabel: false
132-
, style: css {}
134+
[ Button.button Button.secondary
135+
{ title = "Reset"
136+
, onPress = handler_ reset
133137
}
134-
135-
, labeledField
136-
{ label: R.text "Readonly"
137-
, value: Input.input Input.switch
138-
{ checked = if readonly then Input.On else Input.Off
139-
, onChange = handler targetChecked $ send self <<< SetReadonly <<< fromMaybe false
140-
}
141-
, validationError: Nothing
142-
, required: Neither
143-
, forceTopLabel: false
144-
, style: css {}
145-
}
146-
147-
, labeledField
148-
{ label: R.text "Simulate pauses"
149-
, value: Input.input Input.switch
150-
{ checked = if simulatePauses then Input.On else Input.Off
151-
, onChange = handler targetChecked $ send self <<< SetSimulatePauses <<< fromMaybe false
152-
}
153-
, validationError: Nothing
154-
, required: Neither
155-
, forceTopLabel: false
156-
, style: css {}
138+
, Button.button Button.primary
139+
{ title = "Submit"
140+
, type = "submit"
141+
, style = R.css { marginLeft: "12px" }
157142
}
158143
]
159144
}
160-
161-
, example
162-
$ column
163-
{ style: css { alignSelf: "stretch" }
164-
, children:
165-
[ userComponent
166-
{ value: user
167-
, onChange: send self <<< SetUser
168-
, inlineTable
169-
, forceTopLabels: forceTopLabels && not inlineTable
170-
, readonly
171-
, simulatePauses
172-
}
173-
, row
174-
{ style: css { justifyContent: "flex-end" }
175-
, children:
176-
[ Button.button Button.secondary
177-
{ title = "Reset"
178-
, onPress = handler_ $ send self Reset
179-
}
180-
, Button.button Button.primary
181-
{ title = "Submit"
182-
, style = css { marginLeft: "12px" }
183-
, onPress = handler_ $ send self Submit
184-
}
185-
]
186-
}
187-
]
188-
}
189-
190-
, case result of
145+
, case validated of
191146
Nothing ->
192147
mempty
193148
Just { firstName, lastName } ->
194149
dialog
195150
{ modalOpen
196-
, onRequestClose: send self Reset
151+
, onRequestClose: reset
197152
, onActionButtonClick: Nullable.null
198153
, actionButtonTitle: ""
199154
, size: Medium
200155
, children: R.text $
201156
"Created user " <> toString firstName <> " " <> toString lastName <> "!"
202157
}
203158
]
159+
}
204160

205161
data Country
206162
= BR
@@ -266,19 +222,6 @@ type ValidatedPet =
266222
, color :: Maybe String
267223
}
268224

269-
-- | We have to fully apply `Form.build` in order to avoid
270-
-- | remounting this component on each render.
271-
userComponent
272-
:: { value :: User
273-
, onChange :: (User -> User) -> Effect Unit
274-
, inlineTable :: Boolean
275-
, forceTopLabels :: Boolean
276-
, readonly :: Boolean
277-
, simulatePauses :: Boolean
278-
}
279-
-> JSX
280-
userComponent = F.build userForm
281-
282225
userForm
283226
:: forall props
284227
. FormBuilder
@@ -499,7 +442,6 @@ userForm = ado
499442
, { label: "Blue", value: "blue" }
500443
]
501444

502-
503445
type Address =
504446
{ name :: Validated String
505447
, street :: Validated String

0 commit comments

Comments
 (0)