Simple, expressive, and safe UI library for Scala.js
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docs New: Upgrade Airstream v0.4 -> v0.5.1 Dec 30, 2018
project New: Upgrade Airstream v0.4 -> v0.5.1 Dec 30, 2018
src New: Upgrade Airstream v0.4 -> v0.5.1 Dec 30, 2018
.gitignore Fix: Adjust to the split of DOM Builder into domtypes, dombuilder and… Aug 27, 2017
.travis.yml
CHANGELOG.md New: Upgrade Airstream v0.4 -> v0.5.1 Dec 30, 2018
LICENSE.md
README.md
build.sbt
jitpack.yml
release.sbt
version.sbt Setting version to 0.6.1-SNAPSHOT Dec 30, 2018

README.md

Laminar

Build Status Join the chat at https://gitter.im/Laminar_/Lobby Maven Central

Laminar is a small Scala.js library that lets you build UI components using glitch-free reactive Streams, Signals and State variables. It is simpler and more powerful than virtual DOM based solutions.

"com.raquo" %%% "laminar" % "0.6"

Why Laminar

Laminar offers a unique blend of simplicity, expressiveness and safety.

Simplicity

  • Extremely predictable behaviour – no magic involved, what you write is what you get
  • Source code is very approachable – small, no macros, almost no implicits
  • Minimalistic pragmatism – no hardcore FP, no typed effects, no backpressure, etc.
  • Precise DOM updates – no complicated virtual DOM diffing
  • Native Scala.js lib with no third party or JS dependencies – no JS impedance mismatch

Expressiveness

  • Plain Scala FP and/or OOP composition and abstraction techniques instead of ad-hoc library features ("props" / "context" / "components" / etc.)
  • First class, interoperable Event Streams and State reactive variables
  • Everything from individual attribute keys, key-value pairs, to whole elements is easily composable and abstractable

Safety

  • Automatic and mandatory memory management for all subscriptions, even user created ones – this is significantly safer than what other libraries mean by "automatic memory management"
  • Glitch-free reactive system – consistent observations at no runtime cost
  • Precise Scala and JS types for DOM elements, attributes, etc. – no unsafe casting

I understand that the importance of some of these points might not be immediately apparent. I will eventually write a more detailed blog post about these, but for now the documentation below will have to do.

Community & Support

Documentation

Laminar and Airstream are well documented:

Laminar docs: master, v0.6, v0.5, v0.4, v0.3, v0.2

Airstream docs: master, v0.5.1, v0.4, v0.3, v0.2, v0.1

The latest version of Laminar always uses the latest version of Airstream.

The Problem

To build single page web applications you need a method to keep the user interfaces in sync with the underlying application state. This goes both ways – changes in state should effortlessly propagate to the DOM, and DOM events should trigger changes in application state.

Laminar is a reactive solution to this problem, both UI and state management. See above: Why Laminar.

Quick Start

Laminar's basic building block are elements:

import com.raquo.laminar.api.L._
 
val streamOfNames: EventStream[String] = ???
val helloDiv: Div = div("Hello, ", child.text <-- streamOfNames)

helloDiv is a Laminar Div element that contains the text "Hello, <Name>", where <Name> is the latest value emitted by streamOfNames. As you see, helloDiv is self-contained. It depends on a stream, but is not a stream itself. It manages itself, abstracting away the reactive complexity of its innards from the rest of your program.

Laminar does not use virtual DOM, and a Laminar element is not a virtual DOM node, instead it is linked one-to-one to an actual JS DOM element (available as .ref). That means that if you want something about that element to be dynamic, you should define it inside the element like we did with child <-- nameStream above. This allows for precision DOM updates instead of inefficient virtual DOM diffing.

With that out of the way, here is what a pretty simple Laminar "component" could look like:

def Hello(helloNameStream: EventStream[String], helloColorStream: EventStream[String]): Div = {
  div(
    fontSize := "20px", // static CSS property
    color <-- helloColorStream, // dynamic CSS property
    strong("Hello, "), // static child element with a grandchild text node
    child.text <-- helloNameStream // dynamic child (text node in this case)
  )
}

Almost the same as what we had before, but now with dynamic color and a bit of styling, and more importantly – abstracted away inside a function. Here's how you use it in your app:

import com.raquo.laminar.api.L._
import org.scalajs.dom
 
val nameStream: EventStream[String] = ???
val colorStream: EventStream[String] = ???
 
val appDiv: Div = div(
  h1("User Welcomer 9000"),
  div(
    "Please accept our greeting: ",
    Hello(nameStream, colorStream) // Inserts the Laminar div element here
  )
)

