Home Halogen-06-父子组件
Post
Cancel

Halogen-06-父子组件

父子组件

Halogen是一个没有偏见的UI库:它允许您创建声明式用户界面,而无需强制执行特定架构。

迄今为止,我们的应用程序由单个Halogen组件组成。您可以将大型应用程序构建为单个组件,并随着应用程序的增长将statehandleActionrender函数分解为单独的模块。这让您可以在Halogen中使用Elm架构。

但是,Halogen支持具有任意深度组件树的架构。这意味着您编写的任何组件都可以包含更多组件,每个组件都有自己的状态和行为. 大多数Halogen应用程序都以这种方式使用组件架构,包括Real World Halogen应用程序。

当您从单个组件移动到多个组件时,您开始需要机制,以便组件可以相互通信。Halogen为我们提供了父子组件通信的三种方式:

  • 父组件可以向子组件发送查询,要么告诉子组件做某事,要么从子组件请求一些信息。
  • 父组件为子组件提供它需要的input,每次父组件渲染时都会重新发送。
  • 子组件可以向父组件发射output消息,在发生重要事件时通知它。

这些类型参数在Component类型中表示,有些还可以在ComponentHTMLHalogenM类型中找到。例如,支持queryinputoutput消息的组件将具有以下Component类型:

component :: forall m. H.Component Query Input Output m

您可以将组件与其他组件通信的方式视为其公共接口,而公共接口显示在Component类型中。

在本章中,我们将了解:

  • 如何在你的Halogen HTMLrender组件
  • 组件通信的三种方式: queryinputoutput消息
  • 组件槽、slot函数和Slot类型,使这种通信类型安全

我们将首先渲染一个没有queryoutput消息的简单子组件。然后,我们将构建使用这些方式进行通信的组件,最后一个示例展示了同时使用所有这些机制的父组件和子组件.

尝试将示例加载到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组件为我们的stateaction保留了类型变量,因为它没有内部状态,也没有任何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复制到stateHalogen中的常见模式。另请注意,我们的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中使用了这个新类型,而不是我们之前看到的空行()

slotslot_函数以及Slot类型允许您在Halogen HTML中渲染一个statefuleffectful的子组件,就像它是任何其他HTML元素一样。但是为什么有这么多参数和类型涉及到这样做呢?为什么我们不能用它的input调用button

答案是Halogen为父子组件提供了两种相互通信的方式,我们需要确保这种通信是类型安全的。slot函数允许我们:

  • 决定如何通过标签(类型级别的字符串button,我们在术语级别用代理Proxy :: Proxy "button"表示)和唯一标识符来标识特定组件(在本例中为整数0)以便我们可以向它发送查询。这是父组件与子组件之间必不可少的沟通形式。
  • 渲染组件(button)并为其提供input{ label: "Click Me" }),每次父组件渲染时都会重新发送,以防input随时间发生变化。这是一种从父组件到子组件的声明式沟通形式。
  • 决定如何处理来自子组件的output消息。slot函数允许您为子组件的output提供handler处理程序,而slot_函数可以在子组件没有任何output或您想忽略它们时使用。这是子组件与父组件之间的沟通。

slotslot_函数以及H.Slot类型让我们以类型安全的方式管理这三种通信机制。在本章的其余部分,我们将重点关注父组件和子组件如何相互通信,并在此过程中探索slotsslot类型。

组件间通信

当您从使用一个组件转向使用多个组件时,您很快就会需要某种方式让它们相互通信。在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函数应该提醒你initializefinalize,当组件创建和销毁时它让你选择一个action来评估. 以同样的方式,当父组件发生新的input时,receive函数让您可以选择一个action来评估。

默认情况下,当接收到新input时,HalogendefaultSpec不提供要评估的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,并在下一次渲染时将modalDOM中删除。

