父子组件
Halogen
是一个没有偏见的UI
库:它允许您创建声明式用户界面,而无需强制执行特定架构。
迄今为止,我们的应用程序由单个Halogen
组件组成。您可以将大型应用程序构建为单个组件,并随着应用程序的增长将state
、handleAction
和render
函数分解为单独的模块。这让您可以在Halogen
中使用Elm
架构。
但是,Halogen
支持具有任意深度组件树的架构。这意味着您编写的任何组件都可以包含更多组件,每个组件都有自己的状态和行为. 大多数Halogen
应用程序都以这种方式使用组件架构,包括Real World Halogen应用程序。
当您从单个组件移动到多个组件时,您开始需要机制,以便组件可以相互通信。Halogen
为我们提供了父子组件通信的三种方式:
- 父组件可以向子组件发送查询,要么告诉子组件做某事,要么从子组件请求一些信息。
- 父组件为子组件提供它需要的
input
,每次父组件渲染时都会重新发送。 - 子组件可以向父组件发射
output
消息,在发生重要事件时通知它。
这些类型参数在Component
类型中表示,有些还可以在ComponentHTML
和HalogenM
类型中找到。例如,支持query
、input
和output
消息的组件将具有以下Component
类型:
component :: forall m. H.Component Query Input Output m
您可以将组件与其他组件通信的方式视为其公共接口,而公共接口显示在Component
类型中。
在本章中,我们将了解:
- 如何在你的
Halogen HTML
中render
组件 - 组件通信的三种方式:
query
、input
和output
消息 - 组件槽、
slot
函数和Slot
类型,使这种通信类型安全
我们将首先渲染一个没有query
或output
消息的简单子组件。然后,我们将构建使用这些方式进行通信的组件,最后一个示例展示了同时使用所有这些机制的父组件和子组件.
尝试将示例加载到Try PureScript
中以探索本章中讨论的每个通信机制!
渲染组件
我们通过编写返回Halogen HTML
元素的函数来开始本指南。这些函数可以被其他函数用来构建更大的HTML
元素树。
当我们开始使用组件时,我们开始编写render
函数。从概念上讲,组件通过此函数生成Halogen HTML
并作为其结果,尽管它们还可以维护内部状态和执行effects
等。
事实上,虽然到目前为止我们在编写render
函数时只使用了HTML
元素,但我们也可以使用组件,就好像它们是生成HTML
的函数一样。这个类比是不完美的,但它可以是一个有用的心理模型,用于理解在编写渲染函数时如何处理组件。
当一个组件渲染另一个组件时,它被称为父
组件,它渲染的组件被称为子
组件。
让我们看看如何在我们的render
函数中渲染一个组件,而不是像我们目前看到的那样只渲染HTML
元素。我们将首先编写一个使用辅助函数来渲染按钮的组件。然后,我们将把这个辅助函数变成它自己的组件,我们将调整父组件来渲染这个新的子组件。
首先,我们将编写一个组件,它使用一个辅助函数来渲染一些HTML
:
module Main where
import Prelude
import Halogen as H
import Halogen.HTML as HH
parent :: forall query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
render :: forall state action. state -> H.ComponentHTML action () m
render _ = HH.div_ [ button { label: "Click Me" } ]
button :: forall w i. { label :: String } -> HH.HTML w i
button { label } = HH.button [ ] [ HH.text label ]
这应该看起来很熟悉。我们有一个简单的组件来渲染一个div
,还有一个辅助函数button
,它将一个给定标签渲染为按钮。需要注意的是,我们的parent
组件为我们的state
和action
保留了类型变量,因为它没有内部状态,也没有任何action
。
现在,让我们将我们的button
函数变成一个组件以进行演示(在现实世界的应用程序中,它太小了).
type Input = { label :: String }
type State = { label :: String }
button :: forall query output m. H.Component query Input output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: Input -> State
initialState input = input
render :: forall action. State -> H.ComponentHTML action () m
render { label } = HH.button [ ] [ HH.text label ]
我们采取了几个步骤将我们的button HTML
函数转换为button
组件:
- 我们将辅助函数的参数转换为组件的
Input
类型。父组件负责将此input
提供给我们的组件。我们将在下一节中了解有关input
的更多信息。 - 我们将
HTML
移动到组件的render
函数中。render
函数只能访问我们组件的State
类型,所以在我们的initialState
函数中,我们将输入值复制到我们的state
中,以便我们可以渲染它。将input
复制到state
是Halogen
中的常见模式。另请注意,我们的render
函数未指定action
类型(因为我们没有任何action
)并使用()
指示我们没有子组件。 - 我们使用
defaultEval
,未修改,作为我们的EvalSpec
,因为这个组件不需要响应内部发生的事件 —— 例如,它没有action
,也没有使用生命周期事件。
不过,我们的父组件现在坏了!如果您一直在关注,您现在会看到一个错误:
[1/1 TypesDoNotUnify]
16 render _ = HH.div_ [ button { label: "Click Me" } ]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Could not match type
Component HTML t2 { label :: String }
with type
Function
不能仅仅通过将组件的input
作为函数参数来渲染组件。即使组件生成普通的Halogen HTML
,它们也可以与父组件通信;出于这个原因,组件需要额外的信息才能像普通元素一样渲染。
从概念上讲,组件在HTML
树中占据一个slot
。这个插槽是组件可以生成Halogen HTML
的地方,直到它从DOM
中被移除。插槽中的组件可以被认为是一个动态的、有状态的HTML
元素。您可以将这些动态元素与普通Halogen HTML
元素自由混合,但动态元素需要更多信息。
这些额外的信息来自ComponentHTML
中使用的slot
函数和Slot
类型,到目前为止,我们将它们留作空行, ()
。稍后我们将更多地讨论在插槽中渲染组件,但现在让我们开始编译。
我们可以通过slot
函数在slot
中渲染我们的组件来修复我们的render
函数。我们还将更新ComponentHTML
中的slot
类型,以包含我们现在必须支持的Halogen HTML
组件。此差异演示了渲染HTML
元素和渲染
组件之间的差异:
+ import Type.Proxy (Proxy(..))
+
+ type Slots = ( button :: forall query. H.Slot query Void Int )
+
+ _button = Proxy :: Proxy "button"
parent :: forall query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
- render :: forall state action. state -> H.ComponentHTML action () m
+ render :: forall state action. state -> H.ComponentHTML action Slots m
render _ =
- HH.div_ [ button { label: "Click Me" } ]
+ HH.div_ [ HH.slot_ _button 0 button { label: "Click Me" } ]
我们的父组件现在正在渲染一个子组件 —— 我们的按钮组件。渲染一个组件引入了两个大的变化:
- 我们使用了
slot_
函数来渲染组件,它带有几个我们还没有探索过的参数。其中两个参数是button
组件本身和它需要作为input
的标签。 - 我们添加了一个名为
Slots
的新类型,这是一个row
包含我们的button
组件的标签,其值为H.Slot
,我们在ComponentHTML
中使用了这个新类型,而不是我们之前看到的空行()
。
slot
和slot_
函数以及Slot
类型允许您在Halogen HTML
中渲染一个stateful
、effectful
的子组件,就像它是任何其他HTML
元素一样。但是为什么有这么多参数和类型涉及到这样做呢?为什么我们不能用它的input
调用button
?
答案是Halogen
为父子组件提供了两种相互通信的方式,我们需要确保这种通信是类型安全的。slot
函数允许我们:
- 决定如何通过标签(类型级别的字符串
button
,我们在术语级别用代理Proxy :: Proxy "button"
表示)和唯一标识符来标识特定组件(在本例中为整数0
)以便我们可以向它发送查询。这是父组件与子组件之间必不可少的沟通形式。 - 渲染组件(
button
)并为其提供input
({ label: "Click Me" }
),每次父组件渲染时都会重新发送,以防input
随时间发生变化。这是一种从父组件到子组件的声明式沟通形式。 - 决定如何处理来自子组件的
output
消息。slot
函数允许您为子组件的output
提供handler
处理程序,而slot_
函数可以在子组件没有任何output
或您想忽略它们时使用。这是子组件与父组件之间的沟通。
slot
和slot_
函数以及H.Slot
类型让我们以类型安全的方式管理这三种通信机制。在本章的其余部分,我们将重点关注父组件和子组件如何相互通信,并在此过程中探索slots
和slot
类型。
组件间通信
当您从使用一个组件转向使用多个组件时,您很快就会需要某种方式让它们相互通信。在Halogen
中,父组件和子组件可以通过三种方式直接通信:
- 父组件可以向子组件提供
input
。每次父组件渲染时,它都会再次发送input
,然后由子组件决定如何处理新input
。 - 子组件可以向父组件发送
output
消息,类似于我们目前使用订阅的方式。子组件可以在发生重要事件时通知父组件,就像modal
关闭或提交表单一样,然后父组件可以决定要做什么。 - 父组件可以查询子组件,或者告诉它做某事,或者通过从它那里请求一些信息。父组件可以决定何时需要子组件做某事或给它一些信息,然后由子组件来处理
query
。
这三种机制为您提供了多种组件之间通信的方式。让我们简要探讨一下这三种机制,然后我们将看到您为组件定义的slot
函数和slot
类型如何帮助您以类型安全的方式使用它们。
Input
父组件可以向子组件提供input
,该input
在每次渲染时发送。我们已经多次看到这种情况 —— input
类型用于生成子组件的初始状态。在介绍本章的示例中,我们的button
组件从父组件接收了它的标签。
到目前为止,我们只使用input
来生成我们的初始状态。但是一旦创建了初始状态,input
就不会停止。input
在每次渲染时再次发送,子组件可以通过其eval
规范中的receive
函数处理新的input
。
receive :: input -> Maybe action
eval
规范中的receive
函数应该提醒你initialize
和finalize
,当组件创建和销毁时它让你选择一个action
来评估. 以同样的方式,当父组件发生新的input
时,receive
函数让您可以选择一个action
来评估。
默认情况下,当接收到新input
时,Halogen
的defaultSpec
不提供要评估的action
。如果您的子组件在收到初始值后不需要做任何事情,那么您可以保持原样。例如,一旦我们的按钮收到它的标签并将其复制到state
中,就没有必要继续监听input
,以防它随着时间的推移而改变。
每次父组件渲染时接收新input
的能力是一项强大的功能。这意味着父组件可以声明性地为子组件提供值。父组件还有其他方式与子组件通信,但input
的声明性使其成为大多数情况下的最佳选择。
让我们通过重新审视介绍中的示例来具体说明这一点。在这个版本中,我们的button
是未改变的 —— 它接收它的标签作为input
并使用它来设置它的初始状态 —— 但是我们的父组件已经改变了. 我们的父组件现在在初始化时启动一个计时器,每秒增加一个计数,并将状态中的计数用作按钮的标签。
简而言之,我们按钮的input
将每秒钟重新发送一次。尝试将其粘贴到Try PureScript中,看看会发生什么 —— 我们按钮的标签是否每秒更新一次?
module Main where
import Prelude
import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
import Type.Proxy (Proxy(..))
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI parent unit body
type Slots = ( button :: forall q. H.Slot q Void Unit )
_button = Proxy :: Proxy "button"
type ParentState = { count :: Int }
data ParentAction = Initialize | Increment
parent :: forall query input output m. MonadAff m => H.Component query input output m
parent =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
initialState :: input -> ParentState
initialState _ = { count: 0 }
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { count } =
HH.div_ [ HH.slot_ _button unit button { label: show count } ]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
handleAction = case _ of
Initialize -> do
{ emitter, listener } <- H.liftEffect HS.create
void $ H.subscribe emitter
void
$ H.liftAff
$ Aff.forkAff
$ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener Increment
Increment -> H.modify_ \st -> st { count = st.count + 1 }
-- 现在我们转向我们的子组件,按钮
type ButtonInput = { label :: String }
type ButtonState = { label :: String }
button :: forall query output m. H.Component query ButtonInput output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: forall action. ButtonState -> H.ComponentHTML action () m
render { label } = HH.button_ [ HH.text label ]
如果您将其加载到Try PureScript
中,您将看到我们的按钮…永远不会改变!即使父组件每秒都在向它发送新input
(每次父组件重新渲染),我们的子组件也永远不会收到它。仅仅接受input
是不够的, 我们还需要明确决定每次收到它时要做什么。
尝试用这个修改后的代码替换按钮代码以查看不同之处:
data ButtonAction = Receive ButtonInput
type ButtonInput = { label :: String }
type ButtonState = { label :: String }
button :: forall query output m. H.Component query ButtonInput output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, receive = Just <<< Receive
}
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: ButtonState -> H.ComponentHTML ButtonAction () m
render { label } = HH.button_ [ HH.text label ]
handleAction :: ButtonAction -> H.HalogenM ButtonState ButtonAction () output m Unit
handleAction = case _ of
-- 当我们收到新的input时,我们会更新状态中的`label`字段。
Receive input ->
H.modify_ _ { label = input.label }
我们在新版本中进行了几处更改,以确保我们与来自父组件的input
保持同步:
- 我们添加了一个新动作
Receive
,一个接受Input
类型作为其参数的构造函数。然后我们通过在接收到新input
时更新我们的状态来在我们的handleAction
函数中处理这个action
。 - 我们在
eval
规范中添加了一个新字段,receive
,它包含一个函数,每次接收到新input
时都会调用该函数。我们的函数会返回我们的Receive
操作,以便对其进行评估。
此更改足以让我们的子组件订阅来自父组件的新input
。您现在应该看到我们按钮的标签每秒更新一次. 作为练习,您可以用const Nothing
替换我们的receive
函数,以再次查看input
是如何被忽略的。
Output消息
有时一个事件发生在 它不应该自己处理的 子组件中。
例如,假设我们正在编写一个modal
组件,当用户单击以关闭modal
时,我们需要评估一些代码。为了保持这个modal
的灵活性,我们希望父组件决定当modal
关闭时应该发生什么。
在Halogen
中,我们通过设计modal
(子组件)来向父组件发出output
消息来处理这种情况。然后,父组件可以像处理其handleAction
函数中的任何其他操作一样处理该消息。从概念上讲,就好像子组件是父组件自动订阅的订阅。
具体来说,我们的modal
可以向父组件提出一个Closed
输出。然后,父级可以更改其状态以指示不应再显示modal
,并在下一次渲染时将modal
从DOM
中删除。
作为一个小例子,让我们考虑如何设计一个按钮,让父组件决定点击它时要做什么:
module Button where
-- 这个组件可以通知父组件一个事件,`Clicked`
data Output = Clicked
-- 这个组件可以处理一个内部事件,`Click`
data Action = Click
-- 我们的output类型显示在我们的 `Component` 类型中
button :: forall query input m. H.Component query input Output m
button =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render _ =
HH.button
[ HE.onClick \_ -> Click ]
[ HH.text "Click me" ]
-- 我们的output类型也显示在我们的 `HalogenM` 类型中, 因为这是我们可以发出这些output消息的地方。
handleAction :: forall state. Action -> H.HalogenM state Action () Output m Unit
handleAction = case _ of
-- 当按钮被点击时,我们通过使用`H.raise`发出它来通知父组件`Clicked`事件已经发生。
Click ->
H.raise Clicked
我们采取了一些步骤来实现此output
消息。
- 我们添加了一个
Output
类型,它描述了我们的组件可以发出哪些output
消息. 我们在Component
类型中使用该类型是因为它是组件公共接口的一部分,而我们的HalogenM
类型使用它是因为这是我们可以实际发出output
消息的地方。 - 我们添加了一个带有
Click
构造函数的Action
类型来处理Halogen HTML
中的单击事件. - 我们通过向父组件发送
output
消息来处理我们的handleAction
中的Click
操作。您可以使用H.raise
函数发出output
消息。
我们现在知道组件如何发出output
消息。现在,让我们看看如何处理来自子组件的output
消息。有以下三点需要牢记:
- 当您渲染一个子组件时,您需要将它添加到您的
slots
类型中,然后在您的ComponentHTML
和HalogenM
类型中使用它。您添加的类型将包括子组件的output
消息类型,它允许编译器验证您的处理程序。 - 当您使用
slot
函数渲染子组件时,您可以提供一个应在出现新output
时评估的action
。这类似于initialize
等生命周期函数如何接受一个action
来评估组件何时初始化。 - 然后,您需要在您的
handleAction
添加一个case
操作,用于(您添加的action
来)处理子组件的output
。
让我们通过编写一个slot
类型来开始编写我们的父组件:
module Parent where
import Button as Button
type Slots = ( button :: forall query. H.Slot query Button.Output Int )
-- 我们可以使用符号代理来引用`button`标签,这是一种在值级别引用像`button`这样的类型级别字符串的方式。
-- 我们定义这个是为了方便,所以我们可以使用`_button`在slot类型中引用它的标签,而不是一遍遍地写`Proxy`。
_button = Proxy :: Proxy "button"
我们的slot
类型是一个row
,其中每个标签指定我们支持的特定类型的子组件,在每种情况下都使用类型H.Slot
:
H.Slot query output id
该类型记录了可以发送到该类型组件的queries
、我们可以从该组件处理的output
消息, 以及我们可以用来唯一标识单个组件的类型。
例如,考虑一下我们可以渲染10
个这样的按钮组件 —— 您如何知道向哪个组件发送查询?这就是插槽id
发挥作用的地方。我们将在讨论查询时了解更多相关信息。
我们的父组件的row
类型清楚地表明我们可以支持一种类型的子组件,我们可以通过符号button
和Int
类型的标识符来引用它. 我们无法向该组件发送query
,因为类型变量保持打开状态。但它可以向我们发送Button.Output
类型的output
。
接下来,我们需要提供一个action
来处理这些output
:
data Action = HandleButton Button.Output
当这个action
发生在我们的组件中时,我们可以解开它以获取Button.Output
值并使用它来决定要评估的代码。现在我们已经处理了我们的slot
和action
类型,让我们编写我们的父组件:
parent :: forall query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render :: forall state. state -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot _button 0 button unit HandleButton ]
handleAction :: forall state. Action -> H.HalogenM state Action Slots output m Unit
handleAction = case _ of
HandleButton output ->
case output of
Button.Clicked -> do
...
您会注意到我们的Slots
类型现在已用于ComponentHTML
类型和HalogenM
类型。此外,现在每当Button.Clicked
事件在子组件中发生时都会通知该组件,这让父组件可以评估它想要的任何代码作为响应。
就是这样!您现在知道如何将output
消息从子组件提升到父组件以及如何在父组件中处理这些消息. 这是子组件与父组件通信的主要方式。现在让我们看看父组件如何向子组件发送信息。
Queries
查询表示父组件可以发送给子组件的命令或请求。它们类似于action
,并使用类似于handleAction
函数的handleQuery
函数进行处理。但它们是从组件外部产生的,而不是像action
那样在组件内部,这意味着它们是组件公共接口的一部分。
当父组件需要控制事件何时发生而不是子组件时,查询最有用。例如:
- 父组件可以告诉表单提交,而不是等待用户单击提交按钮。
- 父组件可以从
autocomplete
请求当前的选择,而不是在做出选择时等待子组件的output
消息.
查询是父组件强制控制子组件的一种方式。正如我们在两个示例中所介绍的,有两种常见的查询样式: 当父组件命令子组件做某事时的tell
风格的查询,以及当父组件想要从子组件获取信息时的request
风格的查询。
父组件可以发送query
,但子组件定义query
并处理query
。这使得query
在概念上类似于action
: 就像如何定义Action
类型并使用handleAction
为组件处理action
一样,您可以为queries
定义Query
类型和handleQuery
函数。
下面是一个查询类型的简短示例,其中包括tell-style
和request-style
查询:
data Query a
= Tell a
| Request (Boolean -> a)
我们可以将这个查询解释为”一个父组件可以使用tell
函数告诉这个组件做一些事情,也可以使用request
函数从这个组件请求一个布尔值.” 当我们实现查询类型时,请记住a
类型参数应出现在每个构造函数中。它应该是tell
风格查询的最后一个参数,并且是request
风格查询的函数类型的结果。
在eval
规范中使用handleQuery
函数处理query
,就像使用handleAction
函数处理action
一样. 让我们为我们的自定义数据类型编写一个handleQuery
函数,假设已经定义了一些state
、action
和output
类型:
handleQuery :: forall a m. Query a -> H.HalogenM State Action () Output m (Maybe a)
handleQuery = case _ of
Tell a ->
-- ... 做一些事情,然后返回我们收到的`a`
pure (Just a)
Request reply ->
-- ... 做一些事情,然后将请求的`Boolean`提供给`reply`函数以生成我们需要返回的`a`
pure (Just (reply true))
handleQuery
函数接受类型为Query a
的query
并生成一些返回Maybe a
的HalogenM
代码。这就是为什么我们的query
类型的每个构造函数都需要包含一个a
: 我们需要在handleQuery
中返回它。
当我们收到一个tell
风格的查询时,我们可以将我们收到的a
包装在Just
中以返回它,就像我们在handleQuery
中处理Tell a
的case
所做的那样.
但是,当我们收到request
风格的查询时,我们必须做更多的工作。我们接收的不是一个我们可以返回的a
值,而是一个函数,该函数会给我们一个a
然后我们可以返回. 例如,在我们的Request (Boolean -> a)
示例中,我们收到一个函数(当我们将它应用于Boolean
时,它会给我们一个a
). 按照惯例,当您对request
风格的查询进行模式匹配时,此函数称为reply
。在handleQuery
中,我们给这个函数一个true
以获取a
,然后将a
包裹在Just
中以返回它。
request
风格的查询起初可能看起来很奇怪。但是该风格允许我们的query
类型返回多种类型的值,而不是只返回一种类型的值。以下是一些返回不同内容的不同请求类型:
data Requests a
= GetInt (Int -> a)
| GetRecord ({ a :: Int, b :: String } -> a)
| GetString (String -> a)
| ...
父组件可以使用GetInt
从我们的组件中检索一个Int
,使用GetString
从我们的组件中检索一个String
,等等。您可以考虑query
类型返回的a
类型,而request
风格的查询是一种让a
成为许多不同可能类型的方法。稍后我们将看到如何从父组件执行此操作。
让我们看另一个小例子,它演示了如何在组件中定义和处理queries
:
-- 该组件可以被告知`increment`或可以回答当前`count`的请求
data Query a
= Increment a
| GetCount (Int -> a)
type State = { count :: Int }
-- 我们的query类型显示在我们的 `Component` 类型中
counter :: forall input output m. H.Component Query input output m
counter =
H.mkComponent
{ initialState: \_ -> { count: 0 }
, render
, eval: H.mkEval $ H.defaultEval { handleQuery = handleQuery }
}
where
render { count } =
HH.div_
[ HH.text $ show count ]
-- 我们编写了一个函数来处理出现的查询
handleQuery :: forall action a. Query a -> H.HalogenM State action () output m (Maybe a)
handleQuery = case _ of
-- 当我们收到 `Increment` 查询时,我们将增加我们的状态
Increment a -> do
H.modify_ \state -> state { count = state.count + 1 }
pure (Just a)
-- 当我们收到 `GetCount` 查询时,我们将用`state`进行答复。
GetCount reply -> do
{ count } <- H.get
pure (Just (reply count))
在这个例子中,我们定义了一个计数器,让父组件来tell
它增加或请求它的当前计数。为此,我们:
- 实现了一个查询类型,包括一个
tell
风格的查询,Increment a
,和一个request
风格的查询,GetCount (Int -> a)
。我们将此查询类型添加到组件的公共接口Component
中。 - 实现了一个查询处理程序
handleQuery
,它在这些查询出现时运行代码。我们将把它添加到我们的eval
中。
我们现在知道如何定义queries
并在子组件中评估它们。现在,让我们看看如何从父组件向子组件发送query
。像往常一样,我们可以从定义父组件的slot
类型开始:
module Parent where
type Slots = ( counter :: H.Slot Counter.Query Void Int )
_counter = Proxy :: Proxy "counter"
我们的slot
类型使用其query
类型记录了counter
组件,并将其output
消息类型保留为Void
,以表示没有。
当我们的父组件初始化时,我们将从子组件获取计数,然后increment
它,然后再次获取计数,以便我们可以看到它增加了。为此,我们需要在初始化时运行一个action
:
data Action = Initialize
现在,我们可以继续我们的组件定义:
parent :: forall query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
render :: forall state. state -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot_ _counter unit counter unit ]
handleAction :: forall state. Action -> H.HalogenM state Action Slots output m Unit
handleAction = case _ of
Initialize ->
-- startCount :: Maybe Int
startCount <- H.request _counter unit Counter.GetCount
-- _ :: Maybe Unit
H.tell _counter unit Counter.Increment
-- endCount :: Maybe Int
endCount <- H.request _counter unit Counter.GetCount
when (startCount /= endCount) do
-- ... do something
这里有几件事情需要注意。
- 我们在
slot
类型中使用了计数器标签的代理,_counter
,连同它的标识符,unit
,既可以使用slot
函数渲染组件,也可以使用tell
和request
函数向组件发送查询。标签和标识符始终用于处理特定的子组件。 - 我们使用
H.tell
函数发送tell
风格的查询Increment
,使用H.request
函数发送request
风格的查询GetCount
。GetCount
查询有一个类型为(Int -> a)
的reply
函数,因此您会注意到,当我们使用它时,我们收到了一个Maybe Int
的响应。
tell
和request
函数接受一个标签、一个slot
标识符和一个要发送的查询。tell
函数不返回任何东西,但request
函数返回来自child
组件的响应(包裹在Maybe
中),其中Nothing
表示查询失败(子组件返回Nothing
,或者您提供的标签和slot
标识符中不存在任何组件).还有tellAll
和requestAll
函数向给定标签的所有组件发送相同的查询。
许多人发现查询是Halogen
库中最令人困惑的部分. 幸运的是,查询的使用率远不如我们在本指南中了解的其他Halogen
功能,如果您遇到困难,您可以随时参考指南的这一部分。
组件插槽
我们已经学到了很多关于组件如何相互通信的知识。在我们继续我们的最后一个例子之前,让我们回顾一下我们在此过程中学到的关于slots
的知识。
一个组件需要知道它支持什么类型的子组件,以便它能够与它们通信。它需要知道它可以向子组件发送哪些查询以及它可以从子组件那里接收哪些output
消息。它还需要知道如何识别向哪个特定组件发送query
。
H.Slot
类型捕获父组件可以支持的特定类型的子组件的query
、output
和唯一标识符。您可以将许多插槽组合成一个row
插槽,其中每个标签用于特定类型的组件。以下是如何读取几个不同插槽的类型定义的方法:
type Slots = ()
这意味着该组件不支持子组件。
type Slots = ( button :: forall query. H.Slot query Void Unit )
这意味着组件支持一种类型的子组件,由符号button
标识。您不能向它发送查询(因为q
是一个开放类型变量)并且它不会发出任何output
消息(通常用Void
表示,因此您可以使用absurd
作为处理程序). 您最多可以拥有该组件中的一个,因为只有一个值unit
位于Unit
类型中。
type Slots = ( button :: forall query. H.Slot query Button.Output Int )
这种类型与前一种非常相似。不同之处在于子组件可以引发Button.Output
类型的output
消息,并且您可以拥有与整数一样多的此组件。
type Slots =
( button :: H.Slot Button.Query Void Int
, modal :: H.Slot Modal.Query Modal.Output Unit
)
这种slot
类型意味着组件支持两种类型的子组件,由标签button
和modal
标识。您可以将Button.Query
类型的query
发送到button
组件,并且您不会从它收到任何output
消息. 您可以向modal
组件发送类型为Modal.Query
的query
并从modal
组件接收类型为Modal.Output
的消息。您可以拥有与整数一样多的button
组件,但最多只有一个modal
组件。
Halogen
应用程序中的一个常见模式是组件导出自己的slot
类型,因为它已经知道其query
和消息类型,而不导出标识此特定组件的类型,因为这是父组件的责任。
例如,如果button
和modal
组件模块导出自己的slot
类型,如下所示:
module Button where
type Slot id = H.Slot Query Void id
module Modal where
type Slot id = H.Slot Query Output id
那么我们最后一个slot
类型示例将变成这个更简单的类型:
type Slots =
( button :: Button.Slot Int
, modal :: Modal.Slot Unit
)
这样做的优点是更简洁,更容易随着时间的推移保持最新状态,就好像slot
类型发生更改一样,它们可能发生在源模块中,而不是在使用slot
类型的任何地方。
完整示例
最后,我们使用本章中讨论的所有通信机制编写了一个父组件和子组件的示例。该示例注释了我们如何解释最重要的代码行 —— 我们通过浏览我们自己的代码库中的这些组件定义而收集到的内容。
像往常一样,我们建议将此代码粘贴到Try PureScript中,以便您可以交互地探索它。
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (logShow)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
import Type.Proxy (Proxy(..))
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI parent unit body
-- 父组件支持一种类型的子组件,它使用 `ButtonSlot` 插槽类型。 您可以拥有与整数一样多的此类子组件。
type Slots = ( button :: ButtonSlot Int )
-- 父组件只能评估一个动作: 处理来自`button`组件的`output`消息,类型为"ButtonOutput".
data ParentAction = HandleButton ButtonOutput
-- 父组件在本地状态中保持其所有子组件按钮被点击的次数。
type ParentState = { clicked :: Int }
-- 父组件不使用自己的查询、输入或输出类型。它可以使用任何 monad,只要该 monad 可以运行 `Effect` 函数。
parent :: forall query input output m. MonadEffect m => H.Component query input output m
parent =
H.mkComponent
{ initialState
, render
-- 该组件可以处理的唯一内部事件是在 `ParentAction` 类型中定义的操作。
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
initialState :: input -> ParentState
initialState _ = { clicked: 0 }
-- 我们渲染了三个按钮,使用`HandleButton`操作处理它们的`output`消息。
-- 当我们的状态改变时,这个渲染函数将再次运行,每次发送新的input(包含一个新标签供子按钮组件使用.)
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { clicked } = do
let clicks = show clicked
HH.div_
[ -- 我们用插槽id 0 渲染我们的第1个按钮
HH.slot _button 0 button { label: clicks <> " Enabled" } HandleButton
-- 我们用插槽id 1 渲染我们的第2个按钮
, HH.slot _button 1 button { label: clicks <> " Power" } HandleButton
-- 我们用插槽id 2 渲染我们的第3个按钮
, HH.slot _button 2 button { label: clicks <> " Switch" } HandleButton
]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
handleAction = case _ of
-- 我们处理一个动作,`HandleButton`,它自己处理我们按钮组件的`output`消息。
HandleButton output -> case output of
--只有一个`output`消息,`Clicked`。
Clicked -> do
-- 当 `Clicked` 消息出现时,我们将增加 state 中的点击计数,然后向第一个按钮发送一个查询,并告诉它为 `true`,
-- 然后向 所有请求其当前启用状态的子组件 发送一个查询,我们记录日志到控制台。
H.modify_ \state -> state { clicked = state.clicked + 1 }
H.tell _button 0 (SetEnabled true)
on <- H.requestAll _button GetEnabled
logShow on
-- 我们现在转到子组件,一个名为`button`的组件。
-- 该组件可以接受`ButtonQuery`类型的查询并发送`ButtonOutput`类型的`output`消息。
-- 导出此插槽类型,以便其他组件在构建其插槽`row`时可以使用它。
type ButtonSlot = H.Slot ButtonQuery ButtonOutput
-- We think our button will have the label "button" in the row where it's used,
-- so we're exporting a symbol proxy for convenience.
-- 我们认为我们的按钮将在使用它的`row`中带有标签`button`,因此为方便起见,我们导出了一个符号代理。
_button = Proxy :: Proxy "button"
-- 该组件接受两个查询。第一个是`request`风格的查询,它让父组件从我们这里请求一个`Boolean` 值。
-- 第二个是`tell`风格的查询,它让父组件向我们发送一个`Boolean`值。
data ButtonQuery a
= GetEnabled (Boolean -> a)
| SetEnabled Boolean a
-- 这个组件可以通知父组件一个事件,`Clicked`
data ButtonOutput
= Clicked
-- 该组件可以处理两个内部操作。它可以评估`Click`操作,并且可以在其父组件重新渲染时接收新`input`。
data ButtonAction
= Click
| Receive ButtonInput
-- 这个组件接收一个标签作为输入
type ButtonInput = { label :: String }
-- 该组件在状态中存储标签和启用标志
type ButtonState = { label :: String, enabled :: Boolean }
-- 该组件支持`ButtonQuery`类型的查询,需要`ButtonInput`类型的输入,并且可以发送`ButtonOutput`类型的`output`。
-- 它不执行任何`effects`,我们可以看出这是因为`m`类型参数没有约束。
button :: forall m. H.Component ButtonQuery ButtonInput ButtonOutput m
button =
H.mkComponent
{ initialState
, render
-- 该组件可以处理内部操作,处理父组件发送的`query`,并在收到新`input`时进行更新。
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, handleQuery = handleQuery
, receive = Just <<< Receive
}
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label, enabled: false }
-- This component has no child components. When the rendered button is clicked
-- we will evaluate the `Click` action.
-- 该组件没有子组件。点击已渲染的按钮时,我们将评估`Click`操作。
render :: ButtonState -> H.ComponentHTML ButtonAction () m
render { label, enabled } =
HH.button
[ HE.onClick \_ -> Click ]
[ HH.text $ label <> " (" <> (if enabled then "on" else "off") <> ")" ]
handleAction
:: ButtonAction
-> H.HalogenM ButtonState ButtonAction () ButtonOutput m Unit
handleAction = case _ of
-- 当我们收到新的`input`时,我们会更新状态中的`label`字段。
Receive input ->
H.modify_ _ { label = input.label }
-- 当按钮被点击时,我们更新我们的`enabled`字段,并通知我们的父组件发生了 `Clicked` 事件。
Click -> do
H.modify_ \state -> state { enabled = not state.enabled }
H.raise Clicked
handleQuery
:: forall a
. ButtonQuery a
-> H.HalogenM ButtonState ButtonAction () ButtonOutput m (Maybe a)
handleQuery = case _ of
-- 当我们收到带有布尔值的`tell`风格的`SetEnabled`查询时,我们在state中设置该值。
SetEnabled value next -> do
H.modify_ _ { enabled = value }
pure (Just next)
-- 当我们收到一个`request`风格的`GetEnabled`查询时,它需要一个布尔结果,我们从我们的状态中获取一个布尔值并用它来回复。
GetEnabled reply -> do
enabled <- H.gets _.enabled
pure (Just (reply enabled))
在下一章中,我们将了解有关运行Halogen
应用程序的更多信息。