purescript-contrib / purescript-react

React Bindings for PureScript
MIT License
400 stars 65 forks source link

Consider representing attributes with rows for DOM elements #147

Open ethul opened 6 years ago

ethul commented 6 years ago

Currently, DOM elements accept an array of Props. An alternative to this could be to define a row representing the known attributes one may pass as props to a DOM element. Using a row was briefly mentioned in https://github.com/purescript-contrib/purescript-react/pull/113#issuecomment-325830517.

For instance, we could define two rows: one for HTML attributes and one for SVG attributes. Going this route, we would need a way to handle passing event handler callbacks. Also, if we wanted an attribute to have a type other than a primitive one or one known to React, then we would need a way to map the attribute's value to something React can use.

I think using rows could prove to be nicer in terms of syntax and perhaps a bit more convenient. I wanted to bring up this idea to see if there is interest in going this route. I'd be happy to discuss other ideas or dig in deeper with rows if there is interest. Any comments, questions, or suggestions are most welcome.

Also, below are some options for what this could look like. Option 1 presents passing a record where the row must be a subrow of all HTML attributes. Option 2 is similar, but passes a Data.Record.Builder. Using a builder we have the opportunity to pass data types other than primitive ones; e.g., instead of the target function accepting a String, we could make a Target data type and map it to a String in the function. Just a couple of ideas, others are most welcome.

Option 1 - Records

type HTMLAttributes r
  = ( href :: String
    , target :: String
    | r
    )

a
  :: forall r s t
   . Union r t (HTMLAttributes s)
  => Record r
  -> Array React.ReactElement
  -> React.ReactElement
a props children = React.createElementTagName "a" props children

foo :: React.ReactElement
foo =
  a
    { href: "https://github.com/purescript-contrib/purescript-react"
    , target: "_blank"
    } [ ... ]

Option 2 - Builder

type HTMLAttributes r
  = ( href :: String
    , target :: String
    | r
    )

href
  :: forall r
   . RowLacks "href" r
  => String
  -> Builder { | r } { href :: Int | r }
href = Builder.insert (SProxy :: SProxy "href")

target
  :: forall r
   . RowLacks "target" r
  => String
  -> Builder { | r } { target :: String | r }
target = Builder.insert (SProxy :: SProxy "target")

a
  :: forall r s t
   . Union r t (HTMLAttributes s)
  => Builder { } (Record r)
  -> Array React.ReactElement
  -> React.ReactElement
a builder children = React.createElementTagName "a" (Builder.build builder {}) children

foo :: React.ReactElement
foo =
  a
    ( href "https://github.com/purescript-contrib/purescript-react"
      >>> target "_blank"
    ) []

// @natefaubion

natefaubion commented 6 years ago

react-basic uses records and Union through FFI coercions on the DOM builders. I think the builder approach is an interesting alternative, but I think it could probably be built on top of what we have, right? Maybe we could try that first. I think that the dictionary overhead will be significant right now. In 0.12 we'll get compiler-solved Nub and Lacks which means lighter-weight, and correct record merges, so we may be able to use records directly. When you use rows though you always have to be careful about how constraints are propagated since it can destroy the referential identity of react classes.