Skip to content

Commit

Permalink
initial commit: first draft of shared state example
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-endress committed Feb 13, 2019
0 parents commit 7d92fdb
Show file tree
Hide file tree
Showing 11 changed files with 578 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
# elm-package generated files
elm-stuff
# elm-repl generated files
repl-temp-*

88 changes: 88 additions & 0 deletions README.md
@@ -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
29 changes: 29 additions & 0 deletions elm.json
@@ -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": {}
}
}
22 changes: 22 additions & 0 deletions src/Errored.elm
@@ -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
169 changes: 169 additions & 0 deletions src/Main.elm
@@ -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
}
55 changes: 55 additions & 0 deletions src/Page/Edit.elm
@@ -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 }
]
9 changes: 9 additions & 0 deletions src/Page/Header.elm
@@ -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))

0 comments on commit 7d92fdb

Please sign in to comment.