Added snackbar documentation. Fixes #9.

This commit is contained in:
Søren Debois 2016-04-12 15:09:12 +02:00
parent f0a8591265
commit 040d524d24
6 changed files with 222 additions and 211 deletions

View file

@ -6,6 +6,9 @@ comp:
demo: demo:
(cd demo; elm-make Demo.elm --warn --output ../elm.js) (cd demo; elm-make Demo.elm --warn --output ../elm.js)
docs:
elm-make --docs=docs.json
wip-pages : wip-pages :
elm-make examples/Demo.elm --output $(PAGES)/wip.js elm-make examples/Demo.elm --output $(PAGES)/wip.js
(cd $(PAGES); git commit -am "Update."; git push origin gh-pages) (cd $(PAGES); git commit -am "Update."; git push origin gh-pages)

View file

@ -116,7 +116,7 @@ nth k xs =
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
update action model = update action model =
case action of case Debug.log "Action" action of
LayoutAction a -> LayoutAction a ->
let let
( lifted, layoutFx ) = ( lifted, layoutFx ) =
@ -203,7 +203,7 @@ tabs =
, ("Textfields", "textfields", \addr model -> , ("Textfields", "textfields", \addr model ->
Demo.Textfields.view (Signal.forwardTo addr TextfieldAction) model.textfields) Demo.Textfields.view (Signal.forwardTo addr TextfieldAction) model.textfields)
{- {-
, ("Template", \addr model -> , ("Template", "tempate", \addr model ->
[Demo.Template.view (Signal.forwardTo addr TemplateAction) model.template]) [Demo.Template.view (Signal.forwardTo addr TemplateAction) model.template])
-} -}
] ]

View file

