Skip to content

[WIP] Dom component #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 59 additions & 17 deletions dom/src/main/scala/com/thoughtworks/binding/dom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -536,13 +536,15 @@ object dom {
case Some(id) => TermName(NameTransformer.encode(id))
}

val attributeMountPoints = for {
(key, value) <- attributes if {
key match {
case UnprefixedName("local-id") => false
case _ => true
}
val domAttributes = attributes.withFilter {
case (key, value) => key match {
case UnprefixedName("local-id") => false
case _ => true
}
}

val attributeMountPoints = for {
(key, value) <- domAttributes
} yield {
val attributeAccess = propertyAccess(key, q"$elementName")

Expand Down Expand Up @@ -574,20 +576,37 @@ object dom {
}
}
}
val (valDefs, transformedChild) = children match {
case Seq() =>
Queue.empty -> Nil

val componentParams = for {
(key, value) <- domAttributes
} yield {
val attributePropertyName = TermName(componentPropertyName(key))

atPos(value.pos) {
value match {
case EmptyAttribute() =>
q"""$attributePropertyName = _root_.com.thoughtworks.binding.Binding.Constant.apply("")"""
case Text(textLiteral) =>
q"""$attributePropertyName = _root_.com.thoughtworks.binding.Binding.Constant.apply($textLiteral)"""
case _ =>
q"""$attributePropertyName = ${transform(value)}"""
}
}
}

val (valDefs, transformedChildrenBuffer) = nodeSeq(children)
val transformedChild = children match {
case Seq() => Nil
case _ =>
val (valDefs, transformedBuffer) = nodeSeq(children)
valDefs -> List(atPos(tree.pos) {
List(atPos(tree.pos) {
q"""
_root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.each[
_root_.com.thoughtworks.binding.Binding,
_root_.scala.Unit
](
_root_.com.thoughtworks.binding.dom.Runtime.mount(
$elementName,
$transformedBuffer
$transformedChildrenBuffer
)
)
"""
Expand All @@ -596,19 +615,32 @@ object dom {

val tagAccess = propertyAccess(tag, q"_root_.com.thoughtworks.binding.dom.Runtime.TagsAndTags2")

val elementDef = q"val $elementName = $tagAccess.render"
val (elementDef, elementProcessingSteps) = if(tag == UnprefixedName("dialog")) {
val elementDef = children match {
case Seq() => q"val $elementName = $tagAccess(..$componentParams).bind"
case _ => q"val $elementName = $tagAccess(..$componentParams, children = $transformedChildrenBuffer).bind"
}

elementDef -> Nil
} else {
q"val $elementName = $tagAccess.render" -> List(
q"""
..$transformedChild
..$attributeMountPoints
"""
)
}

idOption match {
case None =>
valDefs -> q"""
$elementDef
..$transformedChild
..$attributeMountPoints
..$elementProcessingSteps
$elementName
"""
case Some(id) =>
(valDefs.enqueue(elementDef)) -> q"""
..$transformedChild
..$attributeMountPoints
..$elementProcessingSteps
$elementName
"""
}
Expand All @@ -631,6 +663,16 @@ object dom {
}
}

private def componentPropertyName(xmlName: QName): String = {
xmlName match {
case UnprefixedName(localPart) =>
NameTransformer.encode(localPart)
case PrefixedName(prefix, localPart) =>
val name = localPart.split(':').map(_.capitalize).mkString(prefix, "", "")
NameTransformer.encode(name)
}
}

private def transformed: PartialFunction[Tree, Tree] = {
case Block(stats, expr) =>
super.transform(Block(stats.flatMap {
Expand Down
134 changes: 134 additions & 0 deletions dom/src/test/scala/com/thoughtworks/binding/DomComponentTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.thoughtworks.binding

import com.thoughtworks.binding.Binding.{BindingSeq, Constant}
import com.thoughtworks.binding.dom.Runtime.TagsAndTags2
import org.scalajs.dom.Node
import org.scalatest.{FreeSpec, Matchers}
import org.scalajs.dom.html.Div

/**
* @author Leonid Turnaev &lt;lmars@mail.ru&gt;
*/
class DomComponentTest extends FreeSpec with Matchers {

"without children" - {
implicit final class UserTags(x: TagsAndTags2.type) {
@dom
def dialog(): Binding[Div] = <div class="dialog"/>
}

"can be self closed tag" in {
@dom val html = <div><dialog/></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"/></div>""")
}

"can be empty tag" in {
@dom val html = <div><dialog></dialog></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"/></div>""")
}

"should not compile with children" in {
assertDoesNotCompile("@dom val html = <div><dialog><div/></dialog></div>")
}

"also should not compile with child text node" in {
assertDoesNotCompile("@dom val html = <div><dialog> </dialog></div>")
}
}

"with children" - {
implicit final class UserTags(x: TagsAndTags2.type) {
@dom
def dialog(children: BindingSeq[Node]): Binding[Div] = <div class="dialog">{children}</div>
}

"can have single child" in {
@dom val html = <div><dialog><div/></dialog></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"><div/></div></div>""")
}

"can have more than one child" in {
@dom val html = <div><dialog><div/><button><i>OK</i></button></dialog></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"><div/><button><i>OK</i></button></div></div>""")
}

"can have child text node" in {
@dom val html = <div><dialog> </dialog></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"> </div></div>""")
}

"should not compile as self closed tag" in {
assertDoesNotCompile("@dom val html = <div><dialog/></div>")
}

"should not compile as empty tag" in {
assertDoesNotCompile("@dom val html = <div><dialog></dialog></div>")
}
}

"dom component" - {
implicit final class UserTags(x: TagsAndTags2.type) {
@dom
def dialog(): Binding[Div] = <div class="dialog"/>

@dom
def dialog(id: Binding[String]): Binding[Div] = <div class="dialog" id={id.bind}/>

@dom
def dialog(children: BindingSeq[Node], id: Binding[String] = Constant("dialog")): Binding[Div] = <div class="rich-dialog" id={id.bind}>{children}</div>
}

"can be used as child" in {
@dom val html = <div><dialog/></div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"/></div>""")
}

"can be used standalone" in {
@dom val html = <dialog/>
html.watch()

assert(html.get.outerHTML == """<div class="dialog"/>""")
}

"can be used in sequence" in {
@dom val components = <dialog/><dialog/>
@dom val html = <div>{components.bind}</div>
html.watch()

assert(html.get.outerHTML == """<div><div class="dialog"/><div class="dialog"/></div>""")
}

"should support attributes" in {
@dom val html = <dialog id="123"/>
html.watch()

assert(html.get.outerHTML == """<div id="123" class="dialog"/>""")
}

"should support overloading" in {
@dom val html = <dialog id="123"><div/></dialog>
html.watch()

assert(html.get.outerHTML == """<div id="123" class="rich-dialog"><div/></div>""")
}

"should support defaults" in {
@dom val html = <dialog><div/></dialog>
html.watch()

assert(html.get.outerHTML == """<div id="dialog" class="rich-dialog"><div/></div>""")
}
}
}