- Notifications
You must be signed in to change notification settings - Fork102
Reactive data-binding for Scala
License
ThoughtWorksInc/Binding.scala
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Binding.scala is a data-binding library forScala, running on both JVM andScala.js.
Binding.scala can be used as the basis of UI frameworks, however latest Binding.scala 12.x does not contain any build-in UI frameworks any more. For creating reactive HTML UI, you may want to check outhtml.scala, which is an UI framework based on Binding.scala, and it is also the successor of previously built-indom library. See alsoReact / Binding.scala / html.scala Interoperability for using existing React components with Binding.scala,
SeeBinding.scala • TodoMVC orScalaFiddle DEMOs as examples for common tasks when working with Binding.scala.
Binding.scala and html.scala has more features and less concepts than other reactive web frameworks likeReactJS.
| Binding.scala | ReactJS | |
|---|---|---|
| Support HTML literal? | Yes | Partially supported. Regular HTML does not compile, unless developers manually replacesclass andfor attributes toclassName andhtmlFor, and manually converts inlinestyles from CSS syntax to JSON syntax. |
| Algorithm to update DOM | Precise data-binding, which is faster than virtual DOM | Virtual DOM differentiation, which requires manually managedkey attributes for complicated DOM. |
| Lifecycle management for data-binding expressions | Automatically | N/A |
| Statically type checking | Yes, even for HTML tags and attribues | No |
| Learning curve | Always easy | Easy to start. Requires much more efforts to understand its corner cases. |
SeeDesign section for more information.
We will build an Binding.scala web page during the following steps.
Seehttp://www.scala-js.org/tutorial/basic/ for information about how to setup such a project.
// Enable macro annotations by setting scalac flags for Scala 2.13scalacOptions++= {importOrdering.Implicits._if (VersionNumber(scalaVersion.value).numbers>=Seq(3L)) {Nil }if (VersionNumber(scalaVersion.value).numbers>=Seq(2L,13L)) {Seq("-Ymacro-annotations") }else {Nil }}// Enable macro annotations by adding compiler plugins for Scala 2.12libraryDependencies++= {importOrdering.Implicits._if (VersionNumber(scalaVersion.value).numbers>=Seq(2L,13L)) {Nil }else {Seq(compilerPlugin("org.scalamacros"%"paradise"%"2.1.1" crossCrossVersion.full)) }}libraryDependencies+="com.yang-bo"%%%"html"%"latest.release"
Step 2: Create adata field, which contains someVar andVars as data source for your data-binding expressions
caseclassContact(name:Var[String],email:Var[String])valdata=Vars.empty[Contact]
AVar represents a bindable variable,which also implementsBinding trait,hence aVar can be seen as a binding expression as well.If another expression depends on aVar, the value of the expression changes whenever value of theVar changes.
AVars represents a sequence of bindable variables,which also implementsBindingSeq trait,hence aVars can be seen as a binding expression of a sequence as well.If another comprehension expression depends on aVars,the value of the expression changes whenever value of theVars changes.
// For Scala 3deftable:Binding[HTMLTableElement]= {html"""<table border="1" cellPadding="5"> <thead> <tr> <th>Name</th> <th>E-mail</th> </tr> </thead> <tbody>${for (contact<- data)yield {html"""<tr> <td>${contact.name.bind} </td> <td>${contact.email.bind} </td> </tr>""" } } </tbody> </table>"""}
// For Scala 2@htmldeftable:Binding[HTMLTableElement]= { <tableborder="1"cellPadding="5"> <thead> <tr> <th>Name</th> <th>E-mail</th> </tr> </thead> <tbody> {for (contact<- data)yield { <tr> <td> {contact.name.bind} </td> <td> {contact.email.bind} </td> </tr> } } </tbody> </table>}
html"""...""" interpolation in Scala 3 (or @html annotated methods in Scala 3) represents an reactive XHTML template, which supports HTML literal.The type of HTML interpolation/literal is a specific subtype ofcom.thoughtworks.binding.Binding[org.scalajs.dom.Node] orcom.thoughtworks.binding.Binding.BindingSeq[org.scalajs.dom.Node],instead ofscala.xml.Node orscala.xml.NodeSeq.So we could havedef node: Binding[HTMLBRElement] = html"""<br/>"""anddef node: BindingSeq[HTMLBRElement] = html"""<br/><br/>""".
A HTML interpolation/literal method is composed with other data-binding expressions in two ways:
- You could use
bindmethod in an interpolation to get the value of anotherBinding. - You could use
for/yieldexpression in a@htmlmethod to map aBindingSeqto another.
You can nestNode orBindingSeq[Node] in other HTML element literals via{ ... } interpolation syntax.
@JSExportdefmain():Unit= { html.render(document.body, table)}
<!DOCTYPE html><html><head><scripttype="text/javascript"src="js-fastopt.js"></script></head><body><scripttype="text/javascript">SampleMain().main()</script></body></html>
Now you will see a table that just contains a header, becausedata is empty at the moment.
deftable:BindingSeq[Node]= {html"""<div> <button onclick=${event:Event=> data.value+=Contact(Var("Yang Bo"),Var("yang.bo@rea-group.com")) } > Add a contact </button> </div> <table border="1" cellPadding="5"> <thead> <tr> <th>Name</th> <th>E-mail</th> <th>Operation</th> </tr> </thead> <tbody>${for (contact<- data)yield { <tr> <td>${contact.name.bind} </td> <td>${contact.email.bind} </td> <td><button onclick=${event:Event=> contact.name.value="Modified Name" }>Modify the name </button> </td> </tr> } } </tbody> </table>"""}
When you click the "Add a contact" button, it appends a new Contact intodata,then, Binding.scala knows the relationship between DOM anddata,so it decides to append a new<tr> corresponding to the newly appended Contact.
And when you click the "Modify the name", thename field oncontact changes,then, Binding.scala decides to change the content of the correspondingtr to new value ofname field.
ReactJS requires users to provide arender function for each component.Therender function should mapprops andstate to a ReactJS's virtual DOM,then ReactJS framework creates a DOM with the same structure as the virtual DOM.
Whenstate changes, ReactJS framework invokesrender function to get a new virtual DOM.Unfortunately, ReactJS framework does not precisely know what thestate changing is.ReactJS framework has to compare the new virtual DOM and the original virtual DOM,and guess the changeset between the two virtual DOM,then apply the guessed changeset to the real DOM as well.
For example, after you prepend a table row<tr> into an existing<tbody> in a<table>,ReactJS may think you also changed the content of every existing<tr> of the<tbody>.
The reason for this is that therender function for ReactJS does not describe the relationship betweenstate and DOM.Instead, it describes the process to create a virtual DOM.As a result, therender function does not provide any information about the purpose of thestate changing,although a data-binding framework should need the information.
Unlike ReactJS, a Binding.scala@html method is NOT a regular function.It is a template that describes the relationship between data source and the DOM.When part of the data source changes, Binding.scala knows about the exact corresponding partial DOM affected by the change,thus only re-evaluating that part of the@html method to reflect the change in the DOM.
With the help of the ability of precise data-binding provided by Binding.scala,you can get rid of concepts for hinting ReactJS's guessing algorithm,likekey attribute,shouldComponentUpdate method,componentDidUpdate method orcomponentWillUpdate method.
The smallest composable unit in ReactJS is a component.It is fair to say that a React component is lighter than an AngularJS controller,while Binding.scala is better than that.
The smallest composable unit in Binding.scala is a@html method.Every@html method is able to compose other@html methods via.bind.
caseclassContact(name:Var[String],email:Var[String])defbindingButton(contact:Contact)= {html"""<button onclick=${event:Event=> contact.name.value="Modified Name" } > Modify the name </button>"""}defbindingTr(contact:Contact)= {html"""<tr> <td>${ contact.name.bind }</td> <td>${ contact.email.bind }</td> <td>${ bindingButton(contact).bind }</td> </tr>"""}defbindingTable(contacts:BindingSeq[Contact])= {html"""<table> <tbody>${for (contact<- contacts)yield { bindingTr(contact) } } </tbody> </table>"""}@JSExportdefmain():Unit= {valdata=Vars(Contact(Var("Yang Bo"),Var("yang.bo@rea-group.com"))) dom.render(document.body, bindingTable(data))}
You may find out this approach is much simpler than ReactJS, as:
- Instead of passing
propsin ReactJS, you just simply provide parameters for Binding.scala. - Instead of specifying
propTypesin ReactJS, you just simply define the types of parameters in Binding.scala. - Instead of raising a run-time error when types of props do not match in ReactJS, you just check the types at compile-time.
The ability of precise data-binding in Binding.scala requires listener registrations on the data source.Other reactive frameworks that have the ability ask users manage the lifecycle of data-binding expressions.
For example,MetaRx provides adispose method to unregister the listeners created when building data-binding expressions.The users of MetaRx have the responsibility to calldispose method for everymap andflatMap call after the expression changes,otherwise MetaRx leaks memory. Unfortunately, manuallydisposeing everything is too hard to be right for complicated binding expressions.
Another reactive web frameworkWidok did not provide any mechanism to manage lifecycle of of data-binding expressions.As a result, it simply always leaks memory.
In Binding.scala, unlike MetaRx or Widok, all data-binding expressions are pure functional, with no side-effects.Binding.scala does not register any listeners when users create individual expressions,thus users do not need to manually unregister listeners for a single expression like MetaRx.
Instead, Binding.scala creates all internal listeners together,when the user callsdom.render orBinding.watch on the root expression.Note thatdom.render orBinding.watch manages listeners on all upstream expressions,not only the direct listeners of the root expression.
In brief, Binding.scala separates functionality in two kinds:
- User-defined
@htmlmethods, which produce pure functional expressions with no side-effects. - Calls to
dom.renderorBinding.watch, which manage all side-effects automatically.
As you see, you can embed HTML literals in@html methods in Scala source files.You can also embed Scala expressions in braces in content or attribute values of the HTML literal.
defnotificationBox(message:String):Binding[Div]= {html"""<div title=${s"Tooltip:$message" }> { message } </div>"""}
Despite the similar syntax of HTML literal between Binding.scala and ReactJS,Binding.scala creates real DOM instead of ReactJS's virtual DOM.
In the above example,<div>...</div> creates a DOM element with the type oforg.scalajs.dom.html.Div.Then, the magic@html lets the method wrap the result as aBinding.
You can even assign theHTMLDivElement to a local variable and invoke native DOM methods on the variable:
defnotificationBox(message:String):Binding[HTMLDivElement]= {valresult:Binding.Stable[HTMLDivElement]=html"""<div title=${s"Tooltip:$message" }>${ message } </div>""" result.value.scrollIntoView() result}
scrollIntoView method will be invoked when theHTMLDivElement is created.If you invoke another method not defined inHTMLDivElement,the Scala compiler will report a compile-time error instead of bringing the failure to run-time,because Scala is a statically typed language and the Scala compiler understands the type ofDiv.
You may also noticeclass andtitle. They are DOM properties or HTML attributes onDiv.They are type-checked by Scala compiler as well.
For example, given the followingtypo method:
deftypo= {valmyDiv=html"""<div typoProperty="xx">content</div>""" myDiv.value.typoMethod() myDiv}
The Scala compiler will report errors like this:
typo.scala:23: typoProperty is neither a valid property nor a valid attribute for <DIV> val myDiv = html"""<div typoProperty="xx">content</div>""" ^typo.scala:24: value typoMethod is not a member of org.scalajs.dom.HTMLDivElement myDiv.value.typoMethod() ^With the help of the static type system,@html methods can be much more robust than ReactJS components.
You can find a complete list of supported properties and methods onscaladoc of scalajs-dom orMDN
- TodoMVC: a project which offers the same Todo application implemented using MV* concepts in most of the popular JavaScript MV* frameworks of today.
- Granblue Raid Finder: a site for finding Granblue Fantasy raid tweets.
- Game of Life: Conway's Game of Life implemented with Binding.scala.
- playground-binding.scala: Various DEMOs with scala, scalajs and binding.scala
- CITE Application: A single-page browser application for exploring citable resources.
- hmt-reader: A package of application and data for reading Homer Multitext textual data, in its current release.
- Full-Stack-Scala-Starter: Play 2.5, ScalaJS, Binding.scala starter project.
- scala-adapters: A simple framework to implement your server jobs - providing a standard UI-client to monitor and test them. (Used in Production)
- Binding.scala-Google-Maps: A step-by-step tutorial to get you started with Binding.scala.
- scala-adapters.g8: A Giter8 template for a full-stack Scala project that usesscala-adapters.
- play-akka-telegrambot4s-incidents: An incident management app - where you can send incidents with a chat bot.
- scala-adapters-images: A demo project that usesscala-adapters andplay-akka-telegrambot4s.
- jmh-view: An embeddable JMH report viewer, which is used to to createthe report for the benchmarks of Scala parsers.
- RL-Playground: A web-based interactive reinforcement learning demonstration for games.
- Word Cloud Generator: A browser extension to generate word cloud visualizations of web pages, text files, or other arbitrary text inputs.
(Feel free to add your project here)
Binding.scala has an extremely tiny code base.The source files are split into few libraries, one file per library.
This module is available for both JVM and Scala.js. You could add it in yourbuild.sbt.
// For JVM projectslibraryDependencies+="com.thoughtworks.binding"%%"binding"%"latest.release"
// For Scala.js projects, or JS/JVM cross projectslibraryDependencies+="com.thoughtworks.binding"%%%"binding"%"latest.release"
This is the new HTML templating library based onName Based XML Literals, the module is only available for Scala.js, and the Scala version must between 2.12 and 2.13. You could add it in yourbuild.sbt.
// Enable macro annotations by setting scalac flags for Scala 2.13scalacOptions++= {importOrdering.Implicits._if (VersionNumber(scalaVersion.value).numbers>=Seq(2L,13L)) {Seq("-Ymacro-annotations") }else {Nil }}// Enable macro annotations by adding compiler plugins for Scala 2.12libraryDependencies++= {importOrdering.Implicits._if (VersionNumber(scalaVersion.value).numbers>=Seq(2L,13L)) {Nil }else {Seq(compilerPlugin("org.scalamacros"%"paradise"%"2.1.1" crossCrossVersion.full)) }}// For Scala.js projects (Scala 2.12 - 2.13)libraryDependencies+="com.yang-bo"%%%"html"%"latest.release"
Seehtml.scala for more information.
This module is available for both JVM and Scala.js. You could add it in yourbuild.sbt.
// For JVM projectslibraryDependencies+="com.thoughtworks.binding"%%"futurebinding"%"latest.release"
// For Scala.js projects, or JS/JVM cross projectslibraryDependencies+="com.thoughtworks.binding"%%%"futurebinding"%"latest.release"
SeeFutureBinding for more information.
This module is only available for Scala.js. You could add it in yourbuild.sbt.
// For Scala.js projectslibraryDependencies+="com.thoughtworks.binding"%%%"jspromisebinding"%"latest.release"
SeeFutureBinding for more information.
Due to collection API changes, Binding.scala 12.x only works on Scala 2.13, targeting JVM, Scala.js 0.6 and Scala.js 1.x.
For Scala 2.10, 2.11 and 2.12 on JVM or Scala.js 0.6, useBinding.scala 11.x instead.
- html.scala - HTML templating library built with Binding.scala
- LatestEvent.scala - Event handling and URL routing for Binding.scala
- FutureBinding.scala - A wrapper that wraps a
scala.concurrent.Futureto aBinding. - JSPromiseBinding.scala - A wrapper that wraps a JavaScript
Promiseto aBinding. - ReactToBindingHtml.scala - React / Binding.scala / html.scala Interoperability
- scalajs-all-in-one-template - All-in-One Scala.js Static Web Project Template (including Binding.scala)
- Binding.scala • TodoMVC - An example todo app built with Binding.scala
About
Reactive data-binding for Scala
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
