diff --git a/website/blog/2023-12-04-laminar-v17.0.0.md b/website/blog/2024-05-14-laminar-v17.0.0.md similarity index 77% rename from website/blog/2023-12-04-laminar-v17.0.0.md rename to website/blog/2024-05-14-laminar-v17.0.0.md index bc02cdfe..a4b3f2f4 100644 --- a/website/blog/2023-12-04-laminar-v17.0.0.md +++ b/website/blog/2024-05-14-laminar-v17.0.0.md @@ -1,10 +1,10 @@ --- -title: Laminar v17.0.0 +title: Laminar v17.0.0 & Shoelace Web Components v0.1.0 author: Nikita authorURL: https://twitter.com/raquo --- -This release has it all: Shoelace web components, new features, ergonomic improvements, and bug fixes, in both Laminar and Airstream. +This release has it all: new features, ergonomic improvements, and bug fixes, in both Laminar and Airstream. @@ -13,31 +13,37 @@ This release has it all: Shoelace web components, new features, ergonomic improv ## Releases -* Laminar 17.0.0 -* Airstream 17.0.0 (and Airstream 17.0.0-M3 – read below) -* Waypoint 8.0.0 -* ew 0.2.0 -* Scala DOM Types 18.1.0 -* Scala DOM Test Utils 18.0.1 -* Laminar Shoelace 0.1.0 -* Laminar Demo updated +* [Laminar](https://github.com/raquo/laminar) 17.0.0 +* [Airstream](https://github.com/raquo/airstream) 17.0.0 (and Airstream 17.0.0-M3 migration helper – read below) +* [Waypoint](https://github.com/raquo/waypoint) 8.0.0 +* [Laminar Shoelace Web Components](https://github.com/raquo/laminar-shoelace-components) 0.1.0 +* [ew](https://github.com/raquo/ew) 0.2.0 +* [Scala DOM Types](https://github.com/raquo/scala-dom-types) 18.1.0 +* [Scala DOM Test Utils](https://github.com/raquo/scala-dom-testutils) 18.0.1 +* [Laminar Demo](https://github.com/raquo/laminar-full-stack-demo/) updated -TODO: +Ecosystem updates for v17: -* Laminext -* Laminar SAP UI5 bindings +* [Laminext](https://laminext.dev) 0.17.0, [Frontroute 0.19.0](https://frontroute.dev) (docs to be updated) +* [Laminar SAP UI5 bindings](https://github.com/sherpal/LaminarSAPUI5Bindings) 1.21.1 (currently compiled against v17.0.0-M8, but should work with v17.0.0 as well) +* [Animus](https://github.com/kitlangton/animus) 6.0.1 -## New Laminar Features +### Laminar Shoelace Web Components v0.1.0 + +[Shoelace.js](https://shoelace.style/) is a well made library of Web Components like [Button](https://shoelace.style/components/button), [Dropdown](https://shoelace.style/components/dropdown), [Dialog](https://shoelace.style/components/dialog), etc. It features a pleasant modern design, both visually and technically, and is **easily customizable to match your style guide**. +To make type-safe and ergonomic Laminar bindings for these components, I created a semi-automatic generator that parses the type information from Shoelace's `custom-elements.json` manifest, and outputs Laminar code. This gets us 70% of the way there, but requires a slew of manual additions and adjustments because `custom-elements.json` does not contain all of the type information that we need. -### Laminar-Shoelace Web Components v0.1.0 +And thus, [Laminar Shoelace Bindings](https://github.com/raquo/laminar-shoelace-components) v0.1.0 is now available as the first published release, and lets you use almost all Shoelace components, albeit with some limitations and caveats. See the repo for more details. See the [demo page](https://demo.laminar.dev/app/integrations/web-components/shoelace) for some of these components in action. -TODO: +Aside from Shoelace, we already had [Laminar bindings for SAP UI5](https://github.com/sherpal/LaminarSAPUI5Bindings) courtesy of Antoine. While UI5 offers more advanced components such as a date picker, it is much harder to visually customize than Shoelace. -(TLDR - Works, but feature support is limited. See repo for details.) +In turn, Shoelace has recently [raised half a million dollars](https://www.kickstarter.com/projects/fontawesome/web-awesome/description) on Kickstarter to create more advanced components, which they intend to offer on paid plans similar to [Font Awesome](https://fontawesome.com/plans), with the existing Shoelace components remaining free. With sustainable funding like this, I'm sure Shoelace will grow to be a great option for both enterprise users and hobbyists. +## New Laminar Features + ### New Inserter Type @@ -82,7 +88,7 @@ Previously, Laminar's `children <-- streamOfChildren` syntax required an Observa At the moment this mechanism isn't extensible to custom collection types. If you need that, please let me know. -When putting mutable collections in a Var, remember that mutating such an observable does not on its own cause the Var to emit an update, simply because the Var knows nothing of it. You need to call `Var.set` or `Var.update` to actually trigger the update, e.g. like this: +When putting mutable collections in a Var, remember that mutating such an observable does not on its own cause the Var to emit an update – simply because the Var knows nothing of it. You need to call `Var.set` or `Var.update` to actually trigger the update, e.g. like this: ```scala myVar.update { mutableArr => @@ -91,7 +97,7 @@ myVar.update { mutableArr => } ``` -Also, note that the `distinct` operator will filter out your updates based on mutation. +Also, note that the `distinct` operator will filter out your updates based on mutation, because the reference does not change, and it remembers previous values by reference. Read more about all this, including performance considerations, in the new doc section [Rendering Mutable Collections](https://laminar.dev/documentation#rendering-mutable-collections) @@ -107,11 +113,11 @@ As part of this change, I simplified Laminar implicits to use the new `Renderabl ### Goodbye flatMap -Users who are new to Airstream tend to over-rely on `flatMap`, perhaps because they're thinking of Airstream observables as effect types. They are **not**, they're not even monads, strictly speaking, because of time and transactions. Airstream observables are a good alternative to strictly lawful monadic effect systems on the frontend, not a bad reimplementation of their ideas. +Users who are new to Airstream tend to over-rely on `flatMap`, perhaps because they're thinking of Airstream observables as effect types. They are **not**, they're not even monads, strictly speaking, because of time and transactions. And that's ok, for our purposes. -As Airstream users (should) know, using `flatMap` _unnecessarily_ – i.e. in cases when other operators like `combineWith` would suffice – creates observable graphs that can suffer from [FRP glitches](https://github.com/raquo/Airstream/#frp-glitches), and defeats Airstream's painstakingly fine-tuned Transaction mechanism that prevents such glitches. To be super clear, using `flatMap` does not cause glitches on its own. It can only cause glitches when it's used _unnecessarily_, and even then, only under certain conditions. When `flatMap` is used by true necessity, the observable graph is always structured in such a way that a glitch can't possibly happen (simplifying a bit here, but it really does work like that. Airstream docs about transactions, topological rank, and loopy vs flowy operators explain all that in more detail). +As Airstream users (should) know, using `flatMap` _unnecessarily_ – i.e. in cases when other operators like `combineWith` would suffice – creates observable graphs that can suffer from [FRP glitches](https://github.com/raquo/Airstream/#frp-glitches), and defeats Airstream's painstakingly fine-tuned Transaction mechanism that prevents such glitches. To be super clear, using `flatMap` does not cause glitches on its own. It can only cause glitches when it's used _unnecessarily_, and even then, only under certain conditions. When `flatMap` is used by true necessity, the observable graph is pretty much always structured in such a way that a glitch can't possibly happen (simplifying a bit here, but it really does work like that. Airstream docs about transactions, topological rank, and loopy vs flowy operators explain all that in more detail). -Unfortunately, with `flatMap` being such a common operation on many data types, developers tend to reach for it before they learn about why it's a bad idea in Airstream, and many never read the entirety of the documentation – which does explain the problem of `flatMap` in great detail. And so, they end up using `flatMap` in a way that is _unnecessary_, and can thus cause FRP glitches. +Unfortunately, with `flatMap` being such a common operation on many data types, developers tend to reach for it before they learn about why it's a bad idea in Airstream, and many never read the entirety of the documentation – which does explain the undesirable characteristics of `flatMap` in great detail. And so, they end up using `flatMap` in a way that is _unnecessary_, and can thus cause FRP glitches. Most of the problem with `flatMap` is its very inviting / innocuous name, as well as Scala's for-comprehensions using it invisibly under the hood, resulting in developers using it on autopilot. And so, to improve the user experience, especially for non-experts, the method called `flatMap` on Observables is now renamed into several variants, such as `flatMapSwitch`, `flatMapMerge`, and `flatMapCustom`. It is thus no longer available via for-comprehensions. @@ -140,7 +146,7 @@ case class Pending[+In](input: In) extends Status[In, Nothing] { /* ... */ } case class Resolved[+In, +Out](input: In, output: Out, ix: Int) extends Status[In, Out] { /* ... */ } ``` -Suppose we have a stream of networks request arguments (`requestS`), and we want to execute those requests, and show a "loading" indicator while the requests are in progress. This is how we could do it: +Suppose we have a stream of network request arguments (`requestS`), and we want to execute those requests, and show a "loading" indicator while the requests are in progress. This is how we could do it: ```scala val requestS: EventStream[Request] = ??? @@ -181,7 +187,7 @@ div( ### New Special Type Helpers -Airstream now has operators that help you work with observables of some popular "branched" types. For example, all of the following types have two possible branches: +Airstream now has operators that help you work with observables of some popular "branched" types. For example, all of the following types have two possible "branches": - `Boolean` has `true` and `false` - `Option[Foo]` has `Some[Foo]` and `None` - `Try[V]` has `Success[V]` and `Failure` @@ -194,7 +200,7 @@ For each of those types, we have operators that help you map / collect / etc. ov - `signalOfEither.foldEither(rightValue => C, leftValue => C)` - `signalOfBool.invert` – well, you get the idea -Importantly, one category of those helpers are the specialized `split` operators. They have the same semantics as the usual `split` operator, except they treat events in the same branch the way the usual `split` operator treats updates to the "same" item, so for example, you can say: +One important category of these new helpers are the **specialized `split` operators**. They have the same semantics as the usual `split` operator, except they split events by branch, instead of splitting them by e.g. `_.id`, so for example, you can say: ```scala val userTrySignal: Signal[Try[User]] = ??? @@ -208,7 +214,7 @@ div( ) ``` -As you can see, `splitTry`'s callbacks are very similar to the standard `split` callback, except the key was implicitly decided for you (`_.isSuccess`), and you get separate callbacks that are more precisely typed for each case. +As you can see, `splitTry`'s callbacks are very similar to the standard `split` callback, except the discriminator key was implicitly decided for you (`_.isSuccess`), and you get separate callbacks that are more precisely typed for each case. For more details, see the new Airstream doc sections: - [Split Operators for Special Types](https://github.com/raquo/Airstream/#split-operators-for-special-types) @@ -234,7 +240,7 @@ render( You would expect the mounted div to contain two text nodes: `1` and `10`. Obviously. But in fact, before v17, the div would only contain `1`, not `10`. In short, this happened because by the time the `child.text <-- stream.map(_ * 10)` subscription was activated, the stream's `1` event has already finished propagating, so `stream.map(_ * 10)` did not receive any events. -This happened whenever you've _started_ a stream that emits an event on startup (`EventStream.fromValue(1)`) by adding multiple subscribers at the same time: only the first subscriber would receive the event. Again, we're only talking about that one event that fires _right when the stream is being started_ (i.e. when the div element is being mounted). Doing this is the entire purpose of the `fromValue` stream, but most streams don't actually do this, and are unaffected. +This happened whenever you've _started_ a stream that emits an event on startup (`EventStream.fromValue(1)`) by adding multiple subscribers at the same time: due to the bug, only the first subscriber would receive the event. Again, we're only talking about that one event that fires _right when the stream is being started_ (i.e. when the div element is being mounted). Doing this is the entire purpose of the `fromValue` stream, but most streams don't actually do this, and are unaffected. After some struggles, I've fixed this bug, and the code above now works as expected. The trigger conditions for it are pretty niche, and I discovered the bug myself, with no reports of similar-sounding problems that I recall, so I'm guessing you are quite unlikely to be affected by it. In a couple of Airstream's own tests, the order of events in complex flatMap-s changed slightly due to this fix. The change did not go against any advertised contract, but could potentially break implicit assumptions in your code. Still, keep in mind that we use _a lot_ of `fromValue` / `fromSeq` streams in our test suite, have detailed timing checks, and still got very few observable changes in behaviour. @@ -245,9 +251,9 @@ To help **migration**, I published Airstream version `17.0.0-M3` that contains t Airstream's old Transaction code was not stack safe. It is now. -I also added a (configurable) limit to how deep you can nest transactions (`Transaction.maxDepth`). It defaults to 1000, and you should never hit it unless you have an infinite loop of transactions (e.g. two Var-s updating each other with no filter). If you do hit the limit, it will prevent the execution of the offending transaction (thus breaking the loop), report a `TransactionDepthExceeded` error into Airstream's unhandled errors, and proceed with the rest of your code. +I also added a (configurable) limit to how deep you can nest transactions (`Transaction.maxDepth`). It defaults to 1000, and in practice you should never hit it unless you have an infinite loop of transactions (e.g. two Var-s updating each other with no filter). If you do hit the limit, it will prevent the execution of the offending transaction (thus breaking the loop), report a `TransactionDepthExceeded` error into Airstream's unhandled errors, and proceed with the rest of your code. -**Migration:** no action needed unless you actually run into this error. You may want to check deeply nested or recursive code, but it's unlikely that you're hitting this limit yet don't hit the higher but still finite JS runtime stack depth with Airstream 16. +**Migration:** no action needed unless you actually run into this error. You may want to check deeply nested or recursive code, but it's unlikely that you're hitting this limit yet aren't hitting the higher but still finite JS runtime stack depth with Airstream v16. See [Airstream#115](https://github.com/raquo/Airstream/issues/115) and [Laminar#116](https://github.com/raquo/Laminar/issues/116). @@ -300,7 +306,7 @@ See [Airstream#115](https://github.com/raquo/Airstream/issues/115) and [Laminar# ## Other Goodies -* Latest Laminar includes _Scala DOM Types_ v18.0.0 – see its [Changelog](https://github.com/raquo/scala-dom-types/blob/master/CHANGELOG.md#v1800--dec-2023) +* Latest Laminar includes _Scala DOM Types_ v18.1.0 – see its [Changelog](https://github.com/raquo/scala-dom-types/blob/master/CHANGELOG.md) * [ew](https://github.com/raquo/ew) v0.2.0 now includes [JsVector](https://github.com/raquo/ew/blob/master/src/main/scala/com/raquo/ew/JsVector.scala), which is just `JsArray` in an immutable trench coat. @@ -329,7 +335,9 @@ See [Airstream#115](https://github.com/raquo/Airstream/issues/115) and [Laminar# ## Thank you -TODO +First of all, special thanks to the Scala.js core team for continued improvements of our already magnificent foundation! Building things on Scala.js has been consistently pleasant and productive every year since I've started using it. Cheers for [Sébastien](https://github.com/sjrd/) and [Tobias](https://github.com/gzm0/)! + +Laminar development is kindly supported by [my sponsors](https://github.com/sponsors/raquo), and I am very grateful for being able to work on all this. ### DIAMOND sponsor: diff --git a/website/docs/documentation.md b/website/docs/documentation.md index 0baa04f9..45c707f8 100644 --- a/website/docs/documentation.md +++ b/website/docs/documentation.md @@ -2634,7 +2634,7 @@ Bottom line: 2) Do not abuse transaction-creating methods like `flatMap`, `Var.set`, and `EventBus.emit` to achieve outcomes that do not require transaction boundary. 3) Feel free to use these methods when they are actually needed, as this will not cause glitches. -**See also Airstream docs about [avoiding unnecessary flatMap](github.com/raquo/Airstream/#avoid-unnecessary-flatmap).** +**See also Airstream docs about [avoiding unnecessary flatMap](https://github.com/raquo/Airstream/#avoid-unnecessary-flatmap).** ### Creating Elements Instead of Updating Them diff --git a/website/docs/examples/ajax.md b/website/docs/examples/ajax.md index 15c3d801..985a37ac 100644 --- a/website/docs/examples/ajax.md +++ b/website/docs/examples/ajax.md @@ -44,7 +44,7 @@ val app: HtmlElement = div( "Send", inContext { thisNode => val clickStream = thisNode.events(onClick).sample(selectedOptionVar.signal) - val responseStream = clickStream.flatMap { opt => + val responseStream = clickStream.flatMapSwitch { opt => AjaxStream .get( url = opt.url, diff --git a/website/docs/examples/time.md b/website/docs/examples/time.md index 6b0a6dd3..90004046 100644 --- a/website/docs/examples/time.md +++ b/website/docs/examples/time.md @@ -44,7 +44,7 @@ val clickBus = new EventBus[Unit] val maybeAlertStream = EventStream.merge( clickBus.events.mapTo(Some(span("Just clicked!"))), - clickBus.events.flatMap { _ => + clickBus.events.flatMapSwitch { _ => EventStream.fromValue(None, emitOnce = true).delay(500) } ) diff --git a/website/docs/examples/todomvc.md b/website/docs/examples/todomvc.md index 670dbfc2..4e74e338 100644 --- a/website/docs/examples/todomvc.md +++ b/website/docs/examples/todomvc.md @@ -205,7 +205,7 @@ object TodoMvcApp { private def pluralize(num: Int, singular: String, plural: String): String = s"$num ${if (num == 1) singular else plural}" - private val onEnterPress = onKeyPress.filter(_.keyCode == dom.ext.KeyCode.Enter) + private val onEnterPress = onKeyPress.filter(_.keyCode == dom.KeyCode.Enter) } diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 95d6c004..48a286d6 100644 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -59,10 +59,10 @@ class HomeSplash extends React.Component { tagline="Native Scala.js library for building user interfaces" /> - +
Laminar 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. @@ -258,7 +258,7 @@ class Index extends React.Component {
- + ); }