diff --git a/examples/Component-EA.elm b/examples/Component-TEA.elm similarity index 100% rename from examples/Component-EA.elm rename to examples/Component-TEA.elm diff --git a/examples/Component.elm b/examples/Component.elm index 5a1624c..d648048 100644 --- a/examples/Component.elm +++ b/examples/Component.elm @@ -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 ] diff --git a/examples/Demo/Snackbar.elm b/examples/Demo/Snackbar.elm index fc73a9f..3c3c2ca 100644 --- a/examples/Demo/Snackbar.elm +++ b/examples/Demo/Snackbar.elm @@ -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 + AddToast -> + 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 ] diff --git a/src/Material.elm b/src/Material.elm index 82a8ae1..388c4c8 100644 --- a/src/Material.elm +++ b/src/Material.elm @@ -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,69 +50,69 @@ All examples in this subsection is from the Here is how you use component support in general. First, boilerplate. 1. Include `Material`: - `import Material` + + `import Material` 2. Add a model container Material components to your model: - type alias Model = - { ... - , mdl : Material.Model - } + type alias Model = + { ... + , mdl : Material.Model + } - model : Model = - { ... - , mdl = Material.model - } + model : Model = + { ... + , mdl = Material.model + } 3. Add an action for Material components. - type Action = - ... - | MDL (Material.Action Action) + type Action = + ... + | MDL (Material.Action Action) 4. Handle that action in your update function as follows: - update action model = - case action of - ... - MDL action' -> - let (mdl', fx) = - Material.update MDL action' model.mdl - in - ( { model | mdl = mdl' } , fx ) - + update action model = + case action of + ... + MDL action' -> + let (mdl', fx) = + Material.update MDL action' model.mdl + 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: - import Material.Textfield as Textfield + import Material.Textfield as Textfield - ... - - type Action = - ... - | NameChanged String - - ... - - update action model = - case action of ... - NameChanged name -> - -- Do whatever you need to do. - ... + type Action = + ... + | NameChanged String - nameInput : Textfield.Instance Material.Model Action - nameInput = - Textfield.instance 2 MDL Textfield.model - [ Textfield.fwdInput NameChanged ] + ... - - view addr model = - ... - nameInput.view addr model.mdl + update action model = + case action of + ... + NameChanged name -> + -- Do whatever you need to do. + + ... + + nameInput : Textfield.Instance Material.Model Action + nameInput = + Textfield.instance 2 MDL Textfield.model + [ Textfield.fwdInput NameChanged + ] + + view addr model = + ... + nameInput.view addr model.mdl The win relative to using plain Elm Architecture is that adding a component @@ -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 - [here](https://github.com/debois/elm-mdl/blob/master/src/Material.elm). +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 diff --git a/src/Material/Component.elm b/src/Material/Component.elm index 99a7407..ab9e1df 100644 --- a/src/Material/Component.elm +++ b/src/Material/Component.elm @@ -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 @@ -26,37 +25,43 @@ This module provides an extensible mechanism for collecting arbitrary a single Action type and update function. The module is used internally to produce `instance` functions; if you are using elm-mdl (and are not interested in optimising for compiled program size), you should ignore this module and look -instead at `Material`. +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,9 +70,9 @@ 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 = +type alias Embedding model container action a = { view : View container action a , update : Update container action , getModel : container -> model @@ -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,31 +294,58 @@ 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 - -> Update model action - -> (container -> Indexed model) - -> (Indexed model -> container -> container) - -> Int - -> (Action container observation -> observation) - -> model - -> List (Observer action observation) - -> Instance model container action observation a + : View model action a + -> Update model action + -> (container -> Indexed model) + -> (Indexed model -> container -> container) + -> Int + -> (Action container obs -> obs) + -> model + -> 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 diff --git a/src/Material/Snackbar.elm b/src/Material/Snackbar.elm index 0a1eba3..6afa30d 100644 --- a/src/Material/Snackbar.elm +++ b/src/Material/Snackbar.elm @@ -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) -