Skip to content
This repository has been archived by the owner on Sep 27, 2018. It is now read-only.

Simplified API (open to discussion) #3

Open
danielnarey opened this issue Sep 6, 2018 · 11 comments
Open

Simplified API (open to discussion) #3

danielnarey opened this issue Sep 6, 2018 · 11 comments

Comments

@danielnarey
Copy link
Owner

danielnarey commented Sep 6, 2018

Requesting input from @arsduo, @carolineartz, and other users:

Background and motivation

Read here if you are interested.

Semantics of record updates

When argument is a single value or key/value pair:

  • add__ = append a value or key/value pair to the current list contained in a field
  • add__Conditional = same as add__, but only updates when a conditional expression evaluates to True
  • remove__ = delete all instances of a value or key from the current list contained in a field
  • replace__ = delete the current value contained in a field, replacing it with the new value

When argument is a list:

  • add__List = append a list to the current list contained in a field
  • add__ListConditional = same as add__List, but only updates when a conditional expression evaluates to True
  • replace__List = delete the current list contained in a field, replacing it with the new list (to remove the current list, just replace it with [])

Variations for particular fields:

  • set__ = same as replace__, but signals that field that should only need to be updated once
  • append__ and prepend__ = used instead of add__ to provide an option for inserting the argument before or after the field's current string/list

Core module

Discussion of name is in issue #4

Types:

type Element msg =
  Element (Data msg)

type alias Data msg =
  { tag : String
  , id : String
  , classes : List String
  , styles : List (String, String)
  , attributes : List (VirtualDom.Attribute msg)
  , listeners: List (String, VirtualDom.Handler msg)
  , text : String
  , children : List (VirtualDom.Node msg)
  , namespace : String
  , keys : List String
}

Constructor for element records:
This replaces leaf, textWrapper, and container with a single constructor. The string argument sets the tag. See below for how to add child nodes.

element : String -> Element msg

Modifier functions for attributes:
In the previous version, "modifier" had a specific meaning pertaining to style classes that modify a base class. In the new version, I want to use "modifier" to mean any function that takes an existing element record, updates some of its internal data, and returns the updated element record. I'd like to call these "modifier functions" instead of "update functions" to make clear that they are just part of the view code and don't have anything to do with the Elm program's update function.

-- ID --

setId : String -> Element msg -> Element msg

-- CLASS --

addClass : String -> Element msg -> Element msg
addClassConditional : String -> Bool -> Element msg -> Element msg
addClassList : List String -> Element msg -> Element msg
addClassListConditional : List String -> Bool -> Element msg -> Element msg
removeClass : String -> Element msg -> Element msg
replaceClassList : List String -> Element msg -> Element msg

-- STYLE --

addStyle : (String, String) -> Element msg -> Element msg
addStyleConditional : (String, String) -> Bool -> Element msg -> Element msg
addStyleList : List (String, String) -> Element msg -> Element msg
addStyleListConditional : List (String, String) -> Bool -> Element msg -> Element msg
removeStyle : String -> Element msg -> Element msg
replaceStyleList : List (String, String) -> Element msg -> Element msg

-- OTHER ATTRIBUTES --
-- you can add any attribute using an `Html.Attribute` function or `VirtualDom` primitive

addAttribute : VirtualDom.Attribute msg -> Element msg -> Element msg
addAttributeConditional : VirtualDom.Attribute msg -> Bool -> Element msg -> Element msg
addAttributeList : List (VirtualDom.Attribute msg) -> Element msg -> Element msg
addAttributeListConditional : List (VirtualDom.Attribute msg) -> Bool -> Element msg -> Element msg
replaceAttributeList : List (VirtualDom.Attribute msg) -> Element msg -> Element msg

-- EVENT LISTENERS --

---- ACTIONS 
---- send a message in response to a mouse or keyboard event

addAction : (String, msg) -> Element msg -> Element msg
addActionConditional : (String, msg) -> Bool -> Element msg -> Element msg
addActionStopPropagation : (String, msg) -> Element msg -> Element msg
addActionPreventDefault : (String, msg) -> Element msg -> Element msg
addActionStopAndPrevent : (String, msg) -> Element msg -> Element msg

---- INPUT HANDLERS
---- capture a value from the event target on "input" or "change"

