module SamplingStartApp (Config, App, start) where
import Html
import Effects
import StartApp
import Task
{-| SamplingStartApp is much like StartApp but allows sampling an `initParam`
signal when building the initial model. This functionality is represented in
the replacement of the pre-built model and effects `init` config option from
StartApp with a function to create a model and any initial effects based on
values sampled from a new `initParam` signal. Because we won't have a model
right away, we also won't have a view. Hence, the configuration options now
also include a initial placeholder view (`initialView`). Simplarly, lack of
an initial model changes the resulting `model` signal in the resulting app
to a `Maybe Model` valued signal. When we do construct the initial model,
we run any actions that have come in from inputs before the initialization.
This is useful for some inputs but for signals sampled via `initParam`, we
won't want to see stale values. Hence, we introduce a `postInitInputs` list
of action-valued signals that will be ignored until after the initialization
is processed.
-}
type alias Config model action param view =
{ init : param -> (model, Effects.Effects action)
, initParam : Signal.Signal param
, update : action -> model -> (model, Effects.Effects action)
, view : (Signal.Address action) -> model -> view
, initialView : view
, inputs : List (Signal.Signal action)
, postInitInputs : List (Signal.Signal action)
}
type alias App model =
{ html : Signal Html.Html
, tasks : Signal (Task.Task Effects.Never ())
, model : Signal (Maybe model)
}
start : Config model action param Html.Html -> App model
start config =
let
initSignal =
Signal.sampleOn startupMailbox.signal config.initParam
|> Signal.map Init
sendStartup =
Signal.send startupMailbox.address ()
|> Task.toResult
|> Task.map ( \_ -> Nop )
|> Effects.task
startApp = StartApp.start
{ init = ( Waiting [], sendStartup )
, update = updateWrapped config.init config.update
, view = viewWrapped config.view config.initialView
, inputs =
initSignal ::
List.append
(List.map (Signal.map (Forward False)) config.inputs)
(List.map (Signal.map (Forward True)) config.postInitInputs)
}
in
{ html = startApp.html
, tasks = startApp.tasks
, model = startApp.model
|> Signal.map (\wrappedModel ->
case wrappedModel of
Waiting actions -> Nothing
Running model -> Just model)
}
{- The model is either waiting and accumulating actions (in reverse order) or
is running.
-}
type WrappedModel model action
= Waiting (List action)
| Running model
{- We can receive actions to do nothing (used for the task which sends to the
startup mailbox), cause initialization (we should only see this once), or wrap
an action to be forwarded.
-}
type WrappedAction action params
= Nop
| Init params
| Forward Bool action
updateWrapped :
(startParams -> (model, Effects.Effects action))
-> (action -> model -> (model, Effects.Effects action))
-> WrappedAction action startParams
-> WrappedModel model action
-> (WrappedModel model action, Effects.Effects (WrappedAction action startParams))
updateWrapped init update wrappedAction wrappedModel =
case wrappedAction of
Nop -> ( wrappedModel, Effects.none )
Init startParams ->
case wrappedModel of
Waiting actions ->
let
initialState = init startParams
(finalModel, finalEffects) =
actions |> List.foldr (applyUpdate update) initialState
-- fold from right since we add values at the head
in
(Running finalModel, finalEffects |> Effects.map (Forward False))
Running model ->
-- This should never happen!
(wrappedModel, Effects.none)
Forward postInitOnly action ->
case wrappedModel of
Waiting actions ->
if postInitOnly then
(wrappedModel, Effects.none)
else
(Waiting (action :: actions), Effects.none)
Running model ->
let
(newModel, effects) = update action model
in
(Running newModel, effects |> Effects.map (Forward False))
applyUpdate :
(action -> model -> (model, Effects.Effects action))
-> action
-> (model, Effects.Effects action)
-> (model, Effects.Effects action)
applyUpdate update action (incomingModel, incomingEffects) =
let
(newModel, newEffects) = update action incomingModel
in
(newModel, Effects.batch [ incomingEffects, newEffects ] )
viewWrapped :
(Signal.Address action -> model -> view)
-> view
-> (Signal.Address (WrappedAction action params))
-> WrappedModel model action
-> view
viewWrapped view initialView wrappedAddress wrappedModel =
case wrappedModel of
Waiting actions -> initialView
Running model -> view (Signal.forwardTo wrappedAddress (Forward False)) model
startupMailbox : Signal.Mailbox ()
startupMailbox =
Signal.mailbox ()
This then allows code like the following:
app = SamplingStartApp.start
{ init = makeInitialModelAndEffectsForWindowWidth -- makeInitialModelAndEffectsForWindowWidth : Int -> (Model, Effects.Effects Action)
, initParam = Window.width
, update = update -- as usual
, view = view -- as usual
, initialView = text "Waiting to initialize..."
, inputs = []
, postInitInputs = [ Window.width |> Signal.map WindowWidth ]
}
-- Now set main and tasks as usual.
Mark