@ -22,11 +22,13 @@ import Demo.Page as Page
type alias Mdl = type alias Mdl =
Material.Model Action Material.Model
type Square' type Square'
= Appearing = Appearing
| Waiting
| Active
| Idle | Idle
| Disappearing | Disappearing
@ -38,6 +40,7 @@ type alias Square =
type alias Model = type alias Model =
{ count : Int { count : Int
, squares : List Square , squares : List Square
, snackbar : Snackbar.Model Int
, mdl : Mdl , mdl : Mdl
} }
@ -46,6 +49,7 @@ model : Model
model = model =
{ count = 0 { count = 0
, squares = [] , squares = []
, snackbar = Snackbar.model
, mdl = Material.model , mdl = Material.model
} }
@ -57,19 +61,20 @@ type Action
= AddSnackbar = AddSnackbar
| AddToast | AddToast
| Appear Int | Appear Int
| Disappear Int
| Gone Int | Gone Int
| Snackbar (Snackbar.Action Int)
| MDL (Material.Action Action) | MDL (Material.Action Action)
add : Model -> (Int -> Snackbar.Contents Action) -> (Model, Effects Action) add : (Int -> Snackbar.Contents Int) -> Model -> (Model, Effects Action)
add model f = add f model =
let let
(mdl', fx) = (snackbar', fx) =
Snackbar.add (f model.count) snackbar model.mdl Snackbar.add (f model.count) model.snackbar
|> map2nd (Effects.map Snackbar)
model' = model' =
{ model { model
| mdl = mdl' | snackbar = snackbar'
, count = model.count + 1 , count = model.count + 1
, squares = (model.count, Appearing) :: model.squares , squares = (model.count, Appearing) :: model.squares
} }
@ -92,24 +97,31 @@ mapSquare k f model =
} }
update : Action -> Model -> (Model, Effects Action) update : Action -> Model -> (Model, Effects Action)
update action model = update action model =
case action of case action of
AddSnackbar -> AddSnackbar ->
add model add (\k -> Snackbar.snackbar k ("Snackbar message #" ++ toString k) "UNDO") model
<| \k -> Snackbar.snackbar ("Snackbar message #" ++ toString k) "UNDO" (Disappear k)
AddToast -> AddToast ->
add model add (\k -> Snackbar.toast k <| "Toast message #" ++ toString k) model
<| \k -> Snackbar.toast <| "Toast message #" ++ toString k
Appear k -> Appear k ->
( model |> mapSquare k (\sq -> if sq == Appearing then Waiting else sq)
, none
)
Snackbar (Snackbar.Begin k) ->
( model |> mapSquare k (always Active)
, none
)
Snackbar (Snackbar.End k) ->
( model |> mapSquare k (always Idle) ( model |> mapSquare k (always Idle)
, none , none
) )
Disappear k -> Snackbar (Snackbar.Click k) ->
( model |> mapSquare k (always Disappearing) ( model |> mapSquare k (always Disappearing)
, delay transitionLength (Gone k) , delay transitionLength (Gone k)
) )
@ -120,6 +132,11 @@ update action model =
} }
, none) , none)
Snackbar action' ->
Snackbar.update action' model.snackbar
|> map1st (\s -> { model | snackbar = s })
|> map2nd (Effects.map Snackbar)
MDL action' -> MDL action' ->
Material.update MDL action' model.mdl Material.update MDL action' model.mdl
|> map1st (\m -> { model | mdl = m }) |> map1st (\m -> { model | mdl = m })
@ -143,12 +160,6 @@ addToastButton =
[ Button.fwdClick AddToast ] [ Button.fwdClick AddToast ]
-- TODO: Bad name
snackbar : Snackbar.Instance Mdl Action
snackbar =
Snackbar.instance MDL Snackbar.model
boxHeight : String boxHeight : String
boxHeight = "48px" boxHeight = "48px"
@ -166,27 +177,38 @@ transitions =
("transition" ("transition"
, "box-shadow 333ms ease-in-out 0s, " , "box-shadow 333ms ease-in-out 0s, "
++ "width " ++ toString transitionLength ++ "ms, " ++ "width " ++ toString transitionLength ++ "ms, "
++ "height " ++ toString transitionLength ++ "ms" ++ "height " ++ toString transitionLength ++ "ms, "
++ "background-color " ++ toString transitionLength ++ "ms"
) )
clickView : Model -> Square -> Html clickView : Model -> Square -> Html
clickView model (k, square) = clickView model (k, square) =
let let
color = palette =
Array.get ((k + 4) % Array.length Color.palette) Color.palette Array.get ((k + 4) % Array.length Color.palette) Color.palette
|> Maybe.withDefault Color.Teal |> Maybe.withDefault Color.Teal
|> flip Color.color Color.S500
shade =
case square of
Idle ->
Color.S100
_ ->
Color.S500
color =
Color.color palette shade
selected' = selected' =
Snackbar.activeAction (snackbar.get model.mdl) == Just (Disappear k) square == Active
(width, height, margin, selected) = (width, height, margin, selected) =
case square of if square == Appearing || square == Disappearing then
Idle -> ("0", "0", "16px 0", False)
(boxWidth, boxHeight, "16px 16px", selected') else
_ -> (boxWidth, boxHeight, "16px 16px", selected')
("0", "0", "16px 0", False)
in in
div div
[ style [ style
@ -236,7 +258,7 @@ view addr model =
[ text """Click the buttons below to activate the snackbar. Note that [ text """Click the buttons below to activate the snackbar. Note that
multiple activations are automatically queued.""" multiple activations are automatically queued."""
] ]
, grid [ ] --css "margin-top" "32px" ] , grid [ ]
[ cell [ cell
[ size All 2, size Phone 2, align Top ] [ size All 2, size Phone 2, align Top ]
[ addToastButton.view addr model.mdl [ addToastButton.view addr model.mdl
@ -261,7 +283,7 @@ view addr model =
] ]
(model.squares |> List.reverse |> List.map (clickView model)) (model.squares |> List.reverse |> List.map (clickView model))
] ]
, snackbar.view addr model.mdl , Snackbar.view (Signal.forwardTo addr Snackbar) model.snackbar
] ]

View file

@ -16,7 +16,7 @@ import Demo.Page as Page
type alias Model = type alias Model =
{ mdl : Material.Model Action { mdl : Material.Model
, rx : (String, Regex.Regex) , rx : (String, Regex.Regex)
} }
@ -108,7 +108,7 @@ m0 =
type alias Mdl = type alias Mdl =
Material.Model Action Material.Model
field0 : Textfield.Instance Mdl Action field0 : Textfield.Instance Mdl Action

View file

