Laminar Documentation

repository·master·Indexed 21 days ago

https://github.com/raquo/laminar

Laminar is a small library for building web application interfaces in Scala.js, focusing on keeping UI state in sync with application state using Airstream observables. It requires Scala.js 1.16.0+ and provides tools for DOM attribute manipulation via Codecs, type-safe CSS color generation, MathML tag construction, and utilities for parsing HTML and SVG strings.

Tokens
54.7K
Snippets
166
Records
247
Agent score
74%

What's inside Laminar

  1. Explore new Laminar and Airstream features in v15.0.0

    master

    The v15.0.0 release introduces significant improvements to ergonomics, performance, and correctness. Key areas of improvement include:

    Laminar Features

    • DOM Events: Support for flatMap and compose for DOM events.
    • Third-Party Integration: Easier integration with third-party DOM elements.
    • CSS API: Improvements to the CSS API.
    • Window/Document Events: More options for window and document events.
    • Sinks: Support for :=> Unit sinks.
    • Children Operations: Improvements to children operations.
    • Custom Types: Better rendering of custom types.
    • Scala DOM Types: A Scala DOM types generator.

    Airstream Semantics & Features

    • Signal Equality: Automatic == checks in Signals have been removed. Use new distinct* operators when needed.
    • Signal Restarting: Signals now attempt to re-sync after restarting.
    • Observables: Observables no longer reset their internal state when stopped.
    • New Operators: splitByIndex, splitOption, take, drop, and filterWith.
  2. Access Laminar documentation and demos

    master

    Laminar provides several resources for developers:

    • Official Website (laminar.dev): Contains the sales pitch, quick start guides, full documentation, and live examples.
    • Live Demo (demo.laminar.dev): A fully working client + server setup including dev and prod build configurations. You can use this to experiment with code snippets and see how to deploy to the cloud.
  3. Render custom types and primitives

    master

    Laminar can natively render various types if appropriate implicit instances are available:

    • Primitives: Int, Boolean, Double, and Long can be rendered directly via text <-- observableOfInt without calling .toString.
    • Custom Components: Any type Component with an implicit RenderableNode[Component] can be used with child <-- or children <--.
    • Custom Text: Any type A with an implicit RenderableText[A] can be used with text <--.
  4. Use Modifiers to set attributes and properties

    master

    In Laminar, elements are created by calling an HTML tag function (e.g., div(), input()) and passing Modifier objects to it. A Modifier[El] (aliased as Mod[El]) is a function that modifies an element.

    To set an attribute or property, use the := operator with an HtmlAttr or HtmlProp.

    Example:

    input(typ := "checkbox", defaultChecked := true)

    Alternatively, you can use the shorthand syntax where the attribute name is called as a method:

    input(typ("checkbox"), defaultChecked(true))

    Note: typ and other keys come from Scala DOM Types. You should consult their documentation for naming differences compared to the native JS DOM API.

    input(typ := "checkbox", defaultChecked := true)
  5. Implement custom event sources with CustomSource

    master

    When integrating third-party libraries that require explicit initialization and cleanup (like D3, ZIO, or the DOM), use CustomSource. This provides onStart and onStop hooks, which are more robust than using an EventBus for lifecycle-managed resources.

    To implement a custom source, use CustomStreamSource and provide a CustomSource.Config containing onStart and onStop logic.

    // Conceptual implementation of a custom source
    CustomStreamSource[Ev]( (fireValue, fireError, getStartIndex, getIsStarted) => {
      val eventHandler: js.Function1[Ev, Unit] = fireValue
    
      CustomSource.Config(
        onStart = () => {
          eventTarget.addEventListener(eventKey, eventHandler, useCapture)
        },
        onStop = () => {
          eventTarget.removeEventListener(eventKey, eventHandler, useCapture)
        }
      )
    })
  6. Binding Observables with `-->` and `<--`

    master

    The --> (Observer) and <-- (Observable) methods are the primary syntax for connecting data streams to elements. These methods return a Modifier that handles the lifecycle of the subscription:

    1. Activation: The subscription starts when the element is mounted to the DOM.
    2. Deactivation: The subscription automatically stops when the element is unmounted.

    Modifiers are categorized into two types:

    • Binder: Handles properties, attributes, or events (e.g., onClick.map(...) --> observer).
    • Inserter: Handles adding children to an element (e.g., children <-- observable).
  7. How Signals re-sync after restarting

    master

    In Airstream 15.0.0, Signals now attempt to re-sync with their parents when they are restarted (e.g., when a component is re-mounted in the DOM).

    The Re-sync Mechanism

    If a childSignal is derived from a parentSignal (via .map, etc.) and the childSignal is stopped (unmounted), it stops listening to the parent. When the childSignal is restarted, it now "pulls" the latest value from the parentSignal to ensure it is in sync, even if the parent didn't emit a new value while the child was stopped.

    Important Caveats

    • Latest Only: The child only gets the latest value from the parent. If the parent emitted multiple values while the child was stopped, intermediate values are missed.
    • Streams vs Signals: This does not work for EventStream because streams do not have a "current value" to pull from.
    • signal.changes behavior: The .changes stream also re-syncs. However, during re-syncing, multiple .changes streams may emit in the same transaction.

    Migration for Reused Elements

    If you define an element as a val and reuse it (e.g., child <-- boolSignal.map(if (_) warningElement else emptyNode)), the element will re-sync. If this causes issues with shared transactions in .changes, change the val to a def to force the element to be re-created instead of re-mounted.

    // Example of a reused element that will now re-sync with parentSignal
    val boolSignal: Signal[Boolean] = ???
    val parentSignal: Signal[String] = ???
    val warningElement = div(
      h1("The yeti is onto us!"),
      text <-- parentSignal.map(_.toUpperCase)
    )
    
    div(
      child <-- boolSignal.map(if (_) warningElement else emptyNode)
    )
  8. Core Concept: How Laminar elements work

    master

    Laminar elements are not Virtual DOM nodes; they are one-to-one linked to actual JS DOM elements (accessible via the .ref property).

    Because there is no Virtual DOM diffing, you must define dynamic behavior inside the element using reactive operators. This allows for precision DOM updates. For example, to make a text node or a CSS property dynamic, you use the <-- operator to bind a stream to that specific property or child.

    // Static property
    fontSize := "20px", 
    
    // Dynamic property via stream
    color <-- helloColorStream, 
    
    // Dynamic child (text node)
    text <-- streamOfNames
  9. How Observables handle stopping and restarting

    master

    Airstream 15.0.0 introduces a new paradigm: instead of tearing down state when an observable is stopped, it now pauses and resumes.

    • Old Behavior: Stopping an observable often reset its internal state. For example, a combineWith stream would "forget" previous values and require both parents to emit again before producing a new combined value.
    • New Behavior: Observables generally remember their last known state. When restarted, they resume from that state, allowing for a more seamless experience when components are unmounted and remounted.
  10. Use EventBus to manage event streams

    master

    An EventBus[A] is a specialized structure that acts as both a source and a sink for events. It contains a .writer (an Observer) and .events (an EventStream). This design allows you to share read-only or write-only parts of the bus easily.

    To use an EventBus:

    1. Create a new EventBus[T].
    2. Use element.eventProp --> eventBus.writer to feed events into the bus.
    3. Use eventBus.events.map(...) to derive new streams from the bus.
    4. Use stream --> observer to consume the processed events.
    val clickBus = new EventBus[dom.MouseEvent]
    val coordinateStream: EventStream[Int] = clickBus.events.map(ev => ev.screenX)
    val coordinateObserver = Observer[Int](onNext = x => dom.console.log(x))
     
    val element: Div = div(
      onClick --> clickBus.writer,
      "Click me",
      coordinateStream --> coordinateObserver
    )
  11. Split Observables by pattern match

    master

    Airstream (Scala 3 only) provides macro-based methods to split an Observable[ADT] or an Observable[Seq[ADT]] into multiple signals based on pattern matching. This allows for type-safe, exhaustive rendering of different subtypes within a collection or a single stream.

    Split an Observable[ADT]

    Use splitMatchOne along with handleValue and handleType to transform a signal of a sealed trait into specific signals for each subtype.

    Split an Observable[Seq[ADT]]

    Use splitMatchSeq to handle lists where each item might be a different subtype. This is particularly useful for rendering lists of heterogeneous items in Laminar.

    // Example: Splitting an Observable[Seq[Item]] by type
    trait Item { val id: String }
    case class Stock(ticker: String, currency: String, value: Double) { override val id: String = ticker }
    case class FxRate(currency1: String, currency2: String, rate: Double) { override val id: String = currency1 + "-" + currency2 }
    
    val itemsSignal: Signal[Seq[Item]] = ???
    val elementsSignal: Signal[Seq[HtmlElement]] =
      itemsSignal
        .splitMatchSeq(_.id)
        .handleType[Stock] { (initialStock, stockSignal) =>
          div(
            initialStock.id + ": ",
            text <-- stockSignal.map(_.value),
            " " + initialStock.currency
          )
        }
        .handleType[FxRate] { (initialRate, rateSignal) =>
          div(initialRate.id + ": ", text <-- rateSignal.map(_.rate))
        }
        .toSignal
    
    // In Laminar:
    children <-- elementsSignal