gdotdesign / elm-ui

UI library for making web applications with Elm
https://elm-ui.netlify.com
BSD 2-Clause "Simplified" License
920 stars 39 forks source link

Access to Ui.Chooser.Msg definitions (and maybe in other components) #50

Closed mrozbarry closed 7 years ago

mrozbarry commented 7 years ago

For context, I'm working on a project where I have a table of ui choosers. No chooser can have a repeat value (unless nothing is selected), and when data is selected, it also propagates to other choosers, removing the option that was selected from the source chooser. There are other filters and rules, but I think that is enough to understand what I'm doing.

My setup looks something like this (I've trimmed it down to the most basic example):

type Msg
  = UiMessage Ui.App.Msg
  | SelectableChanged Selectable Ui.Chooser.Msg

type alias Selectable =
  { row : Int
  , column : Int
  , chooser : Ui.Chooser.Model
  }

type alias Model =
  { app : Ui.App.Model
  , selectables : List Selectable
  , rows : List String
  , columns : List String
  , bigCollectionThatGetsConvertedIntoChooserItems : List a -- Has a string id and name field that can be mapped into Ui.Chooser.Items
  }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    UiMessage subMsg ->
      let
        (nextApp, effect) = Ui.App.update subMsg model.app
      in
        ( { model | app = nextApp }
        , Cmd.map UiMessage effect
        )

    SelectableChanged selectable subMsg ->
      let
        (chooser, effect) = Ui.Chooser.update subMsg selectable.chooser
        nextSelectable = { selectable | chooser = chooser }

        replaceSelectable selectable =
          if selectable.row == nextSelectable.row && selectable.column == nextSelectable.column then
            nextSelectable
          else
            selectable

        selectables =
          model.selectables
            |> List.map replaceSelectable
            |> List.map myFiltersThatImNotGoingToWriteBecauseItCloudsThisExample
      in
        ( { model | selectables = selectables }
        , Cmd.map (SelectableChanged nextSelectable) effect

My big collection can have over 100 items, and the filters may map over each item a few times per update. I have a dynamic number of rows and columns, but it wouldn't be unusual for me to have 4 columns and 20 rows, so 100 cells, each with potentially 100 items in each cell.

Now to the problem; I only need to update the other choosers when a single chooser gets a new value, but SelectableChanged occurs with focus, open, close, blur, key movement, and select.

My current solution is to do something like:

selectables =
  let
    replacedSelectables =
      model.selectables
        |> List.map replaceSelectable
  in
    case subMsg of
      Ui.Chooser.Select value ->
        replacedSelectables
          |> List.filter myFiltersThatImNotGoingToWriteBecauseItCloudsThisExample

      _ ->
        replacedSelectables

This occasionally does some sort of infinite loop that locks up my browser and ramps up memory use, but I'm sure with some tweaks it will do the right thing.

Optionally, I'm up for other solutions that don't require modifying the source in my elm-stuff directory. I'm mostly an elm beginner, so maybe there is some other sort of map I can do, but it's not at all obvious to me right now.

So if my solution works (and I'm just doing something wrong), it would be great to have access to the various component Msg types so I can do my filtering on demand, and otherwise, you can close this issue and I'll ask on an elm message board for advice.

gdotdesign commented 7 years ago

Hey, thanks for writing this long explanation :+1: I think I understand your problem.

Maybe it's not very well explained in the docs (http://elm-ui.info/documentation/getting-started/reacting-to-changes) but you can use subscriptions to subscribe to the changes of the components.

For the chooser it has this signature:

subscribe : (Set.Set String-> msg)-> Ui.Chooser.Model-> Sub msg

To use it in your example:

-- Modifiy the type
type Msg
  = UiMessage Ui.App.Msg
  | SelectableChanged Selectable Ui.Chooser.Msg
  | ChooserChanged Selectable (Set String)

-- Geather subscriptions for all selectables
subscriptions : Model -> Sub Msg
subscriptions model =
  let
    subscribe ({ chooser } as selectable) =
      Ui.Chooser.subscribe (ChooserChanged selectable) chooser
  in
    model.selectables
      |> List.map subscribe 
      |> Sub.batch

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    ChooserChanged selectable value ->
      let
        singleValue =
          Set.toList value
            |> List.head
            |> Maybe.withDefault "defaultValue"
      in
        -- do something with the new selected value
    ...

-- in your app
main =
  Html.App.program
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

FYI in the next version subscribe will be changed to onChange to make it's purpose more visible.

mrozbarry commented 7 years ago

Thank you very much! I notices the Set String in the Ui.Chooser.subscribe method, but I just wasn't sure how to use it, but this clears it all up.