Skip to content

Commit d45f47f

Browse files
Use link-style button in editable table and Form.Table improvements (#164)
* Add fallback body font-size * Use link style button in editable table * Add 'removeCell' prop to table form * Add 'revalidate' to Form.Table * Use aff for adding new rows in Form.Table * Make form result available in table summary * Make form result available in row menu * Code review feedback
1 parent 01dac0e commit d45f47f

File tree

5 files changed

+156
-50
lines changed

5 files changed

+156
-50
lines changed

docs/Examples/Form.example.purs

+2-1
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,15 @@ userForm = ado
344344
)
345345
$ FT.editableTable
346346
{ addLabel: "Add pet"
347-
, defaultValue: Just
347+
, addRow: Just $ pure $ Just
348348
{ firstName: F.Fresh ""
349349
, lastName: F.Fresh ""
350350
, animal: F.Fresh Nothing
351351
, age: F.Fresh "1"
352352
, color: Nothing
353353
}
354354
, maxRows: top
355+
, rowMenu: FT.defaultRowMenu
355356
, summary: mempty
356357
, formBuilder: ado
357358
name <- FT.column_ "Name" ado

docs/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
font-family: -apple-system, BlinkMacSystemFont, "San Francisco",
1818
"Roboto", "Droid Sans", Ubuntu, "Helvetica Neue", Helvetica,
1919
sans-serif;
20+
font-size: 1.5em;
2021
margin: 0;
2122
}
2223
</style>

src/Lumi/Components/EditableTable.purs

+60-32
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@ import Data.Array.NonEmpty (NonEmptyArray)
88
import Data.Array.NonEmpty as NonEmptyArray
99
import Data.Either (Either(..))
1010
import Data.Maybe (Maybe(..))
11+
import Data.Monoid (guard)
1112
import Effect (Effect)
1213
import JSS (JSS, jss)
13-
import Lumi.Components.Button as Button
14+
import Lumi.Components (($$$))
1415
import Lumi.Components.Color (colors)
1516
import Lumi.Components.Column (column_)
16-
import Lumi.Components.Icon (IconType(..), icon_)
17-
import Lumi.Components.Row as Row
17+
import Lumi.Components.Icon (IconType(..), icon, icon_)
18+
import Lumi.Components.Text (nbsp)
19+
import Lumi.Components2.Box (row)
20+
import Lumi.Components2.Button (button, _linkStyle)
21+
import Lumi.Components2.Text as T
22+
import Lumi.Styles as S
23+
import Lumi.Styles.Box (FlexAlign(..), _align, _justify, _row)
24+
import Lumi.Styles.Theme (LumiTheme(..))
1825
import React.Basic (Component, JSX, createComponent, element, empty, makeStateless)
1926
import React.Basic.DOM as R
20-
import React.Basic.DOM.Events (capture_, preventDefault, stopPropagation)
21-
import React.Basic.Events (handler)
2227

2328
type EditableTableProps row =
2429
{ addLabel :: String
@@ -53,13 +58,26 @@ editableTableDefaults =
5358
defaultRemoveCell :: forall row. Maybe (row -> Effect Unit) -> row -> JSX
5459
defaultRemoveCell onRowRemove item =
5560
onRowRemove # Array.foldMap \onRowRemove' ->
56-
R.a
57-
{ children: [ icon_ Bin ]
58-
, className: "lumi"
59-
, onClick: capture_ $ onRowRemove' item
60-
, role: "button"
61-
, style: R.css { fontSize: "20px", lineHeight: "20px", textDecoration: "none" }
62-
}
61+
button
62+
$ _linkStyle
63+
$ S.style
64+
( \(LumiTheme { colors }) ->
65+
S.css
66+
{ fontSize: S.px 20
67+
, lineHeight: S.px 20
68+
, textDecoration: S.important S.none
69+
, color: S.color colors.black1
70+
, "&:hover": S.nested $ S.css
71+
{ color: S.color colors.black
72+
}
73+
, "lumi-font-icon::before": S.nested $ S.css
74+
{ verticalAlign: S.str "baseline"
75+
}
76+
}
77+
)
78+
$ _ { onPress = onRowRemove' item
79+
, content = [ icon_ Bin ]
80+
}
6381

6482
component :: forall row. Component (EditableTableProps row)
6583
component = createComponent "EditableTableExample"
@@ -93,7 +111,7 @@ editableTable = makeStateless component render
93111
(Array.length props.columns + 1)
94112
]
95113
where
96-
row_ = row props.columns props.onRowRemove props.removeCell
114+
row_ = tableRow props.columns props.onRowRemove props.removeCell
97115

