Skip to content

Commit

Permalink
API: Upgrade to dombuilder 0.2, update README
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Oct 3, 2017
1 parent a0ab475 commit 4bbdfc1
Show file tree
Hide file tree
Showing 36 changed files with 271 additions and 223 deletions.
2 changes: 1 addition & 1 deletion LICENSE.txt → LICENSE.md
@@ -1,4 +1,4 @@
The MIT License (MIT)
# The MIT License (MIT)

Copyright 2017 Nikita Gazarov

Expand Down
34 changes: 19 additions & 15 deletions README.md
@@ -1,18 +1,16 @@
# Laminar

Laminar is a small reactive UI library for Scala.js, allowing you to interact with the DOM using reactive streams.
_Laminar_ is a small reactive UI library for Scala.js, allowing you to interact with the DOM using reactive streams. I'm building _Laminar_ because I believe that UI logic is best expressed with functional reactive programming patterns in a type-safe environment.

Laminar has no concept of components because it has no need for them. Write your own functions that accept any kind of inputs and one way or another provide you with a reference to a Node.
"com.raquo" %%% "laminar" % "0.1"

Laminar is very efficient. It makes minimal required changes to the DOM. For example, if you say you need to update an attribute of a node, that's exactly what will happen. No other code of yours will be re-evaluated because of that (unlike for example React.js where the render() methods on components will be called recursively on every little change in props).
We have no concept of components because we don't need the conceptual overhead. Write your own functions or classes or anything that somehow provides a reference to a `ReactiveNode`, and you're good.