addInputHandler : (String -> msg) -> Element msg -> Element msg
addInputHandlerWithParser : (a -> msg, String -> a) -> Element msg -> Element msg
addChangeHandler : (String -> msg) -> Element msg -> Element msg
addChangeHandlerWithParser : (a -> msg, String -> a) -> Element msg -> Element msg
addToggleHandler : (Bool -> msg) -> Element msg -> Element msg

---- CUSTOM LISTENERS
---- use a custom decoder to construct a listener for any event type 

addListener : (String, Decoder msg) -> Element msg -> Element msg
addListenerConditional : (String, Decoder msg) -> Bool -> Element msg -> Element msg
addListenerStopPropagation : (String, Decoder msg) -> Element msg -> Element msg
addListenerPreventDefault : (String, Decoder msg) -> Element msg -> Element msg
addListenerStopAndPrevent : (String, Decoder msg) -> Element msg -> Element msg

---- REMOVE A LISTENER
---- works for actions, input handlers (with "input" or "change"), and custom listeners

removeListener: String -> Element msg -> Element msg

Modifier functions for internal text:
Text is rendered to VirtualDom as the first child node, containing plain HTML text. Helper functions for styling text that were included in the previous version will be moved to a separate package.

appendText : String -> Element msg -> Element msg
appendTextConditional : String -> Bool -> Element msg -> Element msg
prependText : String -> Element msg -> Element msg
prependTextConditional :  String -> Bool -> Element msg -> Element msg
replaceText : String -> Element msg -> Element msg
replaceTextConditional :  String -> Bool -> Element msg -> Element msg

Modifier functions to construct an element's internal tree:
Child elements are immediately rendered to VirtualDom when this function is executed. setChildListWithKeys uses the VirtualDom.KeyedNode optimization and should only be called once on any given node.

appendChild : Element msg -> Element msg -> Element msg
appendChildConditional : Element msg -> Bool -> Element msg -> Element msg
appendChildList : List (Element msg) -> Element msg -> Element msg
appendChildListConditional : List (Element msg) -> Bool -> Element msg -> Element msg
appendNodeList : List (VirtualDom.Node msg) -> Element msg -> Element msg
prependChild : Element msg -> Element msg -> Element msg
prependChildConditional : Element msg -> Bool -> Element msg -> Element msg
prependChildList : List (Element msg) -> Element msg -> Element msg
prependChildListConditional : List (Element msg) -> Bool -> Element msg -> Element msg
prependNodeList : List (VirtualDom.Node msg) -> Element msg -> Element msg
replaceChildList : List (Element msg) -> Element msg -> Element msg
replaceNodeList : List (VirtualDom.Node msg) -> Element msg -> Element msg
setChildListWithKeys : List (String, Element msg) -> Element msg -> Element msg
setNodeListWithKeys : List (String, VirtualDom.Node msg) -> Element msg -> Element msg

Modifier functions to set tag and namespace
The tag is already set with the element constructor function, but it could be useful to be able to change it when using component libraries. Namespace would typically be used to construct SVG nodes. Whenever the namespace field is not an empty string, VirtualDom.nodeNS is used for rendering.

setTag : String -> Element msg -> Element msg
setNamespace : String -> Element msg -> Element msg

Rendering
This function only needs to be called on the root node of a tree. VirtualDom.Node is interchangeable with Html.Html.

render : Element msg -> VirtualDom.Node msg
@carolineartz
Copy link

Thanks for the ping. Gonna dive into the above this week and I'll follow up.

@arsduo
Copy link

arsduo commented Sep 12, 2018

tl;dr: this makes a ton of sense. I'm excited for the next update.

Semantics of record updates

These make a ton of sense.

remove__ = delete a value or key from the current list contained in a field

Are there ever cases in which a key could exist multiple times? For instance if you've added multiple event handlers for the same action? In that case would the library remove both (seems sensible)?

Should I name it Ui.elm or something different?

I like Ui. It's descriptive, it doesn't conflict with anything, and it's easy to refererence.

Should the record be exposed or hidden behind an opaque type? Does exposing it make it easier for the user to understand what's happening behind the scenes, or does it simplify learning to not have access to internals?

