1

Update 2016-05-14

I'm sorry that I may have failed to explain my problem clearly because of my bad English. This time I've abstracted the question and demonstrated it in a counter example.

There are two sub-modules(CounterPair and CounterTriplet), and each of them uses Counter to display num filed of SharedModel and displays something else (other Counters in this example). I highlighted the Counter displaying SharedModel's num with red color.

The red Counter of CounterPair and CounterTriple should synchronized because they're representing same SharedModel. Also, shared field of Model also need to be updated when any of the red Counters updates. The code shown below have only managed to display the things as I want but the update chain is missing. How should I implement the update chain ?

You can try this code on Try Elm.

import Html exposing (div, button, text)
import Html.App as App exposing (beginnerProgram)
import Html.Events exposing (onClick)
import Html.Attributes exposing (style)

main =
  beginnerProgram { model = init, view = view, update = update }
  
-------------------------------------
type alias SharedModel =
  { num : Int
  , foo : String
  }
  
sharedInit = 
  SharedModel 0 "Foo"
  
--------------------------------------
type alias Model =
  { shared : SharedModel
  , pair : CounterPair
  , triplet : CounterTriplet
  }
  
init = 
  let 
    shared = sharedInit
  in
    Model shared (pairInit shared) (tripletInit shared)
  
view model =
  div []
    [ App.map Pair (pairView model.pair)
    , App.map Triplet (tripletView model.triplet)
    ]
  
type Msg 
  = Pair PairMsg
  | Triplet TripletMsg
  
update msg model =
  case msg of
    Pair sub ->
      let 
        pair = pairUpdate sub model.pair
      in
        { model | pair = pair }
    Triplet sub ->
      let 
        triplet = tripletUpdate sub model.triplet
      in
        { model | triplet = triplet }

----------------------------------------
type alias CounterTriplet = 
  { localFst : CounterModel
  , localSnd : CounterModel
  , global : CounterModel
  }
  
tripletInit shared = 
  CounterTriplet (counterInit 0) (counterInit 0) (counterInit shared.num)
 
tripletView model =
  div [ style [("background-color","lightgray"), ("margin-bottom", "1rem")] ]
    [ App.map TriLocalSnd (counterView "green" model.localFst)
    , App.map TriLocalSnd (counterView "green" model.localSnd)
    , App.map TriGlobal (counterView "red" model.global)
    ]
  
type TripletMsg 
  = TriLocalFst CounterMsg
  | TriLocalSnd CounterMsg
  | TriGlobal CounterMsg

tripletUpdate msg model =
  case msg of 
    TriLocalFst sub ->
      let 
        localFst = counterUpdate sub model.localFst
      in
        { model | localFst = localFst }
    TriLocalSnd sub ->
      let 
        localSnd = counterUpdate sub model.localSnd
      in
        { model | localSnd = localSnd }
    TriGlobal sub ->
      let 
        global = counterUpdate sub model.global
      in
        { model | global = global }


----------------------------------------------

type alias CounterPair =
  { local : CounterModel
  , global : CounterModel
  }
  
pairInit shared =
  CounterPair (counterInit 0) (counterInit shared.num)
  
pairView model =
  div [ style [("background-color","lightgray"), ("margin-bottom", "1rem")] ]
    [ App.map PairLocal (counterView "green" model.local)
    , App.map PairGlobal (counterView "red" model.global)
    ]
    
type PairMsg 
  = PairLocal CounterMsg 
  | PairGlobal CounterMsg

pairUpdate msg model =
  case msg of 
    PairLocal sub ->
      let 
        local = counterUpdate sub model.local
      in
        { model | local = local }
    PairGlobal sub ->
      let 
        global = counterUpdate sub model.global
      in
        { model | global = global }



---------------------------------------

type alias CounterModel =
  { num : Int 
  , btnClicks : Int
  }
  
counterInit num =
  CounterModel num 0

