robb / Swim

A DSL for writing HTML in Swift
310 stars 9 forks source link

Ideas for attributes #16

Open kevinrenskers opened 3 years ago

kevinrenskers commented 3 years ago

First of all, thanks for this library! I've only started to dig in recently, but am enjoying the syntax and the fact that I can just use map, compactMap, if statements etc, without having to worry about wrapping things in .group or .fragment types (as Plot does), or the need for a custom forEach element (like Vaux).

My biggest issue with this library however is that working with tag attributes is rather painful. The autocomplete is useless since there are so many parameters, of which the order actually matters.

Screen Shot 2021-02-10 at 11 15 22

Screen Shot 2021-02-10 at 11 15 07

Here's my initial idea for an improvement:

public enum Attribute {
  case `class`(String)
  case id(String)
  case custom(String, String)

  var name: String {
    switch self {
      case .class:
        return "class"
      case .id:
        return "id"
      case .custom(let name, _):
        return name
    }
  }

  var value: String {
    switch self {
      case .class(let value):
        return value
      case .id(let value):
        return value
      case .custom(_, let value):
        return value
    }
  }
}

public func p(_ attributes: Attribute...,
  @NodeBuilder children: () -> NodeConvertible = { Node.fragment([]) }
) -> Node {
  let attributesDict = attributes.reduce([:]) { (result, next) -> [String: String] in
    var result = result
    result[next.name] = next.value
    return result
  }

  return .element("p", attributesDict, children().asNode())
}

The result:

p(.id("id"), .class("class"), .custom("data-whatever", "foobar")) {
  "Hello"
}

This way you have simple autocompletion (just type . and you get a list of attributes), and the order doesn't matter. Of course in this simple naive way, all attributes would be available to be used on all tags, which is not ideal. But hey, this was my first idea which doesn't require a big architecture change of Swim.

Plot solves the same problem with protocols:

Screen Shot 2021-02-10 at 11 30 32

Vaux on the other hand goes a different route: you add attributes onto a tag:

div {
  paragraph { "Hello" }
}.class("article")

To be honest I'm not a big fan of that approach, it's much less readable in a long(ish) tree of tags. But the idea is the same: you limit what kind of attributes you can use per "type of tag", without having all those repeated function parameters in every function.

Finally, swift-html also uses an array of enum cases:

.div(
  attributes: [.class("article")],
  .p("Hello there")
)

If you look at the function definition of div, you can see their solution to limit which attributes can be used:

div(attributes: [Html.Attribute<Html.Tag.Div>] = [], _ content: Html.Node...) -> Html.Node

And that seems to be a winner to me? So the basic idea is this:

struct Attribute<Node> {
  let key: String
  let value: String?

  init(_ key: String, _ value: String?) {
    self.key = key
    self.value = value
  }
}

extension Attribute {
  static func id(_ value: String) -> Attribute {
    return .init("id", value)
  }
}

extension Attribute where Node: Linkable {
  static func href(_ value: String) -> Attribute {
    return .init("href", value)
  }
}

I'm curious to see what you think. Have the very long function signatures bothered you as well?

robb commented 3 years ago

Interesting, do find yourself often using attributes beyond id, class, href, src? My feeling is that the top 5% of attributes would cover 95% of my usage and I thus rarely rely on auto-correct to guide me.

I've also tried to keep things predictable, every tag is a free function and its implementation follows the same pattern to make it easy to anticipate what it's doing. I hope these factors contribute to your enjoyment of the library. I would totally put id before class myself, much like your example but I'm a big fan of alphabetizing all the things to reduce ambiguity and make diffs small.

I can also empathize with the benefits of the approaches you outlined here, especially the ability to add attributes after the fact which would help with e.g. isolating ARIA attributes. However, it also creates new ambiguity, e.g.:

div(attributes: .class("button"), .class("highlighted"), .role("button")) {
   "Click me"
}

It's somewhat unclear if button and highlighted would be merged into a single class list or if they would override each other. I would much rather see this fail at compile time than to rely on a custom collation scheme that treats different attributes differently. (Btw, I had a special-case for class that was using String varargs but found it to be too complicated it in the end.)

That said, I think we can do much better in terms of attribute types, e.g. have an enum for role so you could write div(class: "highlighted", role: .button) { … } but also div(class: "highlighted", role: .custom("not-standard-yet")) { … }.

As an aside, I would be curious how the patterns these various libraries employ affects the compile time of templates written in them.

kevinrenskers commented 3 years ago

Interesting, do find yourself often using attributes beyond id, class, href, src? My feeling is that the top 5% of attributes would cover 95% of my usage and I thus rarely rely on auto-correct to guide me.

No, but as soon as you open a parenthesis that massive function signature hits you in the head haha. I agree that keeping them ordered alphabetically at least makes it predictable what should go where, to reduce the amount of compiler errors about "this should go before that".

Good point on duplicate attributes. I looked at how other libraries deal with that situation and it's not all good.

swift-html:

.div(
  attributes: [.class("article"), .class("article2")],
  .p("Hello there")
)

results in <div class="article" class="article2"><p>Hello there</p></div> - not great!

Plot simply overrides the earlier given attribute. Vaux returns duplicate attributes just like swift-html does.

chriseidhof commented 3 years ago

We ported a whole bunch of stuff to Swim and I did not find the current API hard to work with. I like the simplicity of it (but yes, the long signature can be a bit daunting and does make autocomplete much less useful).

WRT to duplicate attributes: you could easily check this when generating the HTML, but it's nice that it's not a problem.