@ -36,6 +36,10 @@ We recommend going with the library's
[component support](http://github.com/debois/elm-mdl/blob/master/examples/Component.elm) [component support](http://github.com/debois/elm-mdl/blob/master/examples/Component.elm)
rather than working directly in plain Elm Architecture. rather than working directly in plain Elm Architecture.
# TODO
Using TEA, Style.
# Component Support # Component Support
This module contains only convenience functions for working with nested This module contains only convenience functions for working with nested
@ -153,16 +157,16 @@ import Material.Component as Component exposing (Indexed)
user actions in their model (notably Snackbar), the model is generic in the user actions in their model (notably Snackbar), the model is generic in the
type of such "observations". type of such "observations".
-} -}
type alias Model obs = type alias Model =
{ button : Indexed Button.Model { button : Indexed Button.Model
, textfield : Indexed Textfield.Model , textfield : Indexed Textfield.Model
, snackbar : Maybe (Snackbar.Model obs) , snackbar : Maybe (Snackbar.Model Int)
} }
{-| Initial model. {-| Initial model.
-} -}
model : Model obs model : Model
model = model =
{ button = Dict.empty { button = Dict.empty
, textfield = Dict.empty , textfield = Dict.empty
@ -173,7 +177,7 @@ model =
{-| Action encompassing actions of all Material components. {-| Action encompassing actions of all Material components.
-} -}
type alias Action obs = type alias Action obs =
Component.Action (Model obs) obs Component.Action Model obs
{-| Update function for the above Action. {-| Update function for the above Action.
@ -181,7 +185,7 @@ type alias Action obs =
update : update :
(Action obs -> obs) (Action obs -> obs)
-> Action obs -> Action obs
-> Model obs -> Model
-> (Model obs, Effects obs) -> (Model, Effects obs)
update = update =
Component.update Component.update

View file

@ -1,23 +1,26 @@
module Material.Snackbar module Material.Snackbar
( Contents, Model, model, toast, snackbar, isActive, activeAction ( Contents, Model, add, model, toast, snackbar
, Action(Add, Action), update , Action(Begin, End, Click), update
, view , view
, Instance, instance, add
) where ) where
{-| TODO {-| Material Design "Snackbar" component.
# Model For live demo and intended use, see
@docs Contents, Model, model, toast, snackbar, isActive, activeAction [here](http://localhost:8000/Demo.elm#/snackbar).
# Action, Update # Generating messages
@docs Contents, toast, snackbar, add
# Elm Architecture
@docs Model, model
@docs Action, update @docs Action, update
# View
@docs view @docs view
# Component support # Component support
@docs Instance, add, instance Snackbar does not have component support. It must be used as a regular TEA
component.
-} -}
import Html exposing (..) import Html exposing (..)
@ -28,33 +31,49 @@ import Task
import Time exposing (Time) import Time exposing (Time)
import Maybe exposing (andThen) import Maybe exposing (andThen)
import Material.Component as Component exposing (Indexed) import Material.Helpers exposing (map2nd, delay)
import Material.Helpers exposing (mapFx, addFx, delay)
-- MODEL -- MODEL
{-| TODO
{-| Defines a single snackbar message. Usually, you would use either `toast`
or `snackbar` to construct `Contents`.
- `message` defines the (text) message displayed
- `action` defines a label for the action-button in the snackbar. If
no action is provided, the snackbar is a message-only toast.
- `payload` defines the data returned by Snackbar actions for this message.
You will usually choose this to be an Action of yours for later dispatch,
e.g., if your snackbar has an "Undo" action, you would store the
corresponding action as the payload.
- `timeout` is the amount of time the snackbar should be visible
- `fade` is the duration of the fading animation of the snackbar.
If you are satsified with the default timeout and fade, do not construct
values of this type yourself; use `snackbar` and `toast` below instead.
-} -}
type alias Contents a = type alias Contents a =
{ message : String { message : String
, action : Maybe (String, a) , action : Maybe String
, payload : a
, timeout : Time , timeout : Time
, fade : Time , fade : Time
} }
{-| TODO {-| Do not construct this yourself; use `model` below.
-} -}
type alias Model a = type alias Model a =
{ queue : List (Contents a) { queue : List (Contents a)
, state : State' a , state : State a
, seq : Int , seq : Int
} }
{-| TODO {-| Default snackbar model.
-} -}
model : Model a model : Model a
model = model =
@ -64,66 +83,37 @@ model =
} }
{-| Generate toast with given payload and message. Timeout is 2750ms, fade 250ms.
{-| Generate default toast with given message.
Timeout is 2750ms, fade 250ms.
-} -}
toast : String -> Contents a toast : a -> String -> Contents a
toast message = toast payload message =
{ message = message { message = message
, action = Nothing , action = Nothing
, payload = payload
, timeout = 2750 , timeout = 2750
, fade = 250 , fade = 250
} }
{-| Generate default snackbar with given message, {-| Generate snackbar with given payload, message and label.
action-label, and action. Timeout is 2750ms, fade 250ms. Timeout is 2750ms, fade 250ms.
-} -}
snackbar : String -> String -> a -> Contents a snackbar : a -> String -> String -> Contents a
snackbar message actionMessage action = snackbar payload message label =
{ message = message { message = message
, action = Just (actionMessage, action) , action = Just label
, payload = payload
, timeout = 2750 , timeout = 2750
, fade = 250 , fade = 250
} }
{-| TODO
(Bad name)
-}
isActive : Model a -> Maybe (Contents a)
isActive model =
case model.state of
Active c ->
Just c
_ ->
Nothing
{-| TODO
-}
activeAction : Model a -> Maybe a
activeAction model =
isActive model
|> flip Maybe.andThen .action
|> Maybe.map snd
contentsOf : Model a -> Maybe (Contents a)
contentsOf model =
case model.state of
Inert -> Nothing
Active contents -> Just contents
Fading contents -> Just contents
-- SNACKBAR STATE MACHINE -- SNACKBAR STATE MACHINE
type State' a
type State a
= Inert = Inert
| Active (Contents a) | Active (Contents a)
| Fading (Contents a) | Fading (Contents a)
@ -131,40 +121,65 @@ type State' a
type Transition type Transition
= Timeout = Timeout
| Click | Clicked
move : Transition -> Model a -> (Model a, Effects Transition) forward : a -> Effects a
forward = Task.succeed >> Effects.task
next : Model a -> Effects Transition -> Effects (Action a)
next model =
Effects.map (Move model.seq)
move : Transition -> Model a -> (Model a, Effects (Action a))
move transition model = move transition model =
case (model.state, transition) of case (model.state, transition) of
(Inert, Timeout) -> (Inert, Timeout) ->
tryDequeue model tryDequeue model
(Active contents, _) -> (Active contents, Clicked) ->
( { model | state = Fading contents } ( { model | state = Fading contents }
, delay contents.fade Timeout , Effects.batch
[ delay contents.fade Timeout |> next model
, Click contents.payload |> forward
]
)
(Active contents, Timeout) ->
( { model | state = Fading contents }
, Effects.batch
[ delay contents.fade Timeout |> next model
, Begin contents.payload |> forward
]
) )
(Fading contents, Timeout) -> (Fading contents, Timeout) ->
( { model | state = Inert} ( { model | state = Inert}
, Effects.tick (\_ -> Timeout) , Effects.batch
[ always Timeout |> Effects.tick |> next model
, End contents.payload |> forward
]
) )
_ -> _ ->
(model, none) (model, none)
-- NOTIFICATION QUEUE -- NOTIFICATION QUEUE
enqueue : Contents a -> Model a -> Model a enqueue : Contents a -> Model a -> Model a
enqueue contents model = enqueue contents model =
{ model { model
| queue = List.append model.queue [contents] | queue = List.append model.queue [contents]
} }
tryDequeue : Model a -> (Model a, Effects Transition) tryDequeue : Model a -> (Model a, Effects (Action a))
tryDequeue model = tryDequeue model =
case (model.state, model.queue) of case (model.state, model.queue) of
(Inert, c :: cs) -> (Inert, c :: cs) ->
@ -173,93 +188,86 @@ tryDequeue model =
, queue = cs , queue = cs
, seq = model.seq + 1 , seq = model.seq + 1
} }
, delay c.timeout Timeout , Effects.batch
[ delay c.timeout Timeout |> Effects.map (Move (model.seq + 1))
, forward (Begin c.payload)
]
) )
_ -> _ ->
(model, none) (model, none)
-- ACTIONS, UPDATE -- ACTIONS, UPDATE
{-| TODO
{-| Elm Architecture Action type.
The following actions are observable to you:
- `Begin a`. The snackbar is now displaying the message with payload `a`.
- `End a`. The snackbar is done displaying the message with payload `a`.
- `Click a`. The user clicked the action on the message with payload `a`.
You can consume these three actions without forwarding them to `Snackbar.update`.
(You still need to forward other Snackbar actions.)
-} -}
type Action a type Action a
= Add (Contents a) = Begin a
| Action a | End a
| Click a
-- Private
| Move Int Transition | Move Int Transition
forwardClick : Transition -> Model a -> (Model a, Effects (Action a)) -> (Model a, Effects (Action a)) {-| Elm Architecture update function.
forwardClick transition model =
case (transition, model.state) of
(Click, Active contents) ->
contents.action
|> Maybe.map (snd >> Action >> Task.succeed >> Effects.task >> addFx)
|> Maybe.withDefault (\x -> x)
_ ->
\x -> x
liftTransition : (Model a, Effects Transition) -> (Model a, Effects (Action a))
liftTransition (model, effect) =
(model, Effects.map (Move model.seq) effect)
{-| TODO
-} -}
update : Action a -> Model a -> (Model a, Effects (Action a)) update : Action a -> Model a -> (Model a, Effects (Action a))
update action model = update action model =
case action of case action of
Action _ ->
(model, none)
Add contents ->
enqueue contents model
|> tryDequeue
|> liftTransition
Move seq transition -> Move seq transition ->
if seq == model.seq then if seq == model.seq then
move transition model move transition model
|> liftTransition
|> forwardClick transition model
else else
(model, none) (model, none)
_ ->
-- Begin, End, Click are for external consumption only.
(model, none)
{-| Add a message to the snackbar. If another message is currently displayed,
the provided message will be queued. You will be able to observe a `Begin` action
(see `Action` above) once the action begins displaying.
You must dispatch the returned effect for the Snackbar to begin displaying your
message.
-}
add : Contents a -> Model a -> (Model a, Effects (Action a))
add contents model =
enqueue contents model |> tryDequeue
-- VIEW -- VIEW
{-|
{-| Elm architecture update function.
-} -}
view : Signal.Address (Action a) -> Model a -> Html view : Signal.Address (Action a) -> Model a -> Html
view addr model = view addr model =
let let
active = contents =
model.queue /= [] case model.state of
Inert -> Nothing
textBody = Active c -> Just c
contentsOf model Fading c -> Just c
|> Maybe.map (\c -> [ text c.message ])
|> Maybe.withDefault []
(buttonBody, buttonHandler) =
contentsOf model
|> (flip Maybe.andThen .action)
|> Maybe.map (\(msg, action') ->
([ text msg ],
[ onClick addr (Move model.seq Click) ])
)
|> Maybe.withDefault ([], [])
isActive = isActive =
case model.state of case model.state of
Inert -> False Inert -> False
Active _ -> True Active _ -> True
Fading _ -> False Fading _ -> False
in in
div div
[ classList [ classList
@ -272,70 +280,44 @@ view addr model =
[ div [ div
[ class "mdl-snackbar__text" [ class "mdl-snackbar__text"
] ]
textBody (contents
|> Maybe.map (\c -> [ text c.message ])
|> Maybe.withDefault []
)
, button , button
( class "mdl-snackbar__action" ( class "mdl-snackbar__action"
:: type' "button" :: type' "button"
-- :: ariaHidden "true" -- :: ariaHidden "true"
:: buttonHandler :: ( contents
|> flip Maybe.andThen .action
|> Maybe.map (always [ onClick addr (Move model.seq Clicked) ])
|> Maybe.withDefault []
)
)
( contents
|> flip Maybe.andThen .action
|> Maybe.map (\action -> [ text action ])
|> Maybe.withDefault []
) )
buttonBody
] ]
-- COMPONENT -- COMPONENT
{-| {- Component support for Snackbar is currently disabled. The type "Model a" of
the Snackbar Model escapes into the global Mdl model, polluting users model
with the type variable "a". This is unacceptable in itself; it makes
component support too hard to use for non-expert users.
This problem is compounded by the elm compiler bug
[#1192](https://github.com/elm-lang/elm-compiler/issues/1192), which causes
(apparently) unhelpful error messages on errors related to wrong use of type
constructors.
Component support for snackbar was implemented earlier. In case a solution
presents itself, the last commit to contain this support was:
f0a85912654713238694f48b1a4b7d5a7459965f
-} -}
type alias Container s obs =
{ s | snackbar : Maybe (Model obs) }
{-|
-}
type alias Instance container obs =
Component.Instance (Model obs) container (Action obs) obs Html
{-|
-}
type alias Observer obs =
Component.Observer (Action obs) obs
actionObserver : Observer ons
actionObserver action =
case action of
Action action' ->
Just action'
_ ->
Nothing
{-| Component instance.
-}
instance
: (Component.Action (Container c obs) obs -> obs)
-> (Model obs)
-> Instance (Container c obs) obs
instance lift model0 =
Component.instance1
view update .snackbar (\x y -> {y | snackbar = x}) lift model0 [ actionObserver ]
{-|
TODO
-}
add :
Contents obs
-> Instance (Container c obs) obs
-> (Container c obs)
-> (Container c obs, Effects obs)
add contents inst model =
let
(sb, fx) =
update (Add contents) (inst.get model)
in
(inst.set sb model, Effects.map inst.fwd fx)