Skip to content

Commit 89f5c4d

Browse files
Expanded Tyrian Next examples
1 parent ead0a04 commit 89f5c4d

46 files changed

Lines changed: 970 additions & 16 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.sc

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,85 @@ object `tyrian-next-examples` extends mill.Module {
1010

1111
object `getting-started` extends mill.Module {
1212

13-
object minimal extends examplemodule.ExampleModule {
14-
13+
object minimal extends examplemodule.ExampleModule {
14+
1515
def ivyDeps =
1616
Agg(
1717
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
1818
)
1919

2020
}
21+
2122
object subcomponents extends examplemodule.ExampleModule {
22-
23+
24+
def ivyDeps =
25+
Agg(
26+
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
27+
)
28+
29+
}
30+
31+
object `html-fragments` extends examplemodule.ExampleModule {
32+
33+
def ivyDeps =
34+
Agg(
35+
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
36+
)
37+
38+
}
39+
40+
}
41+
42+
object networking extends mill.Module {
43+
44+
object http extends examplemodule.ExampleModule {
45+
46+
def ivyDeps =
47+
Agg(
48+
ivy"io.indigoengine::tyrian-next::$tyrianVersion",
49+
ivy"io.circe::circe-core::0.14.13",
50+
ivy"io.circe::circe-parser::0.14.13"
51+
)
52+
53+
}
54+
55+
object websockets extends examplemodule.ExampleModule {
56+
57+
def ivyDeps =
58+
Agg(
59+
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
60+
)
61+
62+
}
63+
64+
}
65+
66+
object svg extends mill.Module {
67+
68+
object clock extends examplemodule.ExampleModule {
69+
70+
def ivyDeps =
71+
Agg(
72+
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
73+
)
74+
75+
}
76+
77+
}
78+
79+
object ui extends mill.Module {
80+
81+
object debouncing extends examplemodule.ExampleModule {
82+
83+
def ivyDeps =
84+
Agg(
85+
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
86+
)
87+
88+
}
89+
90+
object field extends examplemodule.ExampleModule {
91+
2392
def ivyDeps =
2493
Agg(
2594
ivy"io.indigoengine::tyrian-next::$tyrianVersion"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Clock SVG
2+
3+
This example demonstrates how to create an animated SVG clock using Tyrian. The clock displays the current time with a moving second hand that updates every second.

classic-examples/svg/clock/src/Main.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ object Main extends TyrianIOApp[Msg, Model]:
1818
def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) =
1919
(new js.Date(), Cmd.None)
2020

21+
/** The update function handles tick messages by simply updating the model with the new time.
22+
*/
23+
//```scala
2124
def update(model: Model): Msg => (Model, Cmd[IO, Msg]) =
2225
case Msg.Tick(newTime) => (newTime, Cmd.None)
2326
case Msg.NoOp => (model, Cmd.None)
27+
//```
2428

29+
/** The view function renders an SVG clock face with a moving second hand.
30+
*
31+
* We calculate the angle of the second hand based on the current seconds (0-59), then compute
32+
* the X and Y coordinates for the tip of the hand using trigonometry.
33+
*/
34+
//```scala
2535
def view(model: Model): Html[Msg] =
2636
val angle = model.getSeconds() * 2 * math.Pi / 60 - math.Pi / 2
2737
val handX = 50 + 40 * math.cos(angle)
@@ -42,9 +52,15 @@ object Main extends TyrianIOApp[Msg, Model]:
4252
stroke := "#023963"
4353
)
4454
)
55+
//```
4556

57+
/** The subscription sends a tick message every second, providing the current time. This drives
58+
* the clock animation by continuously updating the model.
59+
*/
60+
//```scala
4661
def subscriptions(model: Model): Sub[IO, Msg] =
4762
Sub.every[IO](1.second, "clock-ticks").map(Msg.Tick.apply)
63+
//```
4864

4965
type Model = js.Date
5066

tyrian-next-examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# Classic
1+
# Tyrian Next Examples
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Html Fragments
2+
3+
This example is about 'Out of order' rendering.
4+
5+
In the classic Elm style, you construct the view HTML in one go, based on the model. That works, but comes with the drawback that you have to - in effect - render everything 'in order'. Or at least, you have to co-ordinate the construction so that everything turns up in the right place. This has the knock on effect of enticing the developer away from the idea of building encapsulated components.
6+
7+
What would be better, is if we could render all our components in any order (or the order that makes sense for the data they need), and then just mechanically glue them together, safe in the knowledge that the right fragment of HTML would turn up in the right part of the final DOM.
8+
9+
This is the function of `HtmlRoot` and `HtmlFragment`.
10+
11+
> `HtmlFragment` borrows from a similar notion in Indigo, called `SceneUpdateFragment`.
12+
13+
Note that using `Marker`s is _not_ the same as HTML templating. In Tyrian, an HTML template (i.e. HTML that needs to be hydrated with data), is just a function, e.g.:
14+
15+
```scala
16+
def nameInHtml(name: String): Html[GlobalMsg] =
17+
p(name)
18+
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package example
2+
3+
import tyrian.Html.*
4+
import tyrian.*
5+
import tyrian.next.*
6+
import tyrian.next.syntax.*
7+
8+
import scala.scalajs.js.annotation.*
9+
10+
/** In order to control the rendering we need to use markers, and markers have IDs. It's a good idea
11+
* to keep these as constants somewhere.
12+
*/
13+
// ```scala
14+
object ViewMarkers:
15+
val A: MarkerId = MarkerId("a")
16+
val B: MarkerId = MarkerId("b")
17+
val C: MarkerId = MarkerId("c")
18+
// ```
19+
20+
/** First we'll need a basic component. Since the component does almost nothing, this setup feels
21+
* like overkill in our use case, but later on we'll use it to demonstrate how it makes updates and
22+
* rendering easy to wire in.
23+
*
24+
* Notice that the `view` function is making an `HtmlFragment` using `HtmlFragment.insert`, this
25+
* tells the fragment that this HTML will be assigned to a marker somewhere later.
26+
*/
27+
// ```scala
28+
final case class MyComponent(id: MarkerId):
29+
30+
def update: GlobalMsg => Outcome[MyComponent] =
31+
_ => Outcome(this)
32+
33+
def view: HtmlFragment =
34+
HtmlFragment.insert(
35+
id,
36+
p(s"Component: ${id.value}")
37+
)
38+
// ```
39+
40+
/** The model also follows the component pattern, and mechanically updates and presents the
41+
* components. Nice and simple, but this pattern is useful because it allows the Model to manage
42+
* the components under it (whatever manage means, could be adding more, removing, finding or
43+
* altering, etc.).
44+
*/
45+
// ```scala
46+
final case class Model(components: Batch[MyComponent]):
47+
def update: GlobalMsg => Outcome[Model] = e =>
48+
components.map(_.update(e)).sequence.map { updated =>
49+
this.copy(components = updated)
50+
}
51+
52+
def view: Batch[HtmlFragment] =
53+
components.map(_.view)
54+
// ```
55+
56+
@JSExportTopLevel("TyrianApp")
57+
object Main extends TyrianNext[Model]:
58+
59+
def router: Location => GlobalMsg =
60+
Routing.none(NoOp)
61+
62+
/** When we initialise the model we'll add the components purposely out of order, for
63+
* demonstration purposes.
64+
*/
65+
// ```scala
66+
def init(flags: Map[String, String]): Outcome[Model] =
67+
Outcome(
68+
Model(
69+
Batch(
70+
MyComponent(ViewMarkers.A),
71+
MyComponent(ViewMarkers.C),
72+
MyComponent(ViewMarkers.B)
73+
)
74+
)
75+
)
76+
// ```
77+
78+
/** The main update function is simple delegation.
79+
*/
80+
// ```scala
81+
def update(model: Model): GlobalMsg => Outcome[Model] =
82+
case e => model.update(e)
83+
// ```
84+
85+
/** The main function does two things:
86+
*
87+
* 1. Sets up markers - these are placeholders for where we want the Html to end up. Notice
88+
* that these are now in order. This is very simple structure, but markers can be deeply
89+
* nested too.
90+
*
91+
* 2. It combines the fragment containing the markers with the result of calling view on the
92+
* model, and the view is constructed for us.
93+
*/
94+
// ```scala
95+
def view(model: Model): HtmlRoot =
96+
HtmlRoot(
97+
HtmlFragment(
98+
Marker(ViewMarkers.A),
99+
Marker(ViewMarkers.B),
100+
Marker(ViewMarkers.C)
101+
)
102+
).addHtmlFragments(model.view)
103+
// ```
104+
105+
def watchers(model: Model): Batch[Watcher] =
106+
Batch.empty
107+
108+
case object NoOp extends GlobalMsg
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Minimal Setup
2+
3+
A minimal example of how to set up a Tyrian-Next app.

tyrian-next-examples/getting-started/minimal/src/Main.scala

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@ import tyrian.next.*
66

77
import scala.scalajs.js.annotation.*
88

9+
/** The main thing to note is that the use of `Outcome`, `Batch`, `Action`, and `Watchers`.
10+
*
11+
* - `Outcome` replaces `(model, Cmd)`
12+
* - `Batch` is used as a `List` replacement. It isn't quite as friendly as list, but it is more
13+
* performant for our needs.
14+
* - `Action` replaces `Cmd` (to avoid ambiguous terms), but you can convert from one to the
15+
* other, so you can use `Cmd`s in `Action`s.
16+
* - `Watcher` replaces `Sub` (to avoid ambiguous terms), but you can convert from one to the
17+
* other, so you can use `Sub`s in `Watcher`s.
18+
*/
19+
// ```scala
920
@JSExportTopLevel("TyrianApp")
1021
object Main extends TyrianNext[Model]:
1122

1223
def router: Location => GlobalMsg =
1324
Routing.none(NoOp)
1425

15-
def init(flags: Map[String, String]): Outcome[Model]=
26+
def init(flags: Map[String, String]): Outcome[Model] =
1627
Outcome(Model())
1728

1829
def update(model: Model): GlobalMsg => Outcome[Model] =
@@ -23,14 +34,15 @@ object Main extends TyrianNext[Model]:
2334
Outcome(model)
2435

2536
def view(model: Model): HtmlRoot =
26-
HtmlRoot.div(
37+
HtmlRoot(
2738
HtmlFragment(
2839
p("Hello, Tyrian!")
2940
)
3041
)
3142

3243
def watchers(model: Model): Batch[Watcher] =
3344
Batch.empty
45+
// ```
3446

3547
final case class Model()
3648

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Sub-Components
2+
3+
This is the typical Tyrian/Elm sub-component example, written in Tyrian Next style.
4+
5+
The emphasis is on encapsulation of data and logic in 'components'. Components can contain other components and operate on them, but not the other way around. All other communication is done via messages.

tyrian-next-examples/getting-started/subcomponents/src/Counter.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import tyrian.*
44
import tyrian.Html.*
55
import tyrian.next.*
66

7+
/** Each individual Counter is handled with a Counter component instance. The isntance is stateful
8+
* and lives in the CounterManager.
9+
*
10+
* The Counter component follows the typical Elm-component architecture of having an `update` and a
11+
* `view` method.
12+
*
13+
* Since the `Counter` `update` takes a `CounterEvent`, we can have exhaustive message checking.
14+
* The problem is that it means something higher up needs to pre-extract these message type for the
15+
* Counters (CounterManager in this case), which can make message / event wiring fragile.
16+
*
17+
* The counters view is simple enough to return normal HTML.
18+
*/
19+
// ```scala
720
final case class Counter(value: Int):
821
def update: CounterEvent => Counter =
922
case CounterEvent.Increment =>
@@ -18,12 +31,19 @@ final case class Counter(value: Int):
1831
div(text(value.toString)),
1932
button(onClick(CounterEvent.Increment))(text("+"))
2033
)
34+
// ```
2135

2236
object Counter:
2337

2438
val initial: Counter =
2539
Counter(0)
2640

41+
/** In Tyrian-Next, all messages extend the `GlobalMsg` type. This means that you lose absolute
42+
* exhaustive message type checking, but the advantage is that it now feels natural to keep the
43+
* messages types with the components they're related to.
44+
*/
45+
// ```scala
2746
enum CounterEvent extends GlobalMsg:
2847
case Increment
2948
case Decrement
49+
// ```

0 commit comments

Comments
 (0)