Skip to content

Use link-style button in editable table and Form.Table improvements #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/Examples/Form.example.purs
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,15 @@ userForm = ado
)
$ FT.editableTable
{ addLabel: "Add pet"
, defaultValue: Just
, addRow: Just $ pure $ Just
{ firstName: F.Fresh ""
, lastName: F.Fresh ""
, animal: F.Fresh Nothing
, age: F.Fresh "1"
, color: Nothing
}
, maxRows: top
, rowMenu: FT.defaultRowMenu
, summary: mempty
, formBuilder: ado
name <- FT.column_ "Name" ado
Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
font-family: -apple-system, BlinkMacSystemFont, "San Francisco",
"Roboto", "Droid Sans", Ubuntu, "Helvetica Neue", Helvetica,
sans-serif;
font-size: 1.5em;
margin: 0;
}
</style>
Expand Down
92 changes: 60 additions & 32 deletions src/Lumi/Components/EditableTable.purs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ import Data.Array.NonEmpty (NonEmptyArray)
import Data.Array.NonEmpty as NonEmptyArray
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Data.Monoid (guard)
import Effect (Effect)
import JSS (JSS, jss)
import Lumi.Components.Button as Button
import Lumi.Components (($$$))
import Lumi.Components.Color (colors)
import Lumi.Components.Column (column_)
import Lumi.Components.Icon (IconType(..), icon_)
import Lumi.Components.Row as Row
import Lumi.Components.Icon (IconType(..), icon, icon_)
import Lumi.Components.Text (nbsp)
import Lumi.Components2.Box (row)
import Lumi.Components2.Button (button, _linkStyle)
import Lumi.Components2.Text as T
import Lumi.Styles as S
import Lumi.Styles.Box (FlexAlign(..), _align, _justify, _row)
import Lumi.Styles.Theme (LumiTheme(..))
import React.Basic (Component, JSX, createComponent, element, empty, makeStateless)
import React.Basic.DOM as R
import React.Basic.DOM.Events (capture_, preventDefault, stopPropagation)
import React.Basic.Events (handler)

type EditableTableProps row =
{ addLabel :: String
Expand Down Expand Up @@ -53,13 +58,26 @@ editableTableDefaults =
defaultRemoveCell :: forall row. Maybe (row -> Effect Unit) -> row -> JSX
defaultRemoveCell onRowRemove item =
onRowRemove # Array.foldMap \onRowRemove' ->
R.a
{ children: [ icon_ Bin ]
, className: "lumi"
, onClick: capture_ $ onRowRemove' item
, role: "button"
, style: R.css { fontSize: "20px", lineHeight: "20px", textDecoration: "none" }
}
button
$ _linkStyle
$ S.style
( \(LumiTheme { colors }) ->
S.css
{ fontSize: S.px 20
, lineHeight: S.px 20
, textDecoration: S.important S.none
, color: S.color colors.black1
, "&:hover": S.nested $ S.css
{ color: S.color colors.black
}
, "lumi-font-icon::before": S.nested $ S.css
{ verticalAlign: S.str "baseline"
}
}
)
$ _ { onPress = onRowRemove' item
, content = [ icon_ Bin ]
}

component :: forall row. Component (EditableTableProps row)
component = createComponent "EditableTableExample"
Expand Down Expand Up @@ -93,7 +111,7 @@ editableTable = makeStateless component render
(Array.length props.columns + 1)
]
where
row_ = row props.columns props.onRowRemove props.removeCell
row_ = tableRow props.columns props.onRowRemove props.removeCell


container children =
Expand All @@ -111,7 +129,7 @@ editableTable = makeStateless component render
body =
R.tbody_

