Functional component model.

This commit is contained in:
Søren Debois 2016-04-07 09:52:11 +02:00
parent dfda38d0c1
commit 1fdcc78ba7
6 changed files with 249 additions and 192 deletions

View file

@ -15,7 +15,7 @@ import Material.Button as Button
type alias Model =
{ count : Int
, mdl : Material.Model Action
-- Boilerplate: Model store for any and all MDL components you need.
-- Boilerplate: mdl is the Model store for any and all MDL components you need.
}
@ -24,7 +24,7 @@ model : Model
model =
{ count = 0
, mdl = Material.model
-- Always use this initial MDL component model store.
-- Boilerplate: Always use this initial MDL model store.
}
@ -36,7 +36,6 @@ type Action
| Reset
| MDL (Material.Action Action)
-- Boilerplate: Action for MDL actions (ripple animations etc.).
-- It should always look like this.
update : Action -> Model -> (Model, Effects.Effects Action)
@ -83,8 +82,7 @@ button. The arguments are:
-}
increase : Button.Instance Mdl Action
increase =
Button.instance 0 MDL
Button.flat (Button.model True)
Button.instance 0 MDL Button.flat (Button.model True)
[ Button.fwdClick Increase ]
@ -93,8 +91,7 @@ click event to our Reset action.
-}
reset : Button.Instance Mdl Action
reset =
Button.instance 1 MDL
Button.flat (Button.model False)
Button.instance 1 MDL Button.flat (Button.model False)
[ Button.fwdClick Reset ]

View file

