3

I have one Map which contains some attributes, and I'd like to create a xml element which has these attributes.

e.g. config is scala Map, and I want to use it as following, but actually the following doesn't work. Could anyone help me ? Thanks

<input type="text" {config.map(entry => entry._1 + "=" + entry._2)}></input>
OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
zjffdu
  • 25,496
  • 45
  • 109
  • 159

2 Answers2

1

Use the low-level API. This is what's generated by scalac parser when desugaring your xml literals:

import scala.xml.{UnprefixedAttribute, MetaData, TopScope, Text, Elem, Null}

val config: Map[String, String] = Map("k1" -> "v2", "k2" -> "v2")

val seed: MetaData = Null // Don't ask.
val meta: MetaData = config.toList.foldLeft(seed) {
  case (acc, (s1, s2)) =>
    new UnprefixedAttribute(
      key   = s1,
      value = s2,
      next  = acc
    )
}

Elem(
  prefix        = null,
  label         = "input",
  attributes    = meta,
  scope         = TopScope,
  minimizeEmpty = false
)
OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
  • This is a lot of dirty laundry. Maybe Scala could parse the `input` element, and then you could append the attributes? Such code might be simpler. – ashawley May 04 '17 at 19:48
  • Indeed, your solution is much cleaner :). The point I wanted to make here is that it's possible to understand the scala.xml APIs and use them directly. Despite a few weird cornes, they are pretty simple and well documented. – OlivierBlanvillain May 05 '17 at 07:35
  • Yes, that's true. Looking at it for a while now, I guess it's not as complex as I thought it was at first. – ashawley May 05 '17 at 13:00
1

Bummer you can't write what you wanted. I guess the Scala compiler's XML literal parser isn't sophisticated enough to parse XML literals with expressions just inserted anywhere. They can only be between tags or as individual attribute values.

As a workaround, it is possible to create the element with no attributes, or just the one attribute and the rest missing:

val elem = <input type="text"/>

Then you can add the attributes from your Map with a couple of copy constructors.

elem.copy(
  attributes = elem.attributes.copy(
    config.foldLeft(scala.xml.Null: scala.xml.MetaData) {
      case (next, (k, v)) => scala.xml.Attribute(k, scala.xml.Text(v), next)
    }
))
res1: scala.xml.Elem = <input type="text" k2="v2" k1="v2"/>

The above uses the copy constructor for the Elem and then one for the attributes.

There's also the option of just using one copy constructor and folding a little differently:

elem.copy(attributes = config.foldRight(elem.attributes) {
  case ((k, v), next) => scala.xml.Attribute(k, scala.xml.Text(v), next)
})
res2: scala.xml.Elem = <input k1="v2" k2="v2" type="text"/>

That might be the simplest option.

Although, Olivier's solution does show that UnprefixedAttributed will accept a String for an attribute value.

elem.copy(attributes = config.foldRight(elem.attributes) {
  case ((k, v), next) => new scala.xml.UnprefixedAttribute(k, v, next)
})
res3: scala.xml.Elem = <input k1="v2" k2="v2" type="text"/>

That avoids having to make a Text element so that you provide a NodeSeq value for Attribute.

ashawley
  • 4,195
  • 1
  • 27
  • 40