Added component features to textfield and button (needs better docs, though).

This commit is contained in:
Søren Debois 2016-03-31 23:32:20 +02:00
parent a5d96c2258
commit 73256c80db
6 changed files with 275 additions and 139 deletions

View file

@ -11,7 +11,8 @@ import Material.Icon as Icon
import Material.Style exposing (Style)
import Material.Textfield as Textfield
import Material.Component exposing (..)
import Material.Component as Component
import Material.Component.All as Setup
-- MODEL
@ -19,9 +20,11 @@ import Material.Component exposing (..)
type alias Index = (Int, Int)
type alias View =
Signal.Address Button.Action -> Button.Model -> List Style -> List Html -> Html
type alias View' =
Signal.Address Button.Action -> Button.Model -> Html
@ -66,7 +69,7 @@ model =
buttons
|> List.concatMap (List.map <| \(idx, (ripple, _, _)) -> (idx, Button.model ripple))
|> Dict.fromList
, componentState = state0
, componentState = Setup.state
}
@ -75,16 +78,15 @@ model =
type Action
= Action Index Button.Action
| State' (Material.Component.Action Material.Component.State (Maybe Action))
| State (Setup.Action Action)
| Click
| Input String
type alias Model =
{ clicked : String
, buttons : Dict.Dict Index Button.Model
, componentState : State
-- TODO: Exposed Action should not be parametric.
, componentState : Setup.State
}
@ -101,20 +103,28 @@ update action model =
)
|> Maybe.withDefault (model, Effects.none)
State' action' ->
Material.Component.update State' update action' model
State action' ->
Component.update State update action' model
Click ->
( tf.map (\m -> { m | value = "You clicked!" }) model, Effects.none )
Input str ->
( tf.map (\m -> { m | value = "You wrote '" ++ str ++ "' in the other guy."}) model
, Effects.none
)
instance = Component.instance State
instance' = Component.instance' State
tf = instance <| Textfield.component Textfield.model 4
-- VIEW
tf = instance State' (textfieldWidget Textfield.model 4)
view : Signal.Address Action -> Model -> Html
@ -151,12 +161,12 @@ view addr model =
)
|> (\contents ->
div []
[ instance' State' (buttonWidget (Button.model True) 1 |> onClick Click) addr model [] [ text "Click me (1)" ]
, instance' State' (buttonWidget (Button.model False) 2) addr model [] [ text "Click me (2)" ]
, instance' State' (textfieldWidget Textfield.model 3) addr model
[ instance' (Button.component Button.flat (Button.model True) 1 |> onClick Click) addr model [] [ text "Click me (1)" ]
, instance' (Button.component Button.raised (Button.model False) 2) addr model [] [ text "Click me (2)" ]
, instance' (Textfield.component Textfield.model 3 |> Textfield.onInput Input) addr model
, tf.view addr model
, Grid.grid [] contents
]
)
--i = instance' State' (buttonWidget (Button.model True) 1) -- addr model.componentState [] [ text "Click me (1)" ]
--i = instance' State (buttonWidget (Button.model True) 1) -- addr model.componentState [] [ text "Click me (1)" ]

View file

@ -2,6 +2,7 @@ module Material.Button
( Model, model, Action(Click), update
, flat, raised, fab, minifab, icon
, colored, primary, accent
, View, component, Component, onClick
) where
{-| From the [Material Design Lite documentation](http://www.getmdl.io/components/#buttons-section):
@ -25,8 +26,8 @@ module Material.Button
See also the
[Material Design Specification]([https://www.google.com/design/spec/components/buttons.html).
# Component
@docs Model, model, Action, update
# Elm architecture
@docs Model, model, Action, update, View
# Style
@docs colored, primary, accent
@ -38,6 +39,9 @@ for details about what type of buttons are appropriate for which situations.
@docs flat, raised, fab, minifab, icon
# Component
@docs Component, component, onClick
-}
import Html exposing (..)
@ -49,6 +53,7 @@ import Signal exposing (Address, forwardTo)
import Material.Helpers as Helpers
import Material.Style exposing (Style, cs, cs', styled)
import Material.Ripple as Ripple
import Material.Component as Component exposing (Component, Indexed)
{-| MDL button.
-}
@ -137,7 +142,7 @@ view kind addr model styling html =
)
[ Helpers.blurOn "mouseup"
, Helpers.blurOn "mouseleave"
, onClick addr Click
, Html.Events.onClick addr Click
]
(case model of
S (Just ripple) ->
@ -151,12 +156,10 @@ view kind addr model styling html =
_ -> html)
-- Fake address (for link buttons).
addr : Signal.Address Action
addr = (Signal.mailbox Click).address
{-| Type of button views.
-}
type alias View =
Address Action -> Model -> List Style -> List Html -> Html
{-| From the
@ -179,7 +182,7 @@ Example use (uncolored flat button, assuming properly setup model):
flatButton = Button.flat addr model Button.Plain [text "Click me!"]
-}
flat : Address Action -> Model -> List Style -> List Html -> Html
flat : View
flat = view ""
@ -200,7 +203,7 @@ Example use (colored raised button, assuming properly setup model):
raisedButton = Button.raised addr model Button.Colored [text "Click me!"]
-}
raised : Address Action -> Model -> List Style -> List Html -> Html
raised : View
raised = view "mdl-button--raised"
@ -226,13 +229,13 @@ Example use (colored with a '+' icon):
fabButton : Html
fabButton = fab addr model Colored [Icon.i "add"]
-}
fab : Address Action -> Model -> List Style -> List Html -> Html
fab : View
fab = view "mdl-button--fab"
{-| Mini-sized variant of a Floating Action Button; refer to `fab`.
-}
minifab : Address Action -> Model -> List Style -> List Html -> Html
minifab : View
minifab = view "mdl-button--mini-fab"
@ -248,6 +251,45 @@ Example use (no color, displaying a '+' icon):
iconButton : Html
iconButton = icon addr model Plain [Icon.i "add"]
-}
icon : Address Action -> Model -> List Style -> List Html -> Html
icon : View
icon = view "mdl-button--icon"
-- COMPONENT
{-| Button component type.
-}
type alias Component state obs =
Component.Component
Model
{ state | button : Indexed Model }
Action
obs
(List Style -> List Html -> Html)
{-| Component constructor. Provide the view function your button should
have as the first argument, e.g.,
button = Button.component Button.minifab (Button.model True) 0
-}
component : View -> Model -> Int -> Component state action
component view =
Component.setup view update .button (\x y -> {y | button = x})
{-| Lift the button Click action to your own action. E.g.,
-}
onClick : obs -> Component state obs -> Component state obs
onClick o component =
(\action ->
case action of
Click -> Just o
_ -> Nothing)
|> Component.addObserver component

View file

@ -1,63 +1,82 @@
module Material.Component where
module Material.Component
( Component, setup, addObserver
, Instance, instance, instance'
, update
, Indexed
, View, Update, Action
) where
{-|
# Types
## Elm architecture types
@docs View, Update, Action
## Component types
@docs Component, Instance
## Helpers
@docs Indexed
# For component consumers
@docs instance, instance'
@docs update
# For component authors
@docs component, addObserver
-}
import Effects exposing (Effects)
import Html exposing (Html)
import Dict exposing (Dict)
import Material.Button as Button
import Material.Style exposing (Style)
import Material.Textfield as Textfield
import Material.Helpers exposing (map1, map2, map1st, map2nd)
map1 : (a -> a') -> (a, b, c) -> (a', b, c)
map1 f (x,y,z) = (f x, y, z)
-- TYPES
map2 : (b -> b') -> (a, b, c) -> (a, b', c)
map2 f (x,y,z) = (x, f y, z)
map1st : (a -> c) -> (a,b) -> (c,b)
map1st f (x,y) = (f x, y)
map2nd : (b -> c) -> (a,b) -> (a,c)
map2nd f (x,y) = (x, f y)
{-| Indexed families of things.
-}
type alias Indexed a =
Dict Int a
{- Variant of EA update function type, where effects may be
lifted to a different type.
-}
type alias Update' model action action' =
action -> model -> (model, Effects action')
{-| Standard EA update function type.
-}
type alias Update model action =
Update' model action action
type alias Step model action action' =
action -> model -> (model, Effects action, action')
type alias State =
{ button : Indexed Button.Model
, textfield : Indexed Textfield.Model
}
state0 : State
state0 =
{ button = Dict.empty
, textfield = Dict.empty
}
{-| Standard EA view function type.
-}
type alias View model action a =
Signal.Address action -> model -> a
{-| Generic component action.
-}
type Action model obs =
A (model -> (model, Effects (Action model obs), obs))
{-| Generic model.
-}
type alias Model model state =
{ model | componentState : state }
-- FOR CONSUMERS
update :
(Action state (Maybe action) -> action) ->
Update (Model model state) action ->
@ -79,39 +98,13 @@ update fwd update' (A f) model =
|> map2nd (\fx' -> Effects.batch [ fx, fx' ])
pack : (Step model action obs) -> action -> Action model obs
pack update action =
A (update action >> map2 (Effects.map (pack update)))
-- CAUTION. Potential crash from update/update name clash.
-- COMPONENT
type alias View model action a =
Signal.Address action -> model -> a
type alias Component model action a =
{ view : View model action a
, update : Update model action
}
type alias Indexed a =
Dict Int a
buttonComponent : Component Button.Model Button.Action (List Style -> List Html -> Html)
buttonComponent =
{ view = Button.raised
, update = Button.update
}
textfieldComponent : Component Textfield.Model Textfield.Action Html
textfieldComponent =
{ view = Textfield.view
, update = \action model -> (Textfield.update action model, Effects.none)
}
type alias Widget submodel model action obs a =
{-| Component type.
-}
type alias Component submodel model action obs a =
{ view : View model action a
, update : Update model action
, observe : action -> Maybe obs
@ -120,15 +113,30 @@ type alias Widget submodel model action obs a =
}
widget :
Component submodel action a -> -- Given a "Component submodel ..."
{-| Component constructor. You must supply
1. A view function
2. An update function
3. A getter
4. A setter
This will produce a function which needs only
5. An initial model, and
6. an id
to fit with the `instance` function.
-}
setup :
View submodel action a -> -- Given a view function,
Update submodel action -> -- an update function
(model -> Indexed submodel) -> -- a getter
(Indexed submodel -> model -> model) -> -- a setter
submodel -> -- an initial model for this instance
Int -> -- an id for this instance
Widget submodel model action obs a -- ... produce a "Widget ..."
Int -> -- an instance id (*)
Component submodel model action obs a -- ... produce a Component.
widget component get set model0 id =
setup view update get set model0 id =
let
get' model =
Dict.get id (get model) |> Maybe.withDefault model0
@ -137,13 +145,11 @@ widget component get set model0 id =
set (Dict.insert id submodel (get model)) model
in
{ view =
\addr model -> component.view addr (get' model)
\addr model -> view addr (get' model)
, update =
\action model ->
component.update action (get' model)
update action (get' model)
|> map1st (flip set' model)
, getModel = get'
, setModel = set'
, observe = \_ -> Nothing
@ -151,11 +157,27 @@ widget component get set model0 id =
{- EA update function variant where running the function
produces not just a new model and an effect, but also an
observation.
-}
type alias Step model action obs =
action -> model -> (model, Effects action, obs)
{- Convert an update function to a step function by applying a
function that converts the action input to the update function into
an observation.
-}
observe : (action -> obs) -> Update model action -> Step model action obs
observe f update action =
update action >> (\(model', effects) -> (model', effects, f action))
{-| Type of component instances. A component instance contains a view,
and get/set/map for, well, getting, setting, and mapping the component
model.
-}
type alias Instance submodel model action a =
{ view : View model action a
, get : model -> submodel
@ -164,9 +186,22 @@ type alias Instance submodel model action a =
}
{- Partially apply a step (update + observation) function to an action,
producing a generic Action.
-}
pack : (Step model action obs) -> action -> Action model obs
pack update action =
A (update action >> map2 (Effects.map (pack update)))
{-| Instantiate a component. You must supply:
1. A function embedding `Action` into your actions.
2. A component
-}
instance :
(Action model (Maybe action) -> action) ->
Widget submodel model subaction action a ->
Component submodel model subaction action a ->
Instance submodel (Model master model) action a
instance lift widget =
let
@ -176,55 +211,48 @@ instance lift widget =
set x model =
{ model | componentState = widget.setModel x model.componentState }
fwd =
pack (observe widget.observe widget.update) >> lift
in
{ view =
\addr model ->
widget.view (Signal.forwardTo addr (pack (observe widget.observe widget.update) >> lift)) model.componentState
widget.view (Signal.forwardTo addr fwd) model.componentState
, get = get
, set = set
, map = \f model -> set (f (get model)) model
}
{-| Convenience function for instantiating components whose models
you never need to read or write. (E.g,. Snackbar in Toast form.)
-}
instance' :
(Action model (Maybe action) -> action) ->
Widget submodel model subaction action a ->
Component submodel model subaction action a ->
View (Model m model) action a
instance' lift widget = (instance lift widget).view
type alias ButtonStates a =
{ a | button : Indexed Button.Model }
buttonWidget : Button.Model -> Int -> Widget Button.Model (ButtonStates m) Button.Action (Maybe obs) (List Style -> List Html -> Html)
buttonWidget model =
widget buttonComponent .button (\x y -> {y | button = x}) model
type Test = State' (Action State (Maybe Test))
addObserver widget f =
{ widget
{-| Add an observer to a component.
-}
addObserver
: { c | observe : a -> Maybe b }
-> (a -> Maybe b)
-> { c | observe : a -> Maybe b }
addObserver component f =
{ component
| observe =
\action ->
case f action of
Nothing -> widget.observe action
Nothing -> component.observe action
x -> x
}
onClick f widget =
(\action ->
case action of
Button.Click -> Just f
_ -> Nothing)
|> addObserver widget
{-
type alias TextfieldStates a =
{ a | textfield : Indexed Textfield.Model }
@ -235,3 +263,5 @@ textfieldWidget : Textfield.Model -> Int -> Widget Textfield.Model (TextfieldSta
textfieldWidget model =
widget textfieldComponent .textfield (\x y -> { y | textfield = x}) model
-}

View file

@ -9,17 +9,6 @@ filter elem attr html =
elem attr (List.filterMap (\x -> x) html)
mapWithIndex : (Int -> a -> b) -> List a -> List b
mapWithIndex f xs =
let
loop k ys =
case ys of
[] -> []
y :: ys -> f k y :: loop (k+1) ys
in
loop 0 xs
effect : Effects b -> a -> (a, Effects b)
effect e x = (x, e)
@ -43,3 +32,24 @@ clip lower upper k = Basics.max lower (Basics.min k upper)
blurOn : String -> Html.Attribute
blurOn evt =
Html.Attributes.attribute ("on" ++ evt) <| "this.blur()"
-- TUPLES
map1 : (a -> a') -> (a, b, c) -> (a', b, c)
map1 f (x,y,z) = (f x, y, z)
map2 : (b -> b') -> (a, b, c) -> (a, b', c)
map2 f (x,y,z) = (x, f y, z)
map1st : (a -> c) -> (a,b) -> (c,b)
map1st f (x,y) = (f x, y)
map2nd : (b -> c) -> (a,b) -> (a,c)
map2nd f (x,y) = (x, f y)

View file

@ -279,7 +279,7 @@ tabsView addr model tabs =
, ("mds-js-ripple-effect--ignore-events", model.rippleTabs)
]
]
(tabs |> mapWithIndex (\tabIndex tab ->
(tabs |> List.indexedMap (\tabIndex tab ->
filter a
[ classList
[ ("mdl-layout__tab", True)

View file

@ -26,15 +26,21 @@ This implementation provides only single-line.
# Configuration
@docs Kind, Label
# Component
# Elm Architecture
@docs Action, Model, model, update, view
# Component
@docs component, Component, onInput
-}
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Effects
import Material.Helpers exposing (filter)
import Material.Component as Component exposing (Indexed)
-- MODEL
@ -160,3 +166,41 @@ view addr model =
, model.error |> Maybe.map (\e ->
span [class "mdl-textfield__error"] [text e])
]
-- COMPONENT
{-| Textfield component type.
-}
type alias Component state obs =
Component.Component
Model
{ state | textfield : Indexed Model }
Action
obs
Html
{-| Component constructor.
-}
component : Model -> Int -> Component state action
component =
let
update' action model = (update action model, Effects.none)
in
Component.setup view update' .textfield (\x y -> {y | textfield = x})
{-| Lift the button Click action to your own action. E.g.,
-}
onInput : (String -> obs) -> Component state obs -> Component state obs
onInput f component =
(\action ->
case action of
Input str -> Just (f str)
_ -> Nothing)
|> Component.addObserver component