Skip to content

Commit

Permalink
Use high-level API elmfire-extra
Browse files Browse the repository at this point in the history
elmfire-extra provides the two modules ElmFire.Dict and ElmFire.Op
Together they greatly simplify the Firebase communication of TodoMVC.

https://github.com/ThomasWeiser/elmfire-extra
http://package.elm-lang.org/packages/ThomasWeiser/elmfire-extra/latest
  • Loading branch information
ThomasWeiser committed Nov 2, 2015
1 parent 2789625 commit eb14652
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 154 deletions.
9 changes: 4 additions & 5 deletions README.md
Expand Up @@ -7,6 +7,7 @@ extending [Evan Czaplicki's](https://twitter.com/czaplic)
[version](https://github.com/evancz/elm-todomvc),
using [Firebase](https://www.firebase.com/)
via [ElmFire](https://github.com/ThomasWeiser/elmfire)
and [elmfire-extra](https://github.com/ThomasWeiser/elmfire-extra)
for storage and real-time collaboration.

## Build Instructions
Expand All @@ -31,13 +32,13 @@ and [evancz/elm-effects](http://package.elm-lang.org/packages/evancz/elm-effects
A sketch of the data flow:

- Inputs are coming from
- Firebase query results
- Firebase changes
- user interaction
- The `model` comprises two parts
- shared persistent state (list of items)
- shared persistent state, mirrored from Firebase by means of `ElmFire.Dict`
- local state (filter settings, intermediate edit state)
- An `update` function takes an input event and the current model, returning
a new model and possibly an effect, i.e. a task to change the Firebase data.
a new model and possibly an effect, i.e. a task to change the Firebase data (using `ElmFire.Op`).
- A `view` function renders the current model as HTML

Please note that content changes made by the user always flow through the Firebase layer.
Expand All @@ -56,8 +57,6 @@ to map these ids to the items' payload.

## Future Work

- ElmFire will provide a means for [auto syncing a Dict](https://github.com/ThomasWeiser/elmfire-extra) with a Firebase object.
We will use it here to simplify the server interop. Beta version in branch [elmfire-extra](https://github.com/ThomasWeiser/todomvc-elmfire/tree/elmfire-extra)
- Explore architectural variations
- Componentize the model: split it into a shared part and a local part
where the local part depends on the shared part but not the other way round.
Expand Down
3 changes: 2 additions & 1 deletion elm-package.json
Expand Up @@ -13,7 +13,8 @@
"evancz/elm-effects": "2.0.0 <= v < 3.0.0",
"evancz/elm-html": "3.0.0 <= v < 5.0.0",
"evancz/start-app": "2.0.1 <= v < 3.0.0",
"ThomasWeiser/elmfire": "1.0.0 <= v < 2.0.0"
"ThomasWeiser/elmfire": "1.0.0 <= v < 2.0.0",
"ThomasWeiser/elmfire-extra": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.15.0 <= v < 0.16.0"
}
206 changes: 58 additions & 148 deletions src/TodoMVC.elm
Expand Up @@ -27,6 +27,8 @@ import Effects exposing (Effects, Never)
import StartApp

import ElmFire
import ElmFire.Dict
import ElmFire.Op

-----------------------------------------------------------------------

Expand All @@ -52,7 +54,7 @@ config =
{ init = (initialModel, initialEffect)
, update = updateState
, view = view
, inputs = [Signal.map FromServer serverInput.signal]
, inputs = [Signal.map FromServer inputItems]
}

app : StartApp.App Model
Expand All @@ -71,18 +73,19 @@ main = app.html
-- - Local State: Filtering and editing

type alias Model =
{ items: Dict Id Item
{ items: Items
, filter: Filter
, addField: Content
, editingItem: EditingItem
}

type alias Items = Dict Id Item
type alias Id = String
type alias Content = String
type alias Item =
{ title: Content
, completed: Bool
}
type alias Content = String

type Filter = All | Active | Completed

Expand All @@ -98,8 +101,8 @@ initialModel =

type Action
= FromGui GuiEvent
| FromServer ServerEvent
| FromEffect EffectEvent
| FromServer Items
| FromEffect -- no actions from effects here

-----------------------------------------------------------------------

Expand All @@ -123,71 +126,42 @@ type alias GuiAddress = Address GuiEvent

-----------------------------------------------------------------------

-- Events originating from firebase responses

type ServerEvent
= NoServerEvent
| Added (Id, Item)
| Changed (Id, Item)
| Removed Id
-- Mirror Firbase's content as the model's items

serverInput : Mailbox ServerEvent
serverInput = mailbox NoServerEvent
-- initialTask : Task Error (Task Error ())
-- inputItems : Signal Items
(initialTask, inputItems) =
ElmFire.Dict.mirror syncConfig

-- Events originating from effect (mostly firebase commands)
-- Actually there are not meanigful return values, so we have only a single value here.

type EffectEvent
= NoEffectEvent

effectInput : Mailbox EffectEvent
effectInput = mailbox NoEffectEvent
initialEffect : Effects Action
initialEffect = initialTask |> kickOff

-----------------------------------------------------------------------

-- Subscribe to firebase events: adding, removing and changing items

initialEffect : Effects Action
initialEffect =
let
snap2task : ((Id, Item) -> ServerEvent) -> ElmFire.Snapshot -> Task () ()
snap2task eventOp =
( \snapshot ->
case decodeItem snapshot.value of
Just item ->
Signal.send
serverInput.address
(eventOp (snapshot.key, item))
Nothing -> Task.fail ()
syncConfig : ElmFire.Dict.Config Item
syncConfig =
{ location = ElmFire.fromUrl firebaseUrl
, orderOptions = ElmFire.noOrder
, encoder =
\item -> JE.object
[ ("title", JE.string item.title)
, ("completed", JE.bool item.completed)
]
, decoder =
( JD.object2 Item
("title" := JD.string)
("completed" := JD.bool)
)
doNothing = \_ -> Task.succeed ()
loc = (ElmFire.fromUrl firebaseUrl)
in
kickOff
( ElmFire.subscribe
(snap2task Added) doNothing (ElmFire.childAdded ElmFire.noOrder) loc
`andThen`
\_ -> ElmFire.subscribe
(snap2task Changed) doNothing (ElmFire.childChanged ElmFire.noOrder) loc
`andThen`
\_ -> ElmFire.subscribe
(snap2task (\(id, _) -> Removed id)) doNothing (ElmFire.childRemoved ElmFire.noOrder) loc
`andThen`
\_ -> Task.succeed ()
)
}

-----------------------------------------------------------------------

decodeItem : JD.Value -> Maybe Item
decodeItem value =
JD.decodeValue decoderItem value |> Result.toMaybe

decoderItem : JD.Decoder Item
decoderItem =
( JD.object2 Item
("title" := JD.string)
("completed" := JD.bool)
)
effectItems : ElmFire.Op.Operation Item -> Effects Action
effectItems operation =
ElmFire.Op.operate
syncConfig
operation
|> kickOff

-----------------------------------------------------------------------

Expand All @@ -196,29 +170,19 @@ decoderItem =
{- Map any task to an effect, discarding any direct result or error value -}
kickOff : Task x a -> Effects Action
kickOff =
Task.toMaybe >> Task.map (always (FromEffect NoEffectEvent)) >> Effects.task
Task.toMaybe >> Task.map (always (FromEffect)) >> Effects.task

updateState : Action -> Model -> (Model, Effects Action)
updateState action model =
case action of

FromEffect _ ->
FromEffect ->
( model
, Effects.none
)

FromServer (Added (id, item)) ->
( { model | items <- Dict.insert id item model.items }
, Effects.none
)

FromServer (Changed (id, item)) ->
( { model | items <- Dict.insert id item model.items }
, Effects.none
)

FromServer (Removed id) ->
( { model | items <- Dict.remove id model.items }
FromServer (items) ->
( { model | items <- items }
, Effects.none
)

Expand All @@ -227,14 +191,8 @@ updateState action model =
, if model.addField == ""
then Effects.none
else
kickOff <|
ElmFire.set
( JE.object
[ ("title", JE.string model.addField)
, ("completed", JE.bool False)
]
)
( ElmFire.fromUrl firebaseUrl |> ElmFire.push )
effectItems <|
ElmFire.Op.push { title = model.addField, completed = False }
)

FromGui (UpdateItem id) ->
Expand All @@ -243,89 +201,41 @@ updateState action model =
Just (id1, title) ->
if (id == id1)
then
kickOff <|
if title == ""
then
ElmFire.remove
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub id )
else
ElmFire.update
( JE.object [ ("title", JE.string title) ] )
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub id )
if title == ""
then
effectItems <| ElmFire.Op.remove id
else
effectItems <| ElmFire.Op.update id
( Maybe.map (\item -> { item | title <- title }) )
else Effects.none
_ -> Effects.none
)

FromGui (DeleteItem id) ->
( model
, kickOff <|
ElmFire.remove
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub id )
, effectItems <| ElmFire.Op.remove id
)

FromGui (DeleteCompletedItems) ->
( model
, Effects.batch <|
List.filterMap
( \(key, itemFromModel) ->
if itemFromModel.completed
then
Just <| kickOff <| ElmFire.transaction
( \maybeItem ->
case maybeItem of
Just itemJson ->
case decodeItem itemJson of
Just itemFromServer ->
if itemFromServer.completed
then ElmFire.Remove
else ElmFire.Abort
Nothing ->
ElmFire.Abort
Nothing ->
ElmFire.Abort
)
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub key)
True
else Nothing
)
(Dict.toList model.items)
, effectItems <|
ElmFire.Op.filter ElmFire.Op.parallel
(\_ item -> not item.completed)
)

FromGui (CheckItem id completed) ->
( model
, kickOff <|
ElmFire.update
( JE.object [ ("completed", JE.bool completed) ] )
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub id )
, effectItems <| ElmFire.Op.update id
( Maybe.map (\item -> { item | completed <- completed }) )
)

FromGui (CheckAllItems completed) ->
( model
, Effects.batch <|
List.map
( \key ->
kickOff <|
ElmFire.transaction
( \maybeItem ->
case maybeItem of
Just itemJson ->
case decodeItem itemJson of
Just item ->
ElmFire.Set
( JE.object
[ ("title", JE.string item.title)
, ("completed", JE.bool completed)
]
)
Nothing ->
ElmFire.Abort
Nothing ->
ElmFire.Abort
)
( ElmFire.fromUrl firebaseUrl |> ElmFire.sub key)
True
)
(Dict.keys model.items)
, effectItems <|
ElmFire.Op.map ElmFire.Op.parallel
(\_ item ->
{ item | completed <- completed }
)
)

FromGui (EditExistingItem e) ->
Expand Down

0 comments on commit eb14652

Please sign in to comment.