That's a really good question. I can see arguments both ways. As far as I remember, we've never needed to look at (or change) fields on the record directly in our code. (Have you?) It's sometimes been useful to dump the record to the console, but you can still do that with an opaque type (AFAIK).

The risk of making it opaque is that people might run into situations that can't be handled by the existing methods, in which case new framework methods would be needed.

The risk of exposing it is that people might write code that depends on implementation details, making it either painful or impossible to change the implementation later.

Personally, I think the risks of exposing the details are much greater. Modular UI lives on top of Virtual Dom, whose implementation could change in ways that might be best handled by changing the data structure; if that's opaque, it's easy, whereas if it's been exposed that could potentially cause a lot of pain.

Compared to that, having to add methods occasionally doesn't seem to bad. One way to make sure that people don't get stuck could be to make it possible to give a node raw Html Msg objects as children. That way if someone did run into a situation they couldn't solve, they could fall back. (The downside would be people might use it too much, but ideally they'd switch over because Modular UI is just better.)

Constructor for element records

👍

In the previous version, "modifier" had a specific meaning pertaining to style classes that modify a base class. In the new version, I want to use "modifier" to mean any function that takes an existing element record, updates some of its internal data, and returns the updated element record. I'd like to call these "modifier functions" instead of "update functions" to make clear that they are just part of the view code and don't have anything to do with the Elm program's update function.

I think this is really sensible terminology that'll make the library clearer.

addAttribute

Would it make sense to have a removeAttribute method? Or does that get into a confusing situation where an attribute can be added twice with different values and it's not clear what would get removed?

Text is rendered to VirtualDom as the first child node, containing plain HTML text. Helper functions for styling text that were included in the previous version will be moved to a separate package.

👍

addChild : Element msg -> Element msg -> Element msg

To make sure I understand how this would work, would it be

myNode
|> Ui.addChild parentNode
-- return an updated parentNode?

That works for me. It would still be possible to write code like

myNode
|> Ui.addChild (Ui.element "div")
|> Ui.addClass "foo"

It's a little more verbose (I can't promise we wouldn't create Ui.Extra.container in our own codebase), but I think it's a good, clean move for the base library.

@danielnarey
Copy link
Owner Author

danielnarey commented Sep 13, 2018

Thanks for your input, @arsduo.
Here's my response:

1. Implementation of remove__

Are there ever cases in which a key could exist multiple times? For instance if you've added multiple event handlers for the same action? In that case would the library remove both (seems sensible)?

VirtualDom handles duplicate keys in an appropriate way, so my approach has been to preserve duplicates and hand them over to VirtualDom. It makes sense where the list is preserved in an Element record that if you remove a key it should remove all instances, so this is something I will implement.

2. Package / core module name

See #4. Also: I'd like to avoid confusion with mdgriffith/elm-ui. Alternative suggestions welcome.

3. Exposed vs. opaque type

As far as I remember, we've never needed to look at (or change) fields on the record directly in our code. ... Personally, I think the risks of exposing the details are much greater. ... One way to make sure that people don't get stuck could be to make it possible to give a node raw Html Msg objects as children. That way if someone did run into a situation they couldn't solve, they could fall back.

I agree with this reasoning, and I think we can guarantee that there will be a modifier function for anything useful you could do with basic record updates. I do think it makes sense to include functions for adding child nodes fromHtml msg / VirtualDom.Node msg objects.

4. Removing a generic attribute

Would it make sense to have a removeAttribute method? Or does that get into a confusing situation where an attribute can be added twice with different values and it's not clear what would get removed?

I would need to be convinced that this capability is really useful to have. I think it will make the package easier to learn if generic attributes (i.e., not classes, styles, actions) are added via Html.Attribute functions, rather than a specialized syntax. But that means that the attribute keys will not be accessible when working with Element records, so there will be no way to remove an attribute by key. An alternative to removing an attribute would be to use addAttributeConditional: if the condition becomes false when the program updates, the attribute will not be added when the next view is rendered.

5. Adding child nodes

To make sure I understand how this would work, would it be

myNode
|> Ui.addChild parentNode
-- return an updated parentNode?

I think you might have this backwards. I intended it to be:

parentNode
  |> addChild childNode

or

parentNode
  |> addChildList 
    [ child1
    , child2
    , child3
    ]

