Coder Social home page Coder Social logo

raquo / laminar Goto Github PK

View Code? Open in Web Editor NEW
717.0 24.0 44.0 8.16 MB

Simple, expressive, and safe UI library for Scala.js

Home Page: https://laminar.dev

License: MIT License

Scala 96.19% JavaScript 2.00% CSS 1.81%
functional-reactive-programming dom-manipulation reactive-streams scalajs scala ui scala-js

laminar's Introduction

Laminar

Build status Chat on https://discord.gg/JTrUxhq7sj Maven Central

Laminar is a small library that lets you build web application interfaces, keeping UI state in sync with the underlying application state. Its simple yet expressive patterns build on a rock solid foundation of Airstream observables and the Scala.js platform.

Laminar is also a friendly community of passionate people from across the world who help each other learn new skills and achieve their goals. Check out all the learning materials we've put out, and chat us up on Discord if you hit a snag!

"com.raquo" %%% "laminar" % "<version>" // Requires Scala.js 1.13.2+

Look up the latest version of Laminar here, or in git tags above ("v" prefix is not part of the version number).

Where Are The Docs and Everything?

πŸ‘‰ laminar.dev

Sales pitch, quick start, documentation, live examples, and other resources, all there.

Live demo, with examples, code snippets, and a fully working client + server, dev + prod build setup that you can experiment with, and then deploy to the could for free.

Sponsorships

Huge thanks to all of our sponsors – your backing enables me to spend more time on Laminar, Airstream, various add-ons, as well as documentation, learning materials, and community support.

DIAMOND sponsor:

HeartAI.net

HeartAI is a data and analytics platform for digital health and clinical care.

GOLD Sponsors:

Yurique Iurii Malchenko

Aurinko.io

Aurinko is an API platform for workplace addons and integrations.

Author

Nikita Gazarov – @raquo

License

Laminar is provided under the MIT license.

The artwork in the brand and sponsors directories is not covered by the MIT license. No license is granted to you for these assets. However, you may still have "fair use" rights, as stipulated by law.

Comments in the defs directory pertaining to individual DOM element tags, attributes, properties and event properties, as well as CSS properties and their special values / keywords, are taken or derived from content created by Mozilla Contributors and are licensed under Creative Commons Attribution-ShareAlike license (CC-BY-SA), v2.5.

laminar's People

Contributors

ajablonski avatar andrelfpinto avatar cornerman avatar daddykotex avatar doofin avatar fgoepel avatar hollanddm avatar keynmol avatar lolgab avatar pishen avatar quafadas avatar raquo avatar vic avatar yurique avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laminar's Issues

Make it easier to work with third party leaky resources

Say you wanted to add a timer to your component like so:

elem.subscribe(_.mountEvents) {
    var timeoutId = 0
    Observer[MountEvent] {
        case NodeDidMount => timeoutId = dom.window.setTimeout(handler, timeout.toMillis)
        case NodeWillUnmount => dom.window.clearTimeout(timeoutId)
    }
}

This works but perhaps there is a nicer way? Something that would work just as well for resources created at arbitrary times but still expiring on unmount?

For example, users should be able to provide custom instances of Owned to Laminar elements. They can already do that but after #33 that won't work the same way as laminar elements will no longer be Owners themselves. So we'll need to make sure that we continue offering this capability in some way.


Relevant gitter discussion: https://gitter.im/Laminar_/Lobby?at=5ca378517ecbdc29caea0020

XStream.js & diamond pattern

@cornerman I looked into what we talked about regarding outwatch/outwatch#92

The Xstream.combine operator behaves just like RXJS's combineLatest because in these streaming libraries a value emitted to a stream propagates all the way through the chain of its first listener before the library starts propagating it to the second listener. I'm pretty sure...

I haven't looked at how scala-rx is implemented, but I assume that it propagates new values shallowly, i.e. it first updates every immediate listener of a reactive variable, and only after that do those listeners update their own immediate listeners in the same manner. Sorry, using XStream terms here, I don't know what terms scala-rx uses.