counterView color model =
  div [ style [("display","inline-block"), ("margin-right", "1rem")] ]
    [ button [ onClick Decrement ] [ text "-" ]
    , div [ style [("color", color)]] [ text (toString model.num) ]
    , button [ onClick Increment ] [ text "+" ]
    , div [ ] [ text ("btn click: " ++ (toString model.btnClicks)) ]
    ]

type CounterMsg = Increment | Decrement


counterUpdate msg model =
  case msg of
    Increment ->
      { model | num = model.num + 1, btnClicks = model.btnClicks + 1 } 
    Decrement ->
      { model | num = model.num - 1, btnClicks = model.btnClicks + 1 }

Original Question

I'm using The Elm Architecture and the view hierarchy is like

,--------------------------------------------------------------------.
| View                                                               |
| ,------------------.  ,------------------------------------------. |
| | SubView1         |  | SubView2                                 | |
| | ,--------------. |  | ,---------------. ,--------------------. | |
| | | Label(title) | |  | | Label(title)  | | Label(description) | | |
| | `--------------' |  | `---------------' `--------------------' | |
| `------------------'  `------------------------------------------' |
`--------------------------------------------------------------------'

Label is a customized <label> which can turn into <input> field when double clicked. So it is editable.

The Models is like below. The problem is I want the content of ViewModel is updated when the sub-sub label's value is edited. When I update the title label of subview1, that of subview2 should also be updated because they should "share" same Content

But the value of LabelModel is somehow stand-alone so I need to transfer the change of value all way up to ViewModel. I don't think this is a practical way of implementing what I want, because the view hierarchy may become more complicated. Besides, almost all Models have something specific (Foo, Bar, BarBar, etc. used to store states), so they need to be initialized. So Making ViewModel neat, for example type alias ViewModel = Content, is impractical, because I don't know where to get Bar and BarBar for SubViewModel1 and SubViewModel2

type alias ViewModel =
    { content : Content
    , subView1 : SubViewModel1
    , subView2 : SubViewModel2
    }

type alias SubViewModel1 =
    { content : Content
    , label : Label
    , bar : Bar
    }

type alias SubViewModel2 = 
    { content : Content
    , titleLabel : Label
    , descLabel : Label
    , barbar : BarBar
    }

type alias Content = 
    { title: String
    , description : String
    }

type alias LabelModle =
    { value : String
    , state : Foo
    }

I'm very new to Elm so this question may seems stupid, sorry for that.

Thanks for reading (and answering :) )

adius
  • 13,685
  • 7
  • 45
  • 46
Nandin Borjigin
  • 2,094
  • 1
  • 16
  • 37

1 Answers1

0

Elm component hierarchies work best when a single piece of data is represented only in one place. If you need the data in more than one component, you place it at the nearest common ancestor of those components.

In your concrete case, if SubViewModel1.label.value and SubViewModel2.titleLabel.value is really the same piece of data, it should live at ViewModel.

Once the data lives in that one place, you react to updates in it by reacting in the update function of the ViewModel component to a Msg of SubViewModel1 or ...2. Something like this:

module View

import SubView1
import SubView2


type alias ViewModel =
  { content : Content
  , subView1 : SubView1.Model
  , subView2 : SubView2.Model
  , label : String             -- New!
  }


type Msg 
  = SubView1Msg SubView1.Msg
  | SubView2Msg SubView2.Msg
  | ...


update action model = 
  case action of 
    ...
    (SubView1.UpdateLabel str) as action' -> 
      let 
         (subView1', fx) = 
           SubView1.update action' model.subView1
      in 
         ( { model 
           | subView1 = subView1'
           , label = str
           }
         , Cmd.map SubView1Action fx
         )
Søren Debois
  • 5,598
  • 26
  • 48
  • Thanks for replying. The `content` filed of `ViewModel` is the primary source of the strings displayed by those labels, so it is already placed at the ancestor. But transferring all label updates along the `update` function link to the ancestor seems not well-structured. – Nandin Borjigin May 13 '16 at 08:39
  • 1
    I don't think there is any other way: the only way to observe things happening in the sub-views is through the Msgs they dispatch, or the output of their update functions. – Søren Debois May 14 '16 at 07:48