// Mount the application into a pre-existing container
render(dom.document.querySelector("#appContainer"), appDiv)

Easy, eh? But wait a minute, the streams are coming out of thin air! Fair enough, let's add an input text box for users to type their name into, and get the name from there:

import com.raquo.laminar.api.L._
import org.scalajs.dom
 
val nameBus = new EventBus[String]
val colorStream: EventStream[String] = nameBus.events.map { name =>
  if (name == "Sébastien") "red" else "auto" // make Sébastien feel special
}
 
val appDiv: Div = div(
  h1("User Welcomer 9000"),
  div(
    "Please enter your name:",
    input(
      typ := "text",
      inContext(thisNode => onInput.mapTo(thisNode.ref.value) --> nameBus) // extract text entered into this input node whenever the user types in it
    )
  ),
  div(
    "Please accept our greeting: ",
    Hello(nameBus.events, colorStream)
  )
)

render(dom.document.querySelector("#appContainer"), appDiv)

That's a lot to take in, so let's explain some new features we're using:

Inside the input node we're registering an event listener for the onInput event, and apply some transformations to grab the input's current text value. Then we pass those text values into nameBus using -->. nameBus is an EventBus, a special Subject-like object (in FRP terms) that can grab events from a source like this and re-emit them as a stream of events. All this flow is explained in detail in the documentation.

colorStream is now derived entirely out of the event stream provided by nameBus.

For extra clarity: nameBus.events is a stream of all values passed to nameBus. In this case we wired it to contain a stream of values from the input text box. Whenever the user types into the text box, this stream emits an updated name.

mapTo here might seem like magic, but all it does is grab the current value of a mutable DOM reference using a by-name parameter.

We could abstract away the input box to simplify our appDiv code. Here's one way to do it:

def InputBox(caption: String, textBus: WriteBus[String]): Div = {
  div(
    caption,
    input(
      typ := "text",
      inContext(thisNode => onInput.mapTo(thisNode.ref.value) --> textBus)
    )
  )
}

Then you just call InputBox("Please enter your name:", nameBus) instead of div("Please enter your name:", input(...)) in appDiv.

But this is not the only way! Being a generic component, InputBox should probably not assume what events the consumer is interested in (onInput, onKeyUp, onChange?), so instead we could write a component that simply exports the elements that it creates, letting the consumer subscribe to whatever events it cares about on those elements:

class InputBox private ( // create instances of InputBox using InputBox.apply only
  val node: Div, // consumers should add this element into the tree
  val inputNode: Input // consumers can subscribe to events coming from this element
)
 
object InputBox {
  def apply(caption: String): InputBox = {
    val inputNode = input(typ := "text")
    val node = div(caption, inputNode)
    new InputBox(node, inputNode)
  }
}

And this is how we would use it:

import com.raquo.laminar.api.L._
import org.scalajs.dom
 
val inputBox = InputBox("Please enter your name:")

val nameStream = inputBox.inputNode
  .events(onInput) // gets the stream of onInput events (works on any Laminar element)
  .mapTo(inputBox.inputNode.ref.value) // gets the current value from the input text box (note: parameter passed by name)
 
val colorStream = nameStream.map { name =>
  if (name == "Sébastien") "red" else "auto" // make Sébastien feel special
}

val appDiv: Div = div(
  h1("User Welcomer 9000"),
  div(
    "Please enter your name:",
    inputBox.node,
  ),
  div(
    "Please accept our greeting: ",
    Hello(nameStream, colorStream) // Inserts the div element here
  )
)

render(dom.document.querySelector("#appContainer"), appDiv)

It's all the same behaviour, just different composition. In this pattern the InputBox component exposes two important nodes: node that should be included into the DOM tree, and inputNode that it knows the consuming code will want to listen for events. This is a simple yet powerful pattern for generic components.

As you learn more about Laminar you will see that there are even more ways to structure this same relationship.

Laminar has more exciting features to make building your programs a breeze. There's a lot of documentation explaining all of the concepts and features in much greater detail.

Read the docs, check out some examples, and join us in gitter!

My Related Projects

  • Airstream – State propagation and event streaming library used in Laminar
  • Scala DOM Types – Type definitions that we use for all the HTML tags, attributes, properties, and styles
  • Scala DOM Builder – Low-level Scala & Scala.js library for building and manipulating DOM trees
  • Scala DOM TestUtils – Test that your Javascript DOM nodes match your expectations

Author

Nikita Gazarov – raquo.com

License

Laminar is provided under the MIT license.