I understand that this is somewhat of a problem for Outwatch because it uses combineLatest internally to generate fresh virtual nodes, but Laminar doesn't do any of those two things. Regarding end user code, I've rediscovered https://staltz.com/rx-glitches-arent-actually-a-problem.html and it got me convinced that – for now – I can live with this. The solution AndrΓ© proposes seems quite reasonable for end user code.

It probably won't suit Outwatch internals though because there you have no control over what streams you get from end users.

API: Hide thisNodeMountEvents and ancestorMountEvents

These methods should probably be private. I can't imagine them being useful to end users, and their return types promise too much, causing confusion. Advanced users should be able to read or derive the events they need directly from the parentChangeEvents stream.

idiomatic way with `addEventListener`

Currently I implement touch event like this:

    s.svg(
      s.height := "800",
      s.width := "500",
      s.circle(
        inContext { e =>
          e.ref.addEventListener("touchmove", { e: TouchEvent =>
            println(e.changedTouches(0).clientX)
          })
          s.svg()
        },
        onMouseDown --> dragpipe,
        s.cx := "100",
        s.r := "30",
        s.fill := "red",
//        onDrag --> dragpipe,
        s.x := "1000",
        s.y <-- dragpipe.events.map(x => x.clientY.toString)
      ),
      s.text(child.text <-- dragpipe.events.map(x => "aa"))
    )

It works although looks bad.. It would be great if we can do sth like
onEvent("some event")-->eventbus

EventPropTransformation.filter should apply to subsequent transformations

For example:

onKeyPress.filter(_.keyCode == KeyCode.Enter).preventDefault --> observer

As intended, this calls observer only when the Enter key is pressed.

However, preventDefault is called on every key press, not just Enter. This is undesired, as you'd expect the transformation flow to stop dead in its tracks when filter fails to pass.

For now one workaround would be to preventDefault inside the observer, or inside a map with a redundant if-condition inside.

Disallow Airstream's State Observables

I am growing tired of Airstream's State variables. Both as a library author, and as a consumer of my libraries. Unlike lazy reactive variables like Signal and EventStream, State variables are not very compatible with ReactiveElement lifecycle, and are easier to trip on in terms of memory management (e.g. you must not let State escape to outside of its owner's context).

Perhaps I will find solutions to these issues in #33 that are compatible with the whole premise of State, however that is between hard and impossible. Meanwhile, the only benefit that State variables give us over Signal is strict (not lazy) execution. I get a feeling that this quality alone is not desirable enough to justify the API complexities and memory management edge cases, especially since it can be trivially simulated by adding a noop observer to any Signal.


As for the mechanism for banning State – you need an Owner to create State variables, and I will probably make Laminar Elements hide their Owner instances from user code, only allowing interaction with element owners via whitelisted channels (e.g. ReactiveElement.subscribe* methods).

You would still be able to use State for global things like routing and ajax/data services, and who knows, maybe they will prove to be useful there, but not in the components / elements world.

Eventually, if State continues to lack rationale for existence, I might consider removing it altogether from Airstream, but that is far from decided at this point.


I am posting this early to solicit feedback, seeing that this would be quite a significant change. If you do find State useful in a way that Signal isn't, I would very much like to hear from you before I make your life harder :)

Incremental Reactive Programming

I just found a few interesting papers which I think are very relevant for achieving efficient dom updates without virtual dom:

Maier, Ingo, and Martin Odersky. "Higher-order reactive programming with incremental lists." European Conference on Object-Oriented Programming. Springer, Berlin, Heidelberg, 2013.

Prokopec, Aleksandar, Philipp Haller, and Martin Odersky. "Containers and aggregates, mutators and isolates for reactive programming." Proceedings of the Fifth Annual Scala Workshop. ACM, 2014.

Reynders, Bob, and Dominique Devriese. "Efficient Functional Reactive Programming Through Incremental Behaviors." Asian Symposium on Programming Languages and Systems. Springer, Cham, 2017.

Docs: mention thead and tbody

