Elm 0.17: How to subscribe to sibling/nested compo

2019-05-26 09:10发布

问题:

See the full implementation with the suggestions of the accepted answer here: https://github.com/afcastano/elm-nested-component-communication

=================================================================

I have one parent component with two children. See working example

With the Elm Architecture, how can I update the right child when any of the counters in the left child change?

At the moment, I have the parent component reading the nested property of the left child model and setting it to the right child model, but it seems to me that the parent shouldn't know that much about the internal structure of the children.

These are the models and msgs:

type alias MainModel =
  { counterPair : CounterPair
  , totals: Totals
  }

type alias CounterPair =
  {
    greenCounter : Counter
  , redCounter : Counter
  }

type alias Counter =
  {
    value : Int
  , totalClicks : Int  
  }

type alias Totals =
  {
    totalRed : Int
  , totalGreen : Int
  , combinedTotal : Int
  }

type MainMsg
  =  UpdateCounterPair CounterPairMsg
  |  UpdateTotals TotalsMsg


type alias RedVal = Int
type alias GreenVal = Int

type TotalsMsg
  = UpdateTotals RedVal GreenVal

As you can see, the Main model contains two sub models. The pair model in turn, also contains two counter models.

The Total model is interested in changes on the CounterModels of the Pair component.

To do that, the Main update function would be like this:

updateMain: MainMsg -> MainModel -> MainModel
updateMain msg model =
  case msg of
    UpdateCounterPair counterPairMsg ->
    let 
      counterPairModel = updateCounterPair counterPairMsg model.counterPair
      totalsModel = updateTotals (UpdateTotals counterPair.redCounter.value counterPair.greenCounter.value) model.totals
    in
      {model | counterPair = counterPairModel, totals = totalsModel}

The things that I don't like are in this line:

updateTotals (UpdateTotals counterPair.redCounter.value counterPair.greenCounter.value) model.totals

1 - The Main module needs to know how to get the value of the counters so that it can pass the update to the updateTotal function.

2 - The Main module also needs to know about the internals of the union type of the Totals module so that it can use UpdateTotals constructor.

Is there any other way in which Totals component can subscribe to Pair component without the parent knowing the details of the model structure?

Thank you very much.

回答1:

If you have a component and you want that component to have a side-effect, in other words, to have an effect outside of itself, you can return some information (data) together with the model and the Cmd:

update : Msg -> Model -> (Model, Cmd Msg, SomeInfo)

The parent can use SomeInfo to decide what to do in other places.

If a component needs to be updated from outside, it is best to expose this through a custom update function. In the case of a counter this would look like this:

updateValue: Int -> Model -> Model

This way, you are free to use whatever representation you want inside the module. The parent of the counter can then update the Model of the counter when it encounters some condition.

A function like value : Model -> Int can also be use to extract information from the Model of the Counter.

All these ensure that you maintain encapsulation while presenting to the user of the module an interface for retrieval and updating of data.



回答2:

Are you sure you want to split the functionality across the three components? The look to be very coupled. From your description I'd imagine something along these lines:

type alias Model = {green:int, red:int}
type Msg = IncrementGreen
         | IncrementRed

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
  IncrementGreen -> {model|green=(model.green+1)}
  IncrementRed -> {model|red=(model.red+1)}

In the example there is only cone component, which owns the state of the two counters and which handles the messages to update it. Now we have a single canonical state but we'd still like to have a modularised view.

You can achieve that deriving the state that you pass to your components using functions to map from part of the state of the parent to the state of the children.

view : Model -> Html Msg
view m =
  div [][
    (Html.map mapRedMsg <| Counter.view (ToCounterModel m.red)),
    (Html.map mapGreenMsg <| Counter.view (ToCounterModel m.green)),
    (Labels.view (ToLabelsState m))
  ]

Doing that, you have a single source of truth (the state you hold into the model) and every time it gets updated you'll push it to the children in a more declarative way without having to manually copying it around.