98116

99117
container children =
@@ -111,7 +129,7 @@ editableTable = makeStateless component render
111129
body =
112130
R.tbody_
113131

114-
row columns onRowRemove removeCell isRemovable item =
132+
tableRow columns onRowRemove removeCell isRemovable item =
115133
R.tr_ $
116134
(cell item <$> columns)
117135
<> [ R.td_
@@ -130,25 +148,35 @@ editableTable = makeStateless component render
130148
[ R.tr_
131149
[ R.td
132150
{ children:
133-
[ Row.row
134-
{ children:
135-
[ summary
136-
, if not canAddRows
137-
then empty
138-
else Button.iconButton Button.iconButtonDefaults
139-
{ title = addLabel
140-
, onPress =
141-
handler
142-
(preventDefault >>> stopPropagation)
143-
\_ -> onRowAdd
144-
, iconLeft = Just Plus
151+
[ row
152+
$ _align Start
153+
$ _justify SpaceBetween
154+
$ S.style_ (S.css { flexFlow: S.str "row-reverse wrap" })
155+
$$$ [ summary
156+
, guard canAddRows
157+
$ button
158+
$ _linkStyle
159+
$ _row
160+
$ _align Baseline
161+
$ S.style_
162+
( S.css
163+
{ fontSize: S.px 14
164+
, lineHeight: S.px 17
165+
, "lumi-font-icon::before": S.nested $ S.css
166+
{ verticalAlign: S.str "baseline"
167+
}
168+
}
169+
)
170+
$ _ { onPress = onRowAdd
171+
, content =
172+
[ icon
173+
{ type_: Plus
174+
, style: R.css { fontSize: "11px" }
175+
}
176+
, T.text $$$ nbsp <> nbsp <> addLabel
177+
]
145178
}
146-
]
147-
, style: R.css
148-
{ justifyContent: "space-between"
149-
, flexFlow: "row-reverse wrap"
150-
}
151-
}
179+
]
152180
]
153181
, colSpan: columnCount
154182
}

src/Lumi/Components/Form/Table.purs

+91-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
module 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)
1921
import Data.Monoid (guard)
2022
import Data.Newtype (class Newtype, un)
2123
import Data.Nullable as Nullable
22-
import Data.Traversable (traverse)
24+
import Data.Traversable (for_, traverse, traverse_)
2325
import Data.Tuple (Tuple(..))
2426
import Effect (Effect)
27+
import Effect.Aff (Aff, launchAff_)
28+
import Effect.Class (liftEffect)
2529
import Lumi.Components.Column as Column
2630
import Lumi.Components.EditableTable as EditableTable
2731
import 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.
7387
editableTable
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.
122163
nonEmptyEditableTable
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.
171245
column_

src/Lumi/Styles/Button.purs

+2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ button colo kind state size = case kind of
149149
[ css
150150
{ label: str "button"
151151
, appearance: none
152+
, outline: none
152153
, padding: int 0
153154
, background: none
154155
, border: none
@@ -172,6 +173,7 @@ button colo kind state size = case kind of
172173
( css
173174
{ label: str "button"
174175
, appearance: none
176+
, outline: none
175177
, minWidth: int 70
176178
, padding: str "10px 20px"
177179
, fontSize: int 14

0 commit comments

Comments
 (0)