11module Lumi.Components.Form.Table
22 ( TableFormBuilder
3+ , revalidate
34 , editableTable
45 , nonEmptyEditableTable
6+ , defaultRowMenu
57 , column
68 , column_
79 , withProps
@@ -19,9 +21,11 @@ import Data.Maybe (Maybe, fromMaybe, isNothing, maybe)
1921import Data.Monoid (guard )
2022import Data.Newtype (class Newtype , un )
2123import Data.Nullable as Nullable
22- import Data.Traversable (traverse )
24+ import Data.Traversable (for_ , traverse , traverse_ )
2325import Data.Tuple (Tuple (..))
2426import Effect (Effect )
27+ import Effect.Aff (Aff , launchAff_ )
28+ import Effect.Class (liftEffect )
2529import Lumi.Components.Column as Column
2630import Lumi.Components.EditableTable as EditableTable
2731import Lumi.Components.Form.Internal (FormBuilder , FormBuilder' (..), Tree (..), Forest , formBuilder )
@@ -68,30 +72,58 @@ instance applicativeTableFormBuilder :: Applicative (TableFormBuilder props row)
6872 , validate: \_ -> pure a
6973 }
7074
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+
7185-- | A `TableFormBuilder` makes a `FormBuilder` for an array where each row has
7286-- | columns defined by it.
7387editableTable
7488 :: forall props row result
7589 . { 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 ))
7796 , formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
7897 , 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
80111 }
81112 -> FormBuilder
82113 { readonly :: Boolean | props }
83114 (Array row )
84115 (Array result )
85- editableTable { addLabel, defaultValue , formBuilder: builder, maxRows, summary } =
116+ editableTable { addLabel, addRow , formBuilder: builder, maxRows, rowMenu , summary } =
86117 formBuilder \props rows ->
87118 let
88119 { columns, validate } = (un TableFormBuilder builder) props
120+ validateRows = traverse validate rows
89121 in
90122 { edit: \onChange ->
91123 EditableTable .editableTable
92124 { addLabel
93125 , maxRows
94- , readonly: isNothing defaultValue || props.readonly
126+ , readonly: isNothing addRow || props.readonly
95127 , rowEq: unsafeRefEq
96128 , summary:
97129 Row .row
@@ -100,47 +132,67 @@ editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary }
100132 , flexWrap: " wrap"
101133 , justifyContent: " flex-end"
102134 }
103- , children: [ summary ]
135+ , children: [ summary rows validateRows ]
104136 }
105137 , 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
107142 , onRowRemove: \(Tuple index _) ->
108143 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)
110151 , columns:
111152 columns <#> \{ label, render } ->
112153 { label
113154 , renderCell: \(Tuple i r) ->
114155 render r (onChange <<< ix i)
115156 }
116157 }
117- , validate: traverse validate rows
158+ , validate: validateRows
118159 }
119160
120161-- | A `TableFormBuilder` makes a `FormBuilder` for a non-empty array where each
121162-- | row has columns defined by it.
122163nonEmptyEditableTable
123164 :: forall props row result
124165 . { addLabel :: String
125- , defaultValue :: Maybe row
166+ , addRow :: Maybe ( Aff ( Maybe row ))
126167 , formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
127168 , 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
129180 }
130181 -> FormBuilder
131182 { readonly :: Boolean | props }
132183 (NEA.NonEmptyArray row )
133184 (NEA.NonEmptyArray result )
134- nonEmptyEditableTable { addLabel, defaultValue , formBuilder: builder, maxRows, summary } =
185+ nonEmptyEditableTable { addLabel, addRow , formBuilder: builder, maxRows, rowMenu , summary } =
135186 formBuilder \props rows ->
136187 let
137188 { columns, validate } = (un TableFormBuilder builder) props
189+ validateRows = traverse validate rows
138190 in
139191 { edit: \onChange ->
140192 EditableTable .editableTable
141193 { addLabel
142194 , maxRows
143- , readonly: isNothing defaultValue || props.readonly
195+ , readonly: isNothing addRow || props.readonly
144196 , rowEq: unsafeRefEq
145197 , summary:
146198 Row .row
@@ -149,23 +201,45 @@ nonEmptyEditableTable { addLabel, defaultValue, formBuilder: builder, maxRows, s
149201 , flexWrap: " wrap"
150202 , justifyContent: " flex-end"
151203 }
152- , children: [ summary ]
204+ , children: [ summary rows validateRows ]
153205 }
154206 , 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
156211 , onRowRemove: \(Tuple index _) ->
157212 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)
159220 , columns:
160221 columns <#> \{ label, render } ->
161222 { label
162223 , renderCell: \(Tuple i r) ->
163224 render r (onChange <<< ix i)
164225 }
165226 }
166- , validate: traverse validate rows
227+ , validate: validateRows
167228 }
168229
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+
169243-- | Convert a `FormBuilder` into a column of a table form with the specified
170244-- | label where all fields are laid out horizontally.
171245column_
0 commit comments