作为一个小例子,让我们考虑如何设计一个按钮,让父组件决定点击它时要做什么:

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类型中,然后在您的ComponentHTMLHalogenM类型中使用它。您添加的类型将包括子组件的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类型清楚地表明我们可以支持一种类型的子组件,我们可以通过符号buttonInt类型的标识符来引用它. 我们无法向该组件发送query,因为类型变量保持打开状态。但它可以向我们发送Button.Output类型的output

接下来,我们需要提供一个action来处理这些output:

data Action = HandleButton Button.Output

当这个action发生在我们的组件中时,我们可以解开它以获取Button.Output值并使用它来决定要评估的代码。现在我们已经处理了我们的slotaction类型,让我们编写我们的父组件:

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-stylerequest-style查询:

data Query a
  = Tell a
  | Request (Boolean -> a)

我们可以将这个查询解释为”一个父组件可以使用tell函数告诉这个组件做一些事情,也可以使用request函数从这个组件请求一个布尔值.” 当我们实现查询类型时,请记住a类型参数应出现在每个构造函数中。它应该是tell风格查询的最后一个参数,并且是request风格查询的函数类型的结果。

eval规范中使用handleQuery函数处理query,就像使用handleAction函数处理action一样. 让我们为我们的自定义数据类型编写一个handleQuery函数,假设已经定义了一些stateactionoutput类型:

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 aquery并生成一些返回Maybe aHalogenM代码。这就是为什么我们的query类型的每个构造函数都需要包含一个a: 我们需要在handleQuery中返回它。

当我们收到一个tell风格的查询时,我们可以将我们收到的a包装在Just中以返回它,就像我们在handleQuery中处理Tell acase所做的那样.

但是,当我们收到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函数渲染组件,也可以使用tellrequest函数向组件发送查询。标签和标识符始终用于处理特定的子组件。
  • 我们使用H.tell函数发送tell风格的查询Increment,使用H.request函数发送request风格的查询GetCountGetCount查询有一个类型为(Int -> a)reply函数,因此您会注意到,当我们使用它时,我们收到了一个Maybe Int的响应。

tellrequest函数接受一个标签、一个slot标识符和一个要发送的查询。tell函数不返回任何东西,但request函数返回来自child组件的响应(包裹在Maybe中),其中Nothing表示查询失败(子组件返回Nothing,或者您提供的标签和slot标识符中不存在任何组件).还有tellAllrequestAll函数向给定标签的所有组件发送相同的查询。

许多人发现查询是Halogen库中最令人困惑的部分. 幸运的是,查询的使用率远不如我们在本指南中了解的其他Halogen功能,如果您遇到困难,您可以随时参考指南的这一部分。

组件插槽

我们已经学到了很多关于组件如何相互通信的知识。在我们继续我们的最后一个例子之前,让我们回顾一下我们在此过程中学到的关于slots的知识。

一个组件需要知道它支持什么类型的子组件,以便它能够与它们通信。它需要知道它可以向子组件发送哪些查询以及它可以从子组件那里接收哪些output消息。它还需要知道如何识别向哪个特定组件发送query

H.Slot类型捕获父组件可以支持的特定类型的子组件的queryoutput和唯一标识符。您可以将许多插槽组合成一个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类型意味着组件支持两种类型的子组件,由标签buttonmodal标识。您可以将Button.Query类型的query发送到button组件,并且您不会从它收到任何output消息. 您可以向modal组件发送类型为Modal.Queryquery并从modal组件接收类型为Modal.Output的消息。您可以拥有与整数一样多的button组件,但最多只有一个modal组件。

Halogen应用程序中的一个常见模式是组件导出自己的slot类型,因为它已经知道其query和消息类型,而不导出标识此特定组件的类型,因为这是父组件的责任。

例如,如果buttonmodal组件模块导出自己的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应用程序的更多信息。

This post is licensed under CC BY 4.0 by the author.

Halogen-05-生命周期和订阅

Halogen-07-运行应用程序