Stateful Chapters
Sometimes it's useful to display a complex component so people can understand how it works on an isolated environment, not only see their possible static states.
But how to accomplish this with Elm's static typing? Simply provide your own custom model that can be used and updated by your own components across all your chapters.
The Shared Model
Each book has one "shared state" and it's your job to slice in the way that makes sense for your book.
Lets say we have two stateful chapters, one with an input and the other one with a counter.
Let's first define a simple model for the InputChapter
:
module InputChapter exposing (Model, init)
type alias Model =
{ value : String
}
init : Model
init =
{ value = ""
}
Then another even simpler one for the CounterChapter
:
module CounterChapter exposing (Model, init)
type alias Model = Int
init : Model
init = 0
Then, on your book, you put everything together like this:
module Book exposing (main)
import ElmBook exposing (..)
import ElmBook.StatefulOptions
import CounterChapter
import InputChapter
type alias SharedState =
{ inputModel : InputChapter.Model
, counterModel : CounterChapter.Model
}
initialState : SharedState
initialState =
{ inputModel = InputChapter.init
, counterModel = CounterChapter.init
}
main : Book SharedState
main =
book "Stateful Book"
|> withStatefulOptions
[ ElmBook.StatefulOptions.initialState initialState
]
|> withChapters [
CounterChapter.chapter,
InputChapter.chapter
]
Using the Shared State
By now we have our shared state properly setup, but how can we actually use that on our chapters? Let's see!
You need to tell your chapter where its state is located in your book. You also need to tell it how to update that state. It's pretty simple:
module CounterChapter exposing (Model, init, chapter)
import ElmBook.Chapter exposing (Chapter, chapter, renderStatefulComponent)
import ElmBook.Actions exposing (updateState)
type alias Model = Int
type alias SharedState x
= { x | counterModel : Model }
updateSharedState : SharedState x -> SharedState x
updateSharedState x =
{ x | counterModel = x.counterModel + 1 }
chapter : Chapter (SharedState x)
chapter =
chapter "InputChapter"
|> renderStatefulComponent (
\{ counterModel } ->
myCounter
{ value = counterModel.value
, onIncrease = updateState updateSharedState
}
)
The same can be done for the InputChapter. But note that the updateState
is replaced with updateStateWith
now that it receives one parameter.
module InputChapter exposing (Model, init, chapter)
import ElmBook.Chapter exposing (Chapter, chapter, renderStatefulComponent)
import ElmBook.Actions exposing (updateStateWith)
type alias Model = { value : String }
type alias SharedState x
= { x | inputModel : Model }
updateSharedState : Int -> SharedState x -> SharedState x
updateSharedState value x =
{ x | inputModel = { value = value } }
chapter : ElmBook.Chapter (SharedState x)
chapter =
ElmBook.chapter "InputChapter"
|> withStatefulComponent (
\{ inputModel } ->
myInput
{ value = inputModel.value
, onInput = updateStateWith updateSharedState
}
)
Nested Elm Architecture
Sometimes we need to handle components with their own msg, model and update. Well – turns out it can be pretty simple to use that on your elm-book!
module InputChapter exposing (Model, init, chapter)
import ElmBook.Chapter exposing (Chapter, chapter, renderStatefulComponent)
import ElmBook.Actions exposing (mapUpdate)
-- component
type alias Model = { value = String }
type Msg =
UpdateValue String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateValue value ->
{ value = value }
view : Model -> Html Msg
view model =
input [ value model.value, onInput UpdateValue ] []
-- elm-book
chapter : ElmBook.Chapter { x | inputModel : Model }
chapter =
ElmBook.chapter "InputChapter"
|> renderStatefulComponent (
\{ inputModel } ->
view inputModel
|> Html.map (
mapUpdate
{ toState = \state inputModel_ -> { state | inputModel = inputModel_ }
, fromState = \state -> state.inputModel
, update = update
}
)
)