Currently Laminar depends on a virtual DOM library called [Snabbdom](https://github.com/raquo/Snabbdom.scala), however I am working on removing that dependency. Laminar's API allows us to be even more efficient than Snabbdom by making direct DOM updates. Snabbdom's patching model also doesn't map all that well to our precise-DOM-updates use case.
_Laminar_ uses [Scala DOM Builder](https://github.com/raquo/scala-dom-builder) under the hood, and is very efficient. Instead of using a virtual DOM, it makes precision updates to the real DOM. For example, if you say you need to update an attribute of a node, that's exactly what will happen. There is nothing else happening, no diffing of virtual DOM trees, no redundant re-evaluation of your component. Laminar's API lets you express what exactly needs to happen when without the inefficiency and under-the-hood complexity of virtual DOM.

I created Laminar because I believe that UI is best built with functional reactive programming patterns in a type-safe environment. I will publish a TodoMVC example app to showcase how nice Laminar is to work with as soon as I get a bit of time.
## Example Laminar "Component"

## Example Laminar "component"

Here's `Counter.apply`, a contrived example function that produces a Counter "component" that exposes a node that should be provided to Laminar, and a stream of counts generated from user clicks that you do whatever you want with.
Here's `Counter.apply`, a contrived example function that produces a Counter "component" that exposes a `node` that should be provided to Laminar, and `$count`, a stream of counts generated from user clicks that you do whatever you want with.

```scala
class Counter private (
Expand Down Expand Up @@ -42,20 +40,26 @@ object Counter {
}
```

Cosmetically, this looks similar to [Outwatch](https://github.com/OutWatch/outwatch), however Laminar is implemented very differently – instead of a virtual DOM it uses [Scala DOM Builder](https://github.com/raquo/scala-dom-builder), which is a better foundation for the kind of API that we provide. Laminar also supports more flexible use cases. For example Laminar is more accommodating when you need to render a dynamically updated list of children.
Cosmetically, this looks similar to [Outwatch](https://github.com/OutWatch/outwatch), however Laminar is implemented very differently – instead of a virtual DOM it uses [Scala DOM Builder](https://github.com/raquo/scala-dom-builder), which is a simpler foundation for the kind of API that we provide.

There are more _Laminar_ examples in the [`example`](https://github.com/raquo/laminar/tree/master/src/main/scala/com/raquo/laminar/example) directory.

I will eventually write up a detailed _"Laminar vs the World"_ post to compare it to other solutions and explain why Laminar exists.

## Status
## My Related Projects

This is an early preview of an upcoming library. It already works, a few example components are included in the repo.
- [Scala DOM Types](https://github.com/raquo/scala-dom-types) – Type definitions that we use for all the HTML tags, attributes, properties, and styles
- [Scala DOM Builder](https://github.com/raquo/scala-dom-builder) – Low-level Scala & Scala.js library for building and manipulating DOM trees
- [Scala DOM TestUtils](https://github.com/raquo/scala-dom-testutils) – Test that your Javascript DOM nodes match your expectations
- [Snabbdom.scala](https://github.com/raquo/Snabbdom.scala) – Scala.js interface to a popular JS virtual DOM library
- [XStream.scala](https://github.com/raquo/XStream.scala) – Scala.js interface to a simple JS reactive streams library
- [Cycle.scala](https://github.com/raquo/Cycle.scala) – Scala.js interface to a popular JS functional reactive library

One of the things I would like to do is to create a couple wrappers around [XStream](https://github.com/raquo/XStream.scala), the stream library used by Laminar, to provide better types for streams.

Once that is done I will write proper documentation and publish the library to Maven Central. For now you would need to use `sbt publishLocal` to try it out.

## Author

Nikita Gazarov – [raquo.com](http://raquo.com)

License – MIT
## License

_Laminar_ is provided under the [MIT license](https://github.com/raquo/laminar/blob/master/LICENSE.md).
8 changes: 4 additions & 4 deletions build.sbt
Expand Up @@ -16,15 +16,15 @@ crossScalaVersions := Seq("2.11.11", "2.12.3")

homepage := Some(url("https://github.com/raquo/laminar"))

licenses += ("MIT", url("https://github.com/raquo/laminar/blob/master/LICENSE.txt"))
licenses += ("MIT", url("https://github.com/raquo/laminar/blob/master/LICENSE.md"))

libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.3",
// "org.scala-js" %% "scalajs-env-selenium" % "0.1.3",
"com.raquo.xstream" %%% "xstream" % "0.2.1",
"com.raquo" %%% "dombuilder" % "0.1-SNAPSHOT",
"org.scalatest" %%% "scalatest" % "3.0.1" % Test,
"com.raquo" %%% "domtestutils" % "0.1-SNAPSHOT" % Test
"com.raquo" %%% "dombuilder" % "0.2",
"org.scalatest" %%% "scalatest" % "3.0.3" % Test,
"com.raquo" %%% "domtestutils" % "0.2" % Test
)

//jsEnv := new org.scalajs.jsenv.selenium.SeleniumJSEnv(org.scalajs.jsenv.selenium.Firefox()).withKeepAlive()
Expand Down
20 changes: 20 additions & 0 deletions src/main/scala/com/raquo/laminar/DomApi.scala
@@ -0,0 +1,20 @@
package com.raquo.laminar

import com.raquo.dombuilder.jsdom.domapi.{JsCommentApi, JsElementApi, JsEventApi, JsTextApi, JsTreeApi}
import com.raquo.laminar.nodes.ReactiveNode

trait DomApi {
implicit val elementApi: JsElementApi[ReactiveNode] = DomApi.elementApi
implicit val eventApi: JsEventApi[ReactiveNode] = DomApi.eventApi
implicit val commentApi: JsCommentApi[ReactiveNode] = DomApi.commentApi
implicit val textApi: JsTextApi[ReactiveNode] = DomApi.textApi
implicit val treeApi: JsTreeApi[ReactiveNode] = DomApi.treeApi
}

object DomApi {
val elementApi: JsElementApi[ReactiveNode] = new JsElementApi[ReactiveNode] {}
val eventApi: JsEventApi[ReactiveNode] = new JsEventApi[ReactiveNode] {}
val commentApi: JsCommentApi[ReactiveNode] = new JsCommentApi[ReactiveNode] {}
val textApi: JsTextApi[ReactiveNode] = new JsTextApi[ReactiveNode] {}
val treeApi: JsTreeApi[ReactiveNode] = new JsTreeApi[ReactiveNode] {}
}
26 changes: 14 additions & 12 deletions src/main/scala/com/raquo/laminar/Implicits.scala
@@ -1,29 +1,34 @@
package com.raquo.laminar

import com.raquo.dombuilder.jsdom.syntax

import com.raquo.dombuilder.generic.KeyImplicits
import com.raquo.dombuilder.generic.builders.SetterBuilders
import com.raquo.dombuilder.generic.syntax.SyntaxImplicits
import com.raquo.dombuilder.jsdom.JsCallback
import com.raquo.domtypes.generic.keys.{Attr, EventProp, Style}

import com.raquo.laminar.nodes.ReactiveText
import com.raquo.laminar.nodes.{ReactiveNode, ReactiveText}
import com.raquo.laminar.receivers.{AttrReceiver, StyleReceiver}
import com.raquo.laminar.subscriptions.EventEmitter

import org.scalajs.dom

trait Implicits extends syntax.Implicits {
trait Implicits
extends SyntaxImplicits[ReactiveNode, dom.Element, dom.Node, dom.Event, JsCallback]
with KeyImplicits[ReactiveNode, dom.Element, dom.Node]
with SetterBuilders[ReactiveNode, dom.Element, dom.Node]
with DomApi
{

implicit def toAttrReceiver[V](attr: Attr[V]
@inline implicit def toAttrReceiver[V](attr: Attr[V]
): AttrReceiver[V] = {
new AttrReceiver(attr)
}

implicit def toStyleReceiver[V](
@inline implicit def toStyleReceiver[V](
style: Style[V]
): StyleReceiver[V] = {
new StyleReceiver(style)
}

implicit def toEventEmitter[Ev <: dom.Event](
@inline implicit def toEventEmitter[Ev <: dom.Event](
eventProp: EventProp[Ev]
): EventEmitter[Ev] = {
new EventEmitter(eventProp)
Expand All @@ -41,6 +46,3 @@ trait Implicits extends syntax.Implicits {
// Conversions.optionToModifier(maybeModifier)
// }
}

/** You can import this to load implicits if you don't want to `import com.raquo.laminar._` */
object Implicits extends Implicits

This file was deleted.

This file was deleted.

@@ -1,8 +1,9 @@
package com.raquo.laminar.example.components

import com.raquo.laminar._
import com.raquo.laminar.attrs.cls
import com.raquo.laminar.child
import com.raquo.laminar.events.onClick
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveNode
import com.raquo.laminar.tags.{button, div, span}
import com.raquo.xstream.{MemoryStream, XStream}
Expand Down
@@ -1,6 +1,6 @@
package com.raquo.laminar.example.components

import com.raquo.laminar._
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveNode
import com.raquo.laminar.tags._
import com.raquo.laminar.props._
Expand Down
@@ -1,5 +1,6 @@
package com.raquo.laminar.example.components

import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveNode
import com.raquo.laminar.tags._
import com.raquo.xstream.XStream
Expand Down
@@ -1,11 +1,12 @@
package com.raquo.laminar.example.components

import com.raquo.laminar._
import com.raquo.laminar.events._
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.receivers.ChildrenReceiver.{Diff, append}
import com.raquo.laminar.receivers.MaybeChildReceiver.MaybeChildNode
import com.raquo.laminar.tags._
import com.raquo.laminar.{children, maybeChild}
import com.raquo.xstream.XStream
import org.scalajs.dom

Expand Down Expand Up @@ -33,7 +34,7 @@ class TaskList {
count += 1
Some(
button(
onClick --> (append(div("hello")), sendTo = $taskDiff),
onClick --> (append(div("hello")), target = $taskDiff),
// onClick --> (true, sendTo = $showAddTaskInput),
"Add task"
)
Expand Down
@@ -1,8 +1,9 @@
package com.raquo.laminar.example.components

import com.raquo.laminar._
import com.raquo.laminar.attrs._
import com.raquo.laminar.child
import com.raquo.laminar.events._
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.tags._
import com.raquo.xstream.XStream
Expand Down
@@ -1,10 +1,11 @@
package com.raquo.laminar.example.pseudotests

import com.raquo.laminar._
import com.raquo.laminar.child
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.props.className
import com.raquo.laminar.tags.{a, div, h1, span}
import com.raquo.laminar.tags.{div, h1}
import org.scalajs.dom

object Children {
Expand Down
@@ -1,12 +1,12 @@
package com.raquo.laminar.example.pseudotests

import com.raquo.laminar._
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar.tags._
import com.raquo.laminar.attrs.href
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.props.className
import com.raquo.laminar.styles._
import com.raquo.laminar.tags._
import org.scalajs.dom

object MultiSetters {
Expand Down
@@ -1,11 +1,11 @@
package com.raquo.laminar.example.pseudotests

import com.raquo.laminar._
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveNode
import com.raquo.laminar.tags._
import com.raquo.laminar.props._
import com.raquo.laminar.styles._
import com.raquo.laminar.tags._

object MultiStyleProp {

Expand Down
@@ -1,11 +1,12 @@
package com.raquo.laminar.example.pseudotests

import com.raquo.laminar._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.child
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar.tags._
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.props._
import com.raquo.laminar.styles._
import com.raquo.laminar.tags._
import com.raquo.xstream.XStream
import org.scalajs.dom

Expand Down
@@ -1,10 +1,11 @@
package com.raquo.laminar.example.pseudotests

import com.raquo.laminar.child
import com.raquo.laminar.example.components.Toggle
import com.raquo.laminar._
import com.raquo.laminar.implicits._
import com.raquo.laminar.nodes.ReactiveElement
import com.raquo.laminar.tags._
import com.raquo.laminar.styles._
import com.raquo.laminar.tags._
import com.raquo.xstream.XStream
import org.scalajs.dom

Expand Down
@@ -1,11 +1,15 @@
package com.raquo.laminar.nodes

import com.raquo.dombuilder.jsdom
import com.raquo.dombuilder.generic.nodes.ChildNode
import com.raquo.dombuilder.jsdom.domapi.JsTreeApi
import com.raquo.laminar.DomApi
import org.scalajs.dom

trait ReactiveChildNode[+Ref <: dom.Node]
extends ReactiveNode
with jsdom.nodes.ChildNode[ReactiveNode, Ref] {
with ChildNode[ReactiveNode, Ref, dom.Node] {

override val treeApi: JsTreeApi[ReactiveNode] = DomApi.treeApi

override def clearParent(): Unit = {
super.clearParent()
Expand Down
18 changes: 14 additions & 4 deletions src/main/scala/com/raquo/laminar/nodes/ReactiveComment.scala
@@ -1,9 +1,19 @@
package com.raquo.laminar.nodes

import com.raquo.dombuilder.jsdom
import com.raquo.dombuilder.generic
import com.raquo.dombuilder.jsdom.domapi.JsTreeApi
import com.raquo.laminar.DomApi
import org.scalajs.dom

class ReactiveComment(override protected[this] var _text: String)
extends ReactiveChildNode[dom.Comment]
with jsdom.nodes.Comment
class ReactiveComment(val text: String)
extends ReactiveNode
with ReactiveChildNode[dom.Comment]
with generic.nodes.Comment[ReactiveNode, dom.Comment, dom.Node] {

override val treeApi: JsTreeApi[ReactiveNode] = DomApi.treeApi

override val ref: dom.Comment = DomApi.commentApi.createNode

setText(text)(DomApi.commentApi)
}

22 changes: 15 additions & 7 deletions src/main/scala/com/raquo/laminar/nodes/ReactiveElement.scala
@@ -1,13 +1,21 @@
package com.raquo.laminar.nodes

import com.raquo.dombuilder.jsdom
import com.raquo.dombuilder.generic
import com.raquo.dombuilder.jsdom.JsCallback
import com.raquo.dombuilder.jsdom.domapi.JsTreeApi
import com.raquo.laminar.DomApi
import org.scalajs.dom

class ReactiveElement[+Ref <: dom.Element](
override val tagName: String,
override val void: Boolean = false
)
extends ReactiveChildNode[Ref]
with jsdom.nodes.Element[Ref]
with jsdom.nodes.ParentNode[ReactiveNode, Ref]
with jsdom.nodes.EventfulNode[ReactiveNode, Ref]
override val void: Boolean
) extends ReactiveNode
with ReactiveChildNode[Ref]
with generic.nodes.Element[ReactiveNode, Ref, dom.Node]
with generic.nodes.EventfulNode[ReactiveNode, Ref, dom.Element, dom.Node, dom.Event, JsCallback]
with generic.nodes.ParentNode[ReactiveNode, Ref, dom.Node] {

override val treeApi: JsTreeApi[ReactiveNode] = DomApi.treeApi

override val ref: Ref = DomApi.elementApi.createNode(this)
}

0 comments on commit 4bbdfc1

Please sign in to comment.