"when rendering tables don't forget to insert thead or tbody HTML elements. If you don't, the browser will invisibly do it for you, but Laminar won't know about this, so its internal state will become inconsistent with the real DOM, which could be a problem if you're showing reactive lists of rows. This issue is the bane of all JS UI libraries"

Q: Is it possible to use redux-style state management?

I'm curious is it possible with Laminar to have whole app state in one big immutable object, like in Redux or ClojureScript's re-frame?

For example, I have something like:

case class Todo(...)
case class User(...)
case class AppState(user: User, todos: Seq[Todo])

As far as I can understand to build view layer for this app-state we need something like (sorry, don't know well Laminar API, so I use pseudo-code):

def appView(state: ReactiveStream[State]): ReactiveNode[Div] = {
  // .. here we call child components userView(user: ReactiveStream[User])
  //    and todoView(todo: ReactiveStream[Todo]) ...
}

Does it have any sense? If not, what's the recommended way to manage state with event-stream based frameworks?

Btw, thanks for your great work! No doubts we need Scala-"native" solutions to cope with UI issues and I found Laminar and Outwatch very promising! πŸ‘

Perma-kill element owners for better safety

Airstream Owner-s, when killed, simply kill their subscriptions and clear the list of their subscriptions. This is totally ok behaviour for the lowest common denominator, trait Owner.

However, Laminar knows a bit more about how it uses element-based owners (the ones that are used behind the scenes in observable --> observer and such, and the ones that you can get from MountContext). Specifically, Laminar knows that once such an owner is killed, Laminar will never use it again, and the user shouldn't either.

One thing we could do is disallow using element owners after the element has unmounted. As it stands, if the user obtained such an owner manually via MountContext, they could continue to refer to it after it was killed by unmount. That's no good because such an owner won't ever be killed again, essentially rendering those invalid subscriptions immortal.

It would also be good to log this to console or throw an error if this happens, so that users can detect mistakes in their code.

I need to figure out how to best implement this. I should probably update Airstream's DynamicOwner to use some kind of new OneTimeOwner subclass of Owner, but it would also be good to report a detailed error – which element's owner triggered the error – for easier debugging. Need to come up with a small but generic api for this in DynamicOwner. Maybe even just an onLifecycleError callback that defaults to throwing but Laminar would override that.


Also, now that I think about it, users are actually able to manually kill Laminar-provided owners, which is a big nope. Maybe Owner's onKilledExternally method should be private[ownership], or something like that. Basically this would mean that you'd need to extend Owner trait in order to create a custom owner that you could kill on demand. But, that seems to already be the design intent, the public-ness of onKilledExternally seems to be a mistake. Edit: I'm a dufus (but still maybe hide that method)

Server Side Rendering in Laminar

This will likely come up again, thus this issue for easy reference.

For starters, please see the detailed discussion in gitter where I explain what kind of solutions can be implemented at each layer of Laminar dependency tree, what each of those would provide in terms of features, the challenges and limitations of such approaches, and my current strategy to deal with this issue.

Example InputBox value blank?

I've copied the Laminar examples as a standalone App to learn how things work, but I'm having trouble that the InputBox.ref.value is always blank. So in this example code, as soon as you start typing in the text box, Hello, will go and stay red. Console output shows: color check on ''

val scalaVersion = "2.12.6"
val scalaJsVersion = "0.6.24"
val laminar = Agg(ivy"com.raquo::laminar::0.3")

...

import com.raquo.laminar.api.L._
import org.scalajs.dom
import org.scalajs.dom.raw.Event

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

  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)
    }
  }

  def main(args: Array[String]): Unit = {
    val inputBox = InputBox("Please enter your name:")
    val nameStream = inputBox.inputNode
      .events(onInput) // .events(eventProp) gets a stream of <eventProp> events (works on any Laminar element)
      .mapTo(inputBox.inputNode.ref.value) // gets the current value from the input text box

    val colorStream: EventStream[String] = nameStream.map { name =>
      dom.console.log(s"color check on '$name'")
      if (name.isEmpty) "red" else "auto"
    }

    val appDiv: Div = div(
      div(
        inputBox.node,
      ),
      div(
        "Please accept our greeting: ",
        Hello(nameStream, colorStream) // Inserts the div element here
      )
    )

    dom.document.addEventListener("DOMContentLoaded", (_: Event) => {
      dom.console.log("=== DOMContentLoaded ===")

      val container = dom.document.getElementById("app-container")
      render(container, appDiv)
    })
  }
}