@ -20,7 +20,8 @@ import Demo.Page as Page
-- MODEL
type alias Mdl = Material.Model Action
type alias Mdl =
Material.Model Action
type alias Model =
@ -48,22 +49,8 @@ type Action
| MDL (Material.Action Action)
snackbar : Int -> Snackbar.Contents Action
snackbar k =
Snackbar.snackbar
("Snackbar message #" ++ toString k)
"UNDO"
(Undo k)
toast : Int -> Snackbar.Contents Action
toast k =
Snackbar.toast
<| "Toast message #" ++ toString k
add : (Int -> Snackbar.Contents Action) -> Model -> (Model, Effects Action)
add f model =
add : Model -> (Int -> Snackbar.Contents Action) -> (Model, Effects Action)
add model f =
let
(mdl', fx) =
Snackbar.add (f model.count) snackbarComponent model.mdl
@ -81,10 +68,12 @@ update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
AddSnackbar ->
add snackbar model
add model
<| \k -> Snackbar.snackbar ("Snackbar message #" ++ toString k) "UNDO" (Undo k)
AddToast ->
add toast model
add model
<| \k -> Snackbar.toast <| "Toast message #" ++ toString k
Undo k ->
({ model
@ -96,18 +85,19 @@ update action model =
Material.update MDL action' model.mdl
|> map1st (\m -> { model | mdl = m })
-- VIEW
addSnackbar : Button.Instance Mdl Action
addSnackbar =
addSnackbarButton : Button.Instance Mdl Action
addSnackbarButton =
Button.instance 0 MDL
Button.raised (Button.model True)
[ Button.fwdClick AddSnackbar ]
addToast : Button.Instance Mdl Action
addToast =
addToastButton : Button.Instance Mdl Action
addToastButton =
Button.instance 1 MDL
Button.raised (Button.model True)
[ Button.fwdClick AddToast ]
@ -115,8 +105,7 @@ addToast =
snackbarComponent : Snackbar.Instance Mdl Action
snackbarComponent =
Snackbar.instance 2 MDL Snackbar.model
Snackbar.instance MDL Snackbar.model
clickView : Model -> Int -> Html
@ -164,11 +153,11 @@ view addr model =
-- to add css/classes to top-level element of components (div
-- in grid, button in button, div in textfield etc.)
[ cell [ size All 2, size Phone 2, align Top ]
[ addToast.view addr model.mdl [] [ text "Toast" ]
[ addToastButton.view addr model.mdl [] [ text "Toast" ]
]
, cell
[ size All 2, size Phone 2, align Top ]
[ addSnackbar.view addr model.mdl [] [ text "Snackbar" ]
[ addSnackbarButton.view addr model.mdl [] [ text "Snackbar" ]
]
, cell
[ size Desktop 7, size Tablet 3, size Phone 12, align Top ]

View file

@ -15,27 +15,28 @@ for a live demo.
# Component model
The component model of the library is simply the Elm Architecture, e.g.,
each component has Model, Action, view, and update. A minimal example using
this library in plain Elm Architecture can be found
[here](https://github.com/debois/elm-mdl/blob/master/examples/Component-EA.elm).
The component model of the library is simply the Elm Architecture (TEA), i.e.,
each component has types `Model` and `Action`, and values `view` and `update`. A
minimal example using this library in plain TEA can be found
[here](https://github.com/debois/elm-mdl/blob/master/examples/Component-TEA.elm).
Nesting large amounts of components in the Elm Architecture is somewhat
unwieldy because of the large amount of boilerplate one has to write. This
library includes "component support", for getting rid of most of that
boilerplate. A minimal example using component support is
Using more than a few component in plain TEA is unwieldy because of the large
amount of boilerplate one has to write. This library provides the "component
support" for getting rid of most of that boilerplate. A minimal example using
component support is
[here](http://github.com/debois/elm-mdl/blob/master/examples/Component.elm).
It is important to note that component support lives __within__ the Elm
architecture; it is not an alternative architecture.
It is important to note that component support lives __within__ TEA;
it is not an alternative architecture.
# Getting started
The easiest way to get started is to start with one of the minimal examples above.
We recommend going with the library's component support rather than working
directly in plain Elm Architecture.
We recommend going with the library's
[component support](http://github.com/debois/elm-mdl/blob/master/examples/Component.elm)
rather than working directly in plain Elm Architecture.
# This module
# Component Support
This module contains only convenience functions for working with nested
components in the Elm architecture. A minimal example using this library
@ -49,6 +50,7 @@ All examples in this subsection is from the
Here is how you use component support in general. First, boilerplate.
1. Include `Material`:
`import Material`
2. Add a model container Material components to your model:
@ -80,7 +82,6 @@ Here is how you use component support in general. First, boilerplate.
in
( { model | mdl = mdl' } , fx )
Next, make the component instances you need. Do this in the View section of your
source file. Let's say you need a textfield for name entry, and you'd like to
be notifed whenever the field changes value through your own NameChanged action:
@ -106,8 +107,8 @@ be notifed whenever the field changes value through your own NameChanged action:
nameInput : Textfield.Instance Material.Model Action
nameInput =
Textfield.instance 2 MDL Textfield.model
[ Textfield.fwdInput NameChanged ]
[ Textfield.fwdInput NameChanged
]
view addr model =
...
@ -125,13 +126,13 @@ but now it's not boilerplate, its "business logic".)
Using this module will force all elm-mdl components to be built and included in
your application. If this is unacceptable, you can custom-build a version of this
module that uses only the components you need. To do so, you need to re-implement
the present module, modifying the values `model` and `Model`. The module source
can be found
the present module, modifying the values `model` and `Model` by commenting out the
components you are not using. The module source can be found
[here](https://github.com/debois/elm-mdl/blob/master/src/Material.elm).
You do not need to re-build the entire elm-mdl library; simply copy the
source of this module, give it a new name, modify as itMatendicated above, then use
your modified module rather than this one.
source of this module, give it a new name, modify as it as indicated above,
then use your modified module rather than this one.
@docs Model, model, Action, update
-}
@ -145,37 +146,39 @@ import Material.Snackbar as Snackbar
import Material.Component as Component exposing (Indexed)
{-| Model encompassing all Material components.
{-| Model encompassing all Material components. Since some components store
user actions in their model (notably Snackbar), the model is generic in the
type of such "observations".
-}
type alias Model a =
type alias Model obs =
{ button : Indexed Button.Model
, textfield : Indexed Textfield.Model
, snackbar : Indexed (Snackbar.Model a)
, snackbar : Maybe (Snackbar.Model obs)
}
{-| Initial model.
-}
model : Model a
model : Model obs
model =
{ button = Dict.empty
, textfield = Dict.empty
, snackbar = Dict.empty
, snackbar = Nothing
}
{-| Action encompassing actions of all Material components.
-}
type alias Action action =
Component.Action (Model action) action
type alias Action obs =
Component.Action (Model obs) obs
{-| Update function for the above Action.
-}
update :
(Action action -> action)
-> Action action
-> Model action
-> (Model action, Effects action)
(Action obs -> obs)
-> Action obs
-> Model obs
-> (Model obs, Effects obs)
update =
Component.update

View file

@ -1,16 +1,15 @@
module Material.Component
( Embedding, Observer
( embed, embedIndexed, Embedding, Observer
, Indexed
, Instance, instance
, Instance, instance, instance1
, update
, Action
) where
{-|
The Elm Architecture is conceptually very nice, but it forces us
to write large amounts of boilerplate whenever we need to use a "component".
We must:
The Elm Architecture is conceptually very nice, but it forces us to write large
amounts of boilerplate whenever we need to use a "component". We must:
1. Retain the state of the component in our Model
2. Add the components actions to our Action
@ -28,35 +27,41 @@ produce `instance` functions; if you are using elm-mdl (and are not interested i
optimising for compiled program size), you should ignore this module and look
instead at `Material`.
# Component types
@docs Indexed, Embedding, Observer, Instance
# Embeddings
@docs Indexed, Embedding, embed, embedIndexed
# Instance construction
@docs instance
@docs Action, Instance, Observer, instance, instance1
# Instance consumption
@docs update, Action
@docs update
-}
import Effects exposing (Effects)
import Task
import Dict exposing (Dict)
import Material.Helpers exposing (map1, map2, map1st, map2nd, Update, Update')
-- TYPES
{-| Standard EA view function type.
-}
type alias View model action a =
Signal.Address action -> model -> a
-- EMBEDDING MODELS
{-| Indexed families of things.
-}
type alias Indexed a =
@ -65,7 +70,7 @@ type alias Indexed a =
{-| An __embedding__ of an Elm Architecture component is a variant in which
view and update functions know how to extract and update their model
from a larger container model.
from a larger master model.
-}
type alias Embedding model container action a =
{ view : View container action a
@ -78,6 +83,14 @@ type alias Embedding model container action a =
{-| Embed a component. Third and fourth arguments are a getter (extract the
local model from the container) and a setter (update local model in the
container).
It is instructive to compare the types of the view and update function in
the input and output:
{- Input -} {- Output -}
View model action a View container action a
Update model action Update container action
-}
embed :
View model action a -> -- Given a view function,
@ -98,11 +111,9 @@ embed view update get set =
}
{-| We are interested in particular embeddings where components of the same type
all have their state living inside a shared `Dict`; the individual component
has an id used for looking up its own state. Its the responsibility of the user
to make
sure that ids are unique.
{-| We are interested in particular embeddings where components of the same
type all have their state living inside a shared `Dict`; the individual
component has a key used to look up its own state.
-}
embedIndexed :
View model action a -> -- Given a view function,
@ -128,37 +139,90 @@ embedIndexed view update get set model0 id =
-- LIFTING ACTIONS
{-| Generic MDL Action.
{-| Similarly to how embeddings enable collecting models of different type
in a single model container, we need to collect actions in a single "master
action" type. Obviously, actions need to be eventually executed by running
the corresponding update function. To avoid this master action type explicitly
representing the Action/update pairs of elm-mdl components, we represent an
action of an individual component as a partially applied update function; that
is, a function `container -> container`. E.g., the `Click` action of Button is
conceptually represented as:
embeddedButton : Embedding Button.Model container action ...
embeddedButton =
embedIndexed
Button.view Button.update .button {\m x -> {m|button=x} Button.model 0
clickAction : container -> container
clickAction = embeddedButton.update Button.click
When all Material components are embedded in the same `container` model, we
then have a uniform update mechanism.
We lost the ability to inspect the action when we did this, though. To be
able to react to some actions of a component, we add to our `container ->
container` type for actions a potential __observation__ of type `obs`.
In practice, this observation type `obs` will be the Action of the TEA
component __hosting__ MDL components.
Altogether, accounting also for effects, we arrive at the following type.
-}
type Action model obs =
A (model -> (model, Effects (Action model obs), Maybe obs))
type Action container obs =
A (container -> (container, Effects (Action container obs), Maybe obs))
{-| Type of observers, i.e., functions that take an actual action of the
underlying TEA component to an observation. E.g., Button has an Observer for
its `Click` action.
-}
type alias Observer action obs =
action -> Maybe obs
{-| Generic update function for Action.
-}
update :
(Action state action -> action) ->
Update' state (Action state action) action
(Action container obs -> obs) ->
Update' container (Action container obs) obs
update fwd (A f) state =
update fwd (A f) container =
let
(state', fx, obs) =
f state
(container', fx, obs) =
f container
|> map2 (Effects.map fwd)
in
case obs of
Nothing ->
(state', fx)
(container', fx)
Just x ->
(state', Effects.batch [ fx, Effects.tick (always x) ])
(container', Effects.batch [ fx, Effects.task (Task.succeed x) ])
-- INSTANCES
{- EA update function variant where running the function
{-| Type of component instances. A component instance contains a view,
get/set/map for the inner model, and a forwarder lifting component
actions to observations.
-}
type alias Instance model container action obs a =
{ view : View container obs a
, get : container -> model
, set : model -> container -> container
, map : (model -> model) -> container -> container
, fwd : action -> obs
}
{- TEA update function variant where running the function
produces not just a new model and an effect, but also an
observation.
-}
@ -166,34 +230,13 @@ type alias Step model action obs =
action -> model -> (model, Effects action, Maybe obs)
{-| 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 subaction action a =
{ view : View model action a
, get : model -> submodel
, set : submodel -> model -> model
, map : (submodel -> submodel) -> model -> model
, fwd : subaction -> action
}
{- Partially apply a step function to an action,
producing a generic Action.
{- Partially apply a step 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)))
{-| Type of observers.
-}
type alias Observer action obs =
action -> Maybe 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.
@ -216,20 +259,22 @@ pick f xs =
x -> x
connect : List (Observer subaction action) -> Observer subaction action
{- Promote a list of Observers to a single Observer by picking, for a given
action, the first one that succeeds.
-}
connect : List (Observer action obs) -> Observer action obs
connect observers subaction =
pick ((|>) subaction) observers
{-| Given a lifting function, a list of observers and an embedding, construct an
Instance. Notice that the Instance forgets the type parameter `subaction`.
Instance.
-}
instance' :
(Action model action -> action) ->
List (Observer subaction action) ->
Embedding submodel model subaction a ->
Instance submodel model subaction action a
instance'
: (Action container obs -> obs)
-> List (Observer action obs)
-> Embedding model container action a
-> Instance model container action obs a
instance' lift observers embedding =
let
fwd =
@ -249,19 +294,28 @@ instance' lift observers embedding =
}
{-| It is helpful to see parameter names:
instance view update get set id lift model0 observers =
...
Convert a regular Elm Architecture component (view, update) to a component
which knows how to access its state in a generic container model (get, set),
and which dispatches generic Action updates, lifted to the consumers action
type (lift). You can react to actions in custom way by providing observers
(observers). You must also provide an initial model (model0) and an identifier
for the instance (id). The identifier must be unique for all instances of the
same type stored in the same model (rule of thumb: if they are in the same
file, they need distinct ids.)
Convert a regular Elm Architecture component (`view`, `update`) to a component
which knows how to access its model inside a generic container model (`get`,
`set`), and which dispatches generic `Action` updates, lifted to the consumers
action type `obs` (`lift`). You can react to actions in custom way by providing
observers (`observers`). You must also provide an initial model (`model0`) and an
identifier for the instance (`id`). The identifier must be unique for all
instances of the same type stored in the same model (overapproximating rule of
thumb: if they are in the same file, they need distinct ids.)
Its instructive to compare the types of the input and output views:
{- Input -} {- Output -}
View model action a View container obs a
That is, this function fully converts a view from its own `model` and `action`
to the master `container` model and `observation` action.
-}
instance
: View model action a
@ -269,11 +323,29 @@ instance
-> (container -> Indexed model)
-> (Indexed model -> container -> container)
-> Int
-> (Action container observation -> observation)
-> (Action container obs -> obs)
-> model
-> List (Observer action observation)
-> Instance model container action observation a
-> List (Observer action obs)
-> Instance model container action obs a
instance view update get set id lift model0 observers =
embedIndexed view update get set model0 id
|> instance' lift observers
{-| Variant of `instance` for components that are naturally singletons
(e.g., snackbar, layout).
-}
instance1
: View model action a
-> Update model action
-> (container -> Maybe model)
-> (Maybe model -> container -> container)
-> (Action container obs -> obs)
-> model
-> List (Observer action obs)
-> Instance model container action obs a
instance1 view update get set lift model0 observers =
embed view update (get >> Maybe.withDefault model0) (Just >> set)
|> instance' lift observers

View file

@ -281,7 +281,7 @@ view addr model =
{-|
-}
type alias State s obs =
{ s | snackbar : Indexed (Model obs) }
{ s | snackbar : Maybe (Model obs) }
{-|
@ -306,19 +306,16 @@ actionObserver action =
Nothing
{-| Component instance.
-}
instance :
Int
-> (Component.Action (State state obs) obs -> obs)
instance
: (Component.Action (State state obs) obs -> obs)
-> (Model obs)
-> Instance (State state obs) obs
instance id lift model0 =
Component.instance
view update .snackbar (\x y -> {y | snackbar = x}) id lift model0 [ actionObserver ]
instance lift model0 =
Component.instance1
view update .snackbar (\x y -> {y | snackbar = x}) lift model0 [ actionObserver ]
{-|
TODO
@ -334,4 +331,3 @@ add contents inst model =
update (Add contents) (inst.get model)
in
(inst.set sb model, Effects.map inst.fwd fx)