1
1
module Lumi.Components.Form.Table
2
2
( TableFormBuilder
3
+ , revalidate
3
4
, editableTable
4
5
, nonEmptyEditableTable
6
+ , defaultRowMenu
5
7
, column
6
8
, column_
7
9
, withProps
@@ -19,9 +21,11 @@ import Data.Maybe (Maybe, fromMaybe, isNothing, maybe)
19
21
import Data.Monoid (guard )
20
22
import Data.Newtype (class Newtype , un )
21
23
import Data.Nullable as Nullable
22
- import Data.Traversable (traverse )
24
+ import Data.Traversable (for_ , traverse , traverse_ )
23
25
import Data.Tuple (Tuple (..))
24
26
import Effect (Effect )
27
+ import Effect.Aff (Aff , launchAff_ )
28
+ import Effect.Class (liftEffect )
25
29
import Lumi.Components.Column as Column
26
30
import Lumi.Components.EditableTable as EditableTable
27
31
import Lumi.Components.Form.Internal (FormBuilder , FormBuilder' (..), Tree (..), Forest , formBuilder )
@@ -68,30 +72,58 @@ instance applicativeTableFormBuilder :: Applicative (TableFormBuilder props row)
68
72
, validate: \_ -> pure a
69
73
}
70
74
75
+ -- | Revalidate the table form, in order to display error messages or create
76
+ -- | a validated result.
77
+ revalidate
78
+ :: forall props row result
79
+ . TableFormBuilder props row result
80
+ -> props
81
+ -> row
82
+ -> Maybe result
83
+ revalidate form props row = (un TableFormBuilder form props).validate row
84
+
71
85
-- | A `TableFormBuilder` makes a `FormBuilder` for an array where each row has
72
86
-- | columns defined by it.
73
87
editableTable
74
88
:: forall props row result
75
89
. { addLabel :: String
76
- , defaultValue :: Maybe row
90
+ -- | Controls the action that is performed when the button for adding a
91
+ -- | new row is clicked. If this is `Nothing`, the button is not
92
+ -- | displayed. The async effect wrapped in `Maybe` produces the new row
93
+ -- | that will be inserted in the table, and, if it's result is
94
+ -- | `Nothing`, then no rows will be added.
95
+ , addRow :: Maybe (Aff (Maybe row ))
77
96
, formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
78
97
, maxRows :: Int
79
- , summary :: JSX
98
+ -- | Controls what is displayed in the last cell of an editable table row,
99
+ -- | providing access to callbacks that delete or update the current row.
100
+ , rowMenu
101
+ :: { remove :: Maybe (Effect Unit )
102
+ , update :: (row -> row ) -> Effect Unit
103
+ }
104
+ -> row
105
+ -> Maybe result
106
+ -> JSX
107
+ , summary
108
+ :: Array row
109
+ -> Maybe (Array result )
110
+ -> JSX
80
111
}
81
112
-> FormBuilder
82
113
{ readonly :: Boolean | props }
83
114
(Array row )
84
115
(Array result )
85
- editableTable { addLabel, defaultValue , formBuilder: builder, maxRows, summary } =
116
+ editableTable { addLabel, addRow , formBuilder: builder, maxRows, rowMenu , summary } =
86
117
formBuilder \props rows ->
87
118
let
88
119
{ columns, validate } = (un TableFormBuilder builder) props
120
+ validateRows = traverse validate rows
89
121
in
90
122
{ edit: \onChange ->
91
123
EditableTable .editableTable
92
124
{ addLabel
93
125
, maxRows
94
- , readonly: isNothing defaultValue || props.readonly
126
+ , readonly: isNothing addRow || props.readonly
95
127
, rowEq: unsafeRefEq
96
128
, summary:
97
129
Row .row
@@ -100,47 +132,67 @@ editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary }
100
132
, flexWrap: " wrap"
101
133
, justifyContent: " flex-end"
102
134
}
103
- , children: [ summary ]
135
+ , children: [ summary rows validateRows ]
104
136
}
105
137
, rows: Left $ mapWithIndex Tuple rows
106
- , onRowAdd: foldMap (onChange <<< flip Array .snoc) defaultValue
138
+ , onRowAdd:
139
+ for_ addRow \addRow' -> launchAff_ do
140
+ rowM <- addRow'
141
+ traverse_ (liftEffect <<< onChange <<< flip Array .snoc) rowM
107
142
, onRowRemove: \(Tuple index _) ->
108
143
onChange \rows' -> fromMaybe rows' (Array .deleteAt index rows')
109
- , removeCell: EditableTable .defaultRemoveCell
144
+ , removeCell: \onRowRemoveM (Tuple index row) ->
145
+ rowMenu
146
+ { remove: onRowRemoveM <@> Tuple index row
147
+ , update: onChange <<< ix index
148
+ }
149
+ row
150
+ (validate row)
110
151
, columns:
111
152
columns <#> \{ label, render } ->
112
153
{ label
113
154
, renderCell: \(Tuple i r) ->
114
155
render r (onChange <<< ix i)
115
156
}
116
157
}
117
- , validate: traverse validate rows
158
+ , validate: validateRows
118
159
}
119
160
120
161
-- | A `TableFormBuilder` makes a `FormBuilder` for a non-empty array where each
121
162
-- | row has columns defined by it.
122
163
nonEmptyEditableTable
123
164
:: forall props row result
124
165
. { addLabel :: String
125
- , defaultValue :: Maybe row
166
+ , addRow :: Maybe ( Aff ( Maybe row ))
126
167
, formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
127
168
, maxRows :: Int
128
- , summary :: JSX
169
+ , rowMenu
170
+ :: { remove :: Maybe (Effect Unit )
171
+ , update :: (row -> row ) -> Effect Unit
172
+ }
173
+ -> row
174
+ -> Maybe result
175
+ -> JSX
176
+ , summary
177
+ :: NEA.NonEmptyArray row
178
+ -> Maybe (NEA.NonEmptyArray result )
179
+ -> JSX
129
180
}
130
181
-> FormBuilder
131
182
{ readonly :: Boolean | props }
132
183
(NEA.NonEmptyArray row )
133
184
(NEA.NonEmptyArray result )
134
- nonEmptyEditableTable { addLabel, defaultValue , formBuilder: builder, maxRows, summary } =
185
+ nonEmptyEditableTable { addLabel, addRow , formBuilder: builder, maxRows, rowMenu , summary } =
135
186
formBuilder \props rows ->
136
187
let
137
188
{ columns, validate } = (un TableFormBuilder builder) props
189
+ validateRows = traverse validate rows
138
190
in
139
191
{ edit: \onChange ->
140
192
EditableTable .editableTable
141
193
{ addLabel
142
194
, maxRows
143
- , readonly: isNothing defaultValue || props.readonly
195
+ , readonly: isNothing addRow || props.readonly
144
196
, rowEq: unsafeRefEq
145
197
, summary:
146
198
Row .row
@@ -149,23 +201,45 @@ nonEmptyEditableTable { addLabel, defaultValue, formBuilder: builder, maxRows, s
149
201
, flexWrap: " wrap"
150
202
, justifyContent: " flex-end"
151
203
}
152
- , children: [ summary ]
204
+ , children: [ summary rows validateRows ]
153
205
}
154
206
, rows: Right $ mapWithIndex Tuple rows
155
- , onRowAdd: foldMap (onChange <<< flip NEA .snoc) defaultValue
207
+ , onRowAdd:
208
+ for_ addRow \addRow' -> launchAff_ do
209
+ rowM <- addRow'
210
+ traverse_ (liftEffect <<< onChange <<< flip NEA .snoc) rowM
156
211
, onRowRemove: \(Tuple index _) ->
157
212
onChange \rows' -> fromMaybe rows' (NEA .fromArray =<< NEA .deleteAt index rows')
158
- , removeCell: EditableTable .defaultRemoveCell
213
+ , removeCell: \onRowRemoveM (Tuple index row) ->
214
+ rowMenu
215
+ { remove: onRowRemoveM <@> Tuple index row
216
+ , update: onChange <<< ix index
217
+ }
218
+ row
219
+ (validate row)
159
220
, columns:
160
221
columns <#> \{ label, render } ->
161
222
{ label
162
223
, renderCell: \(Tuple i r) ->
163
224
render r (onChange <<< ix i)
164
225
}
165
226
}
166
- , validate: traverse validate rows
227
+ , validate: validateRows
167
228
}
168
229
230
+ -- | Default row menu that displays a bin icon, which, when clicked, deletes the
231
+ -- | current row.
232
+ defaultRowMenu
233
+ :: forall row result
234
+ . { remove :: Maybe (Effect Unit )
235
+ , update :: (row -> row ) -> Effect Unit
236
+ }
237
+ -> row
238
+ -> Maybe result
239
+ -> JSX
240
+ defaultRowMenu { remove } row _ =
241
+ EditableTable .defaultRemoveCell (map const remove) row
242
+
169
243
-- | Convert a `FormBuilder` into a column of a table form with the specified
170
244
-- | label where all fields are laid out horizontally.
171
245
column_
0 commit comments