OutWatch - Functional and reactive Web-Frontend Library with Reactive Programming, VirtualDom and Scala
import outwatch.dom._
import outwatch.dom.dsl._
import monix.execution.Scheduler.Implicits.global
OutWatch.renderInto("#app", h1("Hello World")).unsafeRunSync()
Syntax is almost exactly as in ScalaTags. The UI is made reactive with Monix.
You can find more examples and features at the end of this readme.
For a quick start, install java
, sbt
, nodejs
and yarn
and use the following g8 template:
sbt new outwatch/seed.g8
In your newly created project folder, run:
sbt dev
and point your browser to http://localhost:8080.
Changes to the code will trigger a recompile and automatically refresh the page in the browser.
Install java
, sbt
and nodejs
, if you haven't already.
Create a new SBT project and add the ScalaJS and Scala-js-bundler plugin to your plugins.sbt
:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.25")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.13.1")
Then add the outwatch dependency to your build.sbt
.
libraryDependencies += "io.github.outwatch" %%% "outwatch" % "1.0.0-RC2"
And enable the scalajs-bundler
plugin:
enablePlugins(ScalaJSBundlerPlugin)
If you are curious and want to try the state of the current master
branch, add the following instead:
resolvers += "jitpack" at "https://jitpack.io"
libraryDependencies += "com.github.outwatch" % "outwatch" % "master-SNAPSHOT"
When using JitPack, it is often more useful to point to a specific commit, to make your builds reproducible:
libraryDependencies += "com.github.outwatch" % "outwatch" % "f07849c81"
Like that you can try the latest features from specific commits on master
, other branches or PRs.
To configure hot reloading with webpack devserver, check out build.sbt and webpack.config.dev.js from the g8 template.
If anything is not working, cross-check how things are done in the template.
For bugs, questions and discussions please use Github Issues.
We adopted the Scala Code of Conduct. People are expected to follow it when discussing OutWatch on the Github page, Gitter channel, or other venues.
Outwatch is a web frontend UI framework written in ScalaJS.
If you find any error in the examples, please open an issue on GitHub.
There is a changelog which contains examples of the latest changes: CHANGELOG.md
There is also the outdated but conceptually still correct documentation - contributions welcome.
In your html file, create an element, which you want to replace by dynamic content:
...
<body>
<div id="app"></div>
<!-- your compiled javascript should be imported here -->
</body>
...
To render html content with outwatch, create a component and render it into the given element:
import outwatch.dom._
import outwatch.dom.dsl._
import monix.execution.Scheduler.Implicits.global
object Main {
def main(args: Array[String]): Unit = {
val myComponent = div("Hello World")
OutWatch.renderReplace("#app", myComponent).unsafeRunSync()
}
}
Running Main
will replace <div id="app"></div>
with the content defined in myComponent
:
...
<body>
<div id="app">Hello World</div>
...
</body>
...
Important: In your application, OutWatch.renderReplace
should be called only once at the end of the main method. To create dynamic content, you will design your data-flow with Obvervable
, Subject
and/or Handler
and then instantiate it only once with this method call. Before that, no Observable
subscription will happen.
First, we will focus on creating immutable/static content that will not change over time. The following examples illustrate to construct and transform HTML/SVG tags, attributes and inline stylesheets.
div("Hello ", "World")
// <div>Hello World</div>
div(span("Hey ", b("you"), "!"))
// <div><span>Hey <b>you</b>!</span></div>
div(true, 0, 1000L, 3.0)
// <div>true010003.0</div>
Attributes are put inside the tag.
div(id := "test")
// <div id="test"></div>
The order of content and attributes does not matter.
div("How ", id := "test", "are", title := "cool", " you?")
// <div id="test" title="cool">How are you?</div>
Styles are also written into the tag. All style properties have to be written in camelCase.
div(color := "tomato", "Hello")
// <div style="color: tomato">Hello</div>
Multiple styles will me merged to one style attribute:
div(backgroundColor := "powderblue", border := "2px solid #222", "Hello")
// <div style="background-color: powderblue; border: 2px solid #222">Hello</div>
Again, the order of styles, attributes and inner tags does not matter:
div(h1("Welcome to my website"), backgroundColor := "powderblue", id := "header")
// <div style="background-color: powderblue" id="header">Welcome to my website</div>
Some styles have type safe values:
div(cursor.pointer, fontWeight.bold, display.flex)
// <div style="cursor: pointer; font-weight: bold; display: flex;"></div>
If you are missing more type safe values, please contribute to Scala Dom Types. Example implementation: fontWeight
There are some attributes and styles which are reserved scala keywords. You can use them with backticks:
div(`class` := "item", "My Item")
// <div class="item">My Item</div>
label(`for` := "inputid")
// <label for="inputid" />
There is also a shortcut for the class atrribute:
div(cls := "myclass")
// <div class="myclass"></div>
Source Code: OutwatchAttributes.scala
Attributes and styles with the same name will be overwritten. Last wins.
div(color := "blue", color := "green")
// <div style="color: green"></div>
Source Code: DomUtils.scala
Classes are not overwritten, they accumulate.
div(cls := "tiny", cls := "button")
// <div class="tiny button"></div>
All the tags, attributes and styles available in outwatch come from Scala Dom Types. If you want to use something not available in Scala Dom Types, you can use custom builders:
htmlTag("app")(style("user-select") := "none", attr("everything") := "possible")
// <app style="user-select: none" everything="possible"></div>
You can also define the accumulation behavior of custom attributes:
div(
attr("everything").accum("-") := "is",
attr("everything").accum("-") := "possible",
)
// <div everything="is-possible"></div>
If you think there is something missing in Scala Dom Types, please open a PR or Issue. Usually it's just one line of code.
Source Code: DomTypes.scala
Data attributes make use of scala.Dynamic
, so you can write things like:
div(data.payload := "17")
// <div data-payload="17"></div>
Source Code: OutwatchAttributes.scala, Builder.scala
SVG tags and attributes are available via an extra import. Namespacing is automatically handled for you.
val graphic = {
import svg._
svg(
viewBox := "0 0 10 10",
g(
transform := "matrix(.096584 0 0 .096584 -.0071925 -18.66)",
path(d := "M10 10 C 20 20, 40 20, 50 10", fill := "mistyrose")
)
)
}
// <svg viewBox="0 0 10 10"><g transform="matrix(.096584 0 0 .096584 -.0071925 -18.66)"><path d="M10 10 C 20 20, 40 20, 50 10" fill="mistyrose"></path></g></svg>
Outwatch tries hard to render everything you throw at it. Combine Option
and Seq
to fit your needs. Note, that outwatch does not accept Set
, since the order is undefined.
div(
Some("thing"),
Some(color := "steelblue"),
fontSize :=? Some("70px"),
None,
Seq("Hey", "How are you?"),
List("a", "b", "c").map(span(_)),
Some(Seq("x")),
)
// <div style="color: steelblue; font-size: 70px;">
// thing
// Hey
// How are you?
// <span>a</span>
// <span>b</span>
// <span>c</span>
// x
// </div>
Source Code: AsVDomModifier.scala
The important types we were using in the examples above are VNode
and VDomModifier
:
val vnode: VNode = div()
val modifiers: List[VDomModifier] = List("Hello", id := "main", color := "tomato", vnode)
Every VNode
contains a sequence of VDomModifier
. A VNode
is a VDomModifier
itself.
There are implicits for converting primitives, Option[VDomModifier]
, Seq[VDomModifier]
to VDomModifier
.
To make a set of modifiers reusable you can group them to become one VDomModifier
.
val bigFont = VDomModifier(fontSize := "40px", fontWeight.bold)
div("Argh!", bigFont)
// <div style="font-size: 40px; font-weight: bold;">Argh!</div>
You can also use a Seq[VDomModifier]
directly instead of using apply
defined in the VDomModifier object.
Outwatch does not have the concept of a component itself. You can just pass the VNode
s and VDomModifier
s around and build your own abstractions using functions and classes. When we are talking about components in this documentation, we are usually referring to a VNode
or a function returning a VNode
.
def fancyHeadLine(content: String) = h1(borderBottom := "1px dashed tomato", content)
fancyHeadLine("I like tomatoes.")
// <h1 style="border-bottom: 1px dashed tomato;">I like tomatoes.</h1>
Components are immutable, we can only modify them by creating a changed copy. Like you may know from Scalatags, you can call .apply(...)
on any VNode
, append more modifiers and get a new VNode
with the applied changes back.
val a = div("dog")
a(title := "the dog")
// <div title="the dog">dog</div>
This can be useful for reusing html snippets.
val box = div(width := "100px", height := "100px")
div(
box(backgroundColor := "powderblue"),
box(backgroundColor := "mediumseagreen"),
)
// <div>
// <div style="width: 100px; height: 100px; background-color: powderblue;"> </div>
// <div style="width: 100px; height: 100px; background-color: mediumseagreen;"></div>
// </div>
Since modifiers are appended, they can overwrite existing ones. This is useful to adjust existing components to your needs.
val box = div(width := "100px", height := "100px")
box(backgroundColor := "mediumseagreen", width := "200px")
// <div style="width: 200px; height: 100px; background-color: mediumseagreen;"></div>
You can also prepend modifiers. This can be useful to provide defaults retroactively.
def withBorderIfNotProvided(vnode: VNode) = vnode.prepend(border := "3px solid coral")
div(
withBorderIfNotProvided(div("hello", border := "7px solid moccasin")),
withBorderIfNotProvided(div("hello")),
)
// <div>
// <div style="border: 7px solid moccasin;">hello</div>
// <div style="border: 3px solid coral;">hello</div>
// </div>
Source Code: VDomModifier.scala
When working with Flexbox, you can set styles for the container and children. With VNode.apply()
you can have all flexbox-related styles in one place. The child-components don't have to know anything about flexbox, even though they get specific styles assigned.
val itemA = div("A", backgroundColor := "mediumseagreen")
val itemB = div("B", backgroundColor := "tomato")
div(
height := "100px",
border := "1px solid black",
display.flex,
itemA(flexBasis := "50px"),
itemB(alignSelf.center),
)
// <div style="height: 100px; border: 1px solid black; display: flex;">
// <div style="background-color: mediumseagreen; flex-basis: 50px;">A</div>
// <div style="background-color: tomato; align-self: center;">B</div>
// </div>
To visualize updates, use an Observable[VDomModifier]
as if it was a VDomModifier
.
import outwatch.dom._
import outwatch.dom.dsl._
import monix.reactive.Observable
import monix.execution.Scheduler.Implicits.global
import concurrent.duration._
object Main {
def main(args: Array[String]): Unit = {
val counter = Observable.interval(1 second)
val counterComponent = div("count: ", counter)
OutWatch.renderReplace("#app", counterComponent).unsafeRunSync()
}
}
Important: In your application, OutWatch.renderReplace
should be called only once at the end of the main method. To create dynamic content, you will design your data-flow with Obvervable
, Subject
and/or Handler
and then instantiate it only once with this method call. Before that, no Observable
subscription will happen.
Attributes can also take dynamic values.
val color = Observable.interval(1 second).map(i => if(i % 2 == 0) "deepskyblue" else "gold")
div(width := "100px", height := "100px", backgroundColor <-- color)
You can stream arbitrary VDomModifiers
.
val dynamicSize:Observable[VDomModifier] = Observable.interval(1 second).map(i => fontSize := s"${i}px")
div("Hello!", dynamicSize)
val nodeStream:Observable[VNode] = Observable.interval(1 second).map(i => div(s"Number $i"))
div("Hello ", nodeStream)
We have prepared the two typeclasses AsValueObservable
and AsObserver
to work with arbitrary streaming libraries. AsValueObservable
is for the reading part of the stream, and AsObserver
is for the writing part.
Example: To use outwatch with scala.rx:
import rx._
import monix.reactive._
import monix.execution._
implicit object RxAsValueObservable extends AsValueObservable[Rx] {
override def as[T](stream: Rx[T]): ValueObservable[T] = new ValueObservable[T]{
def value = Option(stream.now)
def observable = Observable.create[T](OverflowStrategy.Unbounded) { observer =>
implicit val ctx = Ctx.Owner.Unsafe
val obs = stream.triggerLater(observer.onNext(_))
Cancelable(() => obs.kill())
}
}
}
implicit object VarAsObserver extends AsObserver[Var] {
override def as[T](stream: Var[_ >: T]): Observer[T] = new Observer.Sync[T] {
override def onNext(elem: T): Ack = {
stream() = elem
Ack.Continue
}
override def onError(ex: Throwable): Unit = throw ex
override def onComplete(): Unit = ()
}
}
// if you want to use managed()
implicit def obsToCancelable(obs: Obs): Cancelable = {
Cancelable(() => obs.kill())
}
helpers.OutwatchTracing.patch.zipWithIndex.foreach { case (proxy, index) =>
org.scalajs.dom.console.log(s"Snabbdom patch ($index)!", JSON.parse(JSON.stringify(proxy)), proxy)
}
Dynamic components with Observables
can have errors. This is if onError
is called on the underlying Observer
. You can trace them in OutWatch with:`
helpers.OutwatchTracing.error.foreach { case throwable =>
org.scalajs.dom.console.log(s"Exception while patching an Outwatch compontent: ${throwable.getMessage}")
}
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.