Release for scalajs 1.0.0

Since there are few (none?) external dependencies my hunch is that this should be pretty straight forward :)

Web Component support

I was wondering if there is any interest in supporting Web Components in Laminar? I am not sure if there is anything that actually needs to change in the core project, perhaps just having an example of a proper way to add and use Web Components. This would hopefully alleviate the complaint that Laminar has no ready to use components.

I got a proof of concept working as follows:

  1. In the host page's head, add:
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">
  <script type="module" src="https://unpkg.com/@material/mwc-button?module"></script>
  1. Now you have mwc-button html tag that behaves just like any other tag. Use it in Laminar (this is just a random button added in TodoMVC example):
L.htmlTag("mwc-button", void = false)(
        new ReactiveHtmlAttr[String]("label", StringAsIsCodec) <-- itemsVar.signal.map(i => s"${i.count(!_.completed)} items"))

In summary, the process to add a Web Component is:

  1. Import component's javascript
  2. Define Laminar tag / attributes based on component's documentation

What is the best way to do those two things? In particular:

  1. Referencing an npm module from unpkg like this, while functional, is very inefficient - those modules should somehow be bundled with the rest of js. How can this be done?
  2. What is the proper way to define custom tags? I didn't see any docs for this. Also once defined, what's the best way to make them available to the app?

Thanks for making Laminar!

Nested child element not showing for concurrent events

val tp=new EventBus[Unit]()
        
div(button("click",(onClick() mapTo(()))-->tp ),
          child<--(tp.$ mapTo div("clicked1", child <-- (tp.$ mapTo (div("clicked2")))))
        )

clicked1 is shown but not clicked2,I guess maybe div("clicked2") needs some delay?

Fix memory management gotcha

Current problematic behaviour described in the docs:

Every Laminar element is an Owner. An element kills the resources that it owns when that element is discarded. An element is discarded when it is removed from the DOM. Now, here is an unfortunate loophole: what about a Laminar element that was created, owns some State or subscriptions, but that was never inserted into the DOM? Since it was never inserted, it will never be removed from it, so the resources that it owns will never be cleaned up. Unfortunately there is currently no way around this. This is a design bug that I will fix in a later version. So for now, do not proactively create elements that will never be added to the DOM. The most trivial workaround is to create a factory that creates an element when it's actually needed instead.

I plan to address this by starting the element's subscriptions when the element is inserted into the DOM rather than when the element is created. That way, the elements that were created but never inserted into the DOM will not hold any leaky resources, and so can be discarded without worrying about the subscriptions.

This means that an element that has subscriptions will need to listen to its own mountEvents stream, but it already does that for the purpose of unsubscribing when the element gets unmounted (see ReactiveElement.onOwned).

This solution will fix the issues when using ReactiveElement's subscribe* methods. However, as elements are Owners, they can also be used directly in methods where an implicit owner is required, which would still not be safe for the reason described above. Perhaps we need to avoid exposing elements as Owners, or maybe even extend Airstream's ownership API to allow for a delayed subscription start (basically, bring the subscribe* methods into the Owner?). This might also indirectly help us deal with the other memory management gotcha:

When a Laminar element is removed from the DOM, the resources that it owns are killed with no built-in way to resurrect them.

However, this starts looking a lot like the dynamic subscriptions system we had in previous versions of Laminar, where subscriptions could be turned on and off as the element gets mounted and unmounted. We should investigate if we can bring back this system without falling into other kinds of memory leaks (esp. zombie references).

div().amend(cls := "foo") does not compile

The following does not compile:

div().amend(cls := "foo")

The reason is understandable, Scala is confused by two overloaded amend methods:

def amend(mod: Modifier[this.type]): this.type
def amend(mods: Modifier[this.type]*): this.type
[error] ambiguous reference to overloaded definition,
[error] both method amend in trait ReactiveElement of type ((mods: com.raquo.domtypes.generic.Modifier[_1.type]*)_1.type) forSome { val _1: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Div] }
[error] and  method amend in trait ReactiveElement of type ((mod: com.raquo.domtypes.generic.Modifier[_1.type])_1.type) forSome { val _1: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Div] }

And yet, somehow this works:

val el = div()
el.amend(cls := "foo")

Before I implement a fix by removing one of the amend methods or with an implicit extension method or a method with (mod1,, mod2 ...restMods) params, does anyone know why the compiler fails in one case but not the other?

ETA: Has to be something with type inference, as amend references this.type in both its arguments and output type. Without this.type in one of those locations, it compiles.

Keyed / memoized children API

Currently rendering dynamic lists of children efficiently requires too much work. You either need to use children.command or wire things together in a weird way similar to how it's done in the laminar-examples repo.

I have a draft alternative that uses a concept of keys for memoization. It's tangentially similar to React's keys, but the decisions made based on the keys are different. I will eventually document it in full, but here's a sneak peek at a draft but functional implementation:

class Collection[Model, El, Key](
  models: Signal[immutable.Seq[Model]],
  renderModel: Model => El,
  memoizeKey: Model => Key
) {

  private[this] var memoizedChildren: Map[Key, El] = Map.empty

  val children: Signal[immutable.Seq[El]] = models.map { nextModels =>
    nextModels.foreach { nextModel =>
      val key = memoizeKey(nextModel)
      if (!memoizedChildren.contains(key)) {
        memoizedChildren = memoizedChildren.updated(key, renderModel(nextModel))
      }
      memoizedChildren
    }
    memoizedChildren.keys.foreach { memoizedKey =>
      if (!nextModels.exists(memoizeKey(_) == memoizedKey)) {
        memoizedChildren = memoizedChildren - memoizedKey
      }
    }
    nextModels.map(model => memoizedChildren(memoizeKey(model)))
  }

}

And example usage:

    val $notes: Signal[List[Note]] = ???
    val collection = new Collection[Note, Div, String](
      models = $notes,
      renderModel = renderNoteSummary, // this returns a reactive Div for a given Note. In this draft implementation this is supposed to obtain a signal of updated Note-s independently, but ultimately it will probably be provided by the Collection class
      memoizeKey = _.id
    )

    div(children <-- collection.children)

I am using this pattern privately (and you can too). I will eventually make something like this a part of Laminar. Existing children and children.command APIs will still be there.

Docs: Fix examples

  • Provide the required imports for examples
  • onInput().mapToThisNode.map(_.ref.value) --> nameBus does not compile
  • child <-- streamOfNames does not work (should be child.text)
  • Did we properly explain when () is needed after the eventProp? (for transformations)

Implement cls.toggle

cls.toggle("myClass") <-- $shouldAddThisClass

Or, well, something like this. Messing with Map-s for a single toggle is getting old

Pre-rendering Laminar pages [SSR]

I am looking for options to pre-render Laminar pages, and would like to see if there is any prior art or best practices.

My current hypothetical plan is as follows:

  1. Render page in headless Chrome and save its html
  2. Save page state in separate json file
  3. Push both html and json files to CDN
  4. We now have static pages that are fast and search engine friendly
  5. Once browser loads static html file, it will evaluate javascript and Laminar will replace static html with interactive version
  6. Any further navigation can be done client side using json state files from CDN

I did some preliminary testing of this scheme, and it appears to be working. My main concern is the lack of hydration support in Laminar, which necessitates double render on first hit. Are there any plans to add hydration support to Laminar? It seems hard - this seems like one of the few areas where DOM diffing has advantage. I am also not sure how much double render matters in practice - hydrating isn't exactly cheap either.

Any thoughts on what is the best way to do this?

amend() should return `this`

Currently div(...modifiers).amend(...more modifiers) returns Unit because in general I don't like builder-style APIs that work on mutable instances (which ReactiveElement is), but I'm starting to think that this particular case might be warranted.

