Handling events
Perhaps the most important field in the application record
type Application model message = {
model :: model
view :: model -> Html message,
update :: Update model message,
subscribe :: Array (Subscription message)
}
is the update function. So far we have been using the Update type alias. Let’s expand it:
type Application model message = {
model :: model,
view :: model -> Html message,
update :: model -> message -> Tuple model (Array (Aff (Maybe message))),
subscribe :: Array (Subscription message)
}
That means that update returns an updated model but also an array of side effects to perform. Each entry in the array may optionally raise another message, which is in turn handled by update as well.
Consider an application to roll dices
type Model = Maybe Int
data Message = Roll | Update Int
update :: Model -> Message -> Tuple Model (Array (Aff (Maybe Message)))
update model = case _ of
Roll -> model /\ [ rollDice ]
Update int -> Just int /\ []
where rollDice = do
n <- EC.liftEffect $ ER.randomInt 1 6
pure <<< Just $ Update n
view :: Model -> Html Message
view model = HE.main [HA.id "main"] [
HE.text $ show model,
HE.button [HA.onClick Roll] [HE.text "Roll"]
]
Roll returns the model as it is. However, generating random numbers is a side effect so we return it on our array. Flame will run this effect and raise Update, which then updates the model with the die number.
Likewise, we could perform some network requests with a loading screen
type Model = {
response :: String,
isLoading :: Boolean
}
data Message =
Perform |
Response String |
DifferentResponse String |
Finish String
fetch :: String -> Aff String
fetch url = ...
useResponse :: Model -> String -> Aff Model
useResponse = ...
useDifferentResponse :: Model -> String -> Aff Model
useDifferentResponse = ...
update :: Model -> Message -> Tuple Model (Array (Aff (Maybe Message)))
update model = case _ of
Perform -> model { isLoading = true } /\ requests
Response contents -> F.noMessages $ useResponse model contents -- noMessages is the same as _ /\ []
Finish contents -> F.noMessages model { isLoading = false, response = model.response <> contents }
where requests = [
Just <<< Response <$> fetch "url",
Just <<< DifferentResponse <$> fetch "url2",
Just <<< Response <$> fetch "url3",
Just <<< DifferentResponse <$> fetch "url4",
pure <<< Just $ Finish "Performed all"
]
view :: Model -> Html Message
view model = HE.main [HA.id "main"] [
HE.button [HA.disabled model.isLoading, HA.onClick Perform] [HE.text "Perform requests"],
if model.isLoading then
HE.div [HA.class' "overlay"] [HE.text "Loading..."]
else
...
]
Here for Perform, we return an array of network calls and a final Finish message. The effects are run in order, and once we have a response their events are raised for update as well.
You may be wondering: why separate model updating and side effects? The reason is that in this way we are “forced” to keep most of our business logic in pure functions, which are easier to reason and test. Effects become interchangeable, decoupled from what we do with their results.
Subscriptions
More often than not, a real world application needs to handle events that don’t come from the view. These may include events targeting window, document, third party JavaScript components or in some cases messages from other mount points or application code. We can tackle these scenarios in a few different ways:
- External event handlers
When mounting the application, subscribe can be used to specify messages as a list of subscriptions. The modules under Flame.Subscription define on event handlers similar to those used in views
F.mount_ (QuerySelector "body") {
...
subscribe: [
FSW.onLoad Message, -- `window` event from `Flame.Subscription.Window`
FSD.onClick Message2, -- `document` event from `Flame.Subscription.Document`,
FS.onCustomEvent (EventType "custom") Message3 -- `CustomEvent` with `Flame.Subscription.onCustomEvent`
]
}
The only restriction is that CustomEvent messages payloads must be JSON serializable
onCustomEvent :: forall arg message. UnserializeState arg => EventType -> (arg -> message) -> Subscription message
since they might come from external JavaScript that is not guaranteed to match PureScript data types.
Once a subscription has been defined, the raised message will be handled by the update function as usual.
- Arbitrary message passing
Sometimes, we need to talk to an application from external events handlers or other points in the code away from the mount point. Consider an app that uses web sockets, or a singe page application that uses multiple mount points for lazy loading, or just some initialization events. For these and other use cases, Flame provides a mount (no trailing underscore) function that takes an application id, as well a send function to raise messages for application ids
mount :: forall id model message. Show id => QuerySelector -> AppId id message -> Application model message -> Effect Unit
send :: forall id message. Show id => AppId id message -> message -> Effect Unit
Anything that has a Show instance can be an id, but the application will crash while mounting if the id isn’t unique. That being said, passing message to an application is straightforward
import Flame as F
import Flame(AppId(..))
import Flame.Subscription as FS
data Applications = FirstApp | SecondApp
instance appShow :: Show Applications where
...
data FirstAppMessage = MyFirstMessage
main :: Effect Unit
main = do
-- application id
let id = AppId FirstApp :: AppId Applications FirstAppMessage
-- mount instead of mount_
F.mount (QuerySelector "body") id {
...
}
-- raise a message for FirstApp
FS.send id MyFirstMessage
Then if there is another mount point someplace else in the code, it can still talk to FirstApp. There is no need to persist application ids, as long as the correct type is used
import Flame as F
import Flame.Subscription as FS
import Flame(AppId(..))
data SecondAppMessage = MySecondMessage
main :: Effect Unit
main = do
-- diffent application id
let secondId = AppId SecondApp :: AppId Applications SecondAppMessage
F.mount (QuerySelector "body") secondId {
...
}
-- raise a message for FirstApp again
FS.send (AppId FirstApp) MyFirstMessage -- with type AppId Applications FirstAppMessage
-- raise a message for SecondApp
FS.send secondId MySecondMessage
- Broadcasting
Flame also provides a way to “broadcast” CustomEvents for all listeners. Flame.Subscription.Unsafe.CustomEvent provides the following function
broadcast :: forall arg. SerializeState arg => EventType -> arg -> Effect Unit
whose events can be handled with onCustomEvent on your subscribe list. Broadcasting events is considered unsafe as it is user code responsibility to make sure all listeners expect the same message payload.
See the API reference for a complete list of built-in external events. See this test application for a full example of subscriptions.
Structuring applications
Having a single model of the whole application state may to lead to unwieldy code structure. For that reason, JavaScript frameworks such as React encourage the use of “components”, i.e, (reusable) units of code with isolated state and business logic. This in turn necessitates some way to keep the state in sync for all moving parts.
In a purely functional language like PureScript, however, we have most of benefits of components (and libraries like Redux) built in. Views or business logic code can be easily broken down into modules. Abstractions and general functions promote composition and reuse. Events from your view, as opposed to messages passed down between hierarchies of components, are also easier to follow and modify. Because of that, splitting a application into smaller ones brings an overhead of type mappings/communication without much of benefits supposed by more imperative frameworks.