从榆树中的子组件发送信号
sending signal from subcomponent in elm
我正在用 Elm 开发一个小应用程序。它在屏幕上显示一个计时器,当计时器归零时,它会播放声音。我无法弄清楚如何从计时器向声音播放器发送消息(?)。
在架构上,我有三个模块:代表计时器的 Clock
模块,可以播放音频的 PlayAudio
模块,以及将 Clock
模块和 PlayAudio
模块。
理想情况下,当时钟到达零时,我想做一些事情,比如从 Clock
模块发送信号。当时钟到达零时,Clock
将向 Main
发送信号,后者将其转发到 PlayAudio
.
但是,通过阅读 Elm 文档,似乎不鼓励使用 Main
以外的任何方式处理信号。所以这引出了我的第一个问题。对这种状态变化建模的好方法是什么? update
是否应该从 Clock
return 函数结束? (这就是我在下面的做法,但我非常愿意接受有关如何做得更好的建议。)
我的第二个问题是关于如何播放声音。我将使用原始 Javascript 来播放声音,我相信这意味着我必须使用端口。但是,我不确定 如何从我的子模块 PlayAudio
.
中与 Main
中定义的端口进行交互
下面是我使用的代码。
Clock.elm
:
module Clock (Model, init, Action, signal, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type ClockState = Running | Ended
type alias Model =
{ time: Time
, state: ClockState
}
init : Time -> Model
init initialTime =
{ time = initialTime
, state = Running
}
-- UPDATE
type Action = Tick Time
update : Action -> Model -> (Model, Bool)
update action model =
case action of
Tick tickTime ->
let hasEnded = model.time <= 1
newModel = { model | time <-
if hasEnded then 0 else model.time - tickTime
, state <-
if hasEnded then Ended else Running }
in (newModel, hasEnded)
-- VIEW
view : Model -> Html
view model =
div []
[ (toString model.time ++ toString model.state) |> text ]
signal : Signal Action
signal = Signal.map (always (1 * second) >> Tick) (every second)
PlaySound.elm
:
module PlaySound (Model, init, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type alias Model =
{ playing: Bool
}
init : Model
init =
{ playing = False
}
-- UPDATE
update : Bool -> Model -> Model
update shouldPlay model =
{ model | playing <- shouldPlay }
-- VIEW
view : Model -> Html
view model =
let node = if model.playing
then audio [ src "sounds/bell.wav"
, id "audiotag" ]
[]
else text "Not Playing"
in div [] [node]
Main.elm
:
module Main where
import Debug (..)
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Html.Lazy (lazy, lazy2)
import Json.Decode as Json
import List
import LocalChannel as LC
import Maybe
import Signal
import String
import Time (..)
import Window
import Clock
import PlaySound
---- MODEL ----
-- The full application state of our todo app.
type alias Model =
{ clock : Clock.Model
, player : PlaySound.Model
}
emptyModel : Model
emptyModel =
{ clock = 10 * second |> Clock.init
, player = PlaySound.init
}
---- UPDATE ----
type Action
= NoOp
| ClockAction Clock.Action
-- How we update our Model on a given Action?
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
ClockAction clockAction ->
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in { model | clock <- newClock
, player <- newPlaySound }
---- VIEW ----
view : Model -> Html
view model =
let context = Clock.Context (LC.create ClockAction actionChannel)
in div [ ]
[ Clock.view context model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map view model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel allSignals
allSignals : Signal Action
allSignals = Signal.mergeMany
[ Signal.map ClockAction Clock.signal
, Signal.subscribe actionChannel
]
initialModel : Model
initialModel = emptyModel
-- updates from user input
actionChannel : Signal.Channel Action
actionChannel = Signal.channel NoOp
port playSound : Signal ()
port playSound = ???
index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="js/elm.js" type="text/javascript"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script type="text/javascript">
var todomvc = Elm.fullscreen(Elm.Main);
todomvc.ports.playSound.subscribe(function() {
setTimeout(function() {
document.getElementById('audiotag').play();
}, 50);
});
</script>
</body>
</html>
这种方法看起来非常有原则,并且符合 Elm Architecture post. In that document at the end is a section called One last pattern 的指导方针,这正是您所做的:如果您需要从您的组件向另一个组件发送信号,让更新函数返回一对.
所以我认为你做得对。当然,在这么小的应用程序中如此严格地遵循这种架构确实会增加 boilerplate/relevant 代码率。
无论如何,您唯一需要做的更改是 Main.elm
。您实际上不需要 Channel
将消息从子组件发送到 Main,因为 Main 启动组件并将更新函数连接在一起。所以你可以只使用组件更新功能的额外输出,并将其从模型信号中分离出来,进入端口。
---- UPDATE ----
-- How we update our Model on a given Action?
update : Clock.Action -> Model -> (Model, Bool)
update clockAction model =
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in ( { model | clock <- newClock
, player <- newPlaySound }, hasEnded)
---- VIEW ----
view : Model -> Html
view model =
div [ ]
[ Clock.view model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map (view << fst) model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel Clock.signal
initialModel : Model
initialModel = emptyModel
port playSound : Signal ()
port playSound =
model
|> Signal.map snd
|> Signal.keepIf ((==) True)
|> Signal.map (always ())
最后说明: Elm 0.15 is out,至少会简化您的导入。但更重要的是,在 Elm(无端口)中与 JavaScript 的互操作变得更加容易,因此一旦有人创建了到声音库的绑定,您就应该能够取消该端口。
我正在用 Elm 开发一个小应用程序。它在屏幕上显示一个计时器,当计时器归零时,它会播放声音。我无法弄清楚如何从计时器向声音播放器发送消息(?)。
在架构上,我有三个模块:代表计时器的 Clock
模块,可以播放音频的 PlayAudio
模块,以及将 Clock
模块和 PlayAudio
模块。
理想情况下,当时钟到达零时,我想做一些事情,比如从 Clock
模块发送信号。当时钟到达零时,Clock
将向 Main
发送信号,后者将其转发到 PlayAudio
.
但是,通过阅读 Elm 文档,似乎不鼓励使用 Main
以外的任何方式处理信号。所以这引出了我的第一个问题。对这种状态变化建模的好方法是什么? update
是否应该从 Clock
return 函数结束? (这就是我在下面的做法,但我非常愿意接受有关如何做得更好的建议。)
我的第二个问题是关于如何播放声音。我将使用原始 Javascript 来播放声音,我相信这意味着我必须使用端口。但是,我不确定 如何从我的子模块 PlayAudio
.
Main
中定义的端口进行交互
下面是我使用的代码。
Clock.elm
:
module Clock (Model, init, Action, signal, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type ClockState = Running | Ended
type alias Model =
{ time: Time
, state: ClockState
}
init : Time -> Model
init initialTime =
{ time = initialTime
, state = Running
}
-- UPDATE
type Action = Tick Time
update : Action -> Model -> (Model, Bool)
update action model =
case action of
Tick tickTime ->
let hasEnded = model.time <= 1
newModel = { model | time <-
if hasEnded then 0 else model.time - tickTime
, state <-
if hasEnded then Ended else Running }
in (newModel, hasEnded)
-- VIEW
view : Model -> Html
view model =
div []
[ (toString model.time ++ toString model.state) |> text ]
signal : Signal Action
signal = Signal.map (always (1 * second) >> Tick) (every second)
PlaySound.elm
:
module PlaySound (Model, init, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type alias Model =
{ playing: Bool
}
init : Model
init =
{ playing = False
}
-- UPDATE
update : Bool -> Model -> Model
update shouldPlay model =
{ model | playing <- shouldPlay }
-- VIEW
view : Model -> Html
view model =
let node = if model.playing
then audio [ src "sounds/bell.wav"
, id "audiotag" ]
[]
else text "Not Playing"
in div [] [node]
Main.elm
:
module Main where
import Debug (..)
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Html.Lazy (lazy, lazy2)
import Json.Decode as Json
import List
import LocalChannel as LC
import Maybe
import Signal
import String
import Time (..)
import Window
import Clock
import PlaySound
---- MODEL ----
-- The full application state of our todo app.
type alias Model =
{ clock : Clock.Model
, player : PlaySound.Model
}
emptyModel : Model
emptyModel =
{ clock = 10 * second |> Clock.init
, player = PlaySound.init
}
---- UPDATE ----
type Action
= NoOp
| ClockAction Clock.Action
-- How we update our Model on a given Action?
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
ClockAction clockAction ->
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in { model | clock <- newClock
, player <- newPlaySound }
---- VIEW ----
view : Model -> Html
view model =
let context = Clock.Context (LC.create ClockAction actionChannel)
in div [ ]
[ Clock.view context model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map view model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel allSignals
allSignals : Signal Action
allSignals = Signal.mergeMany
[ Signal.map ClockAction Clock.signal
, Signal.subscribe actionChannel
]
initialModel : Model
initialModel = emptyModel
-- updates from user input
actionChannel : Signal.Channel Action
actionChannel = Signal.channel NoOp
port playSound : Signal ()
port playSound = ???
index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="js/elm.js" type="text/javascript"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script type="text/javascript">
var todomvc = Elm.fullscreen(Elm.Main);
todomvc.ports.playSound.subscribe(function() {
setTimeout(function() {
document.getElementById('audiotag').play();
}, 50);
});
</script>
</body>
</html>
这种方法看起来非常有原则,并且符合 Elm Architecture post. In that document at the end is a section called One last pattern 的指导方针,这正是您所做的:如果您需要从您的组件向另一个组件发送信号,让更新函数返回一对.
所以我认为你做得对。当然,在这么小的应用程序中如此严格地遵循这种架构确实会增加 boilerplate/relevant 代码率。
无论如何,您唯一需要做的更改是 Main.elm
。您实际上不需要 Channel
将消息从子组件发送到 Main,因为 Main 启动组件并将更新函数连接在一起。所以你可以只使用组件更新功能的额外输出,并将其从模型信号中分离出来,进入端口。
---- UPDATE ----
-- How we update our Model on a given Action?
update : Clock.Action -> Model -> (Model, Bool)
update clockAction model =
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in ( { model | clock <- newClock
, player <- newPlaySound }, hasEnded)
---- VIEW ----
view : Model -> Html
view model =
div [ ]
[ Clock.view model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map (view << fst) model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel Clock.signal
initialModel : Model
initialModel = emptyModel
port playSound : Signal ()
port playSound =
model
|> Signal.map snd
|> Signal.keepIf ((==) True)
|> Signal.map (always ())
最后说明: Elm 0.15 is out,至少会简化您的导入。但更重要的是,在 Elm(无端口)中与 JavaScript 的互操作变得更加容易,因此一旦有人创建了到声音库的绑定,您就应该能够取消该端口。