row columns onRowRemove removeCell isRemovable item =
tableRow columns onRowRemove removeCell isRemovable item =
R.tr_ $
(cell item <$> columns)
<> [ R.td_
Expand All @@ -130,25 +148,35 @@ editableTable = makeStateless component render
[ R.tr_
[ R.td
{ children:
[ Row.row
{ children:
[ summary
, if not canAddRows
then empty
else Button.iconButton Button.iconButtonDefaults
{ title = addLabel
, onPress =
handler
(preventDefault >>> stopPropagation)
\_ -> onRowAdd
, iconLeft = Just Plus
[ row
$ _align Start
$ _justify SpaceBetween
$ S.style_ (S.css { flexFlow: S.str "row-reverse wrap" })
$$$ [ summary
, guard canAddRows
$ button
$ _linkStyle
$ _row
$ _align Baseline
$ S.style_
( S.css
{ fontSize: S.px 14
, lineHeight: S.px 17
, "lumi-font-icon::before": S.nested $ S.css
{ verticalAlign: S.str "baseline"
}
}
)
$ _ { onPress = onRowAdd
, content =
[ icon
{ type_: Plus
, style: R.css { fontSize: "11px" }
}
, T.text $$$ nbsp <> nbsp <> addLabel
]
}
]
, style: R.css
{ justifyContent: "space-between"
, flexFlow: "row-reverse wrap"
}
}
]
]
, colSpan: columnCount
}
Expand Down
108 changes: 91 additions & 17 deletions src/Lumi/Components/Form/Table.purs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
module Lumi.Components.Form.Table
( TableFormBuilder
, revalidate
, editableTable
, nonEmptyEditableTable
, defaultRowMenu
, column
, column_
, withProps
Expand All @@ -19,9 +21,11 @@ import Data.Maybe (Maybe, fromMaybe, isNothing, maybe)
import Data.Monoid (guard)
import Data.Newtype (class Newtype, un)
import Data.Nullable as Nullable
import Data.Traversable (traverse)
import Data.Traversable (for_, traverse, traverse_)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class (liftEffect)
import Lumi.Components.Column as Column
import Lumi.Components.EditableTable as EditableTable
import Lumi.Components.Form.Internal (FormBuilder, FormBuilder'(..), Tree(..), Forest, formBuilder)
Expand Down Expand Up @@ -68,30 +72,58 @@ instance applicativeTableFormBuilder :: Applicative (TableFormBuilder props row)
, validate: \_ -> pure a
}

-- | Revalidate the table form, in order to display error messages or create
-- | a validated result.
revalidate
:: forall props row result
. TableFormBuilder props row result
-> props
-> row
-> Maybe result
revalidate form props row = (un TableFormBuilder form props).validate row

-- | A `TableFormBuilder` makes a `FormBuilder` for an array where each row has
-- | columns defined by it.
editableTable
:: forall props row result
. { addLabel :: String
, defaultValue :: Maybe row
-- | Controls the action that is performed when the button for adding a
-- | new row is clicked. If this is `Nothing`, the button is not
-- | displayed. The async effect wrapped in `Maybe` produces the new row
-- | that will be inserted in the table, and, if it's result is
-- | `Nothing`, then no rows will be added.
, addRow :: Maybe (Aff (Maybe row))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could do with a comment to say what the meaning of each Maybe is. I guess the outer maybe tells the component whether to render the "Add row" link at all, and the inner one allows you to decide whether or not to actually add the row when the link is clicked?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll add a docstring here. Yes, that's exactly what those types mean.

, formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
, maxRows :: Int
, summary :: JSX
-- | Controls what is displayed in the last cell of an editable table row,
-- | providing access to callbacks that delete or update the current row.
, rowMenu
:: { remove :: Maybe (Effect Unit)
, update :: (row -> row) -> Effect Unit
}
-> row
-> Maybe result
-> JSX
, summary
:: Array row
-> Maybe (Array result)
-> JSX
}
-> FormBuilder
{ readonly :: Boolean | props }
(Array row)
(Array result)
editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary } =
editableTable { addLabel, addRow, formBuilder: builder, maxRows, rowMenu, summary } =
formBuilder \props rows ->
let
{ columns, validate } = (un TableFormBuilder builder) props
validateRows = traverse validate rows
in
{ edit: \onChange ->
EditableTable.editableTable
{ addLabel
, maxRows
, readonly: isNothing defaultValue || props.readonly
, readonly: isNothing addRow || props.readonly
, rowEq: unsafeRefEq
, summary:
Row.row
Expand All @@ -100,47 +132,67 @@ editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary }
, flexWrap: "wrap"
, justifyContent: "flex-end"
}
, children: [ summary ]
, children: [ summary rows validateRows ]
}
, rows: Left $ mapWithIndex Tuple rows
, onRowAdd: foldMap (onChange <<< flip Array.snoc) defaultValue
, onRowAdd:
for_ addRow \addRow' -> launchAff_ do
rowM <- addRow'
traverse_ (liftEffect <<< onChange <<< flip Array.snoc) rowM
, onRowRemove: \(Tuple index _) ->
onChange \rows' -> fromMaybe rows' (Array.deleteAt index rows')
, removeCell: EditableTable.defaultRemoveCell
, removeCell: \onRowRemoveM (Tuple index row) ->
rowMenu
{ remove: onRowRemoveM <@> Tuple index row
, update: onChange <<< ix index
}
row
(validate row)
, columns:
columns <#> \{ label, render } ->
{ label
, renderCell: \(Tuple i r) ->
render r (onChange <<< ix i)
}
}
, validate: traverse validate rows
, validate: validateRows
}

-- | A `TableFormBuilder` makes a `FormBuilder` for a non-empty array where each
-- | row has columns defined by it.
nonEmptyEditableTable
:: forall props row result
. { addLabel :: String
, defaultValue :: Maybe row
, addRow :: Maybe (Aff (Maybe row))
, formBuilder :: TableFormBuilder { readonly :: Boolean | props } row result
, maxRows :: Int
, summary :: JSX
, rowMenu
:: { remove :: Maybe (Effect Unit)
, update :: (row -> row) -> Effect Unit
}
-> row
-> Maybe result
-> JSX
, summary
:: NEA.NonEmptyArray row
-> Maybe (NEA.NonEmptyArray result)
-> JSX
}
-> FormBuilder
{ readonly :: Boolean | props }
(NEA.NonEmptyArray row)
(NEA.NonEmptyArray result)
nonEmptyEditableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary } =
nonEmptyEditableTable { addLabel, addRow, formBuilder: builder, maxRows, rowMenu, summary } =
formBuilder \props rows ->
let
{ columns, validate } = (un TableFormBuilder builder) props
validateRows = traverse validate rows
in
{ edit: \onChange ->
EditableTable.editableTable
{ addLabel
, maxRows
, readonly: isNothing defaultValue || props.readonly
, readonly: isNothing addRow || props.readonly
, rowEq: unsafeRefEq
, summary:
Row.row
Expand All @@ -149,23 +201,45 @@ nonEmptyEditableTable { addLabel, defaultValue, formBuilder: builder, maxRows, s
, flexWrap: "wrap"
, justifyContent: "flex-end"
}
, children: [ summary ]
, children: [ summary rows validateRows ]
}
, rows: Right $ mapWithIndex Tuple rows
, onRowAdd: foldMap (onChange <<< flip NEA.snoc) defaultValue
, onRowAdd:
for_ addRow \addRow' -> launchAff_ do
rowM <- addRow'
traverse_ (liftEffect <<< onChange <<< flip NEA.snoc) rowM
, onRowRemove: \(Tuple index _) ->
onChange \rows' -> fromMaybe rows' (NEA.fromArray =<< NEA.deleteAt index rows')
, removeCell: EditableTable.defaultRemoveCell
, removeCell: \onRowRemoveM (Tuple index row) ->
rowMenu
{ remove: onRowRemoveM <@> Tuple index row
, update: onChange <<< ix index
}
row
(validate row)
, columns:
columns <#> \{ label, render } ->
{ label
, renderCell: \(Tuple i r) ->
render r (onChange <<< ix i)
}
}
, validate: traverse validate rows
, validate: validateRows
}

-- | Default row menu that displays a bin icon, which, when clicked, deletes the
-- | current row.
defaultRowMenu
:: forall row result
. { remove :: Maybe (Effect Unit)
, update :: (row -> row) -> Effect Unit
}
-> row
-> Maybe result
-> JSX
defaultRowMenu { remove } row _ =
EditableTable.defaultRemoveCell (map const remove) row

-- | Convert a `FormBuilder` into a column of a table form with the specified
-- | label where all fields are laid out horizontally.
column_
Expand Down
2 changes: 2 additions & 0 deletions src/Lumi/Styles/Button.purs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ button colo kind state size = case kind of
[ css
{ label: str "button"
, appearance: none
, outline: none
, padding: int 0
, background: none
, border: none
Expand All @@ -172,6 +173,7 @@ button colo kind state size = case kind of
( css
{ label: str "button"
, appearance: none
, outline: none
, minWidth: int 70
, padding: str "10px 20px"
, fontSize: int 14
Expand Down