diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml
index 7796cccf..3bc8b50a 100644
--- a/.github/workflows/scala.yml
+++ b/.github/workflows/scala.yml
@@ -13,7 +13,10 @@ jobs:
fail-fast: false
matrix:
scala:
- - 2.12.8
+ - 2.13.4
+ sbt-args:
+ - -J-XX:MaxRAMPercentage=90.0 --addPluginSbtFile=project/plugins.sbt.scala-js.0.6
+ - -J-XX:MaxRAMPercentage=90.0
steps:
- uses: actions/checkout@v2
@@ -33,12 +36,13 @@ jobs:
~/.sbt/
~/.coursier/
key: |
- ${{ runner.os }}-${{matrix.scala}}-${{ hashFiles('**/*.sbt') }}
- ${{ runner.os }}-${{matrix.scala}}-
+ ${{runner.os}}-${{matrix.scala}}-${{hashFiles('**/*.sbt')}}-${{matrix.sbt-args}}
+ ${{runner.os}}-${{matrix.scala}}-${{hashFiles('**/*.sbt')}}-
+ ${{runner.os}}-${{matrix.scala}}-
- name: Run tests
- run: sbt ++${{ matrix.scala }} test
+ run: sbt ${{matrix.sbt-args}} ++${{matrix.scala}} test
- name: Publish to Maven Central Repository
env:
GITHUB_PERSONAL_ACCESS_TOKEN: ${{secrets.PERSONAL_ACCESS_TOKEN}}
if: ${{ env.GITHUB_PERSONAL_ACCESS_TOKEN != '' }}
- run: sbt ++${{ matrix.scala }} "set every Seq(sonatypeSessionName := \"${{github.workflow}} ${{github.run_id}}-${{github.run_number}}-${{github.run_attempt}}-$$ ${{ matrix.scala }}\", publishTo := sonatypePublishToBundle.value)" publishSigned sonatypeBundleRelease
+ run: sbt ${{matrix.sbt-args}} ++${{ matrix.scala }} "set every Seq(sonatypeSessionName := \"${{github.workflow}} ${{github.run_id}}-${{github.run_number}}-${{github.run_attempt}}-$$ ${{ matrix.scala }}\", publishTo := sonatypePublishToBundle.value)" publishSigned sonatypeBundleRelease
diff --git a/.gitignore b/.gitignore
index 5387d4b3..c6011852 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,33 @@
+*.class
+*.log
+*.sjsir
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
+.cache-*
+
+# IntelliJ specific
+.idea/
+.classpath
+.project
+.settings/
+
+# jenv
+.java-version
+
+# Files created for deployment
local.sbt
secret/
.metals/
diff --git a/.sbtopts b/.sbtopts
new file mode 100644
index 00000000..407b6723
--- /dev/null
+++ b/.sbtopts
@@ -0,0 +1,2 @@
+-J-Xss5m
+-J-XX:MaxMetaspaceSize=9999g
diff --git a/.scalafmt.conf b/.scalafmt.conf
index a6300edd..046b4054 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,3 +1,3 @@
runner.dialect = "scala213"
-version = "3.1.1"
+version = "3.3.3"
maxColumn = 120
diff --git a/Binding/.js/build.sbt b/Binding/.js/build.sbt
new file mode 120000
index 00000000..570a7b60
--- /dev/null
+++ b/Binding/.js/build.sbt
@@ -0,0 +1 @@
+../build.sbt.shared
\ No newline at end of file
diff --git a/Binding/.jvm/build.sbt b/Binding/.jvm/build.sbt
new file mode 120000
index 00000000..570a7b60
--- /dev/null
+++ b/Binding/.jvm/build.sbt
@@ -0,0 +1 @@
+../build.sbt.shared
\ No newline at end of file
diff --git a/Binding/build.sbt.shared b/Binding/build.sbt.shared
new file mode 100644
index 00000000..4ec9a4ce
--- /dev/null
+++ b/Binding/build.sbt.shared
@@ -0,0 +1,15 @@
+enablePlugins(Example)
+
+description := "Reactive data-binding for Scala. This artifact is available for both Scala.js and JVM."
+
+libraryDependencies += "com.thoughtworks.enableIf" %% "enableif" % "1.1.7"
+
+libraryDependencies += "com.thoughtworks.sde" %%% "core" % "3.3.4"
+
+libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.10" % Test
+
+libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
+
+libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided
+
+scalacOptions += "-Ymacro-annotations"
diff --git a/Binding/src/main/scala/com/thoughtworks/binding/Binding.scala b/Binding/src/main/scala/com/thoughtworks/binding/Binding.scala
new file mode 100644
index 00000000..104d8f10
--- /dev/null
+++ b/Binding/src/main/scala/com/thoughtworks/binding/Binding.scala
@@ -0,0 +1,2254 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package com.thoughtworks.binding
+
+import java.util.EventObject
+
+import com.thoughtworks.sde.core.MonadicFactory._
+import com.thoughtworks.enableMembersIf
+import com.thoughtworks.sde.core.MonadicFactory
+
+import scala.annotation.meta.companionMethod
+import scala.annotation.tailrec
+import scala.language.implicitConversions
+import scala.language.higherKinds
+import scala.collection.SeqView
+import scala.collection.mutable.Buffer
+import scala.collection.View
+import scalaz.{Monad, MonadPlus}
+
+import scala.language.experimental.macros
+import scala.language.existentials
+import scala.collection.SeqOps
+import scala.collection.mutable.ArrayBuffer
+
+/** @groupname typeClasses
+ * Type class instance
+ * @groupname implicits
+ * Implicits Conversions
+ * @groupname expressions
+ * Binding Expressions
+ * @groupdesc expressions
+ * AST nodes of binding expressions
+ * @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+object Binding extends MonadicFactory.WithTypeClass[Monad, Binding] {
+
+ sealed trait Watchable[+A] {
+ def watch(): Unit
+ def unwatch(): Unit
+ }
+
+ private[binding] type SeqOpsIterable[+A] = Iterable[A] with SeqOps[A, CC, CC[A]] forSome {
+ type CC[+A] <: Iterable[A]
+ }
+
+ private[binding] def addChangedListener[A](binding: Binding[A], listener: ChangedListener[A]) = {
+ binding.addChangedListener(listener)
+ }
+
+ private[binding] def removeChangedListener[A](binding: Binding[A], listener: ChangedListener[A]) = {
+ binding.removeChangedListener(listener)
+ }
+
+ override val typeClass = BindingInstances
+
+ @enableMembersIf(c => !c.compilerSettings.exists(_.matches("""^-Xplugin:.*scalajs-compiler_[0-9\.\-]*\.jar$""")))
+ private[Binding] object Jvm {
+
+ type ConstantsData[+A] = Seq[A]
+
+ @inline
+ def toConstantsData[A](seq: IterableOnce[A]) = Seq.from(seq)
+
+ def toCacheData[A](seq: collection.Iterable[A]) = Vector.from(seq)
+
+ def emptyCacheData[A]: HasCache[A]#Cache = Vector.empty
+
+ trait HasCache[A] {
+
+ private[Binding] type Cache = Vector[A]
+
+ private[Binding] var cacheData: Cache
+
+ @inline
+ private[Binding] final def getCache(n: Int): A = cacheData(n)
+
+ @inline
+ private[Binding] final def updateCache(n: Int, newelem: A): Unit = {
+ cacheData = cacheData.updated(n, newelem)
+ }
+
+ @inline
+ private[Binding] final def cacheLength: Int = cacheData.length
+
+ @inline
+ private[Binding] final def clearCache(): Unit = {
+ cacheData = Vector.empty
+ }
+
+ private[Binding] final def removeCache(n: Int): A = {
+ val result = cacheData(n)
+ cacheData = cacheData.patch(n, Nil, 1)
+ result
+ }
+
+ private[Binding] final def removeCache(idx: Int, count: Int): Unit = {
+ cacheData = cacheData.patch(idx, Nil, count)
+ }
+
+ private[Binding] final def appendCache(elements: IterableOnce[A]): Seq[A] = {
+ val seq = Seq.from(elements)
+ cacheData = cacheData ++ seq
+ seq
+ }
+
+ private[Binding] final def appendCache(elem: A): Unit = {
+ cacheData = cacheData :+ elem
+ }
+
+ private[Binding] final def prependCache(elem: A): Unit = {
+ cacheData = elem +: cacheData
+ }
+
+ private[Binding] final def insertCache(n: Int, elems: IterableOnce[A]): Seq[A] = {
+ val seq = Seq.from(elems)
+ cacheData = cacheData.patch(n, seq, 0)
+ seq
+ }
+
+ private[Binding] final def insertOneCache(n: Int, elem: A): Seq[A] = {
+ val seq = Seq(elem)
+ cacheData = cacheData.patch(n, seq, 0)
+ seq
+ }
+
+ private[Binding] final def cacheIterator: Iterator[A] = {
+ cacheData.iterator
+ }
+
+ private[Binding] final def spliceCache(from: Int, mappedNewChildren: IterableOnce[A], replaced: Int) = {
+ val oldCache = cacheData
+ if (from == 0) {
+ cacheData = mappedNewChildren ++: oldCache.drop(replaced)
+ } else {
+ cacheData = oldCache.patch(from, mappedNewChildren, replaced)
+ }
+ oldCache.view.slice(from, from + replaced)
+ }
+
+ private[Binding] final def indexOfCache[B >: A](a: B): Int = {
+ cacheData.indexOf(a)
+ }
+
+ }
+
+ }
+
+ @enableMembersIf(c => c.compilerSettings.exists(_.matches("""^-Xplugin:.*scalajs-compiler_[0-9\.\-]*\.jar$""")))
+ private[Binding] object Js {
+
+ type ConstantsData[+A] = scalajs.js.Array[_ <: A]
+
+ @inline
+ def toConstantsData[A](seq: IterableOnce[A]) = {
+ import scalajs.js.JSConverters._
+ seq.toJSArray
+ }
+
+ @inline
+ def toCacheData[A](seq: collection.Iterable[A]) = {
+ import scalajs.js.JSConverters._
+ seq.toJSArray
+ }
+
+ @inline
+ def emptyCacheData[A]: HasCache[A]#Cache = scalajs.js.Array()
+
+ trait HasCache[A] {
+
+ private[Binding] type Cache = scalajs.js.Array[A]
+
+ private[Binding] def cacheData: Cache
+
+ @inline
+ private[Binding] final def getCache(n: Int): A = cacheData(n)
+
+ @inline
+ private[Binding] final def updateCache(n: Int, newelem: A): Unit = {
+ cacheData(n) = newelem
+ }
+
+ @inline
+ private[Binding] final def cacheLength: Int = cacheData.length
+
+ @inline
+ private[Binding] final def clearCache(): Unit = {
+ cacheData.length = 0
+ }
+
+ @inline
+ private[Binding] final def removeCache(n: Int): A = {
+ cacheData.remove(n)
+ }
+
+ private[Binding] final def removeCache(idx: Int, count: Int): Unit = {
+ cacheData.remove(idx, count)
+ }
+
+ @inline
+ private[Binding] final def appendCache(elements: IterableOnce[A]): Seq[A] = {
+ val seq = Seq.from(elements)
+ cacheData ++= seq
+ seq
+ }
+
+ @inline
+ private[Binding] final def appendCache(elem: A): Unit = {
+ cacheData += elem
+ }
+
+ @inline
+ private[Binding] final def prependCache(elem: A): Unit = {
+ cacheData.unshift(elem)
+ }
+
+ @inline
+ private[Binding] final def insertOneCache(n: Int, elem: A): Seq[A] = {
+ cacheData.insert(n, elem)
+ Seq(elem)
+ }
+
+ @inline
+ private[Binding] final def insertCache(n: Int, elems: IterableOnce[A]): Seq[A] = {
+ val seq = Seq.from(elems)
+ cacheData.insertAll(n, elems)
+ seq
+ }
+
+ @inline
+ private[Binding] final def cacheIterator: Iterator[A] = {
+ cacheData.iterator
+ }
+
+ @inline
+ private[Binding] final def spliceCache(from: Int, mappedNewChildren: Cache, replaced: Int) = {
+ cacheData.splice(from, replaced, scalajs.runtime.toScalaVarArgs(mappedNewChildren): _*)
+ }
+
+ @inline
+ private[Binding] final def spliceCache(from: Int, mappedNewChildren: IterableOnce[A], replaced: Int) = {
+ cacheData.splice(from, replaced, Seq.from(mappedNewChildren): _*)
+ }
+
+ @inline
+ private[Binding] final def indexOfCache(a: A): Int = {
+ cacheData.indexOf(a)
+ }
+
+ }
+
+ }
+
+ import Js._
+ import Jvm._
+
+ final class ChangedEvent[+Value](source: Binding[Value], val newValue: Value) extends EventObject(source) {
+ override def getSource = super.getSource.asInstanceOf[Binding[Value]]
+ override def toString = raw"""ChangedEvent[source=$source newValue=$newValue]"""
+
+ }
+
+ final class PatchedEvent[+Element](
+ source: BindingSeq[Element],
+ val from: Int,
+ val that: SeqOpsIterable[Element],
+ val replaced: Int
+ ) extends EventObject(source) {
+ override def getSource = super.getSource.asInstanceOf[BindingSeq[Element]]
+ override def toString = raw"""PatchedEvent[source=$source from=$from that=$that replaced=$replaced]"""
+ }
+
+ trait ChangedListener[-Value] {
+ def changed(event: ChangedEvent[Value]): Unit
+ }
+
+ trait PatchedListener[-Element] {
+ def patched(event: PatchedEvent[Element]): Unit
+ }
+
+ /** A data binding expression that never changes.
+ *
+ * @group expressions
+ */
+ final case class Constant[+A](override val value: A) extends Binding[A] {
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[A]): Unit = {
+ // Do nothing because this Constant never changes
+ }
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[A]): Unit = {
+ // Do nothing because this Constant never changes
+ }
+ }
+
+ /** @group expressions
+ */
+ object Var {
+ @inline
+ def apply[A](initialValue: A) = new Var(initialValue)
+ }
+
+ /** Source variable of data binding expression.
+ *
+ * You can manually change the value:
+ *
+ * {{{
+ * import com.thoughtworks.binding.Binding.Var
+ * val bindingVar = Var("initial value")
+ * bindingVar.value = "changed value"
+ * }}}
+ *
+ * Then, any data binding expressions that depend on this [[Var]] will be changed automatically.
+ *
+ * @group expressions
+ */
+ final class Var[A](private var cache: A) extends Binding[A] {
+
+ private val publisher = new SafeBuffer[ChangedListener[A]]
+
+ @inline
+ override def value = cache
+
+ /** Changes the current value of this [[Var]], and reevaluates any expressions that depends on this [[Var]].
+ *
+ * @note
+ * This method must not be invoked inside a `@dom` method body or a `Binding { ... }` block.
+ */
+ def value_=(newValue: A): Unit = {
+ if (cache.isInstanceOf[View[_]] || cache != newValue) {
+ cache = newValue
+ val event = new ChangedEvent(this, newValue)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ }
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[A]): Unit = {
+ publisher.-=(listener)
+ }
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[A]): Unit = {
+ publisher.+=(listener)
+ }
+ }
+
+ /** @group expressions
+ */
+ final class Map[A, B](upstream: Binding[A], f: A => B) extends Binding[B] with ChangedListener[A] {
+
+ private val publisher = new SafeBuffer[ChangedListener[B]]
+
+ private var cache: B = _
+
+ private def refreshCache() = {
+ cache = f(upstream.value)
+ }
+
+ @inline
+ override protected def value: B = {
+ cache
+ }
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[B]): Unit = {
+ if (publisher.isEmpty) {
+ upstream.addChangedListener(this)
+ refreshCache()
+ }
+ publisher.+=(listener)
+ }
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[B]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ upstream.removeChangedListener(this)
+ }
+ }
+
+ override final def changed(upstreamEvent: ChangedEvent[A]): Unit = {
+ val oldCache = cache
+ val newCache = f(upstreamEvent.newValue)
+ cache = newCache
+ if (oldCache.isInstanceOf[View[_]] || oldCache != newCache) {
+ val event = new ChangedEvent(Map.this, newCache)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ }
+ }
+
+ private val ReentryDetector = new Binding[Nothing] {
+ protected def throwException(): Nothing =
+ throw new IllegalStateException(
+ "Must not change an upstream value in a data binding expression that depends on the same upstream value!"
+ )
+ def value: Nothing = throwException()
+
+ protected def removeChangedListener(listener: ChangedListener[Nothing]): Unit = throwException()
+
+ protected def addChangedListener(listener: ChangedListener[Nothing]): Unit = throwException()
+ }
+
+ /** @group expressions
+ */
+ final class FlatMap[A, B](upstream: Binding[A], f: A => Binding[B]) extends Binding[B] with ChangedListener[B] {
+
+ private val publisher = new SafeBuffer[ChangedListener[B]]
+
+ private val forwarder = new ChangedListener[A] {
+
+ override final def changed(upstreamEvent: ChangedEvent[A]): Unit = {
+ val oldCache = cache
+ oldCache.removeChangedListener(FlatMap.this)
+ val newCache = f(upstreamEvent.newValue)
+ cache = newCache
+ newCache.addChangedListener(FlatMap.this)
+ if (oldCache.isInstanceOf[View[_]] || oldCache.value != newCache.value) {
+ val event = new ChangedEvent(FlatMap.this, newCache.value)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ }
+ }
+
+ @inline
+ override def changed(upstreamEvent: ChangedEvent[B]) = {
+ val event = new ChangedEvent(FlatMap.this, upstreamEvent.newValue)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+
+ @inline
+ override def addChangedListener(listener: ChangedListener[B]): Unit = {
+ if (publisher.isEmpty) {
+ upstream.addChangedListener(forwarder)
+ refreshCache()
+ cache.addChangedListener(this)
+ }
+ publisher.+=(listener)
+ }
+
+ private var cache: Binding[B] = ReentryDetector
+
+ private def refreshCache() = {
+ cache = f(upstream.value)
+ }
+
+ override protected def value: B = {
+ @tailrec
+ @inline
+ def tailrecGetValue(binding: Binding[B]): B = {
+ binding match {
+ case flatMap: FlatMap[_, B] => tailrecGetValue(flatMap.cache)
+ case _ => binding.value
+ }
+ }
+ tailrecGetValue(cache)
+ }
+
+ override protected def removeChangedListener(listener: ChangedListener[B]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ upstream.removeChangedListener(forwarder)
+ cache.removeChangedListener(this)
+ }
+ }
+
+ }
+
+ /** Monad instances for [[Binding]].
+ *
+ * @group typeClasses
+ */
+ implicit object BindingInstances extends Monad[Binding] {
+
+ override def map[A, B](fa: Binding[A])(f: A => B): Binding[B] = {
+ fa match {
+ case Constant(a) =>
+ Constant(f(a))
+ case _ =>
+ new Map[A, B](fa, f)
+ }
+ }
+
+ override def bind[A, B](fa: Binding[A])(f: A => Binding[B]): Binding[B] = {
+ fa match {
+ case Constant(a) =>
+ f(a)
+ case _ =>
+ new FlatMap[A, B](fa, f)
+ }
+ }
+
+ @inline
+ override def point[A](a: => A): Binding[A] = Constant(a)
+
+ override def ifM[B](value: Binding[Boolean], ifTrue: => Binding[B], ifFalse: => Binding[B]): Binding[B] = {
+ bind(value)(if (_) ifTrue else ifFalse)
+ }
+
+ override def whileM[G[_], A](p: Binding[Boolean], body: => Binding[A])(implicit G: MonadPlus[G]): Binding[G[A]] = {
+ ifM(p, bind(body)(x => map(whileM(p, body))(xs => G.plus(G.point(x), xs))), point(G.empty))
+ }
+
+ override def whileM_[A](p: Binding[Boolean], body: => Binding[A]): Binding[Unit] = {
+ ifM(p, bind(body)(_ => whileM_(p, body)), point(()))
+ }
+
+ override def untilM[G[_], A](f: Binding[A], cond: => Binding[Boolean])(implicit G: MonadPlus[G]): Binding[G[A]] = {
+ bind(f)(x => map(whileM(map(cond)(!_), f))(xs => G.plus(G.point(x), xs)))
+ }
+
+ override def untilM_[A](f: Binding[A], cond: => Binding[Boolean]): Binding[Unit] = {
+ bind(f)(_ => whileM_(map(cond)(!_), f))
+ }
+
+ }
+
+ private[Binding] class Macros(val c: scala.reflect.macros.blackbox.Context) {
+
+ import c.universe._
+
+ lazy val functionOrFunctionLiteral: PartialFunction[Tree, (List[ValDef], Tree)] = {
+ case Function(vparams, body) =>
+ (vparams, body)
+ case f =>
+ val elementName = TermName(c.freshName("bindingElement"))
+ (List(q"val $elementName: ${TypeTree()} = $EmptyTree"), atPos(f.pos)(q"$f($elementName)"))
+ }
+
+ final def foreach(f: Tree): Tree = {
+ val apply @ Apply(
+ TypeApply(Select(self, TermName("foreach")), List(u)),
+ List(f @ functionOrFunctionLiteral(vparams, body))
+ ) =
+ c.macroApplication
+ val monadicBody =
+ q"""_root_.com.thoughtworks.binding.Binding.apply[$u]($body)"""
+ val monadicFunction = atPos(f.pos)(Function(vparams, monadicBody))
+ atPos(apply.pos)(
+ q"""_root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.each[
+ _root_.com.thoughtworks.binding.Binding,
+ _root_.scala.Unit
+ ]($self.foreachBinding[$u]($monadicFunction))"""
+ )
+ }
+
+ final def map(f: Tree): Tree = {
+ val apply @ Apply(
+ TypeApply(Select(self, TermName("map")), List(b)),
+ List(f @ functionOrFunctionLiteral(vparams, body))
+ ) =
+ c.macroApplication
+ val monadicBody =
+ q"""_root_.com.thoughtworks.binding.Binding.apply[$b]($body)"""
+ val monadicFunction = atPos(f.pos)(Function(vparams, monadicBody))
+ atPos(apply.pos)(q"""$self.mapBinding[$b]($monadicFunction)""")
+ }
+
+ final def flatMap(f: Tree): Tree = {
+ val apply @ Apply(
+ TypeApply(Select(self, TermName("flatMap")), List(b)),
+ List(f @ functionOrFunctionLiteral(vparams, body))
+ ) =
+ c.macroApplication
+ val monadicBody =
+ q"""_root_.com.thoughtworks.binding.Binding.apply[_root_.com.thoughtworks.binding.Binding.BindingSeq[$b]]($body)"""
+ val monadicFunction = atPos(f.pos)(Function(vparams, monadicBody))
+ atPos(apply.pos)(q"""$self.flatMapBinding[$b]($monadicFunction)""")
+ }
+
+ final def withFilter(condition: Tree): Tree = {
+ val apply @ Apply(Select(self, TermName("withFilter")), List(f @ functionOrFunctionLiteral(vparams, body))) =
+ c.macroApplication
+ val monadicBody =
+ q"""_root_.com.thoughtworks.binding.Binding.apply[_root_.scala.Boolean]($body)"""
+ val monadicFunction = atPos(f.pos)(Function(vparams, monadicBody))
+ atPos(apply.pos)(q"""$self.withFilterBinding($monadicFunction)""")
+ }
+
+ final def bind: Tree = {
+ val q"$binding.$methodName" = c.macroApplication
+ q"""_root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.each[
+ _root_.com.thoughtworks.binding.Binding,
+ ${TypeTree(c.macroApplication.tpe)}
+ ]($binding)"""
+ }
+
+ }
+
+ private[binding] case class SingleSeq[+A](element: A) extends IndexedSeq[A] {
+
+ @inline
+ override final def length: Int = 1
+
+ @inline
+ override final def apply(idx: Int) = {
+ if (idx == 0) {
+ element
+ } else {
+ throw new IndexOutOfBoundsException
+ }
+ }
+
+ @inline
+ override final def iterator = Iterator.single(element)
+
+ }
+
+ private[binding] val Empty = new BindingSeq[Nothing] {
+ @inline
+ override protected def removePatchedListener(listener: PatchedListener[Nothing]): Unit = {}
+
+ @inline
+ override protected def addPatchedListener(listener: PatchedListener[Nothing]): Unit = {}
+
+ type All[+A] = List[A]
+
+ @inline
+ override protected def value = Nil
+ }
+
+ private[Binding] abstract class ValueProxy[B] extends SeqView[B] with HasCache[Binding[B]] {
+
+ protected def underlying = cacheData
+
+ @inline
+ override def length: Int = {
+ cacheData.length
+ }
+
+ @inline
+ override def apply(idx: Int): B = {
+ cacheData(idx).value
+ }
+
+ @inline
+ override def iterator: Iterator[B] = {
+ cacheData.iterator.map(_.value)
+ }
+ }
+
+ /** The companion of a data binding expression of a sequence
+ *
+ * @group expressions
+ */
+ object BindingSeq {
+
+ private[binding] final class FlatProxy[B](protected val underlying: collection.Seq[BindingSeq[B]])
+ extends SeqView[B] {
+
+ @inline
+ override def length: Int = {
+ underlying.view.map(_.value.length).sum
+ }
+
+ @inline
+ override def apply(idx: Int): B = {
+ val i = underlying.iterator
+ @tailrec
+ def findIndex(restIndex: Int): B = {
+ if (i.hasNext) {
+ val subSeq = i.next().value
+ val currentLength = subSeq.length
+ if (currentLength > restIndex) {
+ subSeq(restIndex)
+ } else {
+ findIndex(restIndex - currentLength)
+ }
+ } else {
+ throw new IndexOutOfBoundsException()
+ }
+ }
+ findIndex(idx)
+ }
+
+ @inline
+ override def iterator: Iterator[B] = {
+ for {
+ subSeq <- underlying.iterator
+ element <- subSeq.value.iterator
+ } yield element
+ }
+ }
+
+ private[binding] def addPatchedListener[A](binding: BindingSeq[A], listener: PatchedListener[A]) = {
+ binding.addPatchedListener(listener)
+ }
+
+ private[binding] def removePatchedListener[A](binding: BindingSeq[A], listener: PatchedListener[A]) = {
+ binding.removePatchedListener(listener)
+ }
+
+ private[Binding] abstract class MultiMountPoint[-Element](upstream: BindingSeq[Element]) extends MountPoint {
+
+ protected def mount(): Unit = {
+ upstream.addPatchedListener(upstreamListener)
+ set(upstream.value)
+ }
+
+ protected def unmount(): Unit = {
+ upstream.removePatchedListener(upstreamListener)
+ set(Seq.empty)
+ }
+
+ protected def set(children: Iterable[Element]): Unit
+
+ protected def splice(from: Int, that: Iterable[Element], replaced: Int): Unit
+
+ private val upstreamListener = new PatchedListener[Element] {
+
+ @inline
+ override def patched(upstreamEvent: PatchedEvent[Element]): Unit = {
+ splice(upstreamEvent.from, upstreamEvent.that, upstreamEvent.replaced)
+ }
+
+ }
+
+ }
+
+ final class FlatMap[A, B](upstream: BindingSeq[A], f: A => BindingSeq[B])
+ extends BindingSeq[B]
+ with HasCache[BindingSeq[B]] {
+
+ private[Binding] var cacheData: Cache = _
+
+ private def refreshCache() = {
+ cacheData = toCacheData(for {
+ a <- upstream.value /*.view*/
+ } yield f(a))
+ }
+
+ type All[+A] = SeqView[A]
+
+ @inline
+ override protected def value = new FlatProxy(cacheData)
+
+ @inline
+ private def flatIndex(oldCache: Cache, upstreamBegin: Int, upstreamEnd: Int): Int = {
+ oldCache.view.slice(upstreamBegin, upstreamEnd).map(_.value.length).sum
+ }
+
+ private val upstreamListener = new PatchedListener[A] {
+ override def patched(upstreamEvent: PatchedEvent[A]): Unit = {
+ val mappedNewChildren: Cache = toCacheData(for {
+ child <- upstreamEvent.that /*.view*/
+ } yield f(child))
+ val flatNewChildren = new FlatProxy(mappedNewChildren)
+ val flattenFrom = flatIndex(cacheData, 0, upstreamEvent.from)
+ val flattenReplaced = flatIndex(cacheData, upstreamEvent.from, upstreamEvent.from + upstreamEvent.replaced)
+ val oldChildren = spliceCache(upstreamEvent.from, mappedNewChildren, upstreamEvent.replaced)
+ for (newChild <- mappedNewChildren) {
+ newChild.addPatchedListener(childListener)
+ }
+ for (oldChild <- oldChildren) {
+ oldChild.removePatchedListener(childListener)
+ }
+ if (upstreamEvent.replaced != 0 || flatNewChildren.nonEmpty) {
+ val event = new PatchedEvent(FlatMap.this, flattenFrom, flatNewChildren, flattenReplaced)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+ }
+
+ }
+
+ private[binding] val publisher = new SafeBuffer[PatchedListener[B]]
+
+ private val childListener = new PatchedListener[B] {
+ override def patched(upstreamEvent: PatchedEvent[B]): Unit = {
+ val source = upstreamEvent.getSource
+ val index = flatIndex(cacheData, 0, indexOfCache(source)) + upstreamEvent.from
+ val event = new PatchedEvent(FlatMap.this, index, upstreamEvent.that, upstreamEvent.replaced)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+ }
+
+ @inline
+ override protected def removePatchedListener(listener: PatchedListener[B]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ upstream.removePatchedListener(upstreamListener)
+ for (child <- cacheData) {
+ child.removePatchedListener(childListener)
+ }
+ }
+ }
+
+ @inline
+ override protected def addPatchedListener(listener: PatchedListener[B]): Unit = {
+ if (publisher.isEmpty) {
+ upstream.addPatchedListener(upstreamListener)
+ refreshCache()
+ for (child <- cacheData) {
+ child.addPatchedListener(childListener)
+ }
+ }
+ publisher.+=(listener)
+ }
+ }
+
+ private[Binding] final class ForeachBinding[A](upstream: BindingSeq[A], f: A => Binding[Any])
+ extends MountPoint
+ with PatchedListener[A]
+ with HasCache[Binding[Any]] {
+ private[Binding] var cacheData: Cache = _
+
+ private def refreshCache() = {
+ cacheData = toCacheData(for {
+ a <- upstream.value /*.view*/
+ } yield f(a))
+ }
+
+ protected def mount(): Unit = {
+ upstream.addPatchedListener(this)
+ refreshCache()
+ for (child <- cacheData) {
+ child.watch()
+ }
+
+ }
+ protected def unmount(): Unit = {
+ upstream.removePatchedListener(this)
+ for (child <- cacheData) {
+ child.unwatch()
+ }
+
+ }
+ def patched(upstreamEvent: PatchedEvent[A]): Unit = {
+ val mappedNewChildren: Cache = toCacheData(for {
+ child <- upstreamEvent.that /*.view*/
+ } yield f(child))
+ val oldChildren = spliceCache(upstreamEvent.from, mappedNewChildren, upstreamEvent.replaced)
+ for (newChild <- mappedNewChildren) {
+ newChild.watch()
+ }
+ for (oldChild <- oldChildren) {
+ oldChild.unwatch()
+ }
+ }
+ }
+
+ final class MapBinding[A, B](upstream: BindingSeq[A], f: A => Binding[B])
+ extends BindingSeq[B]
+ with HasCache[Binding[B]] {
+
+ private[Binding] var cacheData: Cache = _
+
+ private def refreshCache() = {
+ cacheData = toCacheData(for {
+ a <- upstream.value /*.view*/
+ } yield f(a))
+ }
+
+ type All[+A] = SeqView[A]
+
+ override protected def value: SeqView[B] = {
+ val cacheData0 = cacheData
+ new ValueProxy[B] {
+ var cacheData = cacheData0
+ }
+ }
+
+ private val upstreamListener = new PatchedListener[A] {
+ override def patched(upstreamEvent: PatchedEvent[A]): Unit = {
+ val mappedNewChildren: Cache = toCacheData(for {
+ child <- upstreamEvent.that /*.view*/
+ } yield f(child))
+ val oldChildren = spliceCache(upstreamEvent.from, mappedNewChildren, upstreamEvent.replaced)
+ for (newChild <- mappedNewChildren) {
+ newChild.addChangedListener(childListener)
+ }
+ for (oldChild <- oldChildren) {
+ oldChild.removeChangedListener(childListener)
+ }
+ val proxy = new ValueProxy[B] {
+ var cacheData = mappedNewChildren
+ }
+ val event =
+ new PatchedEvent[B](MapBinding.this, upstreamEvent.from, proxy, upstreamEvent.replaced)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+
+ }
+
+ private[binding] val publisher = new SafeBuffer[PatchedListener[B]]
+
+ private val childListener = new ChangedListener[B] {
+
+ override def changed(event: ChangedEvent[B]): Unit = {
+ val index = indexOfCache(event.getSource)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(MapBinding.this, index, SingleSeq(event.newValue), 1))
+ }
+ }
+ }
+
+ override protected def removePatchedListener(listener: PatchedListener[B]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ upstream.removePatchedListener(upstreamListener)
+ for (child <- cacheData) {
+ child.removeChangedListener(childListener)
+ }
+ }
+ }
+
+ override protected def addPatchedListener(listener: PatchedListener[B]): Unit = {
+ if (publisher.isEmpty) {
+ upstream.addPatchedListener(upstreamListener)
+ refreshCache()
+ for (child <- cacheData) {
+ child.addChangedListener(childListener)
+ }
+ }
+ publisher.+=(listener)
+ }
+
+ }
+
+ private[binding] final class Length(bindingSeq: BindingSeq[_]) extends Binding[Int] with PatchedListener[Any] {
+
+ private val publisher = new SafeBuffer[ChangedListener[Int]]
+
+ @inline
+ override protected def value: Int = bindingSeq.value.length
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[Int]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ bindingSeq.removePatchedListener(this)
+ }
+ }
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[Int]): Unit = {
+ if (publisher.isEmpty) {
+ bindingSeq.addPatchedListener(this)
+ }
+ publisher.+=(listener)
+ }
+
+ @inline
+ override def patched(upstreamEvent: PatchedEvent[Any]): Unit = {
+ val event = new ChangedEvent[Int](this, bindingSeq.value.length)
+ for (subscriber <- publisher) {
+ subscriber.changed(event)
+ }
+ }
+
+ }
+
+ }
+
+ /** Data binding expression of a sequence
+ *
+ * @group expressions
+ */
+ trait BindingSeq[+A] extends Watchable[A] {
+
+ /** Returns a new [[Binding]] expression of all elements in this [[BindingSeq]]. */
+ final def all: Binding[All[A]] = new Binding[All[A]] { asBinding =>
+ private val patchedListener = new PatchedListener[A] {
+ @inline
+ def patched(upstreamEvent: PatchedEvent[A]): Unit = {
+ val event = new ChangedEvent[All[A]](asBinding, asBinding.value)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ }
+ private val publisher = new SafeBuffer[ChangedListener[All[A]]]
+
+ @inline
+ override protected def value: All[A] = BindingSeq.this.value
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[All[A]]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ BindingSeq.this.removePatchedListener(patchedListener)
+ }
+ }
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[All[A]]): Unit = {
+ if (publisher.isEmpty) {
+ BindingSeq.this.addPatchedListener(patchedListener)
+ }
+ publisher.+=(listener)
+ }
+ }
+
+ /** Enables automatic recalculation.
+ *
+ * You may invoke this method more than once. Then, when you want to disable automatic recalculation, you must
+ * invoke [[unwatch]] same times as the number of calls to this method.
+ *
+ * @note
+ * This method is recursive, which means that the dependencies of this [[BindingSeq]] will be watched as well.
+ */
+ @inline
+ final def watch(): Unit = {
+ addPatchedListener(Binding.DummyPatchedListener)
+ }
+
+ /** Disables automatic recalculation.
+ *
+ * @note
+ * This method is recursive, which means that the dependencies of this [[BindingSeq]] will be unwatched as well.
+ */
+ @inline
+ final def unwatch(): Unit = {
+ removePatchedListener(Binding.DummyPatchedListener)
+ }
+
+ /** The value type of [[all]] */
+ type All[+A] <: SeqOpsIterable[A]
+
+ /** Returns the current value of this [[BindingSeq]]. */
+ protected def value: All[A]
+
+ /** Returns the current value of this [[BindingSeq]].
+ *
+ * @note
+ * This method is used for internal testing purpose only.
+ */
+ private[binding] def get: All[A] = value
+
+ protected def removePatchedListener(listener: PatchedListener[A]): Unit
+
+ protected def addPatchedListener(listener: PatchedListener[A]): Unit
+
+ def length: Binding[Int] = new BindingSeq.Length(this)
+
+ def isEmpty: Binding[Boolean] = BindingInstances.map(all)(_.isEmpty)
+
+ def nonEmpty: Binding[Boolean] = BindingInstances.map(all)(_.nonEmpty)
+
+ def foreach[U](f: A => U): Unit = macro Macros.foreach
+
+ /** Returns a [[BindingSeq]] that maps each element of this [[BindingSeq]] via `f`
+ *
+ * @param f
+ * The mapper function, which may contain magic [[Binding#bind bind]] calls.
+ */
+ def map[B](f: A => B): BindingSeq[B] = macro Macros.map
+
+ /** Returns a [[BindingSeq]] that flat-maps each element of this [[BindingSeq]] via `f`
+ *
+ * @param f
+ * The mapper function, which may contain magic [[Binding#bind bind]] calls.
+ */
+ def flatMap[B](f: A => BindingSeq[B]): BindingSeq[B] = macro Macros.flatMap
+
+ /** The underlying implementation of [[foreach]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ def foreachBinding[U](f: A => Binding[U]): Binding[Unit] = {
+ new BindingSeq.ForeachBinding[A](this, f)
+ }
+
+ /** The underlying implementation of [[map]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ final def mapBinding[B](f: A => Binding[B]): BindingSeq[B] = new BindingSeq.MapBinding[A, B](this, f)
+
+ /** The underlying implementation of [[flatMap]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ final def flatMapBinding[B](f: A => Binding[BindingSeq[B]]): BindingSeq[B] = {
+ new BindingSeq.FlatMap[BindingSeq[B], B](new BindingSeq.MapBinding[A, BindingSeq[B]](this, f), locally)
+ }
+
+ /** Returns a view of this [[BindingSeq]] that applied a filter of `condition`
+ *
+ * @param f
+ * The mapper function, which may contain magic [[Binding#bind bind]] calls.
+ */
+ def withFilter(condition: A => Boolean): BindingSeq[A]#WithFilter = macro Macros.withFilter
+
+ /** The underlying implementation of [[withFilter]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ final def withFilterBinding(condition: A => Binding[Boolean]): BindingSeq[A]#WithFilter = {
+ new WithFilter(condition)
+ }
+
+ /** A helper to build complicated comprehension expressions for [[BindingSeq]]
+ */
+ final class WithFilter(condition: A => Binding[Boolean]) {
+
+ /** Returns a [[BindingSeq]] that maps each element of this [[BindingSeq]] via `f`
+ */
+ def map[B](f: A => B): BindingSeq[B] = macro Macros.map
+
+ /** Returns a [[BindingSeq]] that flat-maps each element of this [[BindingSeq]] via `f`
+ */
+ def flatMap[B](f: A => BindingSeq[B]): BindingSeq[B] = macro Macros.flatMap
+
+ /** Returns a view of this [[BindingSeq]] that applied a filter of `condition`
+ */
+ def withFilter(condition: A => Boolean): WithFilter = macro Macros.withFilter
+
+ /** Underlying implementation of [[withFilter.
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ def withFilterBinding(nextCondition: A => Binding[Boolean]): WithFilter = {
+ new WithFilter({ a =>
+ Binding {
+ if (Instructions.each[Binding, Boolean](condition(a))) {
+ Instructions.each[Binding, Boolean](nextCondition(a))
+ } else {
+ false
+ }
+ }
+ })
+ }
+
+ /** Underlying implementation of [[map]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ def mapBinding[B](f: (A) => Binding[B]): BindingSeq[B] = {
+ BindingSeq.this.flatMapBinding { a: A =>
+ Binding {
+ if (Instructions.each[Binding, Boolean](condition(a))) {
+ Constants(Instructions.each[Binding, B](f(a)))
+ } else {
+ Empty
+ }
+ }
+ }
+ }
+
+ /** Underlying implementation of [[flatMap]].
+ *
+ * @note
+ * Don't use this method in user code.
+ */
+ @inline
+ def flatMapBinding[B](f: (A) => Binding[BindingSeq[B]]): BindingSeq[B] = {
+ BindingSeq.this.flatMapBinding { a: A =>
+ Binding {
+ if (Instructions.each[Binding, Boolean](condition(a))) {
+ Instructions.each[Binding, BindingSeq[B]](f(a))
+ } else {
+ Empty
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
+ /** An data binding expression of sequence that never changes.
+ *
+ * @group expressions
+ */
+ final class Constants[+A] private[Binding] (underlying: ConstantsData[A]) extends BindingSeq[A] {
+ type All[+A] = collection.Seq[A]
+
+ @inline
+ override def value: collection.Seq[A] = underlying
+
+ @inline
+ override protected def removePatchedListener(listener: PatchedListener[A]): Unit = {}
+
+ @inline
+ override protected def addPatchedListener(listener: PatchedListener[A]): Unit = {}
+
+ }
+
+ /** @group expressions
+ */
+ object Constants {
+
+ @inline
+ def apply[A](elements: A*) = new Constants(toConstantsData(elements))
+
+ @inline
+ def upapplySeq[A](constants: Constants[A]) = Some(constants.value)
+
+ private final val Empty = Constants[Nothing]()
+
+ @inline
+ def empty[A]: Constants[A] = Empty
+
+ }
+
+ /** @group expressions
+ */
+ object Vars {
+
+ @inline
+ def apply[A](initialValues: A*) = new Vars(toCacheData(initialValues))
+
+ @inline
+ def empty[A] = new Vars(emptyCacheData[A])
+
+ }
+
+ /** Source sequence of data binding expression.
+ *
+ * @group expressions
+ */
+ final class Vars[A] private (private[Binding] var cacheData: HasCache[A]#Cache)
+ extends BindingSeq[A]
+ with HasCache[A] {
+
+ private[binding] val publisher = new SafeBuffer[PatchedListener[A]]
+
+ type All[+A] = Buffer[_ <: A] with SeqOpsIterable[A]
+
+ /** Returns a [[scala.collection.mutable.Buffer]] that allow you change the content of this [[Vars]].
+ *
+ * Whenever you change the returned data, other binding expressions that depend on this [[Vars]] will be
+ * automatically changed.
+ *
+ * @note
+ * This method must not be invoked inside a `@dom` method body or a `Binding { ... }` block..
+ */
+ @inline
+ override def value: Buffer[A] = new Proxy
+
+ private[binding] final class Proxy extends Buffer[A] {
+
+ @inline
+ override def patchInPlace(from: Int, patch: IterableOnce[A], replaced: Int): this.type = {
+ val result = spliceCache(from, patch, replaced)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, from, result, replaced))
+ }
+ this
+ }
+
+ @inline
+ override def apply(n: Int): A = {
+ getCache(n)
+ }
+
+ @inline
+ override def update(n: Int, newelem: A): Unit = {
+ updateCache(n, newelem)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, n, SingleSeq(newelem), 1))
+ }
+ }
+
+ @inline
+ override def clear(): Unit = {
+ val oldLength = cacheLength
+ clearCache()
+ val event = new PatchedEvent[A](Vars.this, 0, List.empty[A], oldLength)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+
+ @inline
+ override def length: Int = {
+ cacheLength
+ }
+
+ @inline
+ override def remove(n: Int): A = {
+ val result = removeCache(n)
+ val event = new PatchedEvent[A](Vars.this, n, List.empty[A], 1)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ result
+ }
+
+ @inline
+ override def remove(idx: Int, count: Int): Unit = {
+ removeCache(idx, count)
+ val event = new PatchedEvent[A](Vars.this, idx, List.empty[A], count)
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+
+ @inline
+ override def addAll(elements: IterableOnce[A]): this.type = {
+ val oldLength = cacheLength
+ val seq = appendCache(elements)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, oldLength, seq, 0))
+ }
+ Proxy.this
+ }
+
+ @inline
+ override def prepend(elem: A): this.type = {
+ prependCache(elem)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, 0, SingleSeq(elem), 0))
+ }
+ Proxy.this
+ }
+
+ @inline
+ override def addOne(elem: A): this.type = {
+ val oldLength = cacheLength
+ appendCache(elem)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, oldLength, SingleSeq(elem), 0))
+ }
+ Proxy.this
+ }
+
+ @inline
+ override def insert(idx: Int, elem: A): Unit = {
+ val seq = insertOneCache(idx, elem)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, idx, seq, 0))
+ }
+
+ }
+
+ @inline
+ override def insertAll(n: Int, elems: IterableOnce[A]): Unit = {
+ val seq = insertCache(n, elems)
+ for (listener <- publisher) {
+ listener.patched(new PatchedEvent(Vars.this, n, seq, 0))
+ }
+ }
+
+ @inline
+ override def iterator: Iterator[A] = {
+ cacheIterator
+ }
+ }
+
+ @inline
+ override protected def removePatchedListener(listener: PatchedListener[A]): Unit = {
+ publisher.-=(listener)
+ }
+
+ @inline
+ override protected def addPatchedListener(listener: PatchedListener[A]): Unit = {
+ publisher.+=(listener)
+ }
+
+ }
+
+ /** A [[BindingSeq]] that contains only one element
+ *
+ * @group expressions
+ */
+ final case class SingletonBindingSeq[A](upstream: Binding[A]) extends BindingSeq[A] {
+
+ private val publisher = new SafeBuffer[PatchedListener[A]]
+
+ private val changedListener = new ChangedListener[A] {
+
+ override def changed(event: ChangedEvent[A]) = {
+ val patchedEvent = new PatchedEvent[A](SingletonBindingSeq.this, 0, SingleSeq(event.newValue), 1)
+ for (listener <- publisher) {
+ listener.patched(patchedEvent)
+ }
+ }
+
+ }
+
+ override def length: Constant[Int] = Constant(1)
+
+ type All[+A] = IndexedSeq[A]
+
+ @inline
+ override protected def value = SingleSeq(upstream.value)
+
+ @inline
+ override protected def removePatchedListener(listener: PatchedListener[A]): Unit = {
+ publisher.-=(listener)
+ if (publisher.isEmpty) {
+ upstream.removeChangedListener(changedListener)
+ }
+ }
+
+ @inline
+ override protected def addPatchedListener(listener: PatchedListener[A]): Unit = {
+ if (publisher.isEmpty) {
+ upstream.addChangedListener(changedListener)
+ }
+ publisher.+=(listener)
+ }
+
+ }
+
+ /** A mechanism that mounts the result of a data binding expression into DOM or other system.
+ *
+ * @group expressions
+ */
+ private[Binding] sealed trait MountPoint extends Binding[Unit] {
+
+ private var referenceCount = 0
+
+ protected def mount(): Unit
+
+ protected def unmount(): Unit
+
+ @inline
+ override protected def addChangedListener(listener: ChangedListener[Unit]): Unit = {
+ if (referenceCount == 0) {
+ mount()
+ }
+ referenceCount += 1
+ }
+
+ @inline
+ override protected def removeChangedListener(listener: ChangedListener[Unit]): Unit = {
+ referenceCount -= 1
+ if (referenceCount == 0) {
+ unmount()
+ }
+ }
+
+ @inline
+ override protected def value: Unit = ()
+
+ }
+
+ /** A mechanism that mounts the result of a data binding expression of a sequence into DOM or other system.
+ *
+ * @group expressions
+ */
+ abstract class MultiMountPoint[-Element](upstream: BindingSeq[Element]) extends BindingSeq.MultiMountPoint(upstream)
+
+ /** A mechanism that mounts the result of a data binding expression of a single value into DOM or other system.
+ *
+ * Use this class only if you must override [[mount]] or [[unmount]]. If you only want to override [[set]], you can
+ * use `Binding[Unit] { onUpstreamChange(upstream.bind) }` instead.
+ *
+ * @group expressions
+ */
+ abstract class SingleMountPoint[-Value](upstream: Binding[Value]) extends MountPoint {
+
+ protected def set(value: Value): Unit
+
+ protected def mount(): Unit = {
+ upstream.addChangedListener(upstreamListener)
+ set(upstream.value)
+ }
+
+ protected def unmount(): Unit = {
+ upstream.removeChangedListener(upstreamListener)
+ }
+
+ private val upstreamListener = new ChangedListener[Value] {
+ @inline
+ override def changed(event: ChangedEvent[Value]): Unit = {
+ set(event.newValue)
+ }
+ }
+
+ }
+
+ private[binding] val DummyPatchedListener = new PatchedListener[Any] {
+ @inline
+ override def patched(event: PatchedEvent[Any]): Unit = {}
+ }
+
+ private[binding] val DummyChangedListener = new ChangedListener[Any] {
+ @inline
+ override def changed(event: ChangedEvent[Any]): Unit = {}
+ }
+
+ private class RxDefer[A](upstream: => Rx.Observable[A]) extends Rx.Observable[A] with ChangedListener[Option[A]] {
+
+ def changed(upstream: ChangedEvent[Option[A]]): Unit = {
+ val event = new ChangedEvent(this, upstream.newValue)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+
+ private var upstreamCache: Rx.Observable[A] = _
+
+ private val publisher = new SafeBuffer[ChangedListener[Option[A]]]
+
+ override protected def value: Option[A] = {
+ if (publisher.isEmpty) {
+ None
+ } else {
+ upstreamCache.value
+ }
+ }
+
+ override protected def removeChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ publisher -= listener
+ if (publisher.isEmpty) {
+ val upstreamLocal = upstreamCache
+ upstreamCache = null
+ upstreamLocal.removeChangedListener(this)
+ }
+ }
+
+ override protected def addChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ if (publisher.isEmpty) {
+ val upstreamLocal = upstream
+ upstreamLocal.addChangedListener(this)
+ upstreamCache = upstreamLocal
+ }
+ publisher += listener
+ }
+
+ }
+
+ private class RxMerge[A](upstream: BindingSeq[A]) extends Rx.Observable[A] with PatchedListener[A] {
+
+ private var cache: Option[A] = None
+
+ override def patched(upstreamEvent: PatchedEvent[A]): Unit = {
+ upstreamEvent.that.headOption match {
+ case None =>
+ if (cache != None && upstream.get.isEmpty) {
+ cache = None
+ val event = new ChangedEvent[Option[A]](this, None)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ case someNew =>
+ if (cache != someNew) {
+ cache = someNew
+ val event = new ChangedEvent[Option[A]](this, someNew)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+ }
+
+ }
+
+ private val publisher = new SafeBuffer[ChangedListener[Option[A]]]
+ protected def value: Option[A] = cache
+ protected def addChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ if (publisher.isEmpty) {
+ BindingSeq.addPatchedListener(upstream, this)
+ cache = upstream.get.headOption
+ }
+ publisher += listener
+ }
+
+ protected def removeChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ publisher -= listener
+ if (publisher.isEmpty) {
+ BindingSeq.removePatchedListener(upstream, this)
+ }
+ }
+
+ }
+
+ private final class RxToBindingSeq[A](observable: Rx.Observable[A])
+ extends BindingSeq[A]
+ with ChangedListener[Option[A]]
+ with HasCache[A] {
+
+ override def changed(upstreamEvent: ChangedEvent[Option[A]]): Unit = {
+ val oldLength = cacheLength
+ val event = upstreamEvent.newValue match {
+ case Some(newValue) =>
+ appendCache(newValue)
+ new PatchedEvent(this, oldLength, Seq(newValue), 0)
+ case None =>
+ clearCache()
+ new PatchedEvent(this, 0, Nil, oldLength)
+ }
+ for (listener <- publisher) {
+ listener.patched(event)
+ }
+ }
+
+ private val publisher = new SafeBuffer[PatchedListener[A]]
+ type All[+A] = SeqOpsIterable[A]
+
+ private[Binding] var cacheData: Cache = emptyCacheData
+
+ override protected def value: All[A] = cacheData
+
+ override protected def removePatchedListener(listener: PatchedListener[A]): Unit = {
+ publisher -= listener
+ if (publisher.isEmpty) {
+ observable.removeChangedListener(this)
+ }
+ }
+
+ override protected def addPatchedListener(listener: PatchedListener[A]): Unit = {
+ if (publisher.isEmpty) {
+ observable.value.foreach(appendCache)
+ observable.addChangedListener(this)
+ }
+ publisher += listener
+ }
+
+ }
+
+ private class RxConcat[A](var observables: LazyList[Rx.Observable[A]])
+ extends Rx.Observable[A]
+ with ChangedListener[Option[A]] {
+ private var pending = false
+ def changed(upstreamEvent: ChangedEvent[Option[A]]): Unit = {
+ if (pending) {
+ throw new IllegalStateException(
+ "Must not trigger a changed event when the listener is just added to a Binding"
+ )
+ }
+ val newValue = upstreamEvent.newValue match {
+ case None =>
+ observables match {
+ case head #:: tail =>
+ if (head != upstreamEvent.getSource) {
+ throw new IllegalStateException(
+ "This ChangedListener should have been removed from the terminated observable."
+ )
+ }
+ head.removeChangedListener(this)
+ observables = tail
+ nextLivingValue()
+ case _ =>
+ throw new IllegalStateException(
+ "This ChangedListener should not be triggered when all observables have been terminated."
+ )
+ }
+ case someValue =>
+ someValue
+ }
+ val event = new ChangedEvent(RxConcat.this, newValue)
+ for (listener <- publisher) {
+ listener.changed(event)
+ }
+ }
+
+ @tailrec
+ private def nextLivingValue(): Option[A] = {
+ observables match {
+ case head #:: tail =>
+ pending = true
+ head.addChangedListener(this)
+ pending = false
+ head.value match {
+ case None =>
+ head.removeChangedListener(this)
+ observables = tail
+ nextLivingValue()
+ case someValue =>
+ someValue
+ }
+ case _ =>
+ None
+ }
+ }
+
+ private val publisher = new SafeBuffer[ChangedListener[Option[A]]]
+ protected def value: Option[A] = observables.headOption.flatMap(_.value)
+ protected def addChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ if (publisher.isEmpty) {
+ nextLivingValue()
+ }
+ publisher += listener
+ }
+ protected def removeChangedListener(listener: ChangedListener[Option[A]]): Unit = {
+ publisher -= listener
+ observables match {
+ case head #:: _ if publisher.isEmpty =>
+ head.removeChangedListener(this)
+ case _ =>
+ }
+ }
+
+ }
+
+ /** Reactive operators for [[Observable]]s.
+ *
+ * @see
+ * [[http://reactivex.io/ ReactiveX]]
+ * @note
+ * [[Rx]] operators are incomplete. Feel free to create a Pull Request if you need a certain operator.
+ */
+ object Rx {
+
+ /** A [[Binding]] that can be terminated.
+ *
+ * Once the value turned into a [[scala.None]], this [[Observable]] would be considered as terminated, and any
+ * future changes of this [[Observable]] will be ignored by any [[Rx]] operators derived from this [[Observable]],
+ * even if this [[Observable]] turns into a [[scala.Some]] value again.
+ *
+ * @note
+ * Even though an [[Observable]] is technically a [[Binding]], an [[Observable]] created from a [[Rx]] operator
+ * does not actually indicates data-binding.
+ *
+ * For example, given an [[Observable]] created from [[Rx.concat]],
+ * {{{
+ * import com.thoughtworks.binding.Binding._
+ * val sourceObservable0 = Var[Option[String]](Some("0"))
+ * val sourceObservable1 = Var[Option[String]](Some("1"))
+ * val sourceObservables = List(sourceObservable0, sourceObservable1)
+ * val derivedObservable = Rx.concat(sourceObservables)
+ * derivedObservable.watch()
+ * }}}
+ *
+ * when a source value gets changed,
+ *
+ * {{{
+ * val originalDerivedObservableValue = derivedObservable.get
+ * sourceObservable0.value = None
+ * }}}
+ *
+ * and the source value is changed back to the original value,
+ *
+ * {{{
+ * sourceObservable0.value = Some("0")
+ * }}}
+ *
+ * then the value of the derived observable might not be the original value.
+ *
+ * {{{
+ * derivedObservable.get shouldNot be(originalDerivedObservableValue)
+ * }}}
+ *
+ * In contrast, if the `concat` operator is implemented by ordinary [[Binding.bind]] macros, the derived Binding is
+ * indeed a data-binding, i.e. it always perform the same calculation for the same values of source [[Binding]]s.
+ *
+ * {{{
+ * import com.thoughtworks.binding.Binding._
+ * val sourceBinding0 = Var[Option[String]](Some("0"))
+ * val sourceBinding1 = Var[Option[String]](Some("1"))
+ * val sourceBindings = List(sourceBinding0, sourceBinding1)
+ * def concatBinding(
+ * sourceBindings: collection.LinearSeq[Rx.Observable[String]]
+ * ): Rx.Observable[String] = {
+ * sourceBindings match {
+ * case head +: tail =>
+ * Binding {
+ * head.bind match {
+ * case None =>
+ * concatBinding(tail).bind
+ * case someValue =>
+ * someValue
+ * }
+ * }
+ * case _ =>
+ * Constant(None)
+ * }
+ * }
+ * val derivedBinding = concatBinding(sourceBindings)
+ * derivedBinding.watch()
+ * val originalDerivedBindingValue = derivedBinding.get
+ * sourceBinding0.value = None
+ * sourceBinding0.value = Some("0")
+ * derivedBinding.get should be(originalDerivedBindingValue)
+ * }}}
+ */
+ type Observable[A] = Binding[Option[A]]
+
+ /** Emit the emissions from two or more [[Observable]]s without interleaving them.
+ *
+ * @see
+ * [[http://reactivex.io/documentation/operators/concat.html ReactiveX - Concat operator]]
+ *
+ * @example
+ * Given a sequence of [[Observable]]s,
+ * {{{
+ * import com.thoughtworks.binding.Binding._, BindingInstances.monadSyntax._
+ * val observable0 = Var[Option[String]](None)
+ * val observable1 = Var[Option[String]](Some("1"))
+ * val observable2 = Var[Option[String]](Some("2"))
+ * val observable3 = Var[Option[String]](None)
+ * val observable4 = Var[Option[String]](None)
+ *
+ * val observable7 = Var[Option[String]](Some("7"))
+ *
+ * val observable8 = Binding { observable7.bind.map { v => s"8-$v-derived" } }
+ * val observable5 = Binding { observable7.bind.map { v => s"5-$v-derived" } }
+ *
+ * val observable6 = Var[Option[String]](None)
+ * val observable9 = Var[Option[String]](Some("9"))
+ * val observables = Seq(
+ * observable0,
+ * observable1,
+ * observable2,
+ * observable3,
+ * observable4,
+ * observable5,
+ * observable6,
+ * observable7,
+ * observable8,
+ * observable9,
+ * )
+ * }}}
+ *
+ * when concatenate them together,
+ *
+ * {{{
+ * val concatenated = Rx.concat(observables).map(identity)
+ * concatenated.watch()
+ * }}}
+ *
+ * the concatenated value should be the first [[scala.Some]] value in the sequence of observables;
+ *
+ * {{{
+ * concatenated.get should be(Some("1"))
+ * }}}
+ *
+ * when the current observable becomes `None`,
+ * {{{
+ * observable1.value = None
+ * }}}
+ *
+ * the concatenated value should be the next [[scala.Some]] value in the sequence of observables,
+ *
+ * {{{
+ * concatenated.get should be(Some("2"))
+ * }}}
+ *
+ * even when the next [[scala.Some]] value is derived from another [[Binding]];
+ *
+ * {{{
+ * observable2.value = None
+ * concatenated.get should be(Some("5-7-derived"))
+ * }}}
+ *
+ * when the value of the upstream [[Binding]] is changed to another [[scala.Some]] value,
+ *
+ * {{{
+ * observable7.value = Some("7-running")
+ * }}}
+ *
+ * the concatenated value should be changed accordingly;
+ * {{{
+ * concatenated.get should be(Some("5-7-running-derived"))
+ * }}}
+ *
+ * when multiple observables become [[scala.None]] at once,
+ *
+ * {{{
+ * observable7.value = None
+ * }}}
+ *
+ * they all should be skipped when calculate the concatenated value;
+ * {{{
+ * concatenated.get should be(Some("9"))
+ * }}}
+ *
+ * when the last observable in the sequence becomes [[scala.None]],
+ * {{{
+ * observable9.value = None
+ * }}}
+ *
+ * the concatenated value should become [[scala.None]] permanently,
+ *
+ * {{{
+ * concatenated.get should be(None)
+ * }}}
+ *
+ * even when some observables in the sequence become [[scala.Some]] again.
+ *
+ * {{{
+ * observable9.value = Some("9-after-termination")
+ * concatenated.get should be(None)
+ * observable7.value = Some("7-after-termination")
+ * concatenated.get should be(None)
+ * }}}
+ */
+ def concat[A](observables: IterableOnce[Observable[A]]): Observable[A] = {
+ new RxConcat(LazyList.from(observables))
+ }
+
+ /** @see
+ * [[http://reactivex.io/documentation/operators/repeat.html ReactiveX - Repeat operator]]
+ */
+ def repeat[A](source: => Observable[A]): Observable[A] = {
+ new RxConcat(LazyList.continually(source))
+ }
+
+ /** @see
+ * [[http://reactivex.io/documentation/operators/merge.html ReactiveX - Merge operator]]
+ */
+ def merge[A](bindingSeq: BindingSeq[A]): Observable[A] = {
+ new RxMerge(bindingSeq)
+ }
+
+ /** do not create the Observable until the observer subscribes
+ *
+ * @see
+ * [[http://reactivex.io/documentation/operators/defer.html ReactiveX - Defer operator]]
+ *
+ * @note
+ * This [[defer]] is slightly different from other implementation the
+ * [[http://reactivex.io/documentation/operators/defer.html ReactiveX Defer]] operator, because this [[defer]]
+ * shares the same upstream [[Observable]] instance for all subscribes.
+ *
+ * @example
+ * Circular referenced [[Observable]]s can be created with the help of [[defer]]
+ * {{{
+ * import Binding._
+ * val source = Var("init")
+ * lazy val observable1: Rx.Observable[String] =
+ * Binding[Option[String]] {
+ * source.bind match {
+ * case "init" =>
+ * None
+ * case v =>
+ * observable2.bind.map(_ + "_" + v)
+ * }
+ * }
+ *
+ * lazy val observable2: Rx.Observable[String] = Rx.defer(
+ * Binding[Option[String]] {
+ * Some(observable1.getClass.getSimpleName)
+ * }
+ * )
+ * }}}
+ *
+ * Initially, `observable1` did not subscribe `observable2` because `source` is `init`,
+ * {{{
+ * observable1.watch()
+ * }}}
+ * therefore observable2 should be `None`,
+ * {{{
+ * observable1.get should be (None)
+ * observable2.get should be (None)
+ * }}}
+ * when `source` changed,
+ * {{{
+ * source.value = "changed"
+ * }}}
+ * `observable1` should subscribe `observable2`, and there should be `Some` values.
+ * {{{
+ * observable1.get should be (Some("FlatMap_changed"))
+ * observable2.get should be (Some("FlatMap"))
+ * }}}
+ * Even though circular referenced [[Observable]]s can be created in this way, their calculation must not be
+ * mutually dependent.
+ */
+ def defer[A](upstream: => Observable[A]): Observable[A] = {
+ new RxDefer(upstream)
+ }
+
+ /** Combine multiple [[Observable]]s into one by merging their emissions.
+ *
+ * @see
+ * [[http://reactivex.io/documentation/operators/merge.html ReactiveX - Merge operator]]
+ *
+ * @example
+ * Given a sequence of [[Observable]]s,
+ * {{{
+ * import com.thoughtworks.binding.Binding._, BindingInstances.monadSyntax._
+ * val observable0 = Var[Option[String]](None)
+ * val observable1 = Var[Option[String]](Some("1"))
+ * val observable2 = Var[Option[String]](Some("2"))
+ * val observable3 = Var[Option[String]](None)
+ * val observable4 = Var[Option[String]](None)
+ *
+ * val observable7 = Var[Option[String]](Some("7"))
+ *
+ * val observable8 = Binding { observable7.bind.map { v => s"8-$v-derived" } }
+ * val observable5 = Binding { observable7.bind.map { v => s"5-$v-derived" } }
+ *
+ * val observable6 = Var[Option[String]](None)
+ * val observable9 = Var[Option[String]](Some("9"))
+ * val observables = Seq(
+ * observable0,
+ * observable1,
+ * observable2,
+ * observable3,
+ * observable4,
+ * observable5,
+ * observable6,
+ * observable7,
+ * observable8,
+ * observable9,
+ * )
+ * }}}
+ *
+ * when merge them together,
+ *
+ * {{{
+ * val merged = Rx.merge(observables).map(identity)
+ * merged.watch()
+ * }}}
+ *
+ * the merged value should be the first [[scala.Some]] value in the sequence of observables;
+ *
+ * {{{
+ * merged.get should be(Some("1"))
+ * }}}
+ *
+ * when the some but not all of the observable becomes `None`,
+ *
+ * {{{
+ * observable1.value = None
+ * }}}
+ *
+ * the merged value should be unchanged,
+ *
+ * {{{
+ * merged.get should be(Some("1"))
+ * }}}
+ *
+ * when any of the observable becomes `Some` value,
+ *
+ * {{{
+ * observable2.value = Some("2-changed")
+ * }}}
+ *
+ * the merged value should be the new value,
+ *
+ * {{{
+ * merged.get should be(Some("2-changed"))
+ * }}}
+ *
+ * even when a previous `None` observable becomes `Some` value,,
+ *
+ * {{{
+ * observable3.value = Some("3-previous-None")
+ * }}}
+ *
+ * the merged value should be the new value of the previous `None` observable,
+ *
+ * {{{
+ * merged.get should be(Some("3-previous-None"))
+ * }}}
+ *
+ * when multiple observables are changed at once,
+ *
+ * {{{
+ * observable7.value = Some("7-changed")
+ * }}}
+ *
+ * the merged value should be the value of the last derived observable
+ *
+ * {{{
+ * merged.get should be(Some("8-7-changed-derived"))
+ * }}}
+ *
+ * when all the observables become `None`,
+ *
+ * {{{
+ * observable1.value = None
+ * merged.get should be(Some("8-7-changed-derived"))
+ * observable2.value = None
+ * merged.get should be(Some("8-7-changed-derived"))
+ * observable3.value = None
+ * merged.get should be(Some("8-7-changed-derived"))
+ * observable6.value = None
+ * merged.get should be(Some("8-7-changed-derived"))
+ * observable7.value = None
+ * merged.get should be(Some("8-7-changed-derived"))
+ * observable9.value = None
+ * merged.get should be(None)
+ * }}}
+ */
+ def merge[A](observables: IterableOnce[Observable[A]]): Observable[A] = {
+ new RxMerge(
+ new Constants(toConstantsData(observables)).flatMapBinding(
+ BindingInstances.map(_) { option =>
+ new Constants(toConstantsData(option))
+ }
+ )
+ )
+ }
+
+ /** convert an Observable into a [[BindingSeq]].
+ *
+ * @see
+ * [[http://reactivex.io/documentation/operators/to.html ReactiveX - To operator]]
+ *
+ * @example
+ * Given a source observable,
+ * {{{
+ * import com.thoughtworks.binding.Binding._
+ * val observable = Var[Option[String]](Some("1"))
+ * }}}
+ *
+ * when converting it into a [[BindingSeq]],
+ *
+ * {{{
+ * val bindingSeq = Rx.toBindingSeq(observable)
+ * }}}
+ *
+ * and flat-mapping to the result,
+ * {{{
+ * val result = new BindingSeq.FlatMap(
+ * bindingSeq,
+ * { value: String => Constants("the value is", value) }
+ * ).all
+ * result.watch()
+ * }}}
+ *
+ * then result should have the values corresponding to the source observable,
+ * {{{
+ * result.get.toSeq should contain theSameElementsInOrderAs Seq("the value is", "1")
+ * }}}
+ *
+ * when the source observable changes,
+ * {{{
+ * observable.value = Some("2")
+ * }}}
+ *
+ * then the corresponding new value should be appended to the result,
+ * {{{
+ * result.get.toSeq should contain theSameElementsInOrderAs Seq(
+ * "the value is", "1",
+ * "the value is", "2"
+ * )
+ * }}}
+ *
+ * when the source observable terminates,
+ * {{{
+ * observable.value = None
+ * }}}
+ *
+ * then the result should be empty
+ * {{{
+ * result.get.toSeq should be(empty)
+ * }}}
+ */
+ def toBindingSeq[A](observable: Observable[A]): BindingSeq[A] = {
+ new RxToBindingSeq(observable)
+ }
+
+ }
+
+}
+
+/** A data binding expression that represents a value that automatically recalculates when its dependencies change.
+ *
+ * @example
+ * You may create a data binding expression via `Binding { ??? }` block annotation.
+ *
+ * {{{
+ * val bindingInt: Binding[Int] = Binding { 100 }
+ * }}}
+ *
+ * A data binding expression may depend on other binding expressions via [[bind]] method:
+ *
+ * {{{
+ * val bindingString: Binding[String] = Binding { bindingInt.bind.toString }
+ * }}}
+ *
+ * @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+trait Binding[+A] extends Binding.Watchable[A] {
+
+ /** Returns the current value of this [[Binding]] and marks the current `@dom` method depend on this [[Binding]].
+ *
+ * Each time the value changes, in the current `@dom` method, all code after the current `bind` expression will be
+ * re-evaluated if the current `@dom` method is [[#watch watch]]ing. However, code in current `@dom` method and
+ * before the current `bind` expression will not be re-evaluated. The above rule is not applied to DOM nodes created
+ * by XHTML literal. A `bind` expression under a DOM node does not affect siblings and parents of that node.
+ *
+ * @note
+ * This method must be invoked inside a `@dom` method body or a `Binding { ... }` block..
+ */
+ final def bind: A = macro Binding.Macros.bind
+
+ private[binding] def get: A = value
+
+ /** Returns the current value of this [[Binding]]
+ *
+ * @note
+ * This method must not be invoked inside a `@dom` method body or a `Binding { ... }` block..
+ */
+ protected def value: A
+
+ protected def removeChangedListener(listener: Binding.ChangedListener[A]): Unit
+
+ protected def addChangedListener(listener: Binding.ChangedListener[A]): Unit
+
+ /** Enable automatic recalculation.
+ *
+ * You may invoke this method more than once. Then, when you want to disable automatic recalculation, you must invoke
+ * [[#unwatch unwatch]] same times as the number of calls to this method.
+ *
+ * @note
+ * This method is recursive, which means that the dependencies of this [[Binding]] will be watched as well.
+ */
+ @inline
+ final def watch(): Unit = {
+ addChangedListener(Binding.DummyChangedListener)
+ }
+
+ /** Disable automatic recalculation.
+ *
+ * @note
+ * This method is recursive, which means that the dependencies of this [[Binding]] will be unwatched as well.
+ */
+ @inline
+ final def unwatch(): Unit = {
+ removeChangedListener(Binding.DummyChangedListener)
+ }
+
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/BindingTest.scala b/Binding/src/test/scala/com/thoughtworks/binding/BindingTest.scala
new file mode 100644
index 00000000..3c7267c9
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/BindingTest.scala
@@ -0,0 +1,831 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package com.thoughtworks.binding
+
+import scala.collection.mutable.ArrayBuffer
+import scala.collection.mutable.Buffer
+
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+import scalaz._
+
+import Binding._
+import BindingSeq.removePatchedListener
+import BindingSeq.addPatchedListener
+
+final class BindingTest extends AnyFreeSpec with Matchers {
+
+ final class BufferListener extends ArrayBuffer[Any] {
+ val listener = new ChangedListener[Any] with PatchedListener[Any] {
+ override def changed(event: ChangedEvent[Any]): Unit = {
+ BufferListener.this += event
+ }
+
+ override def patched(event: PatchedEvent[Any]): Unit = {
+ BufferListener.this += event
+ }
+ }
+ }
+
+ "hello world" in {
+ val target = Var("World")
+ val hello = Binding {
+ "Hello, " + target.bind + "!"
+ }
+ hello.watch()
+
+ assert(hello.get == "Hello, World!")
+ target.value = "Each"
+ assert(hello.get == "Hello, Each!")
+ }
+
+ "TripleBinding" in {
+ val input = Var(0)
+ val output = Binding {
+ input.bind + input.bind + input.bind
+ }
+ output.watch()
+ assert(output.get == 0)
+ for (i <- 0 until 10) {
+ input.value = i
+ assert(output.get == i * 3)
+ }
+ }
+
+ "DataBindingShouldBeSupportedByScalaz" in {
+
+ val expr3: Var[Int] = new Var(2000)
+
+ val expr4: Binding[Int] = Binding {
+ 30000
+ }
+
+ val expr2: Binding[Int] = Binding {
+ expr3.bind + expr4.bind
+ }
+
+ val expr1: Binding[Int] = Binding {
+ expr2.bind + 100
+ }
+
+ var resultChanged = 0
+
+ assert(expr1.get == 0)
+
+ addChangedListener(
+ expr1,
+ new ChangedListener[Any] {
+ override def changed(event: ChangedEvent[Any]): Unit = {
+ resultChanged += 1
+ }
+ }
+ )
+
+ assert(resultChanged == 0)
+ assert(expr1.get == 32100)
+
+ expr3.value = 4000
+
+ assert(resultChanged == 1)
+ assert(expr1.get == 34100)
+
+ }
+
+ "CacheShouldBeUpdated" in {
+ val source = new Var(2.0)
+ val constant = new Constant(1.0)
+ val result = Binding {
+ val sourceValue = source.bind
+ val one = sourceValue / sourceValue / constant.bind
+ one / sourceValue
+ }
+ var resultChanged = 0
+
+ addChangedListener(
+ result,
+ new ChangedListener[Any] {
+ override def changed(event: ChangedEvent[Any]): Unit = {
+ resultChanged += 1
+ }
+ }
+ )
+ assert(result.get == 0.5)
+ assert(resultChanged == 0)
+ source.value = 4.0
+ assert(result.get == 0.25)
+ assert(resultChanged == 1)
+ }
+
+ "ForYieldWithFilter" in {
+ val prefix = new Var("ForYield")
+ val source = Vars(1, 2, 3)
+ val mapped = (for {
+ sourceElement <- source
+ if prefix.bind != sourceElement.toString
+ i <- Constants((0 until sourceElement): _*)
+ } yield {
+ raw"""${prefix.bind} $i/$sourceElement"""
+ })
+ val mappedEvents = new BufferListener
+ val sourceEvents = new BufferListener
+ addPatchedListener(mapped, mappedEvents.listener)
+ assert(source.publisher.nonEmpty)
+ addPatchedListener(source, sourceEvents.listener)
+ assert(source.publisher.nonEmpty)
+
+ assert(sourceEvents == ArrayBuffer.empty)
+ source.value.clear()
+ assert(sourceEvents.length == 1)
+ assert(mappedEvents.length == 1)
+ sourceEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 3)
+ assert(event.getSource == source)
+ }
+ mappedEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 6)
+ assert(event.getSource == mapped)
+ }
+ source.value ++= Seq(2, 3, 4)
+ assert(sourceEvents.length == 2)
+ assert(mappedEvents.length == 2)
+ sourceEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(2, 3, 4))
+ assert(event.getSource == source)
+ }
+ mappedEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(
+ event.that sameElements Seq(
+ "ForYield 0/2",
+ "ForYield 1/2",
+ "ForYield 0/3",
+ "ForYield 1/3",
+ "ForYield 2/3",
+ "ForYield 0/4",
+ "ForYield 1/4",
+ "ForYield 2/4",
+ "ForYield 3/4"
+ )
+ )
+ }
+ source.value += 0
+ assert(sourceEvents.length == 3)
+ assert(mappedEvents.length == 2)
+ sourceEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 3)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(0))
+ }
+ source.value += 3
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 3)
+ sourceEvents(3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 4)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(3))
+ }
+ mappedEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 9)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("ForYield 0/3", "ForYield 1/3", "ForYield 2/3"))
+ }
+ assert(
+ mapped.get sameElements Seq(
+ "ForYield 0/2",
+ "ForYield 1/2",
+ "ForYield 0/3",
+ "ForYield 1/3",
+ "ForYield 2/3",
+ "ForYield 0/4",
+ "ForYield 1/4",
+ "ForYield 2/4",
+ "ForYield 3/4",
+ "ForYield 0/3",
+ "ForYield 1/3",
+ "ForYield 2/3"
+ )
+ )
+ prefix.value = "3"
+ assert(sourceEvents.length == 4)
+ assert(mapped.get sameElements Seq("3 0/2", "3 1/2", "3 0/4", "3 1/4", "3 2/4", "3 3/4"))
+
+ removePatchedListener(mapped, mappedEvents.listener)
+ removePatchedListener(source, sourceEvents.listener)
+
+ assert(source.publisher.isEmpty)
+ }
+
+ "ForYield" in {
+ val prefix = new Var("ForYield")
+ val source = Vars(1, 2, 3)
+ val mapped = (for {
+ sourceElement <- source
+ i <- Constants((0 until sourceElement): _*)
+ } yield {
+ raw"""${prefix.bind} $i/$sourceElement"""
+ })
+ val mappedEvents = new BufferListener
+ val sourceEvents = new BufferListener
+ addPatchedListener(mapped, mappedEvents.listener)
+ assert(source.publisher.nonEmpty)
+ addPatchedListener(source, sourceEvents.listener)
+ assert(source.publisher.nonEmpty)
+
+ assert(sourceEvents == ArrayBuffer.empty)
+ source.value.clear()
+ assert(mappedEvents.length == 1)
+ assert(sourceEvents.length == 1)
+ sourceEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 3)
+ assert(event.getSource == source)
+ }
+ mappedEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 6)
+ assert(event.getSource == mapped)
+ }
+ source.value ++= Seq(2, 3, 4)
+ assert(mappedEvents.length == 2)
+ assert(sourceEvents.length == 2)
+ sourceEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(2, 3, 4))
+ assert(event.getSource == source)
+ }
+ mappedEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(
+ event.that sameElements Seq(
+ "ForYield 0/2",
+ "ForYield 1/2",
+ "ForYield 0/3",
+ "ForYield 1/3",
+ "ForYield 2/3",
+ "ForYield 0/4",
+ "ForYield 1/4",
+ "ForYield 2/4",
+ "ForYield 3/4"
+ )
+ )
+ }
+ source.value += 0
+ assert(sourceEvents.length == 3)
+ assert(mappedEvents.length == 2)
+ sourceEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 3)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(0))
+ }
+ source.value += 3
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 3)
+ sourceEvents(3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 4)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(3))
+ }
+ mappedEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 9)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("ForYield 0/3", "ForYield 1/3", "ForYield 2/3"))
+ }
+ prefix.value = "p"
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 15)
+ val expected =
+ Seq("p 0/2", "p 1/2", "p 0/3", "p 1/3", "p 2/3", "p 0/4", "p 1/4", "p 2/4", "p 3/4", "p 0/3", "p 1/3", "p 2/3")
+ for (i <- 0 until 12) {
+ mappedEvents(i + 3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.replaced == 1)
+ assert(event.that sameElements Seq(expected(event.from)))
+ }
+ }
+
+ removePatchedListener(mapped, mappedEvents.listener)
+ removePatchedListener(source, sourceEvents.listener)
+
+ assert(source.publisher.isEmpty)
+ }
+
+ "FlatMappedVarBuffer" in {
+ val prefix = new Var("")
+ val source = Vars(1, 2, 3)
+ val mapped = new BindingSeq.FlatMap(
+ source,
+ { sourceElement: Int =>
+ new BindingSeq.MapBinding(
+ Constants((0 until sourceElement): _*),
+ { i: Int =>
+ Binding {
+ raw"""${prefix.bind}$sourceElement"""
+ }
+ }
+ )
+ }
+ )
+ val mappedEvents = new BufferListener
+ val sourceEvents = new BufferListener
+ addPatchedListener(mapped, mappedEvents.listener)
+ assert(mapped.publisher.nonEmpty)
+ assert(source.publisher.nonEmpty)
+ addPatchedListener(source, sourceEvents.listener)
+ assert(mapped.publisher.nonEmpty)
+ assert(source.publisher.nonEmpty)
+
+ assert(sourceEvents == ArrayBuffer.empty)
+ source.value.clear()
+ assert(mappedEvents.length == 1)
+ assert(sourceEvents.length == 1)
+ sourceEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 3)
+ assert(event.getSource == source)
+ }
+ mappedEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 6)
+ assert(event.getSource == mapped)
+ }
+ source.value ++= Seq(2, 3, 4)
+ assert(mappedEvents.length == 2)
+ assert(sourceEvents.length == 2)
+ sourceEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(2, 3, 4))
+ assert(event.getSource == source)
+ }
+ mappedEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("2", "2", "3", "3", "3", "4", "4", "4", "4"))
+ }
+ source.value += 0
+ assert(sourceEvents.length == 3)
+ assert(mappedEvents.length == 2)
+ sourceEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 3)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(0))
+ }
+ source.value += 3
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 3)
+ sourceEvents(3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 4)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(3))
+ }
+ mappedEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 9)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("3", "3", "3"))
+ }
+ prefix.value = "p"
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 15)
+ val expected = Seq("p2", "p2", "p3", "p3", "p3", "p4", "p4", "p4", "p4", "p3", "p3", "p3")
+ for (i <- 0 until 12) {
+ mappedEvents(i + 3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.replaced == 1)
+ assert(event.that sameElements Seq(expected(event.from)))
+ }
+ }
+
+ removePatchedListener(mapped, mappedEvents.listener)
+ removePatchedListener(source, sourceEvents.listener)
+
+ assert(mapped.publisher.isEmpty)
+ assert(source.publisher.isEmpty)
+ }
+
+ "MappedVarBuffer" in {
+ val prefix = new Var("")
+ val source = Vars(1, 2, 3)
+ val mapped = new BindingSeq.MapBinding(
+ source,
+ { a: Int =>
+ Binding {
+ raw"""${prefix.bind}${a}"""
+ }
+ }
+ )
+ val mappedEvents = new BufferListener
+ val sourceEvents = new BufferListener
+ addPatchedListener(mapped, mappedEvents.listener)
+ assert(mapped.publisher.nonEmpty)
+ assert(source.publisher.nonEmpty)
+ addPatchedListener(source, sourceEvents.listener)
+ assert(mapped.publisher.nonEmpty)
+ assert(source.publisher.nonEmpty)
+
+ assert(sourceEvents == ArrayBuffer.empty)
+ source.value.clear()
+ assert(mappedEvents.length == 1)
+ assert(sourceEvents.length == 1)
+ sourceEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 3)
+ assert(event.getSource == source)
+ }
+ mappedEvents(0) match {
+ case event: PatchedEvent[_] =>
+ assert(event.that.isEmpty)
+ assert(event.from == 0)
+ assert(event.replaced == 3)
+ assert(event.getSource == mapped)
+ }
+ source.value ++= Seq(2, 3, 4)
+ assert(mappedEvents.length == 2)
+ assert(sourceEvents.length == 2)
+ sourceEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(2, 3, 4))
+ assert(event.getSource == source)
+ }
+ mappedEvents(1) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("2", "3", "4"))
+ }
+ source.value += 20
+ assert(sourceEvents.length == 3)
+ assert(mappedEvents.length == 3)
+ sourceEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 3)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(20))
+ }
+ mappedEvents(2) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 3)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("20"))
+ }
+ 300 +=: source.value
+ assert(mappedEvents.length == 4)
+ assert(sourceEvents.length == 4)
+ sourceEvents(3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == source)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq(300))
+ }
+ mappedEvents(3) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.from == 0)
+ assert(event.replaced == 0)
+ assert(event.that sameElements Seq("300"))
+ }
+ prefix.value = "p"
+ assert(sourceEvents.length == 4)
+ assert(mappedEvents.length == 9)
+ val expected = Seq("p300", "p2", "p3", "p4", "p20")
+ for (i <- 0 until 5) {
+ mappedEvents(i + 4) match {
+ case event: PatchedEvent[_] =>
+ assert(event.getSource == mapped)
+ assert(event.replaced == 1)
+ assert(event.that sameElements Seq(expected(event.from)))
+ }
+ }
+
+ removePatchedListener(mapped, mappedEvents.listener)
+ removePatchedListener(source, sourceEvents.listener)
+
+ assert(mapped.publisher.isEmpty)
+ assert(source.publisher.isEmpty)
+ }
+
+ "Length" in {
+ val source = Vars(1)
+ val length = source.length
+ val lengthEvents = new BufferListener
+ addChangedListener(length, lengthEvents.listener)
+ val length2 = source.length
+ length2.watch()
+ length2.unwatch()
+ source.value(0) = 100
+ assert(lengthEvents.length == 1)
+ lengthEvents(0) match {
+ case event: ChangedEvent[_] =>
+ assert(event.getSource == length)
+ assert(event.newValue == 1)
+ }
+
+ source.value += 200
+ assert(lengthEvents.length == 2)
+ lengthEvents(1) match {
+ case event: ChangedEvent[_] =>
+ assert(event.getSource == length)
+ assert(event.newValue == 2)
+ }
+
+ source.value -= 100
+ assert(lengthEvents.length == 3)
+ lengthEvents(2) match {
+ case event: ChangedEvent[_] =>
+ assert(event.getSource == length)
+ assert(event.newValue == 1)
+ }
+
+ }
+
+ "WithFilter" in {
+ Binding {
+ val myVars = Vars(1, 2, 100, 3)
+ val filtered = myVars.withFilter(_ < 10).map(x => x)
+ filtered.watch()
+ assert(filtered.get sameElements Seq(1, 2, 3))
+ }
+ }
+
+ "++=" in {
+ val myVars = Vars(1, 2, 3)
+ myVars.watch()
+ myVars.value ++= Seq(4, 5)
+ assert(myVars.value sameElements Seq(1, 2, 3, 4, 5))
+ }
+
+ "ScalaRxLeakExample" in {
+ import scalaz._, Scalaz._
+
+ var count: Int = 0
+ val a: Var[Int] = Var(1)
+ val b: Var[Int] = Var(2)
+ def mkRx(i: Int) = (b: Binding[Int]).map { v =>
+ count += 1; i + v
+ }
+
+ val c: Binding[Int] = (a: Binding[Int]).flatMap(mkRx)
+ c.watch()
+
+ var result: (Int, Int) = null
+ assert((3, 1) == ((c.get, count)))
+
+ a.value = 4
+ assert((6, 2) == ((c.get, count)))
+
+ b.value = 3
+ assert((7, 3) == ((c.get, count)))
+
+ (0 to 100).foreach { i =>
+ a.value = i
+ }
+ assert((103, 104) == ((c.get, count)))
+
+ b.value = 4
+ assert((104, 105) == ((c.get, count)))
+ }
+
+ "multi to one dependencies" in {
+ import scalaz.syntax.all._
+ import scalaz._
+
+ val a: Var[Int] = Var(100)
+ val b: Var[Int] = Var(200)
+ var aFlushCount = 0
+ var bFlushCount = 0
+ val aPlusOne = (a: Binding[Int]).map { value =>
+ aFlushCount += 1
+ value + 1
+ }
+ val bPlusOne = (b: Binding[Int]).map { value =>
+ bFlushCount += 1
+ value + 1
+ }
+ val aPlusOneTimesBPlusOn = Binding.BindingInstances.apply2(aPlusOne, bPlusOne) { (aValue, bValue) =>
+ aValue * bValue
+ }
+ aPlusOneTimesBPlusOn.watch()
+ aPlusOneTimesBPlusOn.get should be((100 + 1) * (200 + 1))
+ aFlushCount should be(1)
+ bFlushCount should be(1)
+ a.value = 500
+ aPlusOneTimesBPlusOn.get should be((500 + 1) * (200 + 1))
+ aFlushCount should be(2)
+ bFlushCount should be(2)
+ b.value = 600
+ aPlusOneTimesBPlusOn.get should be((500 + 1) * (600 + 1))
+ aFlushCount should be(2)
+ bFlushCount should be(3)
+
+ }
+
+ "for / yield / if" in {
+ def domMethod() = Binding {
+ val myVars = Vars(1, 2, 100, 3)
+ val filtered = for {
+ myVar <- myVars
+ if myVar < 10
+ } yield myVar
+ filtered.watch()
+ assert(filtered.get sameElements Seq(1, 2, 3))
+ }
+ domMethod()
+ }
+
+ "flatMap" in {
+ val flatMapped = Constants(Constants(1, 2), Constants(), Constants(3)).flatMap(identity)
+ flatMapped.watch()
+ assert(flatMapped.get sameElements Seq(1, 2, 3))
+ }
+
+ "foreach" in {
+ val vars = Vars(Var(1), Var(2), Var(3))
+ val logs = new StringBuilder
+
+ val mounting = Binding {
+ logs ++= "Binding\n"
+ for (i <- vars) {
+ logs ++= s"creating mount point ${i.value}\n"
+ new SingleMountPoint(i) {
+ override def mount() = {
+ super.mount()
+ logs ++= s"mount ${i.value}\n"
+ }
+ override def unmount() = {
+ logs ++= s"unmount ${i.value}\n"
+ super.unmount()
+ }
+ override def set(newValue: Int) = {
+ logs ++= s"set ${newValue}\n"
+ }
+ }.bind
+ }
+ }
+ logs.toString should be("Binding\n")
+ logs.clear()
+
+ mounting.watch()
+ logs.toString should be("""creating mount point 1
+creating mount point 2
+creating mount point 3
+set 1
+mount 1
+set 2
+mount 2
+set 3
+mount 3
+""")
+ logs.clear()
+
+ vars.value -= vars.value(1)
+ logs.toString should be("""unmount 2
+""")
+ logs.clear()
+
+ vars.value(1).value += 5
+ logs.toString should be("""set 8
+""")
+ logs.clear()
+
+ vars.value ++= Seq(Var(10), Var(20))
+ logs.toString should be("""creating mount point 10
+creating mount point 20
+set 10
+mount 10
+set 20
+mount 20
+""")
+ logs.clear()
+
+ vars.value(0) = Var(100)
+ logs.toString should be("""creating mount point 100
+set 100
+mount 100
+unmount 1
+""")
+ logs.clear()
+
+ vars.value.prependAll(Seq(Var(1000), Var(2000)))
+ logs.toString should be("""creating mount point 1000
+creating mount point 2000
+set 1000
+mount 1000
+set 2000
+mount 2000
+""")
+ logs.clear()
+
+ vars.value.map(_.value) should be(Seq(1000, 2000, 100, 8, 10, 20))
+
+ mounting.unwatch()
+
+ }
+
+ "vars.all.bind" in {
+ val vars: Vars[String] = Vars[String]("one", "two", "three")
+ val bufferBinding = Binding {
+ val seq = vars.all.bind
+ seq
+ }
+ bufferBinding.watch()
+ assert(bufferBinding.get == Buffer("one", "two", "three"))
+ }
+
+ "constants.all.bind" in {
+ val constants: Constants[String] = Constants[String]("one", "two", "three")
+ val seqBinding = Binding {
+ val seq = constants.all.bind
+ seq
+ }
+ seqBinding.watch()
+ assert(seqBinding.get == Seq("one", "two", "three"))
+ }
+
+ "bindingSeq.all.bind" in {
+ val bindingSeq: BindingSeq[String] = Constants[String]("one", "two", "three")
+ val seqOpsIterableBinding = Binding {
+ val seq = bindingSeq.all.bind
+ seq
+ }
+ seqOpsIterableBinding.watch()
+ assert(seqOpsIterableBinding.get == Seq("one", "two", "three"))
+ }
+
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/BufferListener.scala b/Binding/src/test/scala/com/thoughtworks/binding/BufferListener.scala
new file mode 100644
index 00000000..8930e827
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/BufferListener.scala
@@ -0,0 +1,45 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package com.thoughtworks.binding
+
+import Binding.{PatchedEvent, ChangedEvent, PatchedListener, ChangedListener}
+import com.thoughtworks.binding.Binding.{PatchedEvent, ChangedEvent, PatchedListener, ChangedListener}
+
+import scala.collection.mutable.ArrayBuffer
+
+/** @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+final class BufferListener extends ArrayBuffer[Any] {
+ val listener = new ChangedListener[Seq[Any]] with PatchedListener[Any] {
+ override def changed(event: ChangedEvent[Seq[Any]]): Unit = {
+ BufferListener.this += event
+ }
+
+ override def patched(event: PatchedEvent[Any]): Unit = {
+ BufferListener.this += event
+ }
+ }
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/FlatMapRemove.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/FlatMapRemove.scala
new file mode 100644
index 00000000..e47d8eac
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/FlatMapRemove.scala
@@ -0,0 +1,45 @@
+package com.thoughtworks.binding.regression
+
+import com.thoughtworks.binding.Binding._
+import com.thoughtworks.binding._
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+import scala.collection.mutable.ArrayBuffer
+
+/** @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+final class FlatMapRemove extends AnyFreeSpec with Matchers {
+ "removed source of a flatMap" in {
+
+ val data = Vars.empty[Either[String, String]]
+
+ val left = for {
+ s <- data
+ if s.isLeft
+ } yield s
+
+ val events = ArrayBuffer.empty[String]
+ val autoPrint = Binding {
+ if (left.length.bind > 0) {
+ events += "has left"
+ } else {
+ events += "does not has left"
+ }
+ }
+ assert(events.forall(_ == "does not has left"))
+ autoPrint.watch()
+ assert(events.forall(_ == "does not has left"))
+ data.value += Right("1")
+ assert(events.forall(_ == "does not has left"))
+ data.value += Right("2")
+ assert(events.forall(_ == "does not has left"))
+ data.value += Right("3")
+ assert(events.forall(_ == "does not has left"))
+ data.value(1) = Left("left 2")
+ assert(events.last == "has left")
+ data.value --= Seq(Left("left 2"))
+ assert(events.last == "does not has left")
+ }
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/InsertThenClear.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/InsertThenClear.scala
new file mode 100644
index 00000000..65526e28
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/InsertThenClear.scala
@@ -0,0 +1,51 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package com.thoughtworks.binding.regression
+
+import com.thoughtworks.binding.Binding._
+import com.thoughtworks.binding._
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+import scala.collection.mutable.ArrayBuffer
+
+/** @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+final class InsertThenClear extends AnyFreeSpec with Matchers {
+ "insert then clear" in {
+ val items = Vars(1 to 10: _*)
+
+ val mapped = items.map(-_)
+ mapped.watch()
+ assert(mapped.get sameElements Seq(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10))
+
+ items.value.insertAll(3, 100 to 103)
+ assert(mapped.get sameElements Seq(-1, -2, -3, -100, -101, -102, -103, -4, -5, -6, -7, -8, -9, -10))
+
+ items.value.clear()
+ assert(mapped.get sameElements Seq.empty)
+ }
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue188.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue188.scala
new file mode 100644
index 00000000..4b560820
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue188.scala
@@ -0,0 +1,27 @@
+package com.thoughtworks.binding.regression
+
+import com.thoughtworks.binding.Binding
+import com.thoughtworks.binding.Binding.{Var, Vars}
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+final class Issue188 extends AnyFreeSpec with Matchers {
+ "non-regression test for https://github.com/ThoughtWorksInc/Binding.scala/issues/188" in {
+
+ val objectCache: Var[Int] = Var(1)
+
+ val objectValue: Binding[Int] = Binding {
+ val cache = objectCache.bind
+
+ if (cache == 1) {
+ objectCache.value = 2
+ 2
+ } else {
+ cache
+ }
+ }
+
+ an[IllegalStateException] should be thrownBy objectValue.watch()
+
+ }
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue56.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue56.scala
new file mode 100644
index 00000000..681b63c7
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/Issue56.scala
@@ -0,0 +1,36 @@
+package com.thoughtworks.binding.regression
+
+import com.thoughtworks.binding.Binding
+import com.thoughtworks.binding.Binding.{Var, Vars}
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+/** Test for https://github.com/ThoughtWorksInc/Binding.scala/issues/56
+ * @author
+ * 杨博 (Yang Bo)
+ */
+final class Issue56 extends AnyFreeSpec with Matchers {
+
+ "test" in {
+ var dataSource = Var[Int](100)
+ val isEnabled = Var[Boolean](false)
+
+ val mappedData = Binding {
+ dataSource.bind + 1
+ }
+
+ val result = Binding {
+ if (isEnabled.bind) {
+ mappedData.bind
+ } else {
+ 0
+ }
+ }
+
+ result.watch()
+ dataSource.value = 300
+ isEnabled.value = true
+ result.get should be(301)
+ }
+
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/Stackoverflow58206168.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/Stackoverflow58206168.scala
new file mode 100644
index 00000000..341dfc6e
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/Stackoverflow58206168.scala
@@ -0,0 +1,25 @@
+package com.thoughtworks.binding
+package regression
+import Binding._
+import scala.collection.mutable
+import Binding.BindingInstances.functorSyntax._
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+final class Stackoverflow58206168 extends AnyFreeSpec with Matchers {
+ // See https://stackoverflow.com/questions/58206168/binding-scala-vars-bind-seems-to-not-work-correctly
+ "Binding.scala: Vars.bind seems to not work correctly" in {
+ val events = mutable.Buffer.empty[List[Int]]
+ val test: Vars[Int] = Vars(1, 2, 3, 4)
+
+ test.all
+ .map {
+ events += _.toList
+ }
+ .watch()
+
+ test.value.append(1111)
+ assert(events == mutable.Buffer(List(1, 2, 3, 4), List(1, 2, 3, 4, 1111)))
+ }
+
+}
diff --git a/Binding/src/test/scala/com/thoughtworks/binding/regression/Zhihu50863924.scala b/Binding/src/test/scala/com/thoughtworks/binding/regression/Zhihu50863924.scala
new file mode 100644
index 00000000..932d0447
--- /dev/null
+++ b/Binding/src/test/scala/com/thoughtworks/binding/regression/Zhihu50863924.scala
@@ -0,0 +1,66 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+package com.thoughtworks.binding.regression
+
+import com.thoughtworks.binding.Binding
+import com.thoughtworks.binding.Binding.Var
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+
+/** @author
+ * 杨博 (Yang Bo) <pop.atry@gmail.com>
+ */
+final class Zhihu50863924 extends AnyFreeSpec with Matchers {
+
+ "Newly created Binding expression should not re-render immediately" in {
+
+ var renderCount0 = 0
+ var renderCount1 = 0
+
+ def subComponent(value: Var[Option[String]]) = Binding {
+ renderCount0 += 1
+ assert(value.bind == Some("Changed"))
+ renderCount1 += 1
+ Right(value.bind.get)
+ }
+
+ val value: Var[Option[String]] = Var(None)
+
+ val render = Binding {
+ if (value.bind.isDefined) {
+ subComponent(value).bind
+ } else {
+ Left("None here!")
+ }
+ }
+
+ render.watch()
+ assert(render.get == Left("None here!"))
+ value.value = Some("Changed")
+ assert(render.get == Right("Changed"))
+ assert(renderCount0 == 1)
+ assert(renderCount1 == 1)
+ }
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..a6b582f6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Yang Bo & REA Group Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README-zh.md b/README-zh.md
new file mode 100644
index 00000000..db0f2151
--- /dev/null
+++ b/README-zh.md
@@ -0,0 +1,493 @@
+# Binding.scala
+
+[![Join the chat at https://gitter.im/ThoughtWorksInc/Binding.scala](https://badges.gitter.im/ThoughtWorksInc/Binding.scala.svg)](https://gitter.im/ThoughtWorksInc/Binding.scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Build Status](https://travis-ci.org/ThoughtWorksInc/Binding.scala.svg)](https://travis-ci.org/ThoughtWorksInc/Binding.scala)
+[![Maven Central (core funtionality)](https://img.shields.io/maven-central/v/com.thoughtworks.binding/binding_2.11.svg?label=maven-central%20%28Binding.scala%29)](https://maven-badges.herokuapp.com/maven-central/com.thoughtworks.binding/binding_2.11)
+[![Maven Central (DOM integration)](https://img.shields.io/maven-central/v/com.thoughtworks.binding/dom_sjs0.6_2.11.svg?label=maven-central%20%28dom.scala%29)](https://maven-badges.herokuapp.com/maven-central/com.thoughtworks.binding/dom_sjs0.6_2.11)
+[![Maven Central (remote data-binding for scala.concurrent.Future)](https://img.shields.io/maven-central/v/com.thoughtworks.binding/futurebinding_2.11.svg?label=maven-central%20%28FutureBinding.scala%29)](https://maven-badges.herokuapp.com/maven-central/com.thoughtworks.binding/futurebinding_2.11)
+[![Maven Central (remote data-binding for ECMAScript 2015 Promise)](https://img.shields.io/maven-central/v/com.thoughtworks.binding/jspromisebinding_sjs0.6_2.11.svg?label=maven-central%20%28JsPromiseBinding.scala%29)](https://maven-badges.herokuapp.com/maven-central/com.thoughtworks.binding/jspromisebinding_sjs0.6_2.11)
+
+**Binding.scala** 是一个用 [Scala](http://www.scala-lang.org/) 语言编写的数据绑定框架,可以在 JVM 和 [Scala.js](http://www.scala-js.org/) 上运行。
+
+Binding.scala 可以用作 **[reactive](https://en.wikipedia.org/wiki/Reactive_programming) web freamework**。
+它允许你使用原生 XHTML 语法去创建 reactive DOM 节点,这种 DOM 节点可以在数据源发生变化时自动地改变。
+
+使用 Binding.scala 时可以参考 [Binding.scala • TodoMVC](http://todomvc.com/examples/binding-scala/) 或者[其他 DEMOs](https://thoughtworksinc.github.io/Binding.scala/),他们中包含了许多常见功能的实现。
+
+## 与其他 reactive web framework 对比
+
+与其他 reactive web framework(例如 [ReactJS](https://facebook.github.io/react/))对比,Binding.scala 有更多的特性以及更少的概念。
+
+
+
+
+ |
+ Binding.scala |
+ ReactJS |
+
+
+
+
+ HTML 语法支持 |
+ 支持 |
+ 部分支持。Regular HTML 不会编译,除非开发者人为地将 class 属性和 for 属性改为 className 和 htmlFor ,并且人为地将 行内样式 的语法从 CSS 语法改为 JSON 语法。 |
+
+
+ DOM 更新算法 |
+ 精准的数据绑定,比虚拟 DOM 更快 |
+ 虚拟 DOM 之间存在差异,对于复杂的 DOM 你需要手动地管理 key 属性。 |
+
+
+ 数据绑定表达式的生命周期管理 |
+ 完全自动 |
+ 无 |
+
+
+ 静态类型检查 |
+ 支持,甚至是 HTML 标签和属性 |
+ 不支持 |
+
+
+ 学习曲线 |
+ 一直很简单 |
+ 容易上手,但在理解边界情况时需要投入大量精力。 |
+
+
+
+
+更多详细信息,请查看[设计](#设计)。
+
+## 开始使用
+
+我们将在接下来的步骤里编写一个 Binding.scala 网页。
+
+### 第 0 步:配置一个 Sbt Scala.js 项目
+
+参考 http://www.scala-js.org/tutorial/basic/ 获取详细信息。
+
+### 第 1 步:在你的 `build.sbt` 中添加 Binding.scala 依赖项
+
+``` scala
+libraryDependencies += "com.thoughtworks.binding" %%% "dom" % "latest.release"
+
+addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
+```
+
+### 第 2 步:创建一个 `data` 域,其中包含一些 `Var` 和 `Vars` 作为你数据绑定表达式的数据源
+
+``` scala
+case class Contact(name: Var[String], email: Var[String])
+
+val data = Vars.empty[Contact]
+```
+
+一个 `Var` 代表一个可绑定变量,
+同时它也实现了 `Binding` 特性,
+这就意味着一个 `Var` 也可以被视为一个是数据绑定表达式。
+如果其他数据绑定表达式依赖与一个 `Var` ,那么该表达式的值将在这个 `Var` 的值改变时作出相应的改变。
+
+一个 `Vars` 代表一个可绑定变量序列,
+同时它也实现了 `BindingSeq` 特性,
+这就意味着一个 `Vars` 也可以被视为一个序列的数据绑定表达式。
+如果另一个数据绑定表达式依赖与一个 `Vars` ,
+那么该表达式的值将在这个 `Vars` 的值改变时作出相应的改变。
+
+### 第 3 步:创建一个含有数据绑定表达式的 `@dom` 方法
+
+``` scala
+@dom
+def table: Binding[Table] = {
+
+
+
+ Name |
+ E-mail |
+
+
+
+ {
+ for (contact <- data) yield {
+
+
+ {contact.name.bind}
+ |
+
+ {contact.email.bind}
+ |
+
+ }
+ }
+
+
+}
+```
+
+一个 `@dom` 方法代表一个数据绑定表达式。
+
+其返回值类型永远被包装成 `com.thoughtworks.binding.Binding` 特性。
+例如, `@dom def x: Binding[Int] = 1`, `@dom def message: Binding[String] = "content"`
+
+`@dom` 方法支持 HTML 语法。
+并不像通常 Scala 方法中的 XML 语法,
+HTML 语法的类型是 `org.scalajs.dom.raw.Node` 或者 `com.thoughtworks.binding.BindingSeq[org.scalajs.dom.raw.Node]` 的子类型,
+而不是 `scala.xml.Node` 或者 `scala.xml.NodeSeq`。
+因此我们写出这样的代码,`@dom def node: Binding[org.scalajs.dom.raw.HTMLBRElement] =
`
+以及 `@dom def node: Binding[BindingSeq[org.scalajs.dom.raw.HTMLBRElement]] =
`。
+
+由其他数据绑定表达式组成的 `@dom` 方法有两种编写方式:
+
+ 1. 你可以在 `@dom` 方法中使用 `bind` 方法来获取其他 `Binding` 的值。
+ 2. 你可以在 `@dom` 方法中使用 `for` 或者 `yield` 表达式将 `BindingSeq` 映射到其他的表达式上。
+
+你可以通过使用 `{ ... }` 插入语法来在其他 HTML 元素中嵌入 `Node` 或者 `BindingSeq[Node]`。
+
+### 第 4 步:在 `main` 方法中将数据绑定表达式渲染至 DOM
+
+``` scala
+@JSExport
+def main(): Unit = {
+ dom.render(document.body, table)
+}
+```
+
+### 第 5 步: 在 HTML 页面中调用 `main` 方法
+
+``` html
+
+
+
+
+
+
+
+
+
+```
+
+至此,你会看见一个只含有表头的空表格,这是因为 `data` 现在是空的。
+
+### 第 6 步:添加一些 `