Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial commit: first draft of shared state example
- Loading branch information
0 parents
commit 7d92fdb
Showing
11 changed files
with
578 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# elm-package generated files | ||
elm-stuff | ||
# elm-repl generated files | ||
repl-temp-* | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Shared State in Elm | ||
|
||
## Introduction | ||
|
||
In Elm, nested or seperate pages may want to share information among each other. | ||
Realizing this comes with several challenges related to consistency and redundancy. | ||
Every submodule could hold relevant information, -> prone to errors. | ||
Common information can be organized across multiple sub-/ modules using a state which is heald by the model of a higher level module, for example the `Main.elm`. | ||
|
||
This example shows how a state of either a single page application or components in a more complex module may be shared among each other. | ||
Individual components can access information stored in the state and mutate the state in a defined way. | ||
|
||
## Previous Solutions | ||
|
||
[Hanhinen](https://github.com/ohanhi) proposed a concept of a shared state which can be found at | ||
[elm-shared-state](https://github.com/ohanhi/elm-shared-state). | ||
In his version, the `SharedState` model holds information which is used by several submodules. | ||
The information is sent to each submodule via the added parameter in the respective functions view and update: | ||
|
||
```elm | ||
view : SharedState -> Model -> Html Msg | ||
``` | ||
|
||
Additionally, the `update` functions of the respective submodules return a `SharedStateUpdate` message which is processed by the `SharedState` module and used to update the state. | ||
This way, the submodules using the state do not have a direct way to manipulate it. | ||
|
||
Therefore, the `update` function now looks like this: | ||
|
||
```elm | ||
update : SharedState -> Msg -> Model -> ( Model, Cmd Msg, SharedStateUpdate ) | ||
``` | ||
|
||
## Description of our approach | ||
|
||
We extended the approach from [Hanhinen](https://github.com/ohanhi) and showed our version with the help of a simple example single page application. | ||
The example site is a simple issue tracker which keeps track of a list of issues. | ||
The list of issues are stored in the shared `State`. | ||
|
||
```elm | ||
type alias State = { list : List String } | ||
``` | ||
|
||
### Modules | ||
|
||
The application is comprised of the following four modules: | ||
|
||
- The `List` module presents an overview of the issues. From here, issues may be added, deleted or edited. | ||
- An issue can be edited on the `Edit` page. | ||
- Issues can be inspected on the `Item` page. | ||
- Additionally, a `Header` shows the current number of issues. | ||
|
||
Every submodule needs information held by the `State`, thus it is passed to respective `view` functions. | ||
|
||
### Update | ||
|
||
The messages of the `Edit` submodule look like the following: | ||
|
||
```elm | ||
type Msg | ||
= SetText String -- Edit the text in the TextField | ||
| GoBack -- Go back to the Overview | ||
| StateMsg State.Msg -- Change the State | ||
``` | ||
The `StateMsg` is of great interest. | ||
It is triggered whenever a change in the `State` is needed. | ||
In our case, clicking the save button triggers the Message `StateMsg <| EditIssue selectIdx text`. | ||
|
||
This message is passed to the following `update` function: | ||
|
||
```elm | ||
update : Key -> Msg -> Model -> ( Model, Cmd Msg, Maybe State.Msg ) | ||
update key msg model = | ||
case msg of | ||
SetText text -> | ||
( { model | text = text }, Cmd.none, Nothing ) | ||
|
||
GoBack -> | ||
( model, Route.pushUrl key Route.List, Nothing ) | ||
|
||
StateMsg stateMsg -> | ||
( model, Cmd.none, Just stateMsg ) | ||
``` | ||
|
||
|
||
--- | ||
- Maybe StateMsg | ||
- No state passed in update | ||
- Introduce StateMsg in each subModule -> smaller update functions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"type": "application", | ||
"source-directories": [ | ||
"src" | ||
], | ||
"elm-version": "0.19.0", | ||
"dependencies": { | ||
"direct": { | ||
"Skinney/murmur3": "2.0.7", | ||
"elm/browser": "1.0.0", | ||
"elm/core": "1.0.0", | ||
"elm/html": "1.0.0", | ||
"elm/http": "1.0.0", | ||
"elm/json": "1.0.0", | ||
"elm/parser": "1.1.0", | ||
"elm/regex": "1.0.0", | ||
"elm/time": "1.0.0", | ||
"elm/url": "1.0.0", | ||
"elm/virtual-dom": "1.0.1", | ||
"elm-community/list-extra": "8.0.0", | ||
"mdgriffith/elm-ui": "1.1.0" | ||
}, | ||
"indirect": {} | ||
}, | ||
"test-dependencies": { | ||
"direct": {}, | ||
"indirect": {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
module Errored exposing (PageLoadError, pageLoadError, view) | ||
|
||
import Browser | ||
import Element exposing (Element, text) | ||
|
||
|
||
type PageLoadError | ||
= PageLoadError Model | ||
|
||
|
||
type alias Model = | ||
{ errorMessage : String } | ||
|
||
|
||
pageLoadError : String -> PageLoadError | ||
pageLoadError errorMessage = | ||
PageLoadError { errorMessage = errorMessage } | ||
|
||
|
||
view : PageLoadError -> Element msg | ||
view (PageLoadError model) = | ||
text model.errorMessage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
module Main exposing (main) | ||
|
||
import Browser | ||
import Browser.Navigation as Navigation | ||
import Element exposing (column, layout, padding, spacing, text) | ||
import Errored exposing (PageLoadError) | ||
import Html exposing (Html) | ||
import Page.Edit | ||
import Page.Header | ||
import Page.Item | ||
import Page.List | ||
import Route exposing (Route) | ||
import State exposing (State) | ||
import Url exposing (Url) | ||
|
||
|
||
type alias Model = | ||
{ page : Page | ||
, currentRoute : Maybe Route | ||
, key : Navigation.Key | ||
, state : State | ||
} | ||
|
||
|
||
type Page | ||
= Edit Page.Edit.Model | ||
| Item Page.Item.Model | ||
| List | ||
| Errored PageLoadError | ||
|
||
|
||
type Msg | ||
= ChangedUrl Url | ||
| ClickedLink Browser.UrlRequest | ||
| SubPage PageMsg | ||
| StateMsg State.Msg | ||
|
||
|
||
type PageMsg | ||
= EditMsg Page.Edit.Msg | ||
| ItemMsg Page.Item.Msg | ||
| ListMsg Page.List.Msg | ||
|
||
|
||
init : () -> Url -> Navigation.Key -> ( Model, Cmd Msg ) | ||
init _ url key = | ||
setRoute (Route.fromUrl url) | ||
{ page = List | ||
, currentRoute = Nothing | ||
, key = key | ||
, state = State.init [ "Issue #1", "Issue #2", "Issue #5" ] | ||
} | ||
|
||
|
||
update : Msg -> Model -> ( Model, Cmd Msg ) | ||
update msg model = | ||
case msg of | ||
SubPage subMsg -> | ||
updatePage model.key subMsg model | ||
|
||
ChangedUrl url -> | ||
setRoute (Route.fromUrl url) model | ||
|
||
a -> | ||
( model, Cmd.none ) | ||
|
||
|
||
updatePage : Navigation.Key -> PageMsg -> Model -> ( Model, Cmd Msg ) | ||
updatePage key msg model = | ||
let | ||
executeStateMsg stateMsg = | ||
stateMsg | ||
|> Maybe.map (\s -> State.update s model.state) | ||
|> Maybe.withDefault model.state | ||
|
||
toPage toModel toMsg subUpdate subMsg subModel = | ||
let | ||
( newModel, newCmd, stateMsg ) = | ||
subUpdate key subMsg subModel | ||
in | ||
( { model | ||
| page = toModel newModel | ||
, state = executeStateMsg stateMsg | ||
} | ||
, Cmd.map (SubPage << toMsg) newCmd | ||
) | ||
in | ||
case ( msg, model.page ) of | ||
( EditMsg subMsg, Edit subModel ) -> | ||
toPage Edit EditMsg Page.Edit.update subMsg subModel | ||
|
||
( ItemMsg subMsg, Item subModel ) -> | ||
toPage Item ItemMsg Page.Item.update subMsg subModel | ||
|
||
( ListMsg subMsg, List ) -> | ||
let | ||
( newCmd, stateMsg ) = | ||
Page.List.update key subMsg | ||
in | ||
( { model | state = executeStateMsg stateMsg } | ||
, Cmd.map (SubPage << ListMsg) newCmd | ||
) | ||
|
||
_ -> | ||
( model, Cmd.none ) | ||
|
||
|
||
view : Model -> Browser.Document Msg | ||
view model = | ||
let | ||
header = | ||
Page.Header.view model.state | ||
|
||
page = | ||
case model.page of | ||
Edit subModel -> | ||
Page.Edit.view subModel | ||
|> Element.map (SubPage << EditMsg) | ||
|
||
Item subModel -> | ||
Page.Item.view model.state subModel | ||
|> Element.map (SubPage << ItemMsg) | ||
|
||
List -> | ||
Page.List.view model.state | ||
|> Element.map (SubPage << ListMsg) | ||
|
||
Errored e -> | ||
Errored.view e | ||
in | ||
Browser.Document "Titel" [ layout [] (column [] [ header, page ]) ] | ||
|
||
|
||
setRoute : Maybe Route -> Model -> ( Model, Cmd Msg ) | ||
setRoute maybeRoute model = | ||
let | ||
transition page = | ||
( { model | page = page, currentRoute = maybeRoute }, Cmd.none ) | ||
in | ||
if model.currentRoute == maybeRoute then | ||
( model, Cmd.none ) | ||
|
||
else | ||
case maybeRoute of | ||
Just route -> | ||
case route of | ||
Route.Edit index -> | ||
transition (Edit (Page.Edit.init model.state index)) | ||
|
||
Route.Item index -> | ||
transition (Item (Page.Item.init index)) | ||
|
||
Route.List -> | ||
transition List | ||
|
||
Nothing -> | ||
( model, Cmd.none ) | ||
|
||
|
||
main : Program () Model Msg | ||
main = | ||
Browser.application | ||
{ init = init | ||
, onUrlChange = ChangedUrl | ||
, onUrlRequest = ClickedLink | ||
, view = view | ||
, update = update | ||
, subscriptions = \a -> Sub.none | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
module Page.Edit exposing (Model, Msg(..), init, update, view) | ||
|
||
import Browser.Navigation as Navigation | ||
import Element exposing (Element, row, text) | ||
import Element.Input as Input exposing (button, labelAbove) | ||
import Route | ||
import State exposing (Msg(..), State) | ||
|
||
|
||
type Msg | ||
= SetText String | ||
| GoBack | ||
| StateMsg State.Msg | ||
|
||
|
||
type alias Model = | ||
{ select : Int | ||
, text : String | ||
} | ||
|
||
|
||
init : State -> Int -> Model | ||
init state select = | ||
{ select = select | ||
, text = State.getIssue state select | ||
} | ||
|
||
|
||
update : Navigation.Key -> Msg -> Model -> ( Model, Cmd Msg, Maybe State.Msg ) | ||
update key msg model = | ||
case msg of | ||
SetText text -> | ||
( { model | text = text }, Cmd.none, Nothing ) | ||
|
||
GoBack -> | ||
( model, Route.pushUrl key Route.List, Nothing ) | ||
|
||
StateMsg stateMsg -> | ||
( model, Cmd.none, Just stateMsg ) | ||
|
||
|
||
view : Model -> Element Msg | ||
view model = | ||
row [] | ||
[ Input.text | ||
[] | ||
{ onChange = SetText | ||
, text = model.text | ||
, placeholder = Nothing | ||
, label = labelAbove [] (text "") | ||
} | ||
, button [] { label = text "edit", onPress = Just (StateMsg (EditIssue model.select model.text)) } | ||
, text " " | ||
, button [] { label = text "back", onPress = Just GoBack } | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module Page.Header exposing (view) | ||
|
||
import Element exposing (Element, text) | ||
import State exposing (State) | ||
|
||
|
||
view : State -> Element msg | ||
view state = | ||
text ("Open Issues: " ++ String.fromInt (List.length state.list)) |
Oops, something went wrong.