avh4 / elm-debug-controls

Easily build interactive UIs for complex data structures
https://avh4.github.io/elm-debug-controls/
BSD 3-Clause "New" or "Revised" License
24 stars 4 forks source link

Lists of Controls? #12

Open greglearns opened 4 years ago

greglearns commented 4 years ago

Great project!

https://avh4.github.io/elm-debug-controls/ demonstrates how to create, view, and interact with (click on and change) a UploadRequest, but how would one create and view a List UploadRequest? It would be great to be able to have a list of N UploadRequests all of which are interactive. That's what I am trying to do with my own model. I have a record like:

type alias Record = {
  name: String,
  notes: List String,
  things: List Thing
}

type alias Thing = {
  one: String,
  two: Int
}

It's not clear to me from the documentation and code how do to this. I saw that there is a "list" Control, but that doesn't do what I am trying to do, and I haven't been able to figure out exactly how to do it. I'm having a bit of a hard time wrapping my head around exactly how all of this works -- I'm a good developer with functional experience so a lot of it makes sense, but the overall approach is translucent to me currently (though, I can see that it is very cool).

Thank you!

avh4 commented 4 years ago

I think it would depend on what you want to do... for example, do you have specific data that you want the list to start with? or do you just want to create some random data? Do you want the list size to be able to be changed? If so, how does data for the new item get created?

greglearns commented 4 years ago

I have a vector of objects, and it would be nice to 1) use the slider (or something like it) to define the number of elements that are in the list (i.e., one could use the slider to "add" a new object to the list) 2) each object in the list can be edited just like the UploadRequest example.

Currently, a record can have fields, but it can't have a field that is a list. I'd love to be able to handle a list.

avh4 commented 4 years ago

Ah yeah, the current list is made for filling in sample data.

Doing something fancier for list controls would need a control for dealing with that, and would also need to figure out how the UI design would work for the general case of lists.

With the current package, you could have a List (Control Record) in your model, and you'd have to make your own UI for the slider and handle updating the List yourself based on the slider changes.

greglearns commented 4 years ago

The main part I can't figure out (even as I have been pouring over the code), is how a nested value gets propogated up after being changed by an event. For example,

type Parent {
  child: Child
}
type Child {
  childValue: String
}

How does the onInput event occuring on the childValue, get recorded in the Parent object?

avh4 commented 4 years ago

Essentially the Control String contains within it a current value of type String, and Control Child contains a current value of type Child. The Control Parent also contains the current Parent value, but also embedded in its view are functions that when given a new String value will construct a new Child value (with the String changed and all other fields the same as their current values), and then a new Parent value (with child changed and all other fields the same as their current values).

In summary, the way this works is by hiding update functions in closures that are stored within the Controls.... storing functions in the model is not recommended by the Elm architecture for production code, which is why this package is called "debug controls", as it uses some tricks that should be avoided for building robust applications. But anyway, that's how it works.

greglearns commented 4 years ago

UPDATE: I think I'm getting it... once I've fully understood it, I'll try to add some documentation to the code. So far, one critical part to understanding things is to realize that most of the time in this type signature, view_ : (Control a -> msg) -> Control a -> Html msg that the (Control a -> msg) is not a Msg constructor (something that will be passed to update), but is instead a function that creates a new field that will get wrapped in other functions that enable the record to be updated, and which is only wrapped at the last step by the actual Msg contructor which will then be passed to the update function. So basically, the function that is finally passed to the update function will look something like:

view (field "lastChild" (...last child's control) (field "childValue" (string "newly typed text from onInput") pipeline)) ...

It's hard to write (and, I don't think I have it quite right either yet), so I can see why you'd have a hard time documenting how this works!

ORIGINAL QUESTION: The whole approach you are taking is very cool, and I love that it enables rapid prototyping to explore a design before verifying that producing it the "right" (Elm) way is worth the time. Hiding the update functions is a good idea, ... and I think that's where I'm getting hung up on how the update event in a child (string) gets propagated back up to the parent (Object)

For the child string, I can see how that happens here (when the child onInput event occurs, the Html.Events.onInput string event handler creates a replacement string that has the new text input:

string : String -> Control String
string initialValue =
    Control
        { currentValue = \() -> initialValue
        , allValues =
            \() ->
                [ initialValue

                -- , ""
                -- , "short"
                -- , "Longwordyesverylongwithnospacessupercalifragilisticexpialidocious"
                -- , "Long text lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
                ]
        , view =
            \() ->
                SingleView <|
                    Html.input
                        [ Html.Attributes.value initialValue
                        , Html.Events.onInput string
                        ]
                        []
        }

(Side note: why do the string and stringTextArea controls have allValue fields with extra strings in them instead of just [initialValue]?)

But, how does the parent object that contains the child string get updated when the child string gets updated? Because I can see where the msg that I passed in (per the documentation) that will replace the root of the tree in my model gets mapped to the new table you create at https://github.com/avh4/elm-debug-controls/blob/master/src/Debug/Control.elm#L605 and the Parent's children are getting viewed here: https://github.com/avh4/elm-debug-controls/blob/master/src/Debug/Control.elm#L590. But I don't see how when the Child's childValue string is updated via the onInput https://github.com/avh4/elm-debug-controls/blob/master/src/Debug/Control.elm#L165, how that gets propagated back up the tree so that the Parent view value gets updated.

As I look at the code, it is also possible that I don't understand https://github.com/avh4/elm-debug-controls/blob/master/src/Debug/Control.elm#L448 where I'm guessing the magic is occuring in the Html.map.

Also, it's possible that I don't understand what the a and b actually are in field (a is the current field being decoded, but b is either the Parent or the ... http://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/latest

avh4 commented 4 years ago

It's similar to how json-decode-pipeline works: If you have type alias Record = { a : String, b : Bool }, then the control

record Record
  |> field "a" (string "")
  |> field "b" (bool False)

will have a view for both the string and the bool. If the string control changes, it applies the new string to Record and ends up with a Control (Bool -> Record), and it then applies the current value of the bool control to create the new Record. If instead the bool control changes, then it uses the current value of type Bool -> Record and applies the new Bool value to create the new `Record.