readme
Welcome to Outwatch! We hope you enjoy this documentation. If you find something that can be improved, please do! Every Pull Request with a big or small improvement is very much appreciated. See Improving The Documentation.
Getting started
Start with a template
Scala3 + Mill + Vite
https://github.com/outwatch/example-mill-vite
Scala2 + SBT + ScalaJSBundler
https://github.com/outwatch/example
Create a project from scratch
Make sure that java
, sbt
, nodejs
and yarn
are installed.
Create a new SBT project and add outwatch to your library dependencies in your build.sbt
:
libraryDependencies ++= Seq(
"io.github.outwatch" %%% "outwatch" % "<latest outwatch version>",
// optional dependencies:
"com.github.cornerman" %%% "colibri-zio" % "x.x.x", // zio support
"com.github.cornerman" %%% "colibri-fs2" % "x.x.x", // fs2 support
"com.github.cornerman" %%% "colibri-airstream" % "x.x.x", // airstream support
"com.github.cornerman" %%% "colibri-rx" % "x.x.x", // scala.rx support
"com.github.cornerman" %%% "colibri-router" % "x.x.x", // Url Router support
)
You may decide whether you want to use vite or webpack for bundling and post processing your javascript project.
vite (recommended)
Have a look at this example: https://github.com/outwatch/example-mill-vite
webpack with scalajs-bundler (legacy)
Add the scalajs
and scalajs-bundler
plugins to your project/plugins.sbt
:
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.x.x")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "x.x.x")
Setup scalajs-bundler in your build.sbt
:
enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin)
useYarn := true // Makes scalajs-bundler use yarn instead of npm
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)) // configure Scala.js to emit a JavaScript module instead of a top-level script
scalaJSUseMainModuleInitializer := true // On Startup, call the main function
Now you can compile your scala code to javascript and bundle it together with their javascript dependencies:
sbt fastOptJS/webpack # development build
sbt fullOptJS/webpack # production build
You will find the resulting javascript files here: target/scala-*/scalajs-bundler/main/webapp-fastopt.js
and webapp-opt.js
.
See here for further documentation: https://github.com/scalacenter/scalajs-bundler
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.
Use common javascript libraries with Outwatch
We have prepared helpers for some javascript libraries. You can find them in the Outwatch-libs Repository.
Convert html to outwatch
You can convert html code to outwatch code on this wonderful page: https://simerplaha.github.io/html-to-scala-converter/
Examples
Hello World
In your html file, create an element, which you want to replace with 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._
import outwatch.dsl._
import cats.effect.SyncIO
object Main {
def main(args: Array[String]): Unit = {
val myComponent = div("Hello World")
Outwatch.renderReplace[SyncIO]("#app", myComponent).unsafeRunSync()
}
}
Running Main
will replace <div id="app"></div>
with myComponent
:
...
<body>
<div id="app">Hello World</div>
...
</body>
...
Interactive Counter
import outwatch._
import outwatch.dsl._
import colibri._
import cats.effect.SyncIO
val component = {
val counter = Subject.behavior(0)
div(
button("+", onClick(counter.map(_ + 1)) --> counter),
counter,
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
To understand how this example works in-depth, please read about Dynamic Content and Handling Events.
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 Scala.Rx
and then instantiate outwatch only once with this method call. Before that, no reactive subscriptions will happen.
Static Content
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.
Note: We created the helpers showHTMLForDoc
and docPreview
for showing results in this documentation. They are not part of outwatch.
Imports
import outwatch._
import outwatch.dsl._
import cats.effect.SyncIO
Concatenating Strings
div("Hello ", "World").showHTMLForDoc(docPreview)
Nesting
div(span("Hey ", b("you"), "!")).showHTMLForDoc(docPreview)
Primitives
div(true, 0, 1000L, 3.0).showHTMLForDoc(docPreview)
Attributes
div(idAttr := "test").showHTMLForDoc(docPreview)
The order of content and attributes does not matter.
div("How ", idAttr := "test", "are", title := "cool", " you?").showHTMLForDoc(docPreview)
Styles
All style properties have to be written in camelCase.
div(backgroundColor := "tomato", "Hello").showHTMLForDoc(docPreview)
Multiple styles will me merged to one style attribute:
div(
backgroundColor := "powderblue",
border := "2px solid #222",
"Hello",
).showHTMLForDoc(docPreview)
Again, the order of styles, attributes and inner tags does not matter:
div(
h1("Welcome to my website"),
backgroundColor := "powderblue",
idAttr := "header"
).showHTMLForDoc(docPreview)
Some styles have type safe values:
div(cursor.pointer, fontWeight.bold, display.flex).showHTMLForDoc(docPreview)
If you are missing more type safe values, please contribute to Scala Dom Types. Example implementation: fontWeight
Reserved Scala keywords: class, for, type
There are some attributes and styles which are reserved scala keywords. You can use them with backticks:
div(`class` := "item", "My Item").showHTMLForDoc(docPreview)
label(`for` := "inputid").showHTMLForDoc(docPreview)
input(`type` := "text").showHTMLForDoc(docPreview)
There are shortcuts for the class and type atrributes:
div(cls := "myclass").showHTMLForDoc(docPreview)
input(tpe := "text").showHTMLForDoc(docPreview)
Overriding attributes
Attributes and styles with the same name will be overwritten. Last wins.
div(color := "blue", color := "green").showHTMLForDoc(docPreview)
CSS class accumulation
Classes are not overwritten, they accumulate.
div(cls := "tiny", cls := "button").showHTMLForDoc(docPreview)
Custom attributes, styles and tags
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:
VNode.html("app")(
VMod.style("user-select") := "none",
VMod.attr("everything") := "possible",
VMod.prop("it") := "is"
).showHTMLForDoc(docPreview)
There also exists VNode.svg(tagName)
.
You can also define the accumulation behavior of custom attributes:
div(
VMod.attr("everything").accum("-") := "is",
VMod.attr("everything").accum("-") := "possible",
).showHTMLForDoc(docPreview)
If you think there is something missing in Scala Dom Types, please open a PR or Issue. Usually it's just a few lines of code.
Source Code: DomTypes.scala
Data and Aria attributes
Data and aria attributes make use of scala.Dynamic
, so you can write things like:
div(
data.payload := "17",
data.`consent-required` := "Are you sure?",
data.message.success := "Message sent!",
aria.hidden := "true",
).showHTMLForDoc(docPreview)
Source Code: OutwatchAttributes.scala, Builder.scala
SVG
SVG tags and attributes are available through an extra import. Namespacing is automatically handled for you.
val component = {
import svg._
svg(
height := "100px",
viewBox := "0 0 471.701 471.701",
g(
path(d := """M433.601,67.001c-24.7-24.7-57.4-38.2-92.3-38.2s-67.7,13.6-92.4,38.3l-12.9,12.9l-13.1-13.1
c-24.7-24.7-57.6-38.4-92.5-38.4c-34.8,0-67.6,13.6-92.2,38.2c-24.7,24.7-38.3,57.5-38.2,92.4c0,34.9,13.7,67.6,38.4,92.3
l187.8,187.8c2.6,2.6,6.1,4,9.5,4c3.4,0,6.9-1.3,9.5-3.9l188.2-187.5c24.7-24.7,38.3-57.5,38.3-92.4
C471.801,124.501,458.301,91.701,433.601,67.001z M414.401,232.701l-178.7,178l-178.3-178.3c-19.6-19.6-30.4-45.6-30.4-73.3
s10.7-53.7,30.3-73.2c19.5-19.5,45.5-30.3,73.1-30.3c27.7,0,53.8,10.8,73.4,30.4l22.6,22.6c5.3,5.3,13.8,5.3,19.1,0l22.4-22.4
c19.6-19.6,45.7-30.4,73.3-30.4c27.6,0,53.6,10.8,73.2,30.3c19.6,19.6,30.3,45.6,30.3,73.3
C444.801,187.101,434.001,213.101,414.401,232.701z""", fill := "tomato")
)
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
VNode and VMod
The important types we are using in the examples above are VNode
and VMod
. VNode
represents a node in the virtual dom, while and VMod
represents atrributes and styles and children of a node.
val vnode: VNode = div()
val modifiers: List[VMod] = List("Hello", idAttr := "main", color := "tomato", vnode)
Every VNode
contains a sequence of VMod
. And a VNode
is a VMod
itself.
Grouping Modifiers
To make a set of modifiers reusable you can group them to become one VMod
.
val bigFont = VMod(fontSize := "40px", fontWeight.bold)
div("Argh!", bigFont).showHTMLForDoc(docPreview)
If you want to reuse bigFont
, but want to overwrite one of its properties, simply append the overwriting modifier. Here the latter fontSize
will overwrite the one from bigFont
:
val bigFont = VMod(fontSize := "40px", fontWeight.bold)
val bigFont2 = VMod(bigFont, fontSize := "99px")
div("Argh!", bigFont2).showHTMLForDoc(docPreview)
You can also use a Seq[VMod]
directly instead of using VMod.apply
.
Components
Outwatch does not have the concept of a component itself. You can just pass VNode
s and VMod
s around and build your own abstractions using functions. 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.").showHTMLForDoc(docPreview)
Transforming Components
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 x = div("dog")
x(title := "the dog").showHTMLForDoc(docPreview)
This can be useful for reusing html snippets.
val box = div(width := "100px", height := "100px")
div(
box(backgroundColor := "powderblue"),
box(backgroundColor := "mediumseagreen"),
).showHTMLForDoc(docPreview)
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").showHTMLForDoc(docPreview)
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")),
).showHTMLForDoc(docPreview)
Source Code: VMod.scala
Example: Flexbox
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 := "cornflowerblue")
val component = div(
height := "100px",
border := "1px solid black",
display.flex,
itemA(flexBasis := "50px"),
itemB(alignSelf.center),
)
component.showHTMLForDoc(docPreview)
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Option and Seq
Outwatch can render anything that implements the type class Render
. Instances for types like Option
and Seq
are built-in and can be arbitrarily combined:
div(
Some("thing"),
Some(color := "steelblue"),
fontSize := Some("70px"),
Seq("Hey", "How are you?"),
List("a", "b", "c").map(div(_)),
Some(Seq("x")),
).showHTMLForDoc(docPreview)
Note, that outwatch does not accept Set
, since the order is undefined.
Rendering Custom Types
You can render any custom type by implementing the typeclass Render
:
case class Person(name: String, age: Int)
// Type class instance for `Render`:
object Person {
implicit object PersonRender extends Render[Person] {
def render(person: Person): VMod = div(
border := "2px dotted coral",
padding := "10px",
marginBottom := "5px",
b(person.name), ": " , person.age
)
}
}
// Now you can just use instances of `Person` in your dom definitions:
val hans = Person("Hans", age = 16)
val peter = Person("Peter", age = 22)
val component = div(hans, peter)
component.showHTMLForDoc(docPreview)
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Source Code: Render.scala
Dynamic Content
Reactive Programming
Outwatch natively renders reactive data types, like Observable
or Rx
(Scala.Rx). Outwatch internally uses the colibri
library and therefore provides lightweight observables and subjects out of the box. BehaviorSubject
(which is also an Observable
) is especially useful to deal with state. All subscriptions are automatically handled by outwatch.
import outwatch._
import outwatch.dsl._
import cats.effect.{IO, SyncIO}
// zio
import zio.Runtime.default
import colibri.ext.zio._
// fs2
import cats.effect.unsafe.IORuntime.global
import colibri.ext.fs2._
// airstream
import colibri.ext.airstream._
import scala.concurrent.duration._
val duration = 1.second
val durationMillis = duration.toMillis.toInt
val zioDuration = zio.Duration.fromScala(1.second)
val component = {
div(
div(
"Observable (colibri): ",
colibri.Observable.interval(duration),
),
div(
"EventStream (airstream): ",
com.raquo.airstream.core.EventStream.periodic(intervalMs = durationMillis)
),
div(
"Stream (fs2): ",
fs2.Stream.awakeDelay[IO](duration).as(1).scan[Int](0)(_ + _),
),
div(
"Stream (zio): ",
zio.stream.ZStream.tick(zioDuration).as(1).scan[Int](0)(_ + _),
)
)
}
Outwatch.renderInto[SyncIO](docPreview, component).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 Observable
, Subject
and/or Scala.Rx
and then instantiate outwatch only once with this method call. Before that, no reactive subscriptions will happen.
Reactive attributes
Attributes can also take dynamic values.
import colibri.Observable
import concurrent.duration._
val component = {
val boxWidth = Observable.interval(1.second).map(i => if(i % 2 == 0) "100px" else "50px")
div(
width <-- boxWidth,
height := "50px",
backgroundColor := "cornflowerblue",
transition := "width 0.5s",
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Reactive Modifiers and VNodes
You can stream any VMod
and therefore whole components, attributes, styles, sets of modifiers, and so on:
import colibri.Observable
import concurrent.duration._
val component = {
div(
div(
width := "50px",
height := "50px",
Observable.interval(1.second)
.map{ i =>
val color = if(i % 2 == 0) "tomato" else "cornflowerblue"
backgroundColor := color
},
),
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
import colibri.Observable
import concurrent.duration._
val component = {
val nodeStream: Observable[VNode] = Observable(div("I am delayed!")).delay(5.seconds)
div("Hello ", nodeStream)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Rendering Futures
Futures are natively supported too:
import scala.concurrent.Future
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
val component = {
div(
Future { 1 + 1 },
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Rendering Async Effects
You can render any type like cats.effect.IO
or zio.Task
(using the typeclass colibri.effect.RunEffect
). The effect will be run whenever an element is rendered with this modifier. The implementation normally tries to run the effect sync and switches if there is an async boundary (e.g. IO#syncStep
). So you can do:
import cats.effect.IO
// import cats.effect.unsafe.Runtime.default
import colibri.ext.zio._
import zio.ZIO
// import zio.Runtime.default
div(
IO {
// doSomething
"result from IO"
},
ZIO.attempt {
// doSomething
"result from ZIO"
}
)
Rendering Sync Effects
You can render synchronous effects like cats.effect.SyncIO
as well (using the typeclass colibri.effect.RunSyncEffect
). The effect will be run sync whenever an element is rendered with this modifier. Example:
import cats.effect.SyncIO
div(
SyncIO {
// doSomething
"result"
}
)
Alternatively you can do the following to achieve the same effect:
div(
VMod.eval {
// doSomething
"result"
}
)
Higher Order Reactiveness
Don't fear to nest different reactive constructs. Outwatch will handle everything for you. Example use-cases include sequences of api-requests, live database queries, etc.
import scala.concurrent.Future
import concurrent.duration._
import colibri.Observable
import cats.effect.{SyncIO, IO}
val component = {
div(
div(Observable.interval(1.seconds).map(i => Future.successful { i*i })),
div(Observable.interval(1.seconds).map(i => IO { i*2 })),
div(IO { Observable.interval(1.seconds) }),
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
This is effectively the same as:
import scala.concurrent.Future
import concurrent.duration._
import colibri.Observable
import cats.effect.{SyncIO, IO}
val component = {
div(
div(Observable.interval(1.seconds).mapFuture(i => Future.successful { i*i })),
div(Observable.interval(1.seconds).mapEffect(i => IO { i*2 })),
div(Observable.interval(1.seconds)),
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Using other streaming libraries
We use the library colibri
for a minimal reactive library and for typeclasses around streaming. These typeclasses like Source
and Sink
allow to integrate third-party libraries for streaming easily.
For using outwatch with zio:
import colibri.ext.zio._
For using outwatch with fs2:
import colibri.ext.fs2._
For using outwatch with airstream:
import colibri.ext.airstream._
For using outwatch with scala.rx (Only Scala 2.x):
import colibri.ext.rx._
Dom Lifecycle Mangement
Outwatch automatically handles subscriptions of streams and observables in your components. You desribe your component with static and dynamic content (subjects, event emitters, -->
and <--
). When using these components, the needed subscriptions will be tied to the lifecycle of the respective dom elements and are managed for you. So whenever an element is mounted the subscriptions are run and whenever it is unmounted the subscriptions are killed.
If you ever need to manually subscribe to a stream, you can let Outwatch manage the subscription for you:
import cats.effect.SyncIO
div(
VMod.managed(myObservable.subscribeF[IO](???)), // this subscription is now bound to the lifetime of the outer div element
VMod.managedSubscribe(myObservable.tapEffect(x => ???).void) // this subscription is now bound to the lifetime of the outer div element
)
Handling Events
Outwatch allows to react to dom events in a very flexible way. Every event handler emits events to be further processed. Events can trigger side-effects, be filtered, mapped, replaced or forwarded to reactive variables. In outwatch this concept is called EmitterBuilder. For example, take the click
event for which onClick
is an EmitterBuilder
:
import org.scalajs.dom.console
val component = {
div(
button(
"Log a Click-Event",
onClick.foreach { e => console.log("Event: ", e) },
),
" (Check the browser console to see the event)"
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
In the next example, we map the event e
to extract e.clientX
and write the result into the reactive variable (BehaviorSubject
) called x
:
import colibri.Subject
val component = {
val x = Subject.behavior(0.0)
div(
div(
"Hover to see mouse x-coordinate",
onMouseMove.map(e => e.clientX) --> x,
backgroundColor := "lightpink",
cursor.crosshair,
),
div(" x = ", x )
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
EmitterBuilder
comes with many useful methods to make your life easier. Check the completions of your editor.
Example: Counter
We don't have to use the dom-event at all. Often it's useful to replace it with a constant or the current value of a reactive variable. Let's revisit the counter example:
import colibri.Subject
val component = {
val counter = Subject.behavior(0)
div(
button("increase", onClick(counter.map(_ + 1)) --> counter),
button("decrease", onClick(counter.map(_ - 1)) --> counter),
button("reset", onClick.as(0) --> counter),
div("counter: ", counter )
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Example: Input field
Another common use-case is the handling of values of input fields. Here the BehaviorSubject
called text
reflects the current value of the input field. On every keystroke, onInput
emits an event, .value
extracts the string from the input field and writes it into text
. But the value
attribute of the input field also reacts on changes to the reactive variable, which we use to clear the text field. We have a second reactive variable that holds the submitted value after pressing enter.
import colibri.Subject
import org.scalajs.dom.ext.KeyCode
// Emitterbuilders can be extracted and reused!
val onEnter = onKeyDown
.filter(e => e.keyCode == KeyCode.Enter)
.preventDefault
val component = {
val text = Subject.behavior("")
val submitted = Subject.behavior("")
div(
input(
tpe := "text",
value <-- text,
onInput.value --> text,
onEnter(text) --> submitted,
),
button("clear", onClick.as("") --> text),
div("text: ", text),
div("length: ", text.map(_.length)),
div("submitted: ", submitted),
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Another example with debounce
functionality. The debounced
reactive variable is filled once you stop typing for 500ms.
import colibri.Subject
import org.scalajs.dom.ext.KeyCode
val component = {
val text = Subject.behavior("")
val debounced = Subject.behavior("")
div(
input(
tpe := "text",
onInput.value --> text,
onInput.value.debounceMillis(500) --> debounced,
),
div("text: ", text),
div("debounced: ", debounced),
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Global events
There are helpers for getting streams of global document
or window
events as a handy colibri.Observable
:
import colibri.Observable
import org.scalajs.dom.document
val component = {
val onBlur = events.window.onBlur.map(_ => "blur")
val onFocus = events.window.onFocus.map(_ => "focus")
div(
div("document.onKeyDown: ", events.document.onKeyDown.map(_.key)),
div("window focus: ", Observable.merge(onBlur, onFocus))
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Referential transparency
When looking at our counter component, you might have noticed that the reactive variable number
is instantiated immediately. It belongs to our component value counter
. Let's see what happens when we're using this component twice:
import colibri.Subject
val counter = {
val number = Subject.behavior(0)
div(
button(number, onClick(number.map(_ + 1)) --> number),
)
}
val component = {
div(
"Counters sharing state:",
counter,
counter
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
As you can see, the state is shared between all usages of the component. Therefore, the component counter is not referentially transparent. We can change this, by wrapping the component in IO
(or here: SyncIO
). With this change, the reactive variable number
is instantiated separately for every usage at rendering time:
import colibri.Subject
import cats.effect.SyncIO
val counter = for {
number <- SyncIO(Subject.behavior(0)) // <-- added IO
} yield div(
number,
button(number, onClick(number.map(_ + 1)) --> number),
)
val component = {
div(
"Referentially transparent counters:",
counter,
counter
)
}
Outwatch.renderInto[SyncIO](docPreview, component).unsafeRunSync()
Why should we care? Because referentially transparent components can easily be refactored without affecting the meaning of the program. Therefore it is easier to reason about them. Read more about the concept in Wikipedia: Referential transparency
Advanced
Accessing the DOM Element
Sometimes you need to access the underlying DOM element of a component. But a VNode in Outwatch is just a description of a dom element and we can create multiple different elements from one VNode. Therefore, there is no static element attached to a component. Though, you can get access to the dom element via hooks (callbacks):
div(
onDomMount.foreach { element => // the element into which this node is mounted
???
},
onDomUnmount.foreach { element => // the element from which this node is unmounted
???
}
)
Outwatch has a higher-level API to work with these kinds of callbacks, called VMod.managedElement
, which can be used like this:
import colibri.Cancelable
div(
VMod.managedElement { element => // the element into which this node is mounted
??? // do something with the dom element
Cancelable(() => ???) // cleanup when the dom element is unmounted
}
)
You can also get the current element when handling dom events, for example onClick:
div(
onClick.asElement.foreach { element =>
???
} // this is the same as onClick.map(_.currentTarget)
)
If the emitter does not emit events or elements, but you still want to access the current element, you can combine it with another emitter. For example:
import colibri.Observable
val someObservable:Observable[Int] = ???
div(
EmitterBuilder.fromSource(someObservable).asLatestEmitter(onDomMount).foreach { element =>
???
()
}
)
If you need an HTML or SVG Element instead of just an Element, you can do:
import org.scalajs.dom._
import colibri.Cancelable
onDomMount.asHtml.foreach{ (elem: html.Element) => ??? }
onDomMount.asSvg.foreach{ (elem: svg.Element) => ??? }
onClick.asHtml.foreach{ (elem: html.Element) => ??? }
onClick.asSvg.foreach{ (elem: svg.Element) => ??? }
VMod.managedElement.asHtml { (elem: html.Element) => ???; Cancelable(() => ???) }
VMod.managedElement.asSvg { (elem: svg.Element) => ???; Cancelable(() => ???) }
Custom EmitterBuilders
You can merge
, map
, collect
, filter
and transform
EmitterBuilder
:
button(EmitterBuilder.merge(onMouseUp.as(false), onMouseDown.as(true)).foreach { isDown => println("Button is down? " + isDown) })
// this is the same as: button(EmitterBuilder.combine(onMouseUp.map(_ => false), onMouseDown.map(_ => true)).foreach { isDown => println("Button is down? " + isDown })
Furthermore, you can create EmitterBuilders from streams with EmitterBuilder.fromSource
or create custom EmitterBuilders with EmitterBuilder.ofModifier
, EmitterBuilder.ofNode
or EmitterBuilder.apply
.
Debugging
Source Code: OutwatchTracing.scala
Tracing snabbdom patches
Show which patches snabbdom emits:
import scala.scalajs.js.JSON
import org.scalajs.dom.console
helpers.OutwatchTracing.patch.zipWithIndex.unsafeForeach { case (proxy, index) =>
console.log(s"Snabbdom patch ($index)!", JSON.parse(JSON.stringify(proxy)), proxy)
}
Tracing exceptions in your components
Dynamic components with Observables
can have errors. This happens if onError
is called on the underlying Observer
. Same for IO
when it fails. In these cases, Outwatch will always print an error message to the dom console.
Furthermore, you can configure whether Outwatch should render errors to the dom by providing a RenderConfig
. RenderConfig.showError
always shows errors, RenderConfig.ignoreError
never shows errors and RenderConfig.default
only shows errors when running on localhost
.
import cats.effect.SyncIO
val component = div("broken?", SyncIO.raiseError[VMod](new Exception("I am broken")))
Outwatch.renderInto[SyncIO](docPreview, component, config = RenderConfig.showError).unsafeRunSync()
import cats.effect.SyncIO
val component = div("broken?", SyncIO.raiseError[VMod](new Exception("I am broken")))
Outwatch.renderInto[SyncIO](docPreview, component, config = RenderConfig.ignoreError).unsafeRunSync()
You can additionally trace and react to these errors in your own code:
import org.scalajs.dom.console
helpers.OutwatchTracing.error.unsafeForeach { throwable =>
console.log(s"Exception while patching an Outwatch compontent: ${throwable.getMessage}")
}
Improving the Documentation
This documentation is written using mdoc. The markdown file is located in docs/readme.md.
To get a live preview of the code examples in the browser:
Clone the outwatch repo:
git clone git@github.com:outwatch/outwatch.git
Run mdoc:
sbt "docs/mdoc --watch"
Point your browser to: http://localhost:4000/readme.md
And edit docs/readme.md
.
Thank you!