We already have warnings to not reuse ReactiveElement-s, and the whole concept of modifiers requires the basic knowledge that modifiers are applied to the element immediately upon creation.

Meanwhile, el.amend() returning this would be very useful when el is an element exposed by an independent component. Being able to easily amend-and-immediately-use such elements makes for some very good composition, for example:

div(
  "Enter your age: ",
  TextInputComponent(..params).amend(onInput --> ???)
)

That's a very easy way for TextInputComponent to not need to care which events the consumer wants. Thanks to @yurique for bringing this up in gitter, good point.

More convenient events for popular cases

For example, desired syntax:

input(
  onChecked --> booleanEventBus,
  onChecked.preventDefault --> booleanEventBus,
  onInputString --> stringEventBus
)

Whereas currently we need incantations like this for e.g. onChecked:

input(
  inContext(thisNode => onClick.preventDefault.mapTo(thisNode.ref.checked) --> booleanEventBus)
)

Which is type safe, and fairly straightforward if you know Laminar's features, but still annoyingly long.

Not sure yet how best to implement this, current event handling is not flexible enough to easily support this, but it's also something that I've wanted to refactor for a while now. The whole EventPropTransformation / EventPropEmitter stuff can probably be done better.

Static site with interactive examples

Premise: it would be great to have a static website with documentation published for Laminar.

To make this work the following things need to happen:

  • mdoc's :js modifier should be able to support SJS 1. PR on mdoc is halfway there, I've started completing it

    mdoc PR has been merged: scalameta/mdoc#381

  • Laminar's build needs to be modularised to enable mdoc and Docusaurus plugin (work started here), the site generation works with locally published custom version of mdoc (see screenshot below)

  • CI pipeline needs to be adjusted to basically push entire contents of website/build (after sbt 'docs/docusaurusCreateSite') to gh-pages branch

  • optional custom color scheme and logo need to be chosen for Laminar site

  • Github Pages needs to be enabled and point at gh-pages branch of this repo

  • optional a custom domain needs to be configured to point at github pages.

After that it's all about populating the docs with lots of interactive examples.

image

Add a abstract layer for streams library?

