Взаимодействие смежных компонентов в Thermite

Сложная проверка шаблонов проектирования пользовательского интерфейса оказывается простой задачей:

  • Создайте универсальный компонент (в нашем случае кнопку)
  • использовать его в родительском компоненте
  • заставить дочерний компонент «приводить в действие» эффекты в родительском компоненте или соседнем родственном компоненте (если он может достичь родителя, у родителя не должно быть проблем с сантехникой)

Философия дизайна Thermite все еще немного недоступна для меня, но я думаю, что понимаю как линзы и призмы можно использовать для объединения Spec, но не как вызывать родительское действие.

Этот вопрос был написан для версии 0.10.5, которая может измениться со временем читателя.


Создаваемое приложение будет простым счетчиком, где кнопка увеличения увеличивает значение счетчика, а кнопка уменьшения уменьшает его. Мы сделаем это, создав общий компонент button, а затем используя несколько из них в компоненте counter. Конструкция выглядит следующим образом:

counter:
  - CounterState = { count :: Int
                   , incButton :: ButtonState
                   , decButton :: ButtonState
                   }
  - init = { count: 0
           , incButton: initButton
           , decButton: initButton
           }
  - CounterAction = Increment
                  | Decrement
                  | IncButton (ButtonAction CounterAction)
                  | DecButton (ButtonAction CounterAction)

button:
  - ButtonState = Unit
  - initButton = unit
  - ButtonAction parentAction = Clicked parentAction

В кнопке ButtonAction я заявляю, что мне нужна parentAction для выполнения Clicked. Я сохранил это как параметр типа, чтобы поддерживать общий интерфейс. Однако это означает, что мы должны предоставить его откуда-то, поэтому я разрешил параметр в спецификации кнопки:

buttonSpec :: forall eff props parentAction
            . { _onClick :: parentAction }
           -> Spec eff ButtonState props (ButtonAction parentAction)
buttonSpec parentActions = T,simpleSpec performAction renderButton

Это означает, что я буду использовать действие _onClick, предоставленное здесь, когда я dispatch буду рендерить:

renderButton :: Render ButtonState props (ButtonAction parentAction)
renderButton dispatch _ state _ =
  [ -- ... html stuff
  , button [onClick $ dispatch $ Clicked parentActions._onClick]
      [] -- with fill text etc
  ]

Теперь самое сложное — объединить два buttonSpec в один counterSpec. Для этого мы используем две линзы incButton и decButton и две призмы _IncButton и _DecButton, которые выполняют очевидные задачи, связанные с состоянием и действием:

counterSpec :: forall eff props
             . Spec eff CounterState props CounterAction
counterSpec =
  T.simpleSpec performAction render
  where
    incButton = focus incButton _IncButton
              $ buttonSpec Increment
    decButton = focus decButton _DecButton
              $ buttonSpec Decrement

которые мы будем использовать в функциях счетчика performAction и render, используя линзу и призму, которые обеспечивает Thermite:

  render :: Render CounterState props CounterAction
  render dispatch props state children =
    [ text $ "Count: " <> state.count
    ] <> (incButton ^. _render) dispatch props state children
      <> (decButton ^. _render) dispatch props state children

  performAction :: PerformAction eff CounterState props CounterAction
  performAction Increment _ _ =
    modifyState $ count %~ (\x -> x + 1)
  performAction Decrement _ _ =
    modifyState $ count %~ (\x -> x - 1)
  performAction action@(IncButton _) props state =
    (incButton ^. _performAction) action props state
  performAction action@(DecButton _) props state =
    (decButton ^. _performAction) action props state

Это должно быть довольно прямолинейно. Когда мы действительно хотим Increment или Decrement, мы изменяем состояние родителя. В противном случае мы изучаем действия, специфичные для подкомпонента, но только достаточно, чтобы сказать, кому он должен принадлежать! Когда он принадлежит кнопкам увеличения или уменьшения, мы передаем ему данные.


Это дизайн для моего идеального сценария — делегировать решение о «деталях» позже, с помощью полиморфизма, и пусть композиция справится с сантехникой. Однако при тестировании реакция, похоже, не отправляет родительские действия дочернего компонента. Я не уверен, предназначен ли dispatch именно так, или в чем на самом деле проблема, но у меня есть репозиторий git с рабочим минимальным примером ошибки.


person Athan Clark    schedule 21.01.2017    source источник


Ответы (1)


Я создал запрос на вытягивание в вашем репозитории GitHub. Я думаю, что вы слишком усложняете ситуацию, пытаясь передать действие кнопке. Что больше соответствует способу ведения дел Thermite, так это позволить кнопке излучать свое действие, а затем использовать призму в родительском компоненте, чтобы отобразить действие кнопки в пространство действий родителя.

Таким образом, вместо 4 действий над родителем у вас есть только 2:

data ParentAction = Increment | Decrement

Затем вы обрабатываете их обычным способом, увеличивая или уменьшая счетчик в своем состоянии. Теперь, чтобы вызвать эти ParentAction с вашими кнопками, вы запускаете простое действие Clicked с ваших кнопок и используете призмы, чтобы сопоставить их с увеличением или уменьшением:

_IncButton :: Prism' CounterAction ButtonAction
_IncButton = prism' (const Increment) $ case _ of
  Increment -> Just Clicked
  _ -> Nothing

_DecButton :: Prism' CounterAction ButtonAction
_DecButton = prism' (const Decrement) $ case _ of
  Decrement -> Just Clicked
  _ -> Nothing

Теперь все, что осталось использовать эти призмы, чтобы сосредоточиться на действиях правой кнопки мыши:

inc = T.focus incButton _IncButton $ buttonSpec {_value: "Increment"}
dec = T.focus decButton _DecButton $ buttonSpec {_value: "Decrement"}

И вуаля!

person Christoph Hegemann    schedule 21.01.2017
comment
Синтаксис вашего регистра кажется колдовским, разве вы не имеете в виду \x -> case x of, а не case _ of? Кстати, это отличное решение! Благодарю вас! - person Athan Clark; 21.01.2017
comment
case _ of — это сокращенный синтаксис для \x -> case x of. Это хорошо для ситуаций, когда вы бы использовали LambdaCase в Haskell :) Рад, что смог помочь! - person Christoph Hegemann; 21.01.2017
comment
Как насчет элемента ввода вместо кнопки? Как я мог проникнуть во что-то, что содержит данные? Должно ли родительское действие также хранить данные? Или это будет случай, когда родительское действие может просто обернуть дочернее действие, как я изначально делал с IncButtonAction? - person Athan Clark; 23.01.2017