Skip to content

Commit

Permalink
Docs: Draft 17.0.0 release blog post
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Dec 5, 2023
1 parent 1254a19 commit 9b18d37
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 2 deletions.
242 changes: 242 additions & 0 deletions website/blog/2023-12-04-laminar-v17.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
---
title: Laminar v17.0.0
author: Nikita
authorURL: https://twitter.com/raquo
---

TODO - summary

<!--truncate-->

> Laminar is a Scala.js library for building web application interfaces and managing UI state. Learn more in [a few short examples](https://demo.laminar.dev) or [one long video](https://www.youtube.com/watch?v=L_AHCkl6L-Q).

## Releases

* Laminar 17.0.0-M2
* Airstream 17.0.0-M2 (and Airstream 17.0.0-M1 – read below)
* Waypoint 8.0.0-M2
* ew 0.2.0
* Scala DOM Types 18.0.0
* Scala DOM Test Utils 18.0.0

TODO - Laminext, SAPUI5, Laminar Demo, Shoelace, etc.


## New Laminar Features


### Laminar-Shoelace Web Components v0.1.0

TODO: Stuff's ready, but publishing is TODO. Might publish separately from this release depending on timing.



### New Inserter Type

Previously, constructs that inserted dynamic nodes like `child <-- stream` or `children <-- stream` were called `Inserter`-s. This type was now renamed to `DynamicInserter`, and we also have a **new** `Inserter` type that `DynamicInserter` extends.

The new Inserter type lets you write more efficient APIs that accept child nodes, regardless of whether they are static or dynamic. In Laminar, this is now used in `onMountInsert`, as well as Web Component slots.

The new Inserter type supports **hooks** – callbacks that happen when it makes DOM operations. So far, only `onWillInsertNode` is supported, and we use it to set the `slot` attributes of elements passed into Web Component slots. This is a low level API that will mostly help Laminar library and addon developers.

Technical notes
* Existing `Inserter`-s (child <-- ...) et al. are now typed as `DynamicInserter` (new name + no type param)
* Introduced the concept of `StaticInserter`-s – they reserve a spot in the DOM just like the old dynamic inserters, but can only render static nodes, not streams of nodes.
* `onMountInsert` now uses these static inserters to render static elements, which is more efficient for that use case.
* If you want to make an API that accepts either static elements or dynamic inserters like `child <-- stream` (for example, for Web Component slots), you can use the new `Inserter` type.
* **Migration:**
* Replace `Inserter.Base` and `Inserter[El]` types with `DynamicInserter` or `Inserter`
* If you have an `onMountInsert` block that returns either static elements like `div()` or dynamic inserters like `child <-- ...` based on some condition evaluated at mounting time, and you re-mount such a component several times – if you have this kind of logic, double-check that switching from static to dynamic (and the other way) still works for you. It should be fine, but I refactored it, so just to be sure.


### New Conditional Rendering Helper

* New conditional syntax for child, children, and text receivers
* `child(el) := true`, `child(el)(true)`, `child(el) <-- signalOfBooleans`
* TODO: document in Laminar

* Replaced cls.toggle("foo") with cls("foo") for more ergonomic syntax
* `cls("foo", "bar")` and all other such setters can now be used conditionally like so:
* cls("foo", "bar") := myBoolean
* cls("foo", "bar")(myBoolean)
* cls("foo", "bar") <-- myBooleanStream
* Eliminate `LockedCompositeKey` type, its functionality is now merged into `CompositeKeySetter`
* Eliminate unused `itemsToRemove` property from `CompositeKeySetter`
* TODO: document in Laminar


## Significant Airstream Improvements


### 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. That does not mean they're bad, they are in fact pretty good at their job, it's just "being a monad" is not part of it.

As Airstream users (should) know, using `flatMap` _unnecessarily_ – i.e. in cases when other operators like `combineWith` would suffice – creates observable graphs that 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_. When it's 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. The docs explain it in more detail).

Unfortunately, with `flatMap` being such a common operation on many data types, people 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 in great detail. And so, they end up using `flatMap` in a way that is _unnecessary_, in thus, a way that can cause FRP glitches.

Most of the problem with `flatMap` is its very inviting / innocuous name, as well as Scala's for-comprehensions using it under the hood, resulting in people 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.

Similar changes apply to the `flatten` operator, of course.

See the renewed [Flattening Observables](https://github.com/raquo/Airstream/#flattening-observables) section of Airstream docs.

TODO: Write more specific doc section, update link.

**Migration:**
- First, see the compiler error caused by `flatMap` usage, and import `FlattenStrategy.flatMapAllowed` as necessary, to make your code compile as-is.
- Note: `flatMap` is only problematic on observables. Event props like `onClick` are `EventProcessor`-s, not `Observable`-s, so `onClick.flatMap` does not cause a problem, and does not raise any errors.
- When the rest of the migration is complete, go and actually check each of the deprecated `flatMap` usages to make sure that it's legitimate. If it can be replaced by operators like `combineWith` / `withCurrentValueOf` / `sample`, you should definitely do it. [Some discussion here](https://github.com/raquo/Airstream/issues/110)
- Legitimate uses of `flatMap` with just a callback, without the second `strategy` parameter, should be changed to `flatMapSwitch` to get the same behaviour. Other legitimate usages should switch to `flatMapMerge` or `flatMapCustom`. Similarly for the `flatten` operator.
- As you address each usage, remove the import allowing legacy flatMaps, so that you know when you're done.



### Status Operators

TODO:

`stream.debounceWithStatus(ms = 100)` gives you a stream of status objects that are like `Pending[Input] | Resolved[Input, Output]`, and similarly for a few other variations, including `requestStream.flatMapWithStatus(request => responseStream)`, which again gives you `Pending[Request] | Resolved[Request, Response]`.

TODO: Document in Airstream, link to docs.

TODO: See https://github.com/raquo/Airstream/commit/9cf679feaccb4f99230a93451f5a7489edb2d31d for now.


### Special Type Helpers

TODO: Write this section. Lots of new helper methods like .invert, mapRight, collectSuccess, splitEither.

TODO: See https://github.com/raquo/Airstream/commit/9cf679feaccb4f99230a93451f5a7489edb2d31d for now.

Boolean, Option, Try, Either

Mention splitBoolean, splitTry, splitEither

TODO when exists - See https://github.com/raquo/Airstream/#special-type-helpers


### Fix Stream Startup with Multiple Observers

The full details are available in [Airstream/issue#111](https://github.com/raquo/Airstream/issues/111). This is quite a complex technical problem, so I'll try to focus on how you may be affected by the solution. So, suppose you have this code:

```scala
val container = dom.document.getElementById("app-container")
val stream = EventStream.fromValue(1)
render(
container,
div(
child.text <-- stream,
child.text <-- stream.map(_ * 10)
)
)
```

You would expect the mounted div to contain two text nodes: `1` and `10`. Obviously. But in fact, until v17, the div would only contain `1`, not `10`. Very briefly, this happened because by the time the `child.text <-- stream.map(_ * 10)` subscription 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). Most streams don't 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.

To help **migration**, I published Airstream version `17.0.0-M1` that contains this one fix, and nothing else from v17. It is binary compatible with Airstream v16.0.0, so you can add it to your project, and test with it to make sure that it does not break anything, before upgrading to 17.0.0. Note that this is Airstream only, Laminar has no such version. Use `sbt evicted` to make sure that Airstream `17.0.0-M1` is actually selected.



## Smaller Laminar Improvements

* New: `modSeq` and `nodeSeq`
* Small helpers for better type inference, e.g. `nodeSeq("text", span("element"))` returns a list of nodes, not a list of `java.Object` like `List("text", span("element"))` does. So, basically copied `nodeSeq` from Laminext.
* `modSeq` works the same, but for modifiers.
* New: `filterNot` event processor, to complement `filter`
* New: `flatMapWithStatus` event processors to match new Airstream operator
* TODO: Document in Laminar
* New: `when(bool)(modifiers: _*)` and `whenNot(bool)(modifiers: _*)` keywords, to reduce the need for `emptyMod` / `emptyNode`
* TODO: Document in laminar, in conod. rendering section
* New `text <-- ...` alias to `child.text <-- ...`
* New: `apply` alias for `compose` event processor
* You can now say e.g. `onClick(_.debounce(100)) --> ...`
* TODO: Document in laminar
* New: Better support for Web Components
* Controlled inputs work with Web Components now
* CustomHtmlTag and Slot (improved API and moved into Laminar from Laminar-Shoelace)
* Fix: Bring back checks against conflicting value controller binders.
* You can't have both `controlled(value <-- ...)` and un-controlled `value <-- ...` binders on the same element, it does not make sense. Similarly for the `checked` property.
* **Migration:** if you are affected, you'll get an exception now. Your code was broken anyway, now more obviously so.



## Smaller Airstream Improvements

* New: [tapEach](https://github.com/raquo/Airstream/#tapEach) operator
* Naming: `eventBus.stream` alias to `eventBus.events`, for consistency with Var's `signal`.
* Naming: `signal.changes(op)` alias to `signal.composeChanges(op)`. `signal.changes` (with no parens) remains the same.
* Misc: Better displayName-s
* Shorten default displayName-s (`com.raquo.airstream.eventbus.EventBus@<hashCode>` -> `EventBus@<hashCode>` etc.) for all `Named` types, including observables, event buses, vars, etc.
* Use pretty default names for var signals and eventbus streams (e.g. `Var@<hashCode>.signal`) (this also affects toString)
* **Migration:** your tests might break if they rely on previous default displayName-s
* Fix: More robust error reporting
* Handle exceptions that happen while printing exceptions
* Yes, that can happen, and yes, that happened.
* Airstream's unhandled-errors now include error class name in error messages, not just its message
* split's duplicate key warnings go to unhandled-errors now
* **Migration:** nothing changes, unless you added / removed Airstream unhandled error callbacks
* `Airstream.sendUnhandledError` is now public


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

* [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 trenchcoat.

* I also added more methods to the JsArray type, and fixed a bunch of errors in `ew`, most notably the `JsArray.from` method, which was simply casting the provided `js.Array` instead of shallow-copying it (the latter is how the real `js.Array.from` behaves, and what one would expect).


## Other Laminar Breaking Changes

**Migration** should be obvious where not specified.

* Internal structure refactor:
* Rename `trait Airstream` -> `AirstreamAliases`
* Move `trait Implicits` to api package
* Extract some code from `Laminar` trait into `MountHooks` and `StyleUnitsApi`
* Move all the inserters, `InsertContext` and `CollectionCommand` to new `inserters` package
* Eliminate `FocusBinder` object - use `focus` directly
* Naming: `ValueController` -> `InputController`



## Thank you

TODO


### DIAMOND sponsor:

<div class="-sponsorsList x-alignItemsStart x-justifyContentCenter">
<div class="-sponsor x-diamond x-company x-heartai">
<a class="x-noHover" href="https://www.heartai.net/">
<img class="-logo" src="/img/sponsors/heartai.svg" alt="" />
<div class="-tagline"><u>HeartAI</u> is a data and analytics platform for digital health and clinical care.</div>
</a>
</div>
</div>

**GOLD sponsors**:

<div class="-sponsorsList x-alignItemsEnd">
<div class="-sponsor x-person x-yurique">
<img class="-avatar x-rounded" src="/img/sponsors/yurique.jpg" alt="" />
<div class="-text">
<div class="-name"><a href="https://github.com/yurique">Iurii Malchenko</a></div>
</div>
</div>
<div class="-sponsor x-company x-aurinko">
<a class="x-noHover" href="https://www.aurinko.io/">
<img class="-logo" src="/img/sponsors/aurinko-light-250px.png" alt="" />
<div class="-tagline"><u>Aurinko</u> is an API platform for workplace addons and integrations.</div>
</a>
</div>
</div>
4 changes: 2 additions & 2 deletions website/pages/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ class Index extends React.Component {
),
contents: [
{
title: '[Live Examples](/examples/hello-world)',
content: "Live app and its code side-by-side.<br />What sorcery is this?"
title: '[Live Examples](https://demo.laminar.dev)',
content: "Live app showing its own source code.<br />What sorcery is this?"
},
{
title: '[Documentation](/documentation)',
Expand Down

1 comment on commit 9b18d37

@Lukah0173
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 🎉

Please sign in to comment.