I am using laminar now and find it very satisfying!(When using some very complex logic Outwatch gives nasty bug and It uses virtual dom so more difficult to debug),However I see you are developing new streams library airstream (#8 worried that the new version with airstream take great effort for migration,so maybe we can add an abstract layer for streams library that allows using arbitrary stream library ?

Scala 2.13.0 support

I am wondering how hard is it to add scala 2.13.0 support for Laminar. Now that many core libraries have supported scala 2.13.0, it would be great if Laminar follows.

Router

UI libraries often offer routing – an idiomatic mapping from URLs to application state and back.

Laminar does not currently have this built in. Unlike other browser integrations such as AJAX / networking, routing might be quite daunting to implement yourself, so I think Laminar should have it.

I do have a working Laminar Router implementation that I am quite happy with, however at the moment I do not have the time to bring it to the level of quality that I am comfortable subjecting other people to, and thus it remains unpublished.

I did share most of my router implementation here: https://gitter.im/Laminar_/Lobby?at=5c57f1338aa5ca5abf7fd85d The missing part is my mechanism of extracting data from URLs as well as populating URL templates with said data. That's the part I'm not quite happy with. I wanted to avoid bringing in a heavyweight third party parser, and my own implementation was optimized for development speed / PoC rather than a public release.

Set up a gitter channel

Questions / discussions are still welcome in github issues as it's easier to use asynchronously and easier to search, but gitter is pretty standard in the Scala ecosystem so I guess we'll do it. Eventually.

Document the implicit uniqueness requirement for children

When you render children <-- $elements, you have to guarantee that each list of elements does not have duplicates. This is sort of obvious because that's how the DOM works – you can't put the same element in two places.

However, this gets less obvious when you abstract away the generation of $elements, e.g. using the split operator: children <-- $models.split(_.id)(renderModel) – in this case you need to ensure that $models has no duplicate records when compared by _.id. Violating that constraint would break Laminar ("can not get null.nextSibling" in my case) because it would try to use the memoized element in two places at once. We should document this constraint.

Yes, we could enforce uniqueness with types, but children is likely to be the performance bottleneck if there is one, and fancy constraints can get quite expensive, so that's not a good fit here.

Ideally we should have a system of dev-only warnings similar to React, but that's a task for later.

Svg does not render

import com.raquo.laminar.api.L._
import com.raquo.laminar.api.L.{svg => s}


    div(
      s.svg(
        s.height := "800",
        s.width := "500",
        s.circle(
          s.cx := "100",
          s.r := "30",
          s.fill := "red"
        )
      )
    )

svg is present in chrom dev tool but is not showed

[META] v0.3 – First release with Airstream

This is what I plan to release in the next version, v0.3, probably/hopefully in a couple weeks.

Features

  • Initial development of Airstream
  • Replace XStream with Airstream in Laminar
  • Laminar: Rename bundle object to L to make it feasible to avoid importing a million DOM names.
  • Laminar/SDB/SDT: Fix for meta modifier support in light of 2.12 SAM magic – see raquo/scala-dom-types#27

Chores

  • Move Airstream into a separate package, and publish it to Maven Central
  • Publish the half-baked TodoMVC app that I have in a private laminar-examples project

Docs

  • Write initial Airstream docs
  • Update Laminar documentation with all the new features and changes (incl. #9)

Things that will not make it to v0.3 (up for debate)

  • Airstream error handling (not implemented, exceptions are pretty much undefined behavior)
  • Airstream helpers for dealing with async data structures (Futures / etc.), e.g. mapFuture, Future.toEventStream, etc.
  • Airstream: Create combine and map methods for N=3
  • Laminar/SDB: Make setParent and other internal methods more private
  • Docs: Add a Cheatsheet section for Laminar
  • Docs: Add a Cheatsheet section for Airstream

Mount events not firing in certain cases due to Transaction delay

Consider this code snippet:

    val textBus = new EventBus[String]

    val readMountEvents: Mod[HtmlElement] = new Mod[HtmlElement] {
      override def apply(element: HtmlElement): Unit = {
        element.subscribe(_.mountEvents){ ev => println(ev) }
      }
    }

    def makeChild(text: String): Div = div(span(readMountEvents, text))

    val $child = textBus.events.map(makeChild)

    render(container, section("Hello, ", child <-- $child))

    textBus.writer.onNext("blah")

The programmer's intent is clear – we expect it to print "NodeDidMount" when the span is mounted, i.e. when we send "blah" to textBus. However, in this specific situation it does not happen due to a complex timing problem inside Laminar.

In this description I assume you know how Laminar lifecycle events work and what mountEvents streams are derived from. No need to rehash the docs. To be honest I'm writing this mostly for myself so I might assume more than that, sorry.

So, here is what happens in makeChild:

  1. an empty detached span element is created
  2. readMountEvents modifier is applied to it. At this point it only listens to its own parent change events because it has no parent
  3. a detached div element is created
  4. div is set to be span's parent

At this point, we're in span's setParent method. Here span knows it needs to start listening to div's mountEvents. It acts on that by means of pushing an event onto its own maybeParentChangeBus. The fatal flaw in my logic is that this does not happen immediately. An event fired into an EventBus is always emitted by the EventBus in a new Airstream Transaction. This means that the current transaction will finish before the event bus starts propagating the event we put on it. Normally this is not a problem, but in this case it is.

The current transaction is the one in which $child is propagating. Before it's over, div will be appended to the section element. At that time, when div's setParent runs, it will check whether anyone is listening to its mountEvents, and will see no listeners, because span didn't yet start looking at div's mountEvents, as it will only start doing that once this transaction finishes.

So, once the current transaction finishes, span will start listening to div's mount events, but by that time div will have been mounted already, so it will not see any events.


One possible workaround for this issue is to add readMountEvents or any other modifier that listens to mount events to the div (in addition to or instead of adding it to span). That would initialize div's lifecycle infrastructure earlier, fixing the timing problem.


EventBus behaviour in this regard is predictable and consistent with the rest of Airstream, I can't fault it. Airstream transactions design is deliberate and so far has been working great to eliminate FRP glitches.

I think the problem is conceptually on Laminar's side, in how the lifecycle events system is implemented. Specifically, I think I'm using event buses wrong here. I should probably switch between parent mountEvents streams differently – instead of creating and flattening a signal of streams in ancestorMountEvents I could make use of EventBus's addSource / removeSource feature. The main design constraint here is keeping a sane transaction boundary – parentChangeEvents stream and mountEvents stream for the same element should probably run in the same transaction to reduce surprises, so the latter needs to be derived from the former, instead of both being derived from different event buses. Maybe I'll just make parentChangeEvents private, not sure if anyone is even using it.

Anyway, I'll try that sometime soon, need to marinade on this first.

Alternatives to xstream?

XStream seems to be the one thing that forces a build to use the ScalaJSBundlerPlugin (this caught me when initially trying the library).

Is there a specific reason that XStream was chosen over something like Monix?

Provide more concrete examples for `cls <-- ` RHS

Documentation mentions it, but more code examples would be nice. For example:

val $isSelected: Signal[Boolean] = ???
div(cls <-- $isSelected.map(isSelected => List("x-selected" -> isSelected)))

Also, seeing that a single-boolean use case is quite cumbersome, we might want to provide a syntax shortcut for this use case, for example:

div(cls.toggle("x-selected") <-- $isSelected)
div(cls.toggle("x-selected") := true) // maybe this too...?

Proposal: amendThis

Consider:

def TextInput: Input = input(typ := "text")

TextInput.amend(inContext { thisNode => onInput.map(_ => thisNode.ref.value) --> nameVar })

We can easily implement an amendThis convenience method which would combine amend and inContext:

TextInput.amendThis(thisNode => onInput.map(_ => thisNode.ref.value) --> nameVar)

The callback can also return a List of modifiers if several are required, thanks to an implicit conversion we already have.

I thought about naming it amendWithContext or amendInContext, but it gets a bit confusing whether it's referring to the context of TextInput or the parent element in which it's being rendered.

Integration with ScalaCSS

Hello,

I'm trying to figure out if I can rebuild my project, which currently using ScalaTags + ScalaCSS, with Laminar.
Would it be possible to integrate ScalaCSS into the DOM builder that Laminar is using?
(Like how it did in ScalaTags https://japgolly.github.io/scalacss/book/ext/scalatags.html)

I have tried to read the document and source code of scala-dom-builder and scala-dom-types. But I get confused by the traits and type parameters design.

Any hint would be appreciated and thanks for this cool project!

Special CSS className handling

In #20 @pishen raised the issue with CSS class name handling being inconvenient in Laminar. I didn't quite appreciate the full extent of the problem initially, but now I think it really should be addressed.

Laminar treats className like any other attribute, but unlike other attributes it's a composite value – a list of values to be precise

Consider this component:

object TextInput {

  def apply(mods: Mod[Input]*): Input = {
    input(
      typ("text"),
      cls("TextInput"),
      mods
    )
  }
}

There is no easy way to add a css class to a single instance of that component without polluting either this component's API or the call site.

The solution I propose is for each ReactiveElement to keep track of css class names that various modifiers want to keep on it in a Map[String, Boolean].

Then the modifier classes("TextInput") would add a ("TextInput" -> true) to that map, and this CSS class will stay with this element unless someone adds a classes.remove("TextInput") modifier that would set ("TextInput" -> false). classes.set("TextInput") would behave like cls("TextInput") does today, clearing the map and setting its own value.

We'd also need something like classes.toggle and/or classes.map which could be used reactively with Observable[Boolean] or Observable[Map[String, Boolean]].


Unfortunately it would be undesirably easy to override class names with the existing cls and className modifiers which we get from Scala DOM Types. Maybe we should extract those into a separate trait in Scala DOM Types so that consuming libraries like Laminar can choose to implement a different way to deal with CSS classes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.