diff --git a/docs/Examples/Form.example.purs b/docs/Examples/Form.example.purs index a51e15ae..e9512c04 100644 --- a/docs/Examples/Form.example.purs +++ b/docs/Examples/Form.example.purs @@ -344,7 +344,7 @@ userForm = ado ) $ FT.editableTable { addLabel: "Add pet" - , defaultValue: Just + , addRow: Just $ pure $ Just { firstName: F.Fresh "" , lastName: F.Fresh "" , animal: F.Fresh Nothing @@ -352,6 +352,7 @@ userForm = ado , color: Nothing } , maxRows: top + , rowMenu: FT.defaultRowMenu , summary: mempty , formBuilder: ado name <- FT.column_ "Name" ado diff --git a/docs/index.html b/docs/index.html index 4bfe7087..194c2df4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -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; } diff --git a/src/Lumi/Components/EditableTable.purs b/src/Lumi/Components/EditableTable.purs index 8ce486f1..7adbe08a 100644 --- a/src/Lumi/Components/EditableTable.purs +++ b/src/Lumi/Components/EditableTable.purs @@ -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 @@ -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" @@ -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 = @@ -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_ @@ -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 } diff --git a/src/Lumi/Components/Form/Table.purs b/src/Lumi/Components/Form/Table.purs index 5310f331..965f9607 100644 --- a/src/Lumi/Components/Form/Table.purs +++ b/src/Lumi/Components/Form/Table.purs @@ -1,7 +1,9 @@ module Lumi.Components.Form.Table ( TableFormBuilder + , revalidate , editableTable , nonEmptyEditableTable + , defaultRowMenu , column , column_ , withProps @@ -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) @@ -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)) , 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 @@ -100,13 +132,22 @@ 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 @@ -114,7 +155,7 @@ editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary } render r (onChange <<< ix i) } } - , validate: traverse validate rows + , validate: validateRows } -- | A `TableFormBuilder` makes a `FormBuilder` for a non-empty array where each @@ -122,25 +163,36 @@ editableTable { addLabel, defaultValue, formBuilder: builder, maxRows, summary } 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 @@ -149,13 +201,22 @@ 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 @@ -163,9 +224,22 @@ nonEmptyEditableTable { addLabel, defaultValue, formBuilder: builder, maxRows, s 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_ diff --git a/src/Lumi/Styles/Button.purs b/src/Lumi/Styles/Button.purs index a5f09579..3ce6b00a 100644 --- a/src/Lumi/Styles/Button.purs +++ b/src/Lumi/Styles/Button.purs @@ -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 @@ -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