Now I am also thinking that the append__ vs. prepend__ option should be available for child nodes. So make it:

parentNode
  |> appendChild childNode

etc.

@danielnarey
Copy link
Owner Author

Started assembling the module here.

@danielnarey
Copy link
Owner Author

Here's a somewhat final list of exposed functions:


Create and Render

  • element
  • render

Build

Using a single argument...

to set the id attribute

  • setId

to add a class, style, or other attribute

  • addClass
  • addStyle
  • addAttribute

to add an event listener

  • addAction
  • addInputHandler
  • addChangeHandler
  • addToggleHandler

to append or prepend internal text

  • appendText
  • prependText

to append or prepend a child element

  • appendChild
  • prependChild

Using a list argument...

to add a list of classes, styles, or other attributes

  • addClassList
  • addStyleList
  • addAttributeList

to append or prepend a list of child elements

  • appendChildList
  • prependChildList

you can also supply a list of Html nodes

  • appendNodeList
  • prependNodeList

or a keyed list for performance optimization

  • setChildListWithKeys
  • setNodeListWithKeys

Using a conditional parameter...

when adding a class, style, or other attribute

  • addClassCondtional
  • addStyleConditional
  • addAttributeConditional

when adding a list of classes, styles, or other attributes

  • addClassListConditional
  • addStyleListConditional
  • addAttributeListConditional

when adding an event listener for an action

  • addActionConditional

when appending or prepending internal text

  • appendTextConditional
  • prependTextConditional

when appending or prepending child elements

  • appendChildConditional
  • appendChildListConditional
  • prependChildConditional
  • prependChildListConditional

Modify

Removing all instances of a single name or key

  • removeClass
  • removeStyle
  • removeListener

Replacing the existing list of classes, styles, or other attributes

  • replaceClassList
  • replaceStyleList
  • replaceAttributeList

Replacing the existing text

  • replaceText
  • replaceTextConditional

Replacing the existing descendant tree

  • replaceChildList
  • replaceChildListConditional
  • replaceNodeList

Advanced Usage

Setting an element's HTML/XML tag and namespace

  • setTag
  • setNamespace

Customizing event handling...

by transforming input values

  • addInputHandlerWithParser
  • addChangeHandlerWithParser

by using a custom decoder

  • addListener
  • addListenerConditional

by using custom handler options

stop propagation

  • addActionStopPropagation
  • addListenerStopPropagation

prevent default

  • addActionPreventDefault
  • addListenerPreventDefault

both

  • addActionStopAndPrevent
  • addListenerStopAndPrevent

Debug

  • getData

@arsduo
Copy link

arsduo commented Sep 19, 2018

That covers everything I can think of. It certainly should be enough for me to reimplement the drag-and-drop functionality, as well as everything we've done at eSpark. Looking forward to the new API!

@carolineartz
Copy link

wow this is going to be fantastic. SUPER excited for this release!!! Sorry if i missed this somehwere but will this be tied with or integrated by default with Bulma?

@danielnarey
Copy link
Owner Author

@carolineartz

Sorry if i missed this somehwere but will this be tied with or integrated by default with Bulma?

No, I'm taking a different approach here. After considering what seemed to be most useful out of my earlier attempts, I started to see the end goal differently. The new package is intended to be a basic set of utilities for UI development in Elm, which will not be tied to a specific CSS framework or component library. In other words, instead of creating components for others to use, my aim is to provide utilities that make it easier for users to build the customized components they need. At the same time, I want to support other package developers in creating libraries of reusable components, so my secondary goal is to make this package a comprehensive and reliable base for those efforts.

That said, some of the functionality of the previous Modular UI package may be incorporated into new packages in the future, so let me know if there are other parts you have found particularly useful that are not included here.

@danielnarey
Copy link
Owner Author

@arsduo @carolineartz

The new package is published under visotype/elm-dom! I'm releasing it under my organization name to differentiate it from my more experimental personal projects. The docs are here: https://package.elm-lang.org/packages/visotype/elm-dom/latest/

@arsduo
Copy link

arsduo commented Sep 26, 2018 via email

@carolineartz
Copy link

ohhhhhh awesome!! can't wait to check it out. Was hoping you'd be at Elm Conf!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants