diff --git a/Readme.md b/Readme.md
index 24fa6e4d..bc93c370 100644
--- a/Readme.md
+++ b/Readme.md
@@ -79,27 +79,28 @@ Let's create a simple hello world script in scala-cli that uses terminal21 serve
```scala
import org.terminal21.client.*
import org.terminal21.client.components.*
+// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
+import org.terminal21.client.components.std.*
-Sessions.withNewSession("hello-world", "Hello World Example"): session =>
+Sessions
+ .withNewSession("hello-world", "Hello World Example")
+ .connect: session =>
given ConnectedSession = session
- Seq(
- Paragraph(text = "Hello World!")
- ).render()
- session.waitTillUserClosesSession()
+
+ Controller.noModel(Paragraph(text = "Hello World!")).render()
+ // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data.
+ session.leaveSessionOpenAfterExiting()
```
If we run this, then we can point our browser to the server, and we will see this UI:
![hello world ui](docs/images/hello-world.png)
-The script will wait until the user clicks the close button, which then will invalidate the
-session it has with the server and terminate the app.
-
-![hello world ui](docs/images/hello-world-terminated.png)
+The script will wait until the user clicks the close button and then the script will terminate.
# Usecases
-Due to it's client-server architecture, terminal21 gives a UI to scripts running i.e. on servers without a desktop environment and
+Due to its client-server architecture, terminal21 gives a UI to scripts running i.e. on servers without a desktop environment and
can be used for things like:
- creating text file editors which run on desktop-less servers but still allows us to edit the text file on our browser, see [textedit.sc](example-scripts/textedit.sc)
@@ -122,7 +123,7 @@ can be used for things like:
# Available UI Components
-Standard html elements
+Standard html elements like paragraphs, headers, cookies etc
[Std](docs/std.md)
Generic components for buttons, menus, forms, text, grids, tables:
@@ -139,6 +140,14 @@ Maths:
Spark:
[Spark](docs/spark.md)
+
+# Apps running on server
+
+User applications can run on the terminal21 server so that they are always available. The api is the same but a bit of extra wiring is required
+for the terminal21 server to be able to use them.
+
+See [running apps on the server][docs/run-on-server.md]
+
# Architecture
Terminal21 consist of :
@@ -156,7 +165,7 @@ the state in the client scripts.
terminal21 ui components are immutable from v0.20. Use `component.withX(...).renderChanges()` to modify a component
and render it. Note that the original `component` is not changed.
-Also when getting a value of i.e. an Input, use `myInput.current.value`. `current` makes sure we read the component with
+Also, when getting a value of i.e. an Input, use `myInput.current.value`. `current` makes sure we read the component with
all changes that may have occurred at the browser and all the changes we did on our script.
# Need help?
@@ -164,6 +173,17 @@ all changes that may have occurred at the browser and all the changes we did on
Please use the [discussions](https://github.com/kostaskougios/terminal21-restapi/discussions) of the project to post any questions, comments or ideas.
# Changelog
+
+## Version 0.30
+
+- apps can now run on the server + server management bundled apps
+- Cookie setter and reader.
+- session builders refactoring for more flexible creation of sessions
+- QuickTabs, QuickFormControl
+- bug fix for old react state re-rendering on new session
+- event iterators allows idiomatic handling of events and overhaul of the event handling for easier testing and easier development of larger apps
+- MVC
+
## Version 0.21
- more std and chakra components like Alert, Progress, Tooltip, Tabs.
diff --git a/build.sbt b/build.sbt
index 495aa8ae..a51dd017 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,11 +1,9 @@
-import sbt.librarymanagement.ModuleFilter
-
/** This build has different sections for each integration. I.e. an http4s section and a kafka section. These sections are not related to each other, please
* examine the section you're interested in.
*/
-val scala3Version = "3.3.1"
+val scala3Version = "3.3.3"
-ThisBuild / version := "0.21"
+ThisBuild / version := "0.30"
ThisBuild / organization := "io.github.kostaskougios"
name := "rest-api"
ThisBuild / scalaVersion := scala3Version
@@ -27,8 +25,9 @@ val FunctionsHelidonClient = "io.github.kostaskougios" %% "helidon-client"
val FunctionsHelidonWsClient = "io.github.kostaskougios" %% "helidon-ws-client" % FunctionsVersion
val FunctionsFibers = "io.github.kostaskougios" %% "fibers" % FunctionsVersion
-val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.15" % Test
+val ScalaTest = "org.scalatest" %% "scalatest" % "3.2.18" % Test
val Mockito = "org.mockito" % "mockito-all" % "2.0.2-beta" % Test
+val Mockito510 = "org.scalatestplus" %% "mockito-5-10" % "3.2.18.0" % Test
val Scala3Tasty = "org.scala-lang" %% "scala3-tasty-inspector" % scala3Version
val CommonsText = "org.apache.commons" % "commons-text" % "1.10.0"
val CommonsIO = "commons-io" % "commons-io" % "2.11.0"
@@ -51,10 +50,10 @@ val HelidonServerLogging = "io.helidon.logging" % "helidon-logging-jul"
val LogBack = "ch.qos.logback" % "logback-classic" % "1.4.14"
val Slf4jApi = "org.slf4j" % "slf4j-api" % "2.0.9"
-val SparkSql = ("org.apache.spark" %% "spark-sql" % "3.5.0" % "provided").cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
+val SparkSql = ("org.apache.spark" %% "spark-sql" % "3.5.1" % "provided").cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
val SparkScala3Fix = Seq(
- "io.github.vincenzobaz" %% "spark-scala3-encoders" % "0.2.5",
- "io.github.vincenzobaz" %% "spark-scala3-udf" % "0.2.5"
+ "io.github.vincenzobaz" %% "spark-scala3-encoders" % "0.2.6",
+ "io.github.vincenzobaz" %% "spark-scala3-udf" % "0.2.6"
).map(_.exclude("org.scala-lang.modules", "scala-xml_2.13"))
// -----------------------------------------------------------------------------------------------
@@ -68,7 +67,7 @@ val commonSettings = Seq(
lazy val `terminal21-server-client-common` = project
.settings(
commonSettings,
- libraryDependencies ++= Seq(
+ libraryDependencies ++= Circe ++ Seq(
ScalaTest,
Slf4jApi,
HelidonClientWebSocket,
@@ -102,6 +101,19 @@ lazy val `terminal21-server` = project
.dependsOn(`terminal21-ui-std-exports` % "compile->compile;test->test", `terminal21-server-client-common`)
.enablePlugins(FunctionsRemotePlugin)
+lazy val `terminal21-server-app` = project
+ .settings(
+ commonSettings,
+ libraryDependencies ++= Seq(
+ Mockito510
+ )
+ )
+ .dependsOn(
+ `terminal21-server` % "compile->compile;test->test",
+ `terminal21-ui-std` % "compile->compile;test->test",
+ `terminal21-server-client-common` % "compile->compile;test->test"
+ )
+
lazy val `terminal21-ui-std-exports` = project
.settings(
commonSettings,
@@ -129,6 +141,7 @@ lazy val `terminal21-ui-std` = project
libraryDependencies ++= Seq(
ScalaTest,
Mockito,
+ Mockito510,
Slf4jApi,
HelidonClient,
FunctionsCaller,
@@ -147,9 +160,10 @@ lazy val `terminal21-ui-std` = project
lazy val `end-to-end-tests` = project
.settings(
commonSettings,
+ publish := {},
libraryDependencies ++= Seq(ScalaTest, LogBack)
)
- .dependsOn(`terminal21-ui-std`, `terminal21-nivo`, `terminal21-mathjax`)
+ .dependsOn(`terminal21-ui-std` % "compile->compile;test->test", `terminal21-nivo`, `terminal21-mathjax`)
lazy val `terminal21-nivo` = project
.settings(
@@ -185,6 +199,7 @@ lazy val `terminal21-mathjax` = project
lazy val `terminal21-code-generation`: Project = project
.settings(
commonSettings,
+ publish := {},
libraryDependencies ++= Seq(
ScalaTest,
Scala3Tasty,
diff --git a/docs/quick.md b/docs/quick.md
index 3f1558d1..38e2a37d 100644
--- a/docs/quick.md
+++ b/docs/quick.md
@@ -5,6 +5,8 @@ simplify creation of this components.
## QuickTable
+This class helps creating tables quickly.
+
```scala
val conversionTable = QuickTable().headers("To convert", "into", "multiply by")
.caption("Imperial to metric conversion factors")
@@ -14,3 +16,33 @@ val tableRows:Seq[Seq[String]] = Seq(
)
conversionTable.rows(tableRows)
```
+
+## QuickTabs
+
+This class simplifies the creation of tabs.
+
+```scala
+
+QuickTabs()
+ .withTabs("Tab 1", "Tab 2")
+ .withTabPanels(
+ Paragraph(text="Tab 1 content"),
+ Paragraph(text="Tab 2 content")
+ )
+
+```
+
+## QuickFormControl
+
+Simplifies creating forms.
+
+```scala
+QuickFormControl()
+ .withLabel("Email address")
+ .withHelperText("We'll never share your email.")
+ .withInputGroup(
+ InputLeftAddon().withChildren(EmailIcon()),
+ emailInput,
+ InputRightAddon().withChildren(CheckCircleIcon())
+ )
+```
\ No newline at end of file
diff --git a/docs/run-on-server.md b/docs/run-on-server.md
new file mode 100644
index 00000000..74e4a968
--- /dev/null
+++ b/docs/run-on-server.md
@@ -0,0 +1,32 @@
+# Running applications on the server
+
+To create an app that runs on the server, implement the `ServerSideApp` trait and then pass your implementation to the `start()` method of the server:
+
+```scala
+class MyServerApp extends ServerSideApp:
+ override def name = "My Server App"
+
+ override def description = "Some app that I want to be available when I start the server"
+
+ override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit =
+ serverSideSessions
+ .withNewSession("my-server-app-session", name)
+ .connect: session =>
+ given ConnectedSession = session
+ ... your app code ...
+```
+
+See for example the [default terminal21 apps](../terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled).
+
+Now make sure your app is included in the server's classpath and then pass it as an argument to `start()`, i.e. with this `scala-cli` script:
+
+```scala
+//> ...
+//> using dep MY_APP_DEP
+
+import org.terminal21.server.Terminal21Server
+
+Terminal21Server.start(apps = Seq(new MyServerApp))
+```
+
+Now start the server and the app should be available in the app list of terminal21.
diff --git a/docs/std.md b/docs/std.md
index c08ba513..b0798a06 100644
--- a/docs/std.md
+++ b/docs/std.md
@@ -30,4 +30,19 @@ Header1(text = "Welcome to the std components demo/test")
val output = Paragraph(text = "This will reflect what you type in the input")
input.onChange: newValue =>
output.withText(newValue).renderChanges()
+```
+
+### Cookies
+
+Set a cookie:
+
+```scala
+Cookie(name = "cookie-name", value = "cookie value")
+```
+
+Read a cookie:
+
+```scala
+val cookieReader = CookieReader(key = "cookie-reader", name = "cookie-name")
+val cookieValue = events.changedValue(cookieReader)
```
\ No newline at end of file
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 7da7ad8d..3bebc76a 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -9,17 +9,31 @@ For a glimpse on what can be done with terminal21, please have a look at the [te
Terminal21 is not meant as a way to create websites. It is rather meant to give UI's to the odd jobs that has to be
performed by scripts and where it would require a lot of effort to create a dedicated web server with a UI. It is perfect
for scripting for i.e. those internal odd tasks that have to be performed at your workplace or even for things you would
-like to do on your box. And you won't have to write a single line of html or javascript.
+like to do on your box or even maybe to present some code of yours running with a UI rather than a powerpoint
+presentation. And you won't have to write a single line of html or javascript.
This tutorial will use `scala-cli` but the same applies for `sbt` or `mill` projects that use the terminal21 libraries. If you
have `scala-cli` installed on your box, you're good to go, there are no other requirements to run terminal21 scripts. Jdk and
dependencies will be downloaded by `scala-cli` for us.
-All example code is under `example-scripts` of this repo, feel free to check the repo and run them.
+All example code is under `example-scripts` of this repo, feel free to checkout the repo and run them:
+
+```shell
+git clone https://github.com/kostaskougios/terminal21-restapi.git
+cd terminal21-restapi/example-scripts
+
+# start the server
+./server.sc
+# ... it will download dependencies & jdk and start the server. Point your browser to http://localhost:8080/ui/
+
+# Open an other terminal window and
+./hello-world.sc
+# Have a look at your browser now.
+```
## Starting the terminal21 server
-The easiest way to start the terminal21 server is to have a `scala-cli` script on the box where the server will run.
+The easiest way to start the terminal21 server is to have a `scala-cli` script on the box where the server will run:
[server.sc](../example-scripts/server.sc)
@@ -28,6 +42,7 @@ The easiest way to start the terminal21 server is to have a `scala-cli` script o
//> using jvm "21"
//> using scala 3
+//> using javaOpt -Xmx128m
//> using dep io.github.kostaskougios::terminal21-server:_VERSION_
import org.terminal21.server.Terminal21Server
@@ -60,16 +75,24 @@ To do this we can create a [hello-world.sc](../example-scripts/hello-world.sc) i
```scala
#!/usr/bin/env -S scala-cli project.scala
+// ------------------------------------------------------------------------------
+// Hello world with terminal21.
+// Run with ./hello-world.sc
+// ------------------------------------------------------------------------------
import org.terminal21.client.*
import org.terminal21.client.components.*
+// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
import org.terminal21.client.components.std.*
-Sessions.withNewSession("hello-world", "Hello World Example"): session =>
- given ConnectedSession = session
+Sessions
+ .withNewSession("hello-world", "Hello World Example")
+ .connect: session =>
+ given ConnectedSession = session
- Paragraph(text = "Hello World!").render()
- session.leaveSessionOpenAfterExiting()
+ Controller.noModel(Paragraph(text = "Hello World!")).render()
+ // since this is a read-only UI, we can exit the app but leave the session open for the user to examine the page.
+ session.leaveSessionOpenAfterExiting()
```
The first line, `#!/usr/bin/env -S scala-cli project.scala`, makes our script runnable from the command line.
@@ -89,21 +112,24 @@ Next it creates a session. Each session has a unique id (globally unique across
title, "Hello World Example", that will be displayed on the browser.
```scala
-Sessions.withNewSession("hello-world", "Hello World Example"): session =>
- ...
+Sessions
+ .withNewSession("hello-world", "Hello World Example")
+ .connect: session =>
+ ...
```
![hello-world](images/hello-world.png)
-Next is the actual user interface, in this example just a paragraph with a "Hello World!":
+Next is the actual user interface, in this example just a paragraph with a "Hello World!". In order for it to be rendered, we
+quickly construct a Controller (terminal21 uses an MVC architecture idiomatic to scala, more on this later on):
```scala
-Paragraph(text = "Hello World!").render()
+Controller.noModel(Paragraph(text = "Hello World!")).render()
```
-The `render()` method sends the UI to the server which in turn sends it to the terminal21 UI so that it is rendered.
+The `render()` method sends the UI components to the server which in turn sends it to the terminal21 react frontend so that it is rendered.
-Finally because this is just a presentation script (we don't expect any feedback from the user), we can terminate it but
+Finally, because this is just a presentation script (we don't expect any feedback from the user), we can terminate it but
inform terminal21 we want to leave the session open so that the user has a chance to see it.
```scala
@@ -125,126 +151,215 @@ the progress bar and also give an informative message regarding which stage of t
```scala
#!/usr/bin/env -S scala-cli project.scala
-import org.terminal21.client.*
+// ------------------------------------------------------------------------------
+// Universe creation progress bar demo
+// Run with ./progress.sc
+// ------------------------------------------------------------------------------
+
+import org.terminal21.client.{*, given}
import org.terminal21.client.components.*
import org.terminal21.client.components.std.*
import org.terminal21.client.components.chakra.*
-
-Sessions.withNewSession("universe-generation", "Universe Generation Progress"): session =>
- given ConnectedSession = session
-
- val msg = Paragraph(text = "Generating universe ...")
- val progress = Progress(value = 1)
-
- Seq(msg, progress).render()
-
- for i <- 1 to 100 do
- val p = progress.withValue(i)
- val m =
- if i < 10 then msg
- else if i < 30 then msg.withText("Creating atoms")
- else if i < 50 then msg.withText("Big bang!")
- else if i < 80 then msg.withText("Inflating")
- else msg.withText("Life evolution")
-
- Seq(p, m).renderChanges()
- Thread.sleep(100)
-
- // clear UI
- session.clear()
- Paragraph(text = "Universe ready!").render()
+import org.terminal21.model.{ClientEvent, SessionOptions}
+
+Sessions
+ .withNewSession("universe-generation", "Universe Generation Progress")
+ .connect: session =>
+ given ConnectedSession = session
+
+ def components(model: Int, events: Events): MV[Int] =
+ val status =
+ if model < 10 then "Generating universe ..."
+ else if model < 30 then "Creating atoms"
+ else if model < 50 then "Big bang!"
+ else if model < 80 then "Inflating"
+ else "Life evolution"
+
+ val msg = Paragraph(text = status)
+ val progress = Progress(value = model)
+
+ MV(
+ model + 1,
+ Seq(msg, progress)
+ )
+
+ // send a ticker to update the progress bar
+ object Ticker extends ClientEvent
+ fiberExecutor.submit:
+ for _ <- 1 to 100 do
+ Thread.sleep(200)
+ session.fireEvent(Ticker)
+
+ Controller(components)
+ .render(1)
+ .iterator
+ .takeWhile(_.model < 100) // terminate when model == 100
+ .foreach(_ => ()) // and run it
+ // clear UI
+ session.render(Seq(Paragraph(text = "Universe ready!")))
+ session.leaveSessionOpenAfterExiting()
```
-Here we create a paragraph and a progress bar.
-
+We start by declaring our components into a function:
```scala
- val msg = Paragraph(text = "Generating universe ...")
- val progress = Progress(value = 1)
+def components(model: Int, events: Events): MV[Int]
```
+This kind of function is the standard way to create reusable UI components in terminal21. It takes the model (the progress so far as an Int between 0 and 100),
+`Events` which holds any event that was received and returns with a model-view class `MV[Int]` because our model is an `Int`. The top-level component of a page
+must have this signature (there are variations but it has to return an `MV`) but sub-components can be any functions with any number of arguments or return type. More on that later.
-Then we render them for the first time on screen. When we want to add a new element to the UI, we use the `render()` method. When
-we want to update an existing element we use the `renderChanges()` method.
+We then create a paragraph and a progress bar.
```scala
- Seq(msg, progress).render()
+ val msg = Paragraph(text = status)
+ val progress = Progress(value = model)
```
-Then we have our main loop where the calculations occur. We just use a `Thread.sleep` to simulate that some important task is being calculated. And we
-update the progress bar and the message in our paragraph.
+Finally, we return the changed model and view:
+
```scala
-val p = progress.withValue(i)
-val m = ... msg.withText("Creating atoms") ...
+ MV(
+ model + 1,
+ Seq(msg, progress)
+ )
```
-Note the `e.withX()` methods. Those help us change a value on a UI element. We get a copy of the UI element which we can render as an update:
+Ok we got our component, but how does it know when to update the progress and increase the model by 1?
+For that we need to send it a custom event. The `components` function is called once when we call the
+`render()` method on the controller and once for each event received. Since we don't have any UI component
+that may send an event, we will send it ourselfs in a separate fiber:
```scala
-Seq(p, m).renderChanges()
+// send a ticker to update the progress bar
+object Ticker extends ClientEvent
+fiberExecutor.submit:
+ for _ <- 1 to 100 do
+ Thread.sleep(200)
+ session.fireEvent(Ticker)
```
-Finally, when the universe is ready, we just clear the UI and render a paragraph before we exit.
+Remember the `events: Events` in our `components` function? This will contain the `Ticker` event, but it is of no use, so
+the components function ignores it.
+
+Now we can create the `Controller` and iterate through all events:
```scala
-session.clear()
-Paragraph(text = "Universe ready!").render()
+Controller(components)
+ .render(1) // render takes the initial model value, in this case our model is the progress as an Int between 0 and 100. We start with 1 and increment it in the components function
+ .iterator // this is a blocking iterator with events. If there is no event it will block.
+ .takeWhile(_.model < 100) // terminate when model == 100
+ .foreach(_ => ()) // and run it
```
+
+Thats it. We have a progress bar that displays different messages depending on the stage of our universe creation. And our code would
+also be easily testable. More on tests later on.
+
## Handling clicks
-Some UI elements allow us to attach an `onClick` handler. When the user clicks the element, our scala code runs.
+Some UI elements like `Button` are clickable. When the user clicks the element, our controller gets an OnClick event.
-Let's see for example [on-click.sc](../example-scripts/on-click.sc). We will create a paragraph and a button. When the
+Let's see for example [mvc-click-form.sc](../example-scripts/mvc-click-form.sc). We will create a paragraph and a button. When the
user clicks the button, the paragraph text will change and the script will exit.
+We will create the Page class we mentioned previously, makes it more structured and easier to test.
+
```scala
#!/usr/bin/env -S scala-cli project.scala
+// ------------------------------------------------------------------------------
+// MVC demo that handles a button click
+// Run with ./mvc-click-form.sc
+// ------------------------------------------------------------------------------
+
import org.terminal21.client.*
import org.terminal21.client.components.*
import org.terminal21.client.components.std.*
import org.terminal21.client.components.chakra.*
+import org.terminal21.model.SessionOptions
+
+Sessions
+ .withNewSession("mvc-click-form", "MVC form with a button")
+ .connect: session =>
+ given ConnectedSession = session
+ new ClickPage(ClickForm(false)).run() match
+ case None => // the user closed the app
+ case Some(model) => println(s"model = $model")
+
+ Thread.sleep(1000) // wait a bit so that the user can see the change in the UI
+
+/** Our model
+ *
+ * @param clicked
+ * will be set to true when the button is clicked
+ */
+case class ClickForm(clicked: Boolean)
+
+/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a
+ * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the
+ * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively.
+ */
+class ClickPage(initialForm: ClickForm)(using ConnectedSession):
+ def run(): Option[ClickForm] = controller.render(initialForm).run()
+
+ def components(form: ClickForm, events: Events): MV[ClickForm] =
+ val button = Button(key = "click-me", text = "Please click me")
+ val updatedForm = form.copy(
+ clicked = events.isClicked(button)
+ )
+ val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button")
+
+ MV(
+ updatedForm,
+ Seq(msg, button),
+ terminate = updatedForm.clicked // terminate the event iteration
+ )
+
+ def controller: Controller[ClickForm] = Controller(components)
+```
-Sessions.withNewSession("on-click-example", "On Click Handler"): session =>
- given ConnectedSession = session
+We create the paragraph and button. Components like the `Button` that receive events must have a unique key, so we set that to "click-me":
- @volatile var exit = false
- val msg = Paragraph(text = "Waiting for user to click the button")
- val button = Button(text = "Please click me").onClick: () =>
- msg.withText("Button clicked.").renderChanges()
- exit = true
+```scala
+val button = Button(key = "click-me", text = "Please click me")
+val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button")
+```
- Seq(msg, button).render()
+If the button is clicked, we update our model accordingly:
- session.waitTillUserClosesSessionOr(exit)
+```scala
+val updatedForm = form.copy(
+ clicked = events.isClicked(button)
+)
```
-First we create the paragraph and button. We attach an `onClick` handler on the button:
+Finally we return the `MV` with our model and view. Note that we inform the controller we want to terminate the event iteration when the button is clicked:
```scala
- val button = Button(text = "Please click me").onClick: () =>
- msg.withText("Button clicked.").renderChanges()
- exit = true
+MV(
+ updatedForm,
+ Seq(msg, button),
+ terminate = updatedForm.clicked // terminate the event iteration
+)
```
-Here we change the paragraph text and also update `exit` to `true`.
-
-Our script waits until var `exit` becomes true and then terminates.
+We are good now to run our page:
```scala
-session.waitTillUserClosesSessionOr(exit)
+ def run(): Option[ClickForm] = controller.render(initialForm).run()
```
-Now if we run it with `./on-click.sc` and click the button, the script will terminate.
+The controller renders the form with an initial model of `initialForm`. This effectively just calls our `def components(form: ClickForm, events: Events)` with
+an `InitialRender` event and form=initialForm. And then sends the resulting view to the terminal21 server.
-## Reading updated values
+Now if we run it with `./on-click.sc` and click the button, the script will terminate with an updated message in the paragraph.
-Some UI element values, like input boxes, can be changed by the user. We can read the changed value at any point of our
-code or install an onChange handler so that we read the value as soon as the user changes it.
+## Reading updated values
-Let's see how we can just read the value. The following script will create an email input box and a button. Whenever
-the button is pressed, it will read the email and create a new paragraph with the email value.
+Some UI element values, like input boxes, can be changed by the user. We can read the changed value and update our model accordingly.
-[read-changed-value.sc](../example-scripts/read-changed-value.sc)
+Lets create a form with an inputbox where the user can enter his/her email and a submit button. Lets follow our Page & Form class approach, it may make
+our code a bit longer but also more structured and easier to test.
-![read-value](images/tutorial/read-value.png)
+[mvc-user-form.sc](../example-scripts/mvc-user-form.sc)
```scala
#!/usr/bin/env -S scala-cli project.scala
@@ -254,88 +369,144 @@ import org.terminal21.client.components.*
import org.terminal21.client.components.std.Paragraph
import org.terminal21.client.components.chakra.*
-Sessions.withNewSession("read-changed-value-example", "Read Changed Value"): session =>
- given ConnectedSession = session
-
- val email = Input(`type` = "email", value = "my@email.com")
- val output = Box()
-
- Seq(
- FormControl().withChildren(
- FormLabel(text = "Email address"),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EmailIcon()),
- email
+// ------------------------------------------------------------------------------
+// MVC demo with an email form
+// Run with ./mvc-user-form.sc
+// ------------------------------------------------------------------------------
+
+Sessions
+ .withNewSession("mvc-user-form", "MVC example with a user form")
+ .connect: session =>
+ given ConnectedSession = session
+ new UserPage(UserForm("my@email.com", false)).run match
+ case Some(submittedUser) =>
+ println(s"Submitted: $submittedUser")
+ case None =>
+ println("User closed session without submitting the form")
+
+/** Our model for the form */
+case class UserForm(
+ email: String, // the email
+ submitted: Boolean // true if user clicks the submit button, false otherwise
+)
+
+/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a
+ * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the
+ * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively.
+ */
+class UserPage(initialForm: UserForm)(using ConnectedSession):
+
+ /** Runs the form and returns the results
+ * @return
+ * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form.
+ */
+ def run: Option[UserForm] =
+ controller.render(initialForm).run().filter(_.submitted)
+
+ /** @return
+ * all the components that should be rendered for the page
+ */
+ def components(form: UserForm, events: Events): MV[UserForm] =
+ val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email)
+ val submitButton = Button(key = "submit", text = "Submit")
+
+ val updatedForm = form.copy(
+ email = events.changedValue(emailInput, form.email),
+ submitted = events.isClicked(submitButton)
+ )
+
+ val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.")
+
+ MV(
+ updatedForm,
+ Seq(
+ QuickFormControl()
+ .withLabel("Email address")
+ .withInputGroup(
+ InputLeftAddon().withChildren(EmailIcon()),
+ emailInput
+ )
+ .withHelperText("We'll never share your email."),
+ submitButton,
+ output
),
- FormHelperText(text = "We'll never share your email.")
- ),
- Button(text = "Read Value").onClick: () =>
- val value = email.current.value
- output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges()
- ,
- output
- ).render()
-
- session.waitTillUserClosesSession()
+ terminate = updatedForm.submitted // terminate the form when the submit button is clicked
+ )
+
+ def controller: Controller[UserForm] = Controller(components)
```
-The important bit is this:
+The important bit is here:
```scala
- Button(text = "Read Value").onClick: () =>
- val value = email.current.value
- output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges()
+val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email)
+val submitButton = Button(key = "submit", text = "Submit")
+
+val updatedForm = form.copy(
+ email = events.changedValue(emailInput, form.email),
+ submitted = events.isClicked(submitButton)
+)
+
+val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.")
```
-When the button is clicked, we get the current state of the `email` input box via `email.current`. And then get it's value, `email.current.value`.
+When we update the model, we set `email = events.changedValue(emailInput, form.email)`. If the event was an `OnChange` event for our `emailInput`, this will set the email
+to the changed value. If not it will revert back to the `form.email`, effectively leaving the email unchanged.
-Also in order to append a new paragraph to the `output`, we get the current state of it (which includes any previous paragraphs we have added) and then
-add a paragraph as a new child. Then we render the changes of `output` which includes the paragraphs.
+## Creating reusable UI components.
-We can now give it a try: `./read-changed-value.sc`
+When we create user interfaces, often we want to reuse our own components.
-We can also add an `onChange` event handler on our input box and get the value whenever the user changes it.
+For instance we may want a component that asks the name of a `Person`. But we want to also be able
+to add this component inside another component that is a table of `Seq[Person]` which lists all people and allows the user
+to edit them.
-[on-change.sc](../example-scripts/on-change.sc)
+With terminal21, a component is just a function. It would normally take a model and `Events` but not necessarily, i.e. there can
+be components that don't have to process events. Also the return
+value is up to us, usually we would need to return at least a `UiElement` like `Paragraph` but many times return the updated model too.
+The component that renders a page should return `MV[Model]` but the rest of the components can return what they see fit.
-```scala
-#!/usr/bin/env -S scala-cli project.scala
+Let's see the `Person` example. Here we have 2 components, `personComponent` that asks for the name of a particular `Person` and
+`peopleComponent` that renders a table with a Seq[Person], using the `personComponent`.
-import org.terminal21.client.*
-import org.terminal21.client.components.*
-import org.terminal21.client.components.std.Paragraph
-import org.terminal21.client.components.chakra.*
+```scala
+case class Person(id: Int, name: String)
+def personComponent(person: Person, events: Events): MV[Person] =
+ val nameInput = Input(s"person-${person.id}", defaultValue = person.name)
+ val component = Box()
+ .withChildren(
+ Text(text = "Name"),
+ nameInput
+ )
+ MV(
+ person.copy(
+ name = events.changedValue(nameInput, person.name)
+ ),
+ component
+ )
+
+def peopleComponent(people: Seq[Person], events: Events): MV[Seq[Person]] =
+ val peopleComponents = people.map(p => personComponent(p, events))
+ val component = QuickTable("people")
+ .withRows(peopleComponents.map(p => Seq(p.view)))
+ MV(peopleComponents.map(_.model), component)
+```
-Sessions.withNewSession("on-change-example", "On Change event handler"): session =>
- given ConnectedSession = session
+`personComponent` take a `Person` model, renders an input box for the person's name and also if there is a change event for this input it updates the model accordingly.
+Now `peopleComponent` creates a table and each row contains the `personComponent`. The `Seq[Person]` model is updated accordingly depending on changes propagating from `personComponent`.
- val output = Paragraph(text = "Please modify the email.")
- val email = Input(`type` = "email", value = "my@email.com").onChange: v =>
- output.withText(s"Email value : $v").renderChanges()
+## Testing
- Seq(
- FormControl().withChildren(
- FormLabel(text = "Email address"),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EmailIcon()),
- email
- ),
- FormHelperText(text = "We'll never share your email.")
- ),
- output
- ).render()
+So far we have seen that structuring our code to a `components`, `controller` and `run()` method allows us to test them easily.
- session.waitTillUserClosesSession()
-```
+The `components` is just a function that returns the model and the UI components, so we can easily assert what
+is rendered based on the model value and if the model is updated correctly based on events. Terminal21's UI components
+are just case classes that can easily be compared.
-The important bit are these lines:
+If you would like to find out more please see this 2 page app, a login and loggedin page, along with their tests:
-```scala
- val output = Paragraph(text = "Please modify the email.")
- val email = Input(`type` = "email", value = "my@email.com").onChange: v =>
- output.withText(s"Email value : $v").renderChanges()
-```
+[LoginPage & LoggedInPage](../end-to-end-tests/src/main/scala/tests/LoginPage.scala)
-For the `Input` box, we add an `onChange` handler that gets the new value as `v`. We then use the value to update the paragraph.
+[LoginPageTest](../end-to-end-tests/src/test/scala/tests/LoginPageTest.scala)
-This script can be run as `./on-change.sc`.
+[LoggedInTest](../end-to-end-tests/src/test/scala/tests/LoggedInTest.scala)
diff --git a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala
index 328ed5b6..8599c988 100644
--- a/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala
+++ b/end-to-end-tests/src/main/scala/tests/ChakraComponents.scala
@@ -2,39 +2,47 @@ package tests
import org.terminal21.client.*
import org.terminal21.client.components.chakra.*
-import org.terminal21.client.components.render
import org.terminal21.client.components.std.Paragraph
import tests.chakra.*
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.atomic.AtomicBoolean
-
@main def chakraComponents(): Unit =
- val keepRunning = new AtomicBoolean(true)
-
- while keepRunning.get() do
+ def loop(): Unit =
println("Starting new session")
- Sessions.withNewSession("chakra-components", "Chakra Components"): session =>
- keepRunning.set(false)
- given ConnectedSession = session
-
- val latch = new CountDownLatch(1)
-
- // react tests reset the session to clear state
- val krButton = Button(text = "Keep Running").onClick: () =>
- keepRunning.set(true)
- latch.countDown()
-
- (Overlay.components ++ Forms.components ++ Editables.components ++ Stacks.components ++ Grids.components ++ Buttons.components(
- latch
- ) ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++ Navigation.components ++ Seq(
- krButton
- ))
- .render()
-
- println("Waiting for button to be pressed for 1 hour")
- session.waitTillUserClosesSessionOr(latch.getCount == 0)
- if !session.isClosed then
- session.clear()
- Paragraph(text = "Terminated").render()
- Thread.sleep(1000)
+ Sessions
+ .withNewSession("chakra-components", "Chakra Components")
+ .connect: session =>
+ given ConnectedSession = session
+
+ def components(m: ChakraModel, events: Events): MV[ChakraModel] =
+ // react tests reset the session to clear state
+ val krButton = Button("reset", text = "Reset state")
+
+ val bcs = Buttons.components(m, events)
+ val elements = Overlay.components(events) ++ Forms.components(
+ m,
+ events
+ ) ++ Editables.components(
+ events
+ ) ++ Stacks.components ++ Grids.components ++ bcs.view ++ Etc.components ++ MediaAndIcons.components ++ DataDisplay.components ++ Typography.components ++ Feedback.components ++ Disclosure.components ++
+ Navigation.components(events) ++ Seq(
+ krButton
+ )
+
+ val modifiedModel = bcs.model
+ val model = modifiedModel.copy(
+ rerun = events.isClicked(krButton)
+ )
+ MV(
+ model,
+ elements,
+ model.rerun || model.terminate
+ )
+
+ Controller(components).render(ChakraModel()).iterator.lastOption.map(_.model) match
+ case Some(m) if m.rerun =>
+ Controller.noModel(Seq(Paragraph(text = "chakra-session-reset"))).render(())
+ Thread.sleep(500)
+ loop()
+ case _ =>
+
+ loop()
diff --git a/end-to-end-tests/src/main/scala/tests/LoginPage.scala b/end-to-end-tests/src/main/scala/tests/LoginPage.scala
new file mode 100644
index 00000000..2b9eaae6
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/LoginPage.scala
@@ -0,0 +1,114 @@
+package tests
+
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.std.{NewLine, Paragraph}
+import org.terminal21.client.*
+
+@main def loginFormApp(): Unit =
+ Sessions
+ .withNewSession("login-form", "Login Form")
+ .connect: session =>
+ given ConnectedSession = session
+ val confirmed = for
+ login <- new LoginPage().run()
+ isYes <- new LoggedIn(login).run()
+ yield isYes
+
+ if confirmed.getOrElse(false) then println("User confirmed the details") else println("Not confirmed")
+
+case class LoginForm(email: String = "my@email.com", pwd: String = "mysecret", submitted: Boolean = false, submittedInvalidEmail: Boolean = false):
+ def isValidEmail: Boolean = email.contains("@")
+
+/** The login form. Displays an email and password input and a submit button. When run() it will fill in the Login(email,pwd) model.
+ */
+class LoginPage(using session: ConnectedSession):
+ private val initialModel = LoginForm()
+ val okIcon = CheckCircleIcon(color = Some("green"))
+ val notOkIcon = WarningTwoIcon(color = Some("red"))
+ val emailInput = Input(key = "email", `type` = "email", defaultValue = initialModel.email)
+
+ val submitButton = Button(key = "submit", text = "Submit")
+
+ val passwordInput = Input(key = "password", `type` = "password", defaultValue = initialModel.pwd)
+
+ val errorsBox = Box()
+ val errorMsgInvalidEmail = Paragraph(text = "Invalid Email", style = Map("color" -> "red"))
+
+ def run(): Option[LoginForm] =
+ controller
+ .render(initialModel)
+ .iterator
+ .map(_.model)
+ .tapEach: form =>
+ println(form)
+ .dropWhile(!_.submitted)
+ .nextOption()
+
+ def components(form: LoginForm, events: Events): MV[LoginForm] =
+ println(events.event)
+ val isValidEmail = form.isValidEmail
+ val newForm = form.copy(
+ email = events.changedValue(emailInput, form.email),
+ pwd = events.changedValue(passwordInput, form.pwd),
+ submitted = events.isClicked(submitButton) && isValidEmail,
+ submittedInvalidEmail = events.isClicked(submitButton) && !isValidEmail
+ )
+ val view = Seq(
+ QuickFormControl()
+ .withLabel("Email address")
+ .withHelperText("We'll never share your email.")
+ .withInputGroup(
+ InputLeftAddon().withChildren(EmailIcon()),
+ emailInput,
+ InputRightAddon().withChildren(if newForm.isValidEmail then okIcon else notOkIcon)
+ ),
+ QuickFormControl()
+ .withLabel("Password")
+ .withHelperText("Don't share with anyone")
+ .withInputGroup(
+ InputLeftAddon().withChildren(ViewOffIcon()),
+ passwordInput
+ ),
+ submitButton,
+ errorsBox.withChildren(if newForm.submittedInvalidEmail then errorMsgInvalidEmail else errorsBox)
+ )
+ MV(
+ newForm,
+ view
+ )
+
+ def controller: Controller[LoginForm] = Controller(components)
+
+class LoggedIn(login: LoginForm)(using session: ConnectedSession):
+ val yesButton = Button(key = "yes-button", text = "Yes")
+
+ val noButton = Button(key = "no-button", text = "No")
+
+ val emailDetails = Text(text = s"email : ${login.email}")
+ val passwordDetails = Text(text = s"password : ${login.pwd}")
+
+ def run(): Option[Boolean] =
+ controller.render(false).iterator.lastOption.map(_.model)
+
+ def components(isYes: Boolean, events: Events): MV[Boolean] =
+ val view = Seq(
+ Paragraph().withChildren(
+ Text(text = "Are your details correct?"),
+ NewLine(),
+ emailDetails,
+ NewLine(),
+ passwordDetails
+ ),
+ HStack().withChildren(yesButton, noButton)
+ )
+ MV(
+ events.isClicked(yesButton),
+ view,
+ events.isClicked(yesButton) || events.isClicked(noButton)
+ )
+
+ /** @return
+ * A controller with a boolean value, true if user clicked "Yes", false for "No"
+ */
+ def controller: Controller[Boolean] = Controller(components)
diff --git a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala
index 731e437a..254bc0e4 100644
--- a/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala
+++ b/end-to-end-tests/src/main/scala/tests/MathJaxComponents.scala
@@ -6,17 +6,22 @@ import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.mathjax.*
@main def mathJaxComponents(): Unit =
- Sessions.withNewSession("mathjax-components", "MathJax Components", MathJaxLib): session =>
- given ConnectedSession = session
- Seq(
- HStack().withChildren(
- Text(text = "Lets write some math expressions that will wow everybody!"),
- MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""")
- ),
- MathJax(expression = """Everyone knows this one : \(ax^2 + bx + c = 0\). But how about this? \(\sum_{i=1}^n i^3 = ((n(n+1))/2)^2 \)"""),
- MathJax(
- expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""",
- style = Map("backgroundColor" -> "gray")
+ Sessions
+ .withNewSession("mathjax-components", "MathJax Components")
+ .andLibraries(MathJaxLib)
+ .connect: session =>
+ given ConnectedSession = session
+
+ val components = Seq(
+ HStack().withChildren(
+ Text(text = "Lets write some math expressions that will wow everybody!"),
+ MathJax(expression = """\[\sum_{n = 200}^{1000}\left(\frac{20\sqrt{n}}{n}\right)\]""")
+ ),
+ MathJax(expression = """Everyone knows this one : \(ax^2 + bx + c = 0\). But how about this? \(\sum_{i=1}^n i^3 = ((n(n+1))/2)^2 \)"""),
+ MathJax(
+ expression = """Does it align correctly? \(ax^2 + bx + c = 0\) It does provided CHTML renderer is used.""",
+ style = Map("backgroundColor" -> "gray")
+ )
)
- ).render()
- session.leaveSessionOpenAfterExiting()
+ Controller.noModel(components).render()
+ session.leaveSessionOpenAfterExiting()
diff --git a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala
index 2eb2aaa6..c1a5b689 100644
--- a/end-to-end-tests/src/main/scala/tests/NivoComponents.scala
+++ b/end-to-end-tests/src/main/scala/tests/NivoComponents.scala
@@ -1,12 +1,16 @@
package tests
-import org.terminal21.client.components.nivo.*
import org.terminal21.client.*
import org.terminal21.client.components.*
import tests.nivo.{ResponsiveBarChart, ResponsiveLineChart}
@main def nivoComponents(): Unit =
- Sessions.withNewSession("nivo-components", "Nivo Components", NivoLib): session =>
- given ConnectedSession = session
- (ResponsiveBarChart() ++ ResponsiveLineChart()).render()
- session.waitTillUserClosesSession()
+ Sessions
+ .withNewSession("nivo-components", "Nivo Components")
+ .andLibraries(NivoLib)
+ .connect: session =>
+ given ConnectedSession = session
+
+ val components = ResponsiveBarChart() ++ ResponsiveLineChart()
+ Controller.noModel(components).render()
+ session.leaveSessionOpenAfterExiting()
diff --git a/end-to-end-tests/src/main/scala/tests/RunAll.scala b/end-to-end-tests/src/main/scala/tests/RunAll.scala
new file mode 100644
index 00000000..fa0eabba
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/RunAll.scala
@@ -0,0 +1,27 @@
+package tests
+
+import functions.fibers.Fiber
+import org.terminal21.client.*
+
+@main def runAll(): Unit =
+ Seq(
+ submit:
+ chakraComponents()
+ ,
+ submit:
+ stdComponents()
+ ,
+ submit:
+ loginFormApp()
+ ,
+ submit:
+ mathJaxComponents()
+ ,
+ submit:
+ nivoComponents()
+ ).foreach(_.get())
+
+private def submit(f: => Unit): Fiber[Unit] =
+ fiberExecutor.submit:
+ try f
+ catch case t: Throwable => t.printStackTrace()
diff --git a/end-to-end-tests/src/main/scala/tests/StdComponents.scala b/end-to-end-tests/src/main/scala/tests/StdComponents.scala
index a822856f..4f8b14f2 100644
--- a/end-to-end-tests/src/main/scala/tests/StdComponents.scala
+++ b/end-to-end-tests/src/main/scala/tests/StdComponents.scala
@@ -5,32 +5,40 @@ import org.terminal21.client.components.*
import org.terminal21.client.components.std.*
@main def stdComponents(): Unit =
- Sessions.withNewSession("std-components", "Std Components"): session =>
- given ConnectedSession = session
+ Sessions
+ .withNewSession("std-components", "Std Components")
+ .connect: session =>
+ given ConnectedSession = session
- val input = Input(defaultValue = Some("Please enter your name"))
- val output = Paragraph(text = "This will reflect what you type in the input")
- input.onChange: newValue =>
- output.withText(newValue).renderChanges()
+ def components(events: Events) =
+ val input = Input(key = "name", defaultValue = "Please enter your name")
+ val cookieReader = CookieReader(key = "cookie-reader", name = "std-components-test-cookie")
- Seq(
- Header1(text = "header1 test"),
- Header2(text = "header2 test"),
- Header3(text = "header3 test"),
- Header4(text = "header4 test"),
- Header5(text = "header5 test"),
- Header6(text = "header6 test"),
- Paragraph(text = "Hello World!").withChildren(
- NewLine(),
- Span(text = "Some more text"),
- Em(text = " emphasized!"),
- NewLine(),
- Span(text = "And the last line")
- ),
- Paragraph(text = "A Form ").withChildren(
- input
- ),
- output
- ).render()
+ val outputMsg = events.changedValue(input, "This will reflect what you type in the input")
+ val output = Paragraph(text = outputMsg)
- session.waitTillUserClosesSession()
+ val cookieMsg = events.changedValue(cookieReader).map(newValue => s"Cookie value $newValue").getOrElse("This will display the value of the cookie")
+ val cookieValue = Paragraph(text = cookieMsg)
+
+ Seq(
+ Header1(text = "header1 test"),
+ Header2(text = "header2 test"),
+ Header3(text = "header3 test"),
+ Header4(text = "header4 test"),
+ Header5(text = "header5 test"),
+ Header6(text = "header6 test"),
+ Paragraph(text = "Hello World!").withChildren(
+ NewLine(),
+ Span(text = "Some more text"),
+ Em(text = " emphasized!"),
+ NewLine(),
+ Span(text = "And the last line")
+ ),
+ Paragraph(text = "A Form").withChildren(input),
+ output,
+ Cookie(name = "std-components-test-cookie", value = "test-cookie-value"),
+ cookieReader,
+ cookieValue
+ )
+
+ Controller.noModel(components).render().iterator.lastOption
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala
index 7e3674e1..5d4c3759 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Buttons.scala
@@ -1,23 +1,23 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.*
import org.terminal21.client.components.chakra.*
+import org.terminal21.client.*
import tests.chakra.Common.*
import java.util.concurrent.CountDownLatch
object Buttons:
- def components(latch: CountDownLatch)(using session: ConnectedSession): Seq[UiElement] =
+ def components(m: ChakraModel, events: Events): MV[ChakraModel] =
val box1 = commonBox(text = "Buttons")
- val exitButton = Button(text = "Click to exit program", colorScheme = Some("red"))
- Seq(
- box1,
- exitButton.onClick: () =>
- Seq(
- box1.withText("Exit Clicked!"),
- exitButton.withText("Stopping...").withColorScheme(Some("green"))
- ).renderChanges()
- Thread.sleep(1000)
- latch.countDown()
+ val exitButton = Button(key = "exit-button", text = "Click to exit program", colorScheme = Some("red"))
+ val model = m.copy(
+ terminate = events.isClicked(exitButton)
+ )
+ MV(
+ model,
+ Seq(
+ box1,
+ exitButton
+ )
)
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala
new file mode 100644
index 00000000..8d5182be
--- /dev/null
+++ b/end-to-end-tests/src/main/scala/tests/chakra/ChakraModel.scala
@@ -0,0 +1,7 @@
+package tests.chakra
+
+case class ChakraModel(
+ rerun: Boolean = false,
+ email: String = "the-test-email@email.com",
+ terminate: Boolean = false
+)
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
index 6954315c..cec78561 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/DataDisplay.scala
@@ -1,22 +1,21 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.std.NewLine
import tests.chakra.Common.*
object DataDisplay:
- def components(using session: ConnectedSession): Seq[UiElement] =
- val headAndFoot = Tr().withChildren(
+ def components: Seq[UiElement] =
+ def headAndFoot = Tr().withChildren(
Th(text = "To convert"),
Th(text = "into"),
Th(text = "multiply by", isNumeric = true)
)
val quickTable1 = QuickTable()
- .headers("id", "name")
+ .withHeaders("id", "name")
.caption("Quick Table Caption")
- .rows(
+ .withRows(
Seq(
Seq(1, "Kostas"),
Seq(2, "Andreas")
@@ -30,7 +29,7 @@ object DataDisplay:
Badge(text = "badge 3", size = "lg", colorScheme = Some("green")),
Badge(text = "badge 4", variant = Some("outline"), colorScheme = Some("tomato")),
Badge(text = "badge 4").withChildren(
- Button(text = "test")
+ Button("test", text = "test")
)
),
commonBox(text = "Quick Tables"),
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala
index a048f372..2f2c16d2 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Disclosure.scala
@@ -1,19 +1,18 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.std.Paragraph
import tests.chakra.Common.commonBox
object Disclosure:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
Seq(
commonBox(text = "Tabs"),
Tabs().withChildren(
TabList().withChildren(
- Tab(text = "tab-one", _selected = Map("color" -> "white", "bg" -> "blue.500")),
- Tab(text = "tab-two", _selected = Map("color" -> "white", "bg" -> "green.400")),
+ Tab(text = "tab-one").withSelected(Map("color" -> "white", "bg" -> "blue.500")),
+ Tab(text = "tab-two").withSelected(Map("color" -> "white", "bg" -> "green.400")),
Tab(text = "tab-three")
),
TabPanels().withChildren(
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala
index 505f5edf..c0e3f5b8 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Editables.scala
@@ -1,28 +1,29 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
+import org.terminal21.client.*
import tests.chakra.Common.*
object Editables:
- def components(using session: ConnectedSession): Seq[UiElement] =
- val status = Box(text = "This will reflect any changes in the form.")
+ def components(events: Events): Seq[UiElement] =
+ val editable1 = Editable(key = "editable1", defaultValue = "Please type here")
+ .withChildren(
+ EditablePreview(),
+ EditableInput()
+ )
- val editable1 = Editable(defaultValue = "Please type here").withChildren(
- EditablePreview(),
- EditableInput()
- )
+ val editable2 = Editable(key = "editable2", defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.")
+ .withChildren(
+ EditablePreview(),
+ EditableTextarea()
+ )
- editable1.onChange: newValue =>
- status.withText(s"editable1 newValue = $newValue, verify editable1.value = ${editable1.current.value}").renderChanges()
+ val statusMsg = (events.changedValue(editable1).map(newValue => s"editable1 newValue = $newValue") ++ events
+ .changedValue(editable2)
+ .map(newValue => s"editable2 newValue = $newValue")).headOption.getOrElse("This will reflect any changes in the form.")
- val editable2 = Editable(defaultValue = "For longer maybe-editable texts\nUse an EditableTextarea\nIt uses a textarea control.").withChildren(
- EditablePreview(),
- EditableTextarea()
- )
- editable2.onChange: newValue =>
- status.withText(s"editable2 newValue = $newValue, verify editable2.value = ${editable2.current.value}").renderChanges()
+ val status = Box(text = statusMsg)
Seq(
commonBox(text = "Editables"),
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
index 42b930c1..76b83037 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Etc.scala
@@ -1,12 +1,11 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import tests.chakra.Common.*
object Etc:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
Seq(
commonBox(text = "Center"),
Center(text = "Center demo, not styled"),
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala b/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala
index 72fd6f7f..ba9c2749 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Feedback.scala
@@ -1,12 +1,11 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import tests.chakra.Common.commonBox
object Feedback:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
Seq(
commonBox(text = "Alerts"),
VStack().withChildren(
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala
index f693b29c..8e60e550 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Forms.scala
@@ -1,87 +1,82 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
+import org.terminal21.client.*
import org.terminal21.client.components.*
import org.terminal21.client.components.chakra.*
import tests.chakra.Common.*
object Forms:
- def components(using session: ConnectedSession): Seq[UiElement] =
- val status = Box(text = "This will reflect any changes in the form.")
+ def components(m: ChakraModel, events: Events): Seq[UiElement] =
val okIcon = CheckCircleIcon(color = Some("green"))
val notOkIcon = WarningTwoIcon(color = Some("red"))
- val emailRightAddOn = InputRightAddon().withChildren(okIcon)
-
- val email = Input(`type` = "email", value = "my@email.com")
- email.onChange: newValue =>
- Seq(
- status.withText(s"email input new value = $newValue, verify email.value = ${email.current.value}"),
- if newValue.contains("@") then emailRightAddOn.withChildren(okIcon) else emailRightAddOn.withChildren(notOkIcon)
- ).renderChanges()
-
- val description = Textarea(placeholder = "Please enter a few things about you")
- description.onChange: newValue =>
- status.withText(s"description input new value = $newValue, verify description.value = ${description.current.value}").renderChanges()
-
- val select1 = Select(placeholder = "Please choose").withChildren(
- Option_(text = "Male", value = "male"),
- Option_(text = "Female", value = "female")
- )
-
- select1.onChange: newValue =>
- status.withText(s"select1 input new value = $newValue, verify select1.value = ${select1.current.value}").renderChanges()
-
- val select2 = Select(value = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren(
- Option_(text = "First", value = "1"),
- Option_(text = "Second", value = "2")
- )
-
- val password = Input(`type` = "password", value = "mysecret")
- val dob = Input(`type` = "datetime-local")
- dob.onChange: newValue =>
- status.withText(s"dob = $newValue , verify dob.value = ${dob.current.value}").renderChanges()
-
- val color = Input(`type` = "color")
-
- color.onChange: newValue =>
- status.withText(s"color = $newValue , verify color.value = ${color.current.value}").renderChanges()
-
- val checkbox2 = Checkbox(text = "Check 2", defaultChecked = true)
- checkbox2.onChange: newValue =>
- status.withText(s"checkbox2 checked is $newValue , verify checkbox2.checked = ${checkbox2.current.checked}").renderChanges()
-
- val checkbox1 = Checkbox(text = "Check 1")
- checkbox1.onChange: newValue =>
- Seq(
- status.withText(s"checkbox1 checked is $newValue , verify checkbox1.checked = ${checkbox1.current.checked}"),
- checkbox2.withIsDisabled(newValue)
- ).renderChanges()
-
- val switch1 = Switch(text = "Switch 1")
- val switch2 = Switch(text = "Switch 2", defaultChecked = true)
-
- switch1.onChange: newValue =>
- Seq(
- status.withText(s"switch1 checked is $newValue , verify switch1.checked = ${switch1.current.checked}"),
- switch2.withIsDisabled(newValue)
- ).renderChanges()
-
- val radioGroup = RadioGroup(defaultValue = "2").withChildren(
- HStack().withChildren(
- Radio(value = "1", text = "first"),
- Radio(value = "2", text = "second"),
- Radio(value = "3", text = "third")
+ val email = Input(key = "email", `type` = "email", defaultValue = m.email)
+ val description = Textarea(key = "textarea", placeholder = "Please enter a few things about you", defaultValue = "desc")
+ val select1 = Select(key = "male/female", placeholder = "Please choose")
+ .withChildren(
+ Option_(text = "Male", value = "male"),
+ Option_(text = "Female", value = "female")
+ )
+ val select2 =
+ Select(key = "select-first-second", defaultValue = "1", bg = Some("tomato"), color = Some("black"), borderColor = Some("yellow")).withChildren(
+ Option_(text = "First", value = "1"),
+ Option_(text = "Second", value = "2")
)
+ val password = Input(key = "password", `type` = "password", defaultValue = "mysecret")
+ val dob = Input(key = "dob", `type` = "datetime-local")
+ val color = Input(key = "color", `type` = "color")
+ val checkbox2 = Checkbox(key = "cb2", text = "Check 2", defaultChecked = true)
+ val checkbox1 = Checkbox(key = "cb1", text = "Check 1")
+
+ val newM = m.copy(
+ email = events.changedValue(email).getOrElse(m.email)
)
-
- radioGroup.onChange: newValue =>
- status.withText(s"radioGroup newValue=$newValue , verify radioGroup.value=${radioGroup.current.value}").renderChanges()
+ val switch1 = Switch(key = "sw1", text = "Switch 1")
+ val switch2 = Switch(key = "sw2", text = "Switch 2", defaultChecked = true)
+ val radioGroup = RadioGroup(key = "radio", defaultValue = "2")
+ .withChildren(
+ HStack().withChildren(
+ Radio(value = "1", text = "first"),
+ Radio(value = "2", text = "second"),
+ Radio(value = "3", text = "third")
+ )
+ )
+ val saveButton = Button(key = "save-button", text = "Save", colorScheme = Some("red"))
+ val cancelButton = Button(key = "cancel-button", text = "Cancel")
+ val formStatus =
+ (events
+ .changedValue(email)
+ .map(v => s"email input new value = $v")
+ .toSeq ++ events
+ .changedValue(description)
+ .map(v => s"description input new value = $v") ++ events
+ .changedValue(select1)
+ .map(v => s"select1 input new value = $v") ++ events
+ .changedValue(dob)
+ .map(v => s"dob = $v") ++ events
+ .changedValue(color)
+ .map(v => s"color = $v") ++ events
+ .changedBooleanValue(checkbox2)
+ .map(v => s"checkbox2 checked is $v") ++ events
+ .changedBooleanValue(checkbox1)
+ .map(v => s"checkbox1 checked is $v") ++ events
+ .changedBooleanValue(switch1)
+ .map(v => s"switch1 checked is $v") ++ events
+ .changedValue(radioGroup)
+ .map(v => s"radioGroup newValue=$v") ++ events
+ .ifClicked(saveButton, "Saved clicked") ++ events
+ .ifClicked(cancelButton, "Cancel clicked")).headOption
+ .getOrElse("This will reflect any changes in the form.")
+
+ val status = Box(text = formStatus)
+
+ val emailRightAddOn = InputRightAddon()
+ .withChildren(if newM.email.contains("@") then okIcon else notOkIcon)
Seq(
commonBox(text = "Forms"),
FormControl().withChildren(
- FormLabel(text = "Email address"),
+ FormLabel(text = "Test-Email-Address"),
InputGroup().withChildren(
InputLeftAddon().withChildren(EmailIcon()),
email,
@@ -132,16 +127,8 @@ object Forms:
switch2
),
ButtonGroup(variant = Some("outline"), spacing = Some("24")).withChildren(
- Button(text = "Save", colorScheme = Some("red"))
- .onClick: () =>
- status
- .withText(
- s"Saved clicked. Email = ${email.current.value}, password = ${password.current.value}, dob = ${dob.current.value}, check1 = ${checkbox1.current.checked}, check2 = ${checkbox2.current.checked}, radio = ${radioGroup.current.value}, switch1 = ${switch1.current.checked}, switch2 = ${switch2.current.checked}"
- )
- .renderChanges(),
- Button(text = "Cancel")
- .onClick: () =>
- status.withText("Cancel clicked").renderChanges()
+ saveButton,
+ cancelButton
),
radioGroup,
status
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala
index cff12e77..0e7eadf7 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Grids.scala
@@ -1,12 +1,11 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.{Box, SimpleGrid}
import tests.chakra.Common.*
object Grids:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
val box1 = commonBox(text = "Simple grid")
Seq(
box1,
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala
index 21bfb2ac..fff9fbf3 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/MediaAndIcons.scala
@@ -1,12 +1,11 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import tests.chakra.Common.commonBox
object MediaAndIcons:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
Seq(
commonBox(text = "Icons"),
HStack().withChildren(
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala
index afa7df70..4e155905 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Navigation.scala
@@ -1,36 +1,38 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
+import org.terminal21.client.*
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.std.Paragraph
import tests.chakra.Common.commonBox
object Navigation:
- def components(using session: ConnectedSession): Seq[UiElement] =
- val clickedBreadcrumb = Paragraph(text = "no-breadcrumb-clicked")
- def breadcrumbClicked(t: String): Unit =
- clickedBreadcrumb.withText(s"breadcrumb-click: $t").renderChanges()
+ def components(events: Events): Seq[UiElement] =
+ val bcLinkHome = BreadcrumbLink("breadcrumb-home", text = "breadcrumb-home")
+ val bcLink1 = BreadcrumbLink("breadcrumb-link1", text = "breadcrumb1")
+ val bcCurrent = BreadcrumbItem(isCurrentPage = Some(true))
+ val bcLink2 = BreadcrumbLink("breadcrumb-link2", text = "breadcrumb2")
+ val link = Link(key = "google-link", text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true))
- val clickedLink = Paragraph(text = "no-link-clicked")
+ val bcStatus =
+ (
+ events.ifClicked(bcLinkHome, "breadcrumb-click: breadcrumb-home").toSeq ++
+ events.ifClicked(bcLink1, "breadcrumb-click: breadcrumb-link1") ++
+ events.ifClicked(bcLink2, "breadcrumb-click: breadcrumb-link2")
+ ).headOption.getOrElse("no-breadcrumb-clicked")
+
+ val clickedBreadcrumb = Paragraph(text = bcStatus)
+ val clickedLink = Paragraph(text = if events.isClicked(link) then "link-clicked" else "no-link-clicked")
Seq(
commonBox(text = "Breadcrumbs"),
Breadcrumb().withChildren(
- BreadcrumbItem().withChildren(
- BreadcrumbLink(text = "breadcrumb-home").onClick(() => breadcrumbClicked("breadcrumb-home"))
- ),
- BreadcrumbItem().withChildren(
- BreadcrumbLink(text = "breadcrumb-link1").onClick(() => breadcrumbClicked("breadcrumb-link1"))
- ),
- BreadcrumbItem(isCurrentPage = Some(true)).withChildren(
- BreadcrumbLink(text = "breadcrumb-link2").onClick(() => breadcrumbClicked("breadcrumb-link2"))
- )
+ BreadcrumbItem().withChildren(bcLinkHome),
+ BreadcrumbItem().withChildren(bcLink1),
+ bcCurrent.withChildren(bcLink2)
),
clickedBreadcrumb,
commonBox(text = "Link"),
- Link(text = "link-external-google", href = "https://www.google.com/", isExternal = Some(true))
- .onClick: () =>
- clickedLink.withText("link-clicked").renderChanges(),
+ link,
clickedLink
)
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
index bed13a35..2fc72ef9 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Overlay.scala
@@ -1,31 +1,39 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
+import org.terminal21.client.*
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.*
import tests.chakra.Common.commonBox
object Overlay:
- def components(using session: ConnectedSession): Seq[UiElement] =
- val box1 = Box(text = "Clicks will be reported here.")
+ def components(events: Events): Seq[UiElement] =
+ val mi1 = MenuItem(key = "download-menu", text = "Download menu-download")
+ val mi2 = MenuItem(key = "copy-menu", text = "Copy")
+ val mi3 = MenuItem(key = "paste-menu", text = "Paste")
+ val mi4 = MenuItem(key = "exit-menu", text = "Exit")
+
+ val box1Msg =
+ if events.isClicked(mi1) then "'Download' clicked"
+ else if events.isClicked(mi2) then "'Copy' clicked"
+ else if events.isClicked(mi3) then "'Paste' clicked"
+ else if events.isClicked(mi4) then "'Exit' clicked"
+ else "Clicks will be reported here."
+
+ val box1 = Box(text = box1Msg)
+
Seq(
commonBox(text = "Menus box0001"),
HStack().withChildren(
- Menu().withChildren(
+ Menu(key = "menu1").withChildren(
MenuButton(text = "Actions menu0001", size = Some("sm"), colorScheme = Some("teal")).withChildren(
ChevronDownIcon()
),
MenuList().withChildren(
- MenuItem(text = "Download menu-download")
- .onClick: () =>
- box1.withText("'Download' clicked").renderChanges(),
- MenuItem(text = "Copy").onClick: () =>
- box1.withText("'Copy' clicked").renderChanges(),
- MenuItem(text = "Paste").onClick: () =>
- box1.withText("'Paste' clicked").renderChanges(),
+ mi1,
+ mi2,
+ mi3,
MenuDivider(),
- MenuItem(text = "Exit").onClick: () =>
- box1.withText("'Exit' clicked").renderChanges()
+ mi4
)
),
box1
diff --git a/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala b/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala
index ea1cab0c..4b980f36 100644
--- a/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala
+++ b/end-to-end-tests/src/main/scala/tests/chakra/Stacks.scala
@@ -1,12 +1,11 @@
package tests.chakra
-import org.terminal21.client.ConnectedSession
import org.terminal21.client.components.UiElement
import org.terminal21.client.components.chakra.{Box, HStack, VStack}
import tests.chakra.Common.*
object Stacks:
- def components(using session: ConnectedSession): Seq[UiElement] =
+ def components: Seq[UiElement] =
Seq(
commonBox(text = "VStack"),
VStack(spacing = Some("24px"), align = Some("stretch")).withChildren(
diff --git a/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala
new file mode 100644
index 00000000..a8d21a86
--- /dev/null
+++ b/end-to-end-tests/src/test/scala/tests/LoggedInTest.scala
@@ -0,0 +1,33 @@
+package tests
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.*
+import org.terminal21.model.CommandEvent
+
+class LoggedInTest extends AnyFunSuiteLike:
+ class App:
+ val login = LoginForm()
+ given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ val form = new LoggedIn(login)
+ def allComponents = form.components(false, Events.Empty).view.flatMap(_.flat)
+
+ test("renders email details"):
+ new App:
+ allComponents should contain(form.emailDetails)
+
+ test("renders password details"):
+ new App:
+ allComponents should contain(form.passwordDetails)
+
+ test("yes clicked"):
+ new App:
+ val eventsIt = form.controller.render(false).iterator
+ session.fireEvents(CommandEvent.onClick(form.yesButton), CommandEvent.sessionClosed)
+ eventsIt.lastOption.map(_.model) should be(Some(true))
+
+ test("no clicked"):
+ new App:
+ val eventsIt = form.controller.render(false).iterator
+ session.fireEvents(CommandEvent.onClick(form.noButton), CommandEvent.sessionClosed)
+ eventsIt.lastOption.map(_.model) should be(Some(false))
diff --git a/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala
new file mode 100644
index 00000000..ff86244e
--- /dev/null
+++ b/end-to-end-tests/src/test/scala/tests/LoginPageTest.scala
@@ -0,0 +1,39 @@
+package tests
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.components.*
+import org.terminal21.client.*
+import org.terminal21.model.CommandEvent
+
+class LoginPageTest extends AnyFunSuiteLike:
+
+ class App:
+ given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ val login = LoginForm()
+ val page = new LoginPage
+ def allComponents: Seq[UiElement] = page.components(login, Events.Empty).view.flatMap(_.flat)
+
+ test("renders email input"):
+ new App:
+ allComponents should contain(page.emailInput)
+
+ test("renders password input"):
+ new App:
+ allComponents should contain(page.passwordInput)
+
+ test("renders submit button"):
+ new App:
+ allComponents should contain(page.submitButton)
+
+ test("user submits validated data"):
+ new App:
+ val eventsIt = page.controller.render(login).iterator // get the iterator before we fire the events, otherwise the iterator will be empty
+ session.fireEvents(
+ CommandEvent.onChange(page.emailInput, "an@email.com"),
+ CommandEvent.onChange(page.passwordInput, "secret"),
+ CommandEvent.onClick(page.submitButton),
+ CommandEvent.sessionClosed // every test should close the session so that the iterator doesn't block if converted to a list.
+ )
+
+ eventsIt.lastOption.map(_.model) should be(Some(LoginForm("an@email.com", "secret", true)))
diff --git a/example-scripts/bouncing-ball.sc b/example-scripts/bouncing-ball.sc
index 11b491fb..441938cc 100755
--- a/example-scripts/bouncing-ball.sc
+++ b/example-scripts/bouncing-ball.sc
@@ -8,26 +8,47 @@
// always import these
import org.terminal21.client.*
import org.terminal21.client.components.*
+import org.terminal21.model.*
// use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components
// The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
import org.terminal21.client.components.chakra.*
-import scala.annotation.tailrec
+Sessions
+ .withNewSession("bouncing-ball", "C64 bouncing ball")
+ .connect: session =>
+ given ConnectedSession = session
-Sessions.withNewSession("bouncing-ball", "C64 bouncing ball"): session =>
- given ConnectedSession = session
+ // We'll do this with an MVC approach. This is our Model:
+ case class Ball(x: Int, y: Int, dx: Int, dy: Int):
+ def nextPosition: Ball =
+ val newDx = if x < 0 || x > 600 then -dx else dx
+ val newDy = if y < 0 || y > 500 then -dy else dy
+ Ball(x + newDx, y + newDy, newDx, newDy)
- println(
- "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs."
- )
- val ball = Image(src = "/web/images/ball.png")
- ball.render()
+ // In order to update the ball's position, we will be sending approx 60 Ticker events per second to our controller.
+ case object Ticker extends ClientEvent
- @tailrec def animateBall(x: Int, y: Int, dx: Int, dy: Int): Unit =
- ball.withStyle("position" -> "fixed", "left" -> (x + "px"), "top" -> (y + "px")).renderChanges()
- Thread.sleep(1000 / 120)
- val newDx = if x < 0 || x > 600 then -dx else dx
- val newDy = if y < 0 || y > 500 then -dy else dy
- if !session.isClosed then animateBall(x + newDx, y + newDy, newDx, newDy)
+ val initialModel = Ball(50, 50, 8, 8)
- animateBall(50, 50, 8, 8)
+ println(
+ "Files under ~/.terminal21/web will be served under /web . Please place a ball.png file under ~/.terminal21/web/images on the box where the server runs."
+ )
+
+ // This is our controller implementation. It takes the model (ball) and events (in this case just the Ticker which we can otherwise ignore)
+ // and results in the next frame's state.
+ def components(ball: Ball, events: Events): MV[Ball] =
+ val b = ball.nextPosition
+ MV(
+ b,
+ Image(src = "/web/images/ball.png").withStyle("position" -> "fixed", "left" -> (b.x + "px"), "top" -> (b.y + "px"))
+ )
+ // We'll be sending a Ticker 60 times per second
+ fiberExecutor.submit:
+ while !session.isClosed do
+ session.fireEvent(Ticker)
+ Thread.sleep(1000 / 60)
+
+ // We are ready to create a controller instance with our components function.
+ Controller(components)
+ .render(initialModel) // and render it with our initial model (it will call the components function and render any resulting UI)
+ .run() // and run this until the user closes the session
diff --git a/example-scripts/csv-editor.sc b/example-scripts/csv-editor.sc
index 75abf9cb..91700aa1 100755
--- a/example-scripts/csv-editor.sc
+++ b/example-scripts/csv-editor.sc
@@ -2,20 +2,19 @@
// ------------------------------------------------------------------------------
// A quick and dirty csv file editor for small csv files.
+// Run with : ./csv-editor -- csv-file-path
// ------------------------------------------------------------------------------
-// always import these
import org.terminal21.client.*
-
-import java.util.concurrent.atomic.AtomicBoolean
import org.terminal21.client.components.*
+import org.terminal21.collections.TypedMapKey
+import org.terminal21.model.*
// use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components
// The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
import org.terminal21.client.components.chakra.*
import org.apache.commons.io.FileUtils
import java.io.File
-import scala.collection.concurrent.TrieMap
if args.length != 1 then
throw new IllegalArgumentException(
@@ -26,83 +25,98 @@ val fileName = args(0)
val file = new File(fileName)
val contents =
if file.exists() then FileUtils.readFileToString(file, "UTF-8")
- else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp"
-
-val csv = contents.split("\n").map(_.split(","))
-
-// store the csv data in a more usable Map
-val initialCsvMap = csv.zipWithIndex
- .flatMap: (row, y) =>
- row.zipWithIndex
- .map: (col, x) =>
- ((x, y), col)
- .toMap
-val csvMap = TrieMap.empty[(Int, Int), String] ++ initialCsvMap
-
-// save the map back to the csv file
-def saveCsvMap() =
- val coords = csvMap.keySet
- val maxX = coords.map(_._1).max
- val maxY = coords.map(_._2).max
-
- val s = (0 to maxX)
- .map: y =>
- (0 to maxY)
- .map: x =>
- csvMap.getOrElse((x, y), "")
- .mkString(",")
- .mkString("\n")
- FileUtils.writeStringToFile(file, s, "UTF-8")
-
- // this will be set to true when we have to exit
-val exitFlag = new AtomicBoolean(false)
-
-Sessions.withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName"): session =>
- given ConnectedSession = session
-
- val status = Box()
- val saveAndExit = Button(text = "Save & Exit")
- .onClick: () =>
- saveCsvMap()
- status.withText("Csv file saved, exiting.").renderChanges()
- exitFlag.set(true)
-
- val exit = Button(text = "Exit Without Saving")
- .onClick: () =>
- exitFlag.set(true)
-
- def newEditable(x: Int, y: Int, value: String) =
- Editable(defaultValue = value)
+ else "type,damage points,hit points\nmage,10dp,20hp\nwarrior,20dp,30hp" // a simple csv for demo purposes
+
+val csv = toCsvModel(contents.split("\n").map(_.split(",").toSeq).toSeq)
+
+Sessions
+ .withNewSession(s"csv-editor-$fileName", s"CsvEdit: $fileName")
+ .connect: session =>
+ given ConnectedSession = session
+ println(s"Now open ${session.uiUrl} to view the UI")
+ val editor = new CsvEditorPage(csv)
+ editor.run()
+
+/** Our model. It stores the csv data as a Map of (x,y) coordinates -> value.
+ */
+case class CsvModel(save: Boolean, exitWithoutSave: Boolean, csv: Map[(Int, Int), String], maxX: Int, maxY: Int, status: String = "Please edit the file.")
+def toCsvModel(csv: Seq[Seq[String]]) =
+ val maxX = csv.map(_.size).max
+ val maxY = csv.size
+ val m = csv.zipWithIndex
+ .flatMap: (row, y) =>
+ row.zipWithIndex.map: (column, x) =>
+ ((x, y), column)
+ .toMap
+ CsvModel(false, false, m, maxX, maxY)
+
+/** A nice approach to coding UI's is to create Page classes for every UI page. In this instance we need a page for our csv editor. The components function can
+ * be easily tested if we want to test what is rendered and how it changes the model when events occur.
+ */
+class CsvEditorPage(initModel: CsvModel)(using session: ConnectedSession):
+
+ val saveAndExit = Button("save-exit", text = "Save & Exit")
+ val exit = Button("exit", text = "Exit Without Saving")
+
+ def run(): Unit =
+ for mv <- controller.render(initModel).iterator.lastOption.filter(_.model.save) // only save if model.save is true
+ do save(mv.model)
+
+ def editorComponent(model: CsvModel, events: Events): MV[CsvModel] =
+ val tableCells =
+ (0 until model.maxY).map: y =>
+ (0 until model.maxX).map: x =>
+ newEditable(x, y, model.csv(x, y))
+
+ val newModel = tableCells.flatten.find(events.isChangedValue) match
+ case Some(editable) =>
+ val coords = editable.storedValue(CoordsKey)
+ val newValue = events.changedValue(editable, "error")
+ model.copy(csv = model.csv + (coords -> newValue), status = s"Changed value at $coords to $newValue")
+ case None => model
+
+ val view = QuickTable("csv-editor", variant = "striped", colorScheme = "teal", size = "mg")
+ .withCaption("Please edit the csv contents above and click save to save and exit")
+ .withRows(tableCells)
+
+ MV(newModel, view)
+
+ def components(model: CsvModel, events: Events): MV[CsvModel] =
+ val cells = editorComponent(model, events)
+ val newModel = cells.model.copy(
+ save = events.isClicked(saveAndExit),
+ exitWithoutSave = events.isClicked(exit)
+ )
+ val view = cells.view ++ Seq(
+ HStack().withChildren(
+ saveAndExit,
+ exit,
+ Box(text = newModel.status)
+ )
+ )
+ MV(
+ newModel,
+ view,
+ terminate = newModel.exitWithoutSave || newModel.save
+ )
+
+ def controller: Controller[CsvModel] = Controller(components)
+
+ def save(model: CsvModel): Unit =
+ val data = (0 until model.maxY).map: y =>
+ (0 until model.maxX).map: x =>
+ model.csv((x, y))
+ FileUtils.writeStringToFile(file, data.map(_.mkString(",")).mkString("\n"), "UTF-8")
+ println(s"Csv file saved to $file")
+
+ object CoordsKey extends TypedMapKey[(Int, Int)]
+ private def newEditable(x: Int, y: Int, value: String): Editable =
+ Editable(s"cell-$x-$y", defaultValue = value) // note: anything receiving events should have a unique key, in this instance s"cell-$x-$y"
.withChildren(
EditablePreview(),
EditableInput()
)
- .onChange: newValue =>
- csvMap((x, y)) = newValue
- status.withText(s"($x,$y) value changed to $newValue").renderChanges()
-
- Seq(
- TableContainer().withChildren(
- Table(variant = "striped", colorScheme = Some("teal"), size = "mg")
- .withChildren(
- TableCaption(text = "Please edit the csv contents above and click save to save and exit"),
- Thead(),
- Tbody(
- children = csv.zipWithIndex.map: (row, y) =>
- Tr(
- children = row.zipWithIndex.map: (column, x) =>
- Td().withChildren(newEditable(x, y, column))
- )
- )
- )
- ),
- HStack().withChildren(
- saveAndExit,
- exit,
- status
- )
- ).render()
-
- println(s"Now open ${session.uiUrl} to view the UI")
- // wait for one of the save/exit buttons to be pressed.
- session.waitTillUserClosesSessionOr(exitFlag.get())
+ .store(
+ CoordsKey,
+ (x, y)
+ ) // every UiElement has a store where we can store arbitrary data. Here we store the coordinates for the value this editable will edit
diff --git a/example-scripts/csv-viewer.sc b/example-scripts/csv-viewer.sc
index bf084500..fab68a7e 100755
--- a/example-scripts/csv-viewer.sc
+++ b/example-scripts/csv-viewer.sc
@@ -2,19 +2,17 @@
// ------------------------------------------------------------------------------
// A csv file viewer
+// Run with: ./csv-viewer.sc -- csv-file
// ------------------------------------------------------------------------------
-// always import these
import org.terminal21.client.*
import org.terminal21.client.components.*
// use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components
// The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
-import org.apache.commons.io.FileUtils
import org.terminal21.client.components.chakra.*
import java.io.File
-import java.util.concurrent.CountDownLatch
-import scala.collection.concurrent.TrieMap
+import org.apache.commons.io.FileUtils
if args.length != 1 then
throw new IllegalArgumentException(
@@ -27,24 +25,29 @@ val contents = FileUtils.readFileToString(file, "UTF-8")
val csv = contents.split("\n").map(_.split(","))
-Sessions.withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName"): session =>
- given ConnectedSession = session
-
- TableContainer()
- .withChildren(
- Table(variant = "striped", colorScheme = Some("teal"), size = "mg")
- .withChildren(
- TableCaption(text = "Csv file contents"),
- Tbody(
- children = csv.map: row =>
- Tr(
- children = row.map: column =>
- Td(text = column)
+Sessions
+ .withNewSession(s"csv-viewer-$fileName", s"CsvView: $fileName")
+ .connect: session =>
+ given ConnectedSession = session
+
+ Controller
+ .noModel(
+ TableContainer() // We could use the QuickTable component here, but lets do it a bit more low level with the Chakra components
+ .withChildren(
+ Table(variant = "striped", colorScheme = Some("teal"), size = "mg")
+ .withChildren(
+ TableCaption(text = "Csv file contents"),
+ Tbody(
+ children = csv.map: row =>
+ Tr(
+ children = row.map: column =>
+ Td(text = column)
+ )
+ )
)
)
- )
- )
- .render()
- println(s"Now open ${session.uiUrl} to view the UI.")
- // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data.
- session.leaveSessionOpenAfterExiting()
+ )
+ .render() // we don't have to process any events here, just let the user view the csv file.
+ println(s"Now open ${session.uiUrl} to view the UI.")
+ // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data.
+ session.leaveSessionOpenAfterExiting()
diff --git a/example-scripts/hello-world.sc b/example-scripts/hello-world.sc
index 6057391d..b191660f 100755
--- a/example-scripts/hello-world.sc
+++ b/example-scripts/hello-world.sc
@@ -1,16 +1,19 @@
#!/usr/bin/env -S scala-cli project.scala
// ------------------------------------------------------------------------------
// Hello world with terminal21.
+// Run with ./hello-world.sc
// ------------------------------------------------------------------------------
-// always import these
import org.terminal21.client.*
import org.terminal21.client.components.*
-// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
+// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
import org.terminal21.client.components.std.*
-Sessions.withNewSession("hello-world", "Hello World Example"): session =>
- given ConnectedSession = session
+Sessions
+ .withNewSession("hello-world", "Hello World Example")
+ .connect: session =>
+ given ConnectedSession = session
- Paragraph(text = "Hello World!").render()
- session.leaveSessionOpenAfterExiting()
+ Controller.noModel(Paragraph(text = "Hello World!")).render()
+ // since this is a read-only UI, we can exit the app but leave the session open for the user to examine the page.
+ session.leaveSessionOpenAfterExiting()
diff --git a/example-scripts/mathjax.sc b/example-scripts/mathjax.sc
index 30c2ea72..4a2343f5 100755
--- a/example-scripts/mathjax.sc
+++ b/example-scripts/mathjax.sc
@@ -1,17 +1,27 @@
#!/usr/bin/env -S scala-cli project.scala
+// ------------------------------------------------------------------------------
+// Render some maths on screen for demo purposes.
+// Run with ./mathjax.sc
+// ------------------------------------------------------------------------------
+
import org.terminal21.client.*
import org.terminal21.client.components.*
import org.terminal21.client.components.mathjax.*
-Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need to register the MathJaxLib in order to use it */ ): session =>
- given ConnectedSession = session
- Seq(
- MathJax(
- expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$"""
- ),
- MathJax(
- expression = """
+Sessions
+ .withNewSession("mathjax", "MathJax Example")
+ .andLibraries(MathJaxLib /* note we need to register the MathJaxLib in order to use it */ )
+ .connect: session =>
+ given ConnectedSession = session
+ Controller
+ .noModel(
+ Seq(
+ MathJax(
+ expression = """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$"""
+ ),
+ MathJax(
+ expression = """
|when \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
|Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam.
|Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus.
@@ -21,6 +31,9 @@ Sessions.withNewSession("mathjax", "MathJax Example", MathJaxLib /* note we need
|Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes,
|nascetur ridiculus mus.
|""".stripMargin
- )
- ).render()
- session.leaveSessionOpenAfterExiting()
+ )
+ )
+ )
+ .render()
+ // since this is a read-only UI, we can exit the app but leave the session open on the UI for the user to examine the data.
+ session.leaveSessionOpenAfterExiting()
diff --git a/example-scripts/mvc-click-form.sc b/example-scripts/mvc-click-form.sc
new file mode 100755
index 00000000..e014c354
--- /dev/null
+++ b/example-scripts/mvc-click-form.sc
@@ -0,0 +1,51 @@
+#!/usr/bin/env -S scala-cli project.scala
+
+// ------------------------------------------------------------------------------
+// MVC demo that handles a button click
+// Run with ./mvc-click-form.sc
+// ------------------------------------------------------------------------------
+
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.std.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.model.SessionOptions
+
+Sessions
+ .withNewSession("mvc-click-form", "MVC form with a button")
+ .connect: session =>
+ given ConnectedSession = session
+ new ClickPage(ClickForm(false)).run() match
+ case None => // the user closed the app
+ case Some(model) => println(s"model = $model")
+
+ Thread.sleep(1000) // wait a bit so that the user can see the change in the UI
+
+/** Our model
+ *
+ * @param clicked
+ * will be set to true when the button is clicked
+ */
+case class ClickForm(clicked: Boolean)
+
+/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a
+ * page for the click form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the
+ * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively.
+ */
+class ClickPage(initialForm: ClickForm)(using ConnectedSession):
+ def run(): Option[ClickForm] = controller.render(initialForm).run()
+
+ def components(form: ClickForm, events: Events): MV[ClickForm] =
+ val button = Button(key = "click-me", text = "Please click me")
+ val updatedForm = form.copy(
+ clicked = events.isClicked(button)
+ )
+ val msg = Paragraph(text = if updatedForm.clicked then "Button clicked!" else "Waiting for user to click the button")
+
+ MV(
+ updatedForm,
+ Seq(msg, button),
+ terminate = updatedForm.clicked // terminate the event iteration
+ )
+
+ def controller: Controller[ClickForm] = Controller(components)
diff --git a/example-scripts/mvc-user-form.sc b/example-scripts/mvc-user-form.sc
new file mode 100755
index 00000000..d8b92d0f
--- /dev/null
+++ b/example-scripts/mvc-user-form.sc
@@ -0,0 +1,72 @@
+#!/usr/bin/env -S scala-cli project.scala
+
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.std.Paragraph
+import org.terminal21.client.components.chakra.*
+
+// ------------------------------------------------------------------------------
+// MVC demo with an email form
+// Run with ./mvc-user-form.sc
+// ------------------------------------------------------------------------------
+
+Sessions
+ .withNewSession("mvc-user-form", "MVC example with a user form")
+ .connect: session =>
+ given ConnectedSession = session
+ new UserPage(UserForm("my@email.com", false)).run match
+ case Some(submittedUser) =>
+ println(s"Submitted: $submittedUser")
+ case None =>
+ println("User closed session without submitting the form")
+
+/** Our model for the form */
+case class UserForm(
+ email: String, // the email
+ submitted: Boolean // true if user clicks the submit button, false otherwise
+)
+
+/** One nice way to structure the code (that simplifies testing too) is to create a class for every page in the user interface. In this instance, we create a
+ * page for the user form to be displayed. All components are in `components` method. The controller is in the `controller` method and we can run to get the
+ * result in the `run` method. We can use these methods in unit tests to test what is rendered and how events are processed respectively.
+ */
+class UserPage(initialForm: UserForm)(using ConnectedSession):
+
+ /** Runs the form and returns the results
+ * @return
+ * if None, the user didn't submit the form (i.e. closed the session), if Some(userForm) the user submitted the form.
+ */
+ def run: Option[UserForm] =
+ controller.render(initialForm).run().filter(_.submitted)
+
+ /** @return
+ * all the components that should be rendered for the page
+ */
+ def components(form: UserForm, events: Events): MV[UserForm] =
+ val emailInput = Input(key = "email", `type` = "email", defaultValue = initialForm.email)
+ val submitButton = Button(key = "submit", text = "Submit")
+
+ val updatedForm = form.copy(
+ email = events.changedValue(emailInput, form.email),
+ submitted = events.isClicked(submitButton)
+ )
+
+ val output = Paragraph(text = if events.isChangedValue(emailInput) then s"Email changed: ${updatedForm.email}" else "Please modify the email.")
+
+ MV(
+ updatedForm,
+ Seq(
+ QuickFormControl()
+ .withLabel("Email address")
+ .withInputGroup(
+ InputLeftAddon().withChildren(EmailIcon()),
+ emailInput
+ )
+ .withHelperText("We'll never share your email."),
+ submitButton,
+ output
+ ),
+ terminate = updatedForm.submitted // terminate the form when the submit button is clicked
+ )
+
+ def controller: Controller[UserForm] = Controller(components)
diff --git a/example-scripts/nivo-bar-chart.sc b/example-scripts/nivo-bar-chart.sc
index 546d1205..d9b8ff01 100755
--- a/example-scripts/nivo-bar-chart.sc
+++ b/example-scripts/nivo-bar-chart.sc
@@ -1,5 +1,10 @@
#!/usr/bin/env -S scala-cli project.scala
+// ------------------------------------------------------------------------------
+// Nivo bar chart demo, animated !
+// Run with ./nivo-bar-chart.sc
+// ------------------------------------------------------------------------------
+
import org.terminal21.client.*
import org.terminal21.client.fiberExecutor
import org.terminal21.client.components.*
@@ -8,48 +13,58 @@ import org.terminal21.client.components.nivo.*
import scala.util.Random
import NivoBarChart.*
+import org.terminal21.model.ClientEvent
-Sessions.withNewSession("nivo-bar-chart", "Nivo Bar Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session =>
- given ConnectedSession = session
+Sessions
+ .withNewSession("nivo-bar-chart", "Nivo Bar Chart")
+ .andLibraries(NivoLib /* note we need to register the NivoLib in order to use it */ )
+ .connect: session =>
+ given ConnectedSession = session
- val chart = ResponsiveBar(
- data = createRandomData,
- keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"),
- indexBy = "country",
- padding = 0.3,
- defs = Seq(
- Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)),
- Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10))
- ),
- fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))),
- axisLeft = Some(Axis(legend = "food", legendOffset = -40)),
- axisBottom = Some(Axis(legend = "country", legendOffset = 32)),
- valueScale = Scale(`type` = "linear"),
- indexScale = Scale(`type` = "band", round = Some(true)),
- legends = Seq(
- Legend(
- dataFrom = "keys",
- translateX = 120,
- itemsSpacing = 2,
- itemWidth = 100,
- itemHeight = 20,
- symbolSize = 20,
- symbolShape = "square"
+ def components(events: Events): Seq[UiElement] =
+ val data = createRandomData
+ val chart = ResponsiveBar(
+ data = data,
+ keys = Seq("hot dog", "burger", "sandwich", "kebab", "fries", "donut"),
+ indexBy = "country",
+ padding = 0.3,
+ defs = Seq(
+ Defs("dots", "patternDots", "inherit", "#38bcb2", size = Some(4), padding = Some(1), stagger = Some(true)),
+ Defs("lines", "patternLines", "inherit", "#eed312", rotation = Some(-45), lineWidth = Some(6), spacing = Some(10))
+ ),
+ fill = Seq(Fill("dots", Match("fries")), Fill("lines", Match("sandwich"))),
+ axisLeft = Some(Axis(legend = "food", legendOffset = -40)),
+ axisBottom = Some(Axis(legend = "country", legendOffset = 32)),
+ valueScale = Scale(`type` = "linear"),
+ indexScale = Scale(`type` = "band", round = Some(true)),
+ legends = Seq(
+ Legend(
+ dataFrom = "keys",
+ translateX = 120,
+ itemsSpacing = 2,
+ itemWidth = 100,
+ itemHeight = 20,
+ symbolSize = 20,
+ symbolShape = "square"
+ )
+ )
+ )
+ Seq(
+ Paragraph(text = "Various foods.", style = Map("margin" -> 20)),
+ chart
)
- )
- )
-
- Seq(
- Paragraph(text = "Various foods.", style = Map("margin" -> 20)),
- chart
- ).render()
- fiberExecutor.submit:
- while !session.isClosed do
- Thread.sleep(2000)
- chart.withData(createRandomData).renderChanges()
+ // we'll send new data to our controller every 2 seconds via a custom event
+ case object Ticker extends ClientEvent
+ fiberExecutor.submit:
+ while !session.isClosed do
+ Thread.sleep(2000)
+ session.fireEvent(Ticker)
- session.waitTillUserClosesSession()
+ Controller
+ .noModel(components)
+ .render()
+ .run()
object NivoBarChart:
def createRandomData: Seq[Seq[BarDatum]] =
diff --git a/example-scripts/nivo-line-chart.sc b/example-scripts/nivo-line-chart.sc
index 6f5843ad..92993718 100755
--- a/example-scripts/nivo-line-chart.sc
+++ b/example-scripts/nivo-line-chart.sc
@@ -1,5 +1,10 @@
#!/usr/bin/env -S scala-cli project.scala
+// ------------------------------------------------------------------------------
+// Nivo line chart demo, animated !
+// Run with ./nivo-line-chart.sc
+// ------------------------------------------------------------------------------
+
import org.terminal21.client.*
import org.terminal21.client.fiberExecutor
import org.terminal21.client.components.*
@@ -8,32 +13,40 @@ import org.terminal21.client.components.nivo.*
import scala.util.Random
import NivoLineChart.*
+import org.terminal21.model.ClientEvent
-Sessions.withNewSession("nivo-line-chart", "Nivo Line Chart", NivoLib /* note we need to register the NivoLib in order to use it */ ): session =>
- given ConnectedSession = session
-
- val chart = ResponsiveLine(
- data = createRandomData,
- yScale = Scale(stacked = Some(true)),
- axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)),
- axisLeft = Some(Axis(legend = "count", legendOffset = -40)),
- legends = Seq(Legend())
- )
-
- Seq(
- Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)),
- chart
- ).render()
+Sessions
+ .withNewSession("nivo-line-chart", "Nivo Line Chart")
+ .andLibraries(NivoLib /* note we need to register the NivoLib in order to use it */ )
+ .connect: session =>
+ given ConnectedSession = session
- fiberExecutor.submit:
- while !session.isClosed do
- Thread.sleep(2000)
- chart.withData(createRandomData).renderChanges()
+ def components(events: Events): Seq[UiElement] =
+ val chart = ResponsiveLine(
+ data = createRandomData,
+ yScale = Scale(stacked = Some(true)),
+ axisBottom = Some(Axis(legend = "transportation", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "count", legendOffset = -40)),
+ legends = Seq(Legend())
+ )
+ Seq(
+ Paragraph(text = "Means of transportation for various countries", style = Map("margin" -> 20)),
+ chart
+ )
+ // we'll send new data to our controller every 2 seconds via a custom event
+ case object Ticker extends ClientEvent
+ fiberExecutor.submit:
+ while !session.isClosed do
+ Thread.sleep(2000)
+ session.fireEvent(Ticker)
- session.waitTillUserClosesSession()
+ Controller
+ .noModel(components)
+ .render()
+ .run()
object NivoLineChart:
- def createRandomData: Seq[Serie] =
+ def createRandomData: Seq[Serie] =
Seq(
dataFor("Japan"),
dataFor("France"),
diff --git a/example-scripts/on-change.sc b/example-scripts/on-change.sc
deleted file mode 100755
index a55b92e9..00000000
--- a/example-scripts/on-change.sc
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env -S scala-cli project.scala
-
-import org.terminal21.client.*
-import org.terminal21.client.components.*
-import org.terminal21.client.components.std.Paragraph
-import org.terminal21.client.components.chakra.*
-
-Sessions.withNewSession("on-change-example", "On Change event handler"): session =>
- given ConnectedSession = session
-
- val output = Paragraph(text = "Please modify the email.")
- val email = Input(`type` = "email", value = "my@email.com").onChange: v =>
- output.withText(s"Email value : $v").renderChanges()
-
- Seq(
- FormControl().withChildren(
- FormLabel(text = "Email address"),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EmailIcon()),
- email
- ),
- FormHelperText(text = "We'll never share your email.")
- ),
- output
- ).render()
-
- session.waitTillUserClosesSession()
diff --git a/example-scripts/on-click.sc b/example-scripts/on-click.sc
deleted file mode 100755
index ef877a14..00000000
--- a/example-scripts/on-click.sc
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env -S scala-cli project.scala
-
-import org.terminal21.client.*
-import org.terminal21.client.components.*
-import org.terminal21.client.components.std.*
-import org.terminal21.client.components.chakra.*
-
-Sessions.withNewSession("on-click-example", "On Click Handler"): session =>
- given ConnectedSession = session
-
- @volatile var exit = false
- val msg = Paragraph(text = "Waiting for user to click the button")
- val button = Button(text = "Please click me").onClick: () =>
- msg.withText("Button clicked.").renderChanges()
- exit = true
-
- Seq(msg, button).render()
-
- session.waitTillUserClosesSessionOr(exit)
diff --git a/example-scripts/postit.sc b/example-scripts/postit.sc
index 4c2326a7..d515f6e0 100755
--- a/example-scripts/postit.sc
+++ b/example-scripts/postit.sc
@@ -1,47 +1,69 @@
#!/usr/bin/env -S scala-cli project.scala
// ------------------------------------------------------------------------------
// A note poster, where anyone can write a note
+// Run with ./postit.sc
// ------------------------------------------------------------------------------
-// always import these
import org.terminal21.client.*
import org.terminal21.client.components.*
-// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
+// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
import org.terminal21.client.components.std.*
// use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components
// The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
import org.terminal21.client.components.chakra.*
-Sessions.withNewSession("postit", "Post-It"): session =>
- given ConnectedSession = session
+Sessions
+ .withNewSession("postit", "Post-It")
+ .connect: session =>
+ given ConnectedSession = session
+ println(s"Now open ${session.uiUrl} to view the UI.")
+ new PostItPage().run()
- val editor = Textarea(placeholder = "Please post your note by clicking here and editing the content")
- val messages = VStack(align = Some("stretch"))
- val add = Button(text = "Post It.").onClick: () =>
- // add the new msg.
- // note: editor.value is automatically updated by terminal-ui
- val currentMessages = messages.current
- currentMessages
- .addChildren(
- HStack().withChildren(
- Image(
- src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png",
- boxSize = Some("32px")
+class PostItPage(using ConnectedSession):
+ case class PostIt(message: String = "", messages: List[String] = Nil)
+ def run(): Unit = controller.render(PostIt()).iterator.lastOption
+
+ def components(model: PostIt, events: Events): MV[PostIt] =
+ val editor = Textarea("postit-message", placeholder = "Please post your note by clicking here and editing the content")
+ val addButton = Button("postit", text = "Post It.")
+ val clearButton = Button("clear-it", text = "Clear board.")
+
+ val updatedMessages = if events.isClicked(clearButton) then Nil else model.messages ++ events.ifClicked(addButton, model.message)
+ val updatedModel = model.copy(
+ message = events.changedValue(editor, model.message),
+ messages = updatedMessages
+ )
+
+ val messagesVStack = VStack(
+ "the-board",
+ align = Some("stretch"),
+ children = updatedMessages.map: msg =>
+ HStack()
+ .withSpacing("8px")
+ .withChildren(
+ Image(
+ src = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_Notes_icon.svg/2048px-Apple_Notes_icon.svg.png",
+ boxSize = Some("32px")
+ ),
+ Box(text = msg)
+ )
+ )
+ MV(
+ updatedModel,
+ Seq(
+ Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."),
+ InputGroup().withChildren(
+ InputLeftAddon().withChildren(EditIcon()),
+ editor
+ ),
+ HStack()
+ .withSpacing("8px")
+ .withChildren(
+ addButton,
+ clearButton
),
- Box(text = editor.current.value)
- )
+ messagesVStack
)
- .renderChanges()
-
- Seq(
- Paragraph(text = "Please type your note below and click 'Post It' to post it so that everyone can view it."),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EditIcon()),
- editor
- ),
- add,
- messages
- ).render()
+ )
- println(s"Now open ${session.uiUrl} to view the UI.")
- session.waitTillUserClosesSession()
+ def controller = Controller(components)
diff --git a/example-scripts/progress.sc b/example-scripts/progress.sc
index 29782daa..aad7f8c5 100755
--- a/example-scripts/progress.sc
+++ b/example-scripts/progress.sc
@@ -1,30 +1,49 @@
#!/usr/bin/env -S scala-cli project.scala
-import org.terminal21.client.*
+// ------------------------------------------------------------------------------
+// Universe creation progress bar demo
+// Run with ./progress.sc
+// ------------------------------------------------------------------------------
+
+import org.terminal21.client.{*, given}
import org.terminal21.client.components.*
import org.terminal21.client.components.std.*
import org.terminal21.client.components.chakra.*
+import org.terminal21.model.{ClientEvent, SessionOptions}
-Sessions.withNewSession("universe-generation", "Universe Generation Progress"): session =>
- given ConnectedSession = session
+Sessions
+ .withNewSession("universe-generation", "Universe Generation Progress")
+ .connect: session =>
+ given ConnectedSession = session
- val msg = Paragraph(text = "Generating universe ...")
- val progress = Progress(value = 1)
+ def components(model: Int, events: Events): MV[Int] =
+ val status =
+ if model < 10 then "Generating universe ..."
+ else if model < 30 then "Creating atoms"
+ else if model < 50 then "Big bang!"
+ else if model < 80 then "Inflating"
+ else "Life evolution"
- Seq(msg, progress).render()
+ val msg = Paragraph(text = status)
+ val progress = Progress(value = model)
- for i <- 1 to 100 do
- val p = progress.withValue(i)
- val m =
- if i < 10 then msg
- else if i < 30 then msg.withText("Creating atoms")
- else if i < 50 then msg.withText("Big bang!")
- else if i < 80 then msg.withText("Inflating")
- else msg.withText("Life evolution")
+ MV(
+ model + 1,
+ Seq(msg, progress)
+ )
- Seq(p, m).renderChanges()
- Thread.sleep(100)
+ // send a ticker to update the progress bar
+ object Ticker extends ClientEvent
+ fiberExecutor.submit:
+ for _ <- 1 to 100 do
+ Thread.sleep(200)
+ session.fireEvent(Ticker)
- // clear UI
- session.clear()
- Paragraph(text = "Universe ready!").render()
+ Controller(components)
+ .render(1) // render takes the initial model value, in this case our model is the progress as an Int between 0 and 100. We start with 1 and increment it in the components function
+ .iterator
+ .takeWhile(_.model < 100) // terminate when model == 100
+ .foreach(_ => ()) // and run it
+ // clear UI
+ session.render(Seq(Paragraph(text = "Universe ready!")))
+ session.leaveSessionOpenAfterExiting()
diff --git a/example-scripts/project.scala b/example-scripts/project.scala
index a8914413..4cd68731 100644
--- a/example-scripts/project.scala
+++ b/example-scripts/project.scala
@@ -1,8 +1,10 @@
//> using jvm "21"
//> using scala 3
-//> using dep io.github.kostaskougios::terminal21-ui-std:0.21
-//> using dep io.github.kostaskougios::terminal21-nivo:0.21
-//> using dep io.github.kostaskougios::terminal21-mathjax:0.21
+//> using dep io.github.kostaskougios::terminal21-ui-std:0.30
+//> using dep io.github.kostaskougios::terminal21-nivo:0.30
+//> using dep io.github.kostaskougios::terminal21-mathjax:0.30
//> using dep commons-io:commons-io:2.15.1
+
+//> using javaOpt -Xmx128m
diff --git a/example-scripts/read-changed-value.sc b/example-scripts/read-changed-value.sc
deleted file mode 100755
index 65f2693d..00000000
--- a/example-scripts/read-changed-value.sc
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env -S scala-cli project.scala
-
-import org.terminal21.client.*
-import org.terminal21.client.components.*
-import org.terminal21.client.components.std.Paragraph
-import org.terminal21.client.components.chakra.*
-
-Sessions.withNewSession("read-changed-value-example", "Read Changed Value"): session =>
- given ConnectedSession = session
-
- val email = Input(`type` = "email", value = "my@email.com")
- val output = Box()
-
- Seq(
- FormControl().withChildren(
- FormLabel(text = "Email address"),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EmailIcon()),
- email
- ),
- FormHelperText(text = "We'll never share your email.")
- ),
- Button(text = "Read Value").onClick: () =>
- val value = email.current.value
- output.current.addChildren(Paragraph(text = s"The value now is $value")).renderChanges()
- ,
- output
- ).render()
-
- session.waitTillUserClosesSession()
diff --git a/example-scripts/server.sc b/example-scripts/server.sc
index 08612e3d..7ba686b7 100755
--- a/example-scripts/server.sc
+++ b/example-scripts/server.sc
@@ -2,7 +2,8 @@
//> using jvm "21"
//> using scala 3
-//> using dep io.github.kostaskougios::terminal21-server:0.21
+//> using javaOpt -Xmx128m
+//> using dep io.github.kostaskougios::terminal21-server-app:0.30
import org.terminal21.server.Terminal21Server
diff --git a/example-scripts/textedit.sc b/example-scripts/textedit.sc
index 77fe5756..292c070c 100755
--- a/example-scripts/textedit.sc
+++ b/example-scripts/textedit.sc
@@ -2,23 +2,20 @@
// ------------------------------------------------------------------------------
// A text file editor for small files.
+// run with ./textedit.sc -- text-file
// ------------------------------------------------------------------------------
import org.apache.commons.io.FileUtils
-
import java.io.File
-// always import these
import org.terminal21.client.*
import org.terminal21.client.components.*
-// std components, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
+// std components like Paragraph, https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdElement.scala
import org.terminal21.client.components.std.*
// use the chakra components for menus, forms etc, https://chakra-ui.com/docs/components
// The scala case classes : https://github.com/kostaskougios/terminal21-restapi/blob/main/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
import org.terminal21.client.components.chakra.*
-import java.util.concurrent.CountDownLatch
-
if args.length != 1 then
throw new IllegalArgumentException(
"Expecting 1 argument, the name of the file to edit"
@@ -29,58 +26,60 @@ val file = new File(fileName)
val contents =
if file.exists() then FileUtils.readFileToString(file, "UTF-8") else ""
-def saveFile(content: String) = FileUtils.writeStringToFile(file, content, "UTF-8")
+def saveFile(content: String): Unit =
+ println(s"Saving file $fileName")
+ FileUtils.writeStringToFile(file, content, "UTF-8")
-Sessions.withNewSession(s"textedit-$fileName", s"Edit: $fileName"): session =>
- given ConnectedSession = session
- // we will wait till the user clicks the "Exit" menu, this latch makes sure the main thread of the app waits.
- val exitLatch = new CountDownLatch(1)
- // the main editor area.
- val editor = Textarea(value = contents)
- // This will display a "saved" badge for a second when the user saves the file
- val status = Badge()
- // This will display an asterisk when the contents of the file are changed in the editor
- val modified = Badge(colorScheme = Some("red"))
+Sessions
+ .withNewSession(s"textedit-$fileName", s"Edit: $fileName")
+ .connect: session =>
+ given ConnectedSession = session
- // when the user changes the textarea, we get the new text and we can compare it with the loaded value.
- editor.onChange: newValue =>
- modified.withText(if newValue != contents then "*" else "").renderChanges()
+ // the model for our editor form
+ case class Edit(content: String, savedContent: String, save: Boolean)
+ // the main editor area.
+ def components(edit: Edit, events: Events): MV[Edit] =
+ val editorTextArea = Textarea("editor", defaultValue = edit.content)
+ val saveMenu = MenuItem("save-menu", text = "Save")
+ val exitMenu = MenuItem("exit-menu", text = "Exit")
+ val isSave = events.isClicked(saveMenu)
+ val updatedContent = events.changedValue(editorTextArea, edit.content)
+ val updatedEdit = edit.copy(
+ content = updatedContent,
+ save = isSave,
+ savedContent = if isSave then updatedContent else edit.savedContent
+ )
+ val modified = Badge(colorScheme = Some("red"), text = if updatedEdit.content != updatedEdit.savedContent then "*" else "")
+ val status = Badge(text = if updatedEdit.save then "Saved" else "")
- Seq(
- HStack().withChildren(
- Menu().withChildren(
- MenuButton(text = "File").withChildren(ChevronDownIcon()),
- MenuList().withChildren(
- MenuItem(text = "Save")
- .onClick: () =>
- saveFile(editor.current.value)
- // we'll display a "Saved" badge for 1 second.
- Seq(
- status.withText("Saved"),
- modified.withText("")
- ).renderChanges()
- // each event handler runs on a new fiber, it is ok to sleep here
- Thread.sleep(1000)
- status.withText("").renderChanges()
- ,
- MenuItem(text = "Exit")
- .onClick: () =>
- exitLatch.countDown()
+ val view = Seq(
+ HStack().withChildren(
+ Menu().withChildren(
+ MenuButton("file-menu", text = "File").withChildren(ChevronDownIcon()),
+ MenuList().withChildren(
+ saveMenu,
+ exitMenu
+ )
+ ),
+ status,
+ modified
+ ),
+ FormControl().withChildren(
+ FormLabel(text = "Editor"),
+ InputGroup().withChildren(
+ InputLeftAddon().withChildren(EditIcon()),
+ editorTextArea
+ )
)
- ),
- status,
- modified
- ),
- FormControl().withChildren(
- FormLabel(text = "Editor"),
- InputGroup().withChildren(
- InputLeftAddon().withChildren(EditIcon()),
- editor
)
- )
- ).render()
- println(s"Now open ${session.uiUrl} to view the UI")
- exitLatch.await()
- session.clear()
- Paragraph(text = "Terminated").render()
+ MV(updatedEdit, view, terminate = events.isClicked(exitMenu))
+
+ println(s"Now open ${session.uiUrl} to view the UI")
+ Controller(components)
+ .render(Edit(contents, contents, false))
+ .iterator
+ .tapEach: mv =>
+ if mv.model.save then saveFile(mv.model.content)
+ .foreach(_ => ())
+ session.render(Seq(Paragraph(text = "Terminated")))
diff --git a/example-spark/project.scala b/example-spark/project.scala
index 72a41970..c4788bc7 100644
--- a/example-spark/project.scala
+++ b/example-spark/project.scala
@@ -8,10 +8,10 @@
//> using javaOpt -Dlogback.configurationFile=file:etc/logback.xml
// terminal21 dependencies
-//> using dep io.github.kostaskougios::terminal21-ui-std:0.21
-//> using dep io.github.kostaskougios::terminal21-spark:0.21
-//> using dep io.github.kostaskougios::terminal21-nivo:0.21
-//> using dep io.github.kostaskougios::terminal21-mathjax:0.21
+//> using dep io.github.kostaskougios::terminal21-ui-std:0.30
+//> using dep io.github.kostaskougios::terminal21-spark:0.30
+//> using dep io.github.kostaskougios::terminal21-nivo:0.30
+//> using dep io.github.kostaskougios::terminal21-mathjax:0.30
//> using dep ch.qos.logback:logback-classic:1.4.14
diff --git a/example-spark/spark-notebook.sc b/example-spark/spark-notebook.sc
index 02a6a39c..54e0ec03 100755
--- a/example-spark/spark-notebook.sc
+++ b/example-spark/spark-notebook.sc
@@ -1,86 +1,107 @@
#!/usr/bin/env -S scala-cli --restart project.scala
-/**
- * note we use the --restart param for scala-cli. This means every time we change this file, scala-cli will terminate
- * and rerun it with the changes. This way we get the notebook feel when we use spark scripts.
- *
- * terminal21 spark lib caches datasets by storing them into disk. This way complex queries won't have to be re-evaluated
- * on each restart of the script. We can force re-evaluation by clicking the "Recalculate" buttons in the UI.
- */
+/** note we use the --restart param for scala-cli. This means every time we change this file, scala-cli will terminate and rerun it with the changes. This way
+ * we get the notebook feel when we use spark scripts.
+ *
+ * terminal21 spark lib caches datasets by storing them into disk. This way complex queries won't have to be re-evaluated on each restart of the script. We can
+ * force re-evaluation by clicking the "Recalculate" buttons in the UI.
+ */
-// We need these imports
import org.apache.spark.sql.*
import org.terminal21.client.components.*
import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.nivo.*
-import org.terminal21.client.{*, given}
+import org.terminal21.client.*
import org.terminal21.sparklib.*
import java.util.concurrent.atomic.AtomicInteger
-import scala.util.Random
+import scala.util.{Random, Using}
import SparkNotebook.*
import org.terminal21.client.components.mathjax.{MathJax, MathJaxLib}
+import org.terminal21.client.components.std.Paragraph
-SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(/* configure your spark session here */), "spark-notebook", "Spark Notebook", NivoLib, MathJaxLib): (spark, session) =>
- given ConnectedSession = session
- given SparkSession = spark
- import scala3encoders.given
- import spark.implicits.*
+Using.resource(SparkSessions.newSparkSession( /* configure your spark session here */ )): spark =>
+ Sessions
+ .withNewSession("spark-notebook", "Spark Notebook")
+ .andLibraries(NivoLib, MathJaxLib)
+ .connect: session =>
+ given ConnectedSession = session
+ given SparkSession = spark
+ import scala3encoders.given
+ import spark.implicits.*
- // lets get a Dataset, the data are random so that when we click refresh we can see the data actually
- // been refreshed.
- val peopleDS = createPeople
+ // lets get a Dataset, the data are random so that when we click refresh we can see the data actually
+ // been refreshed.
+ val peopleDS = createPeople
+ val peopleSample = Cached("People sample"):
+ peopleDS
+ .sort($"id")
+ .limit(5)
+ val peopleOrderedByAge = Cached("Oldest people"):
+ peopleDS
+ .orderBy($"age".desc)
- // We will display the data in a table
- val peopleTable = QuickTable().headers("Id", "Name", "Age").caption("People")
+ /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long
+ * to calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is
+ * re-evaluated. If the Dataset schema changes, please click "Recalculate" or manually delete this folder.
+ *
+ * The key for the cache is "People sample"
+ */
+ println(s"Cache path: ${peopleSample.cachePath}")
- val peopleTableCalc = peopleDS.sort($"id").visualize("People sample", peopleTable): data =>
- peopleTable.rows(data.take(5).map(p => Seq(p.id, p.name, p.age)))
+ def components(events: Events) =
+ given Events = events
+ // We will display the data in a table
+ val peopleTable = QuickTable().withHeaders("Id", "Name", "Age").withCaption("People")
- /** The calculation above uses a directory to store the dataset results. This way we can restart this script without loosing datasets that may take long to
- * calculate, making our script behave more like a notebook. When we click "Recalculate" in the UI, the cache directory is deleted and the dataset is
- * re-evaluated. If the Dataset schema changes, please click "Recalculate" or manually delete this folder.
- *
- * The key for the cache is "People sample"
- */
- println(s"Cache path: ${peopleTableCalc.cachePath}")
+ val peopleTableCalc = peopleSample.visualize(peopleTable): data =>
+ peopleTable.withRows(data.collect.toList.map(p => Seq(p.id, p.name, p.age)))
- val oldestPeopleChart = ResponsiveLine(
- axisBottom = Some(Axis(legend = "Person", legendOffset = 36)),
- axisLeft = Some(Axis(legend = "Age", legendOffset = -40)),
- legends = Seq(Legend())
- )
-
- val oldestPeopleChartCalc = peopleDS
- .orderBy($"age".desc)
- .visualize("Oldest people", oldestPeopleChart): data =>
- oldestPeopleChart.withData(Seq(
- Serie(
- "Person",
- data = data.take(5).map(person => Datum(person.name, person.age))
+ val oldestPeopleChart = ResponsiveLine(
+ axisBottom = Some(Axis(legend = "Person", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "Age", legendOffset = -40)),
+ legends = Seq(Legend())
)
- ))
- Seq(
- // just make it look a bit more like a proper notebook by adding some fake maths
- MathJax(
- expression = """
- |The following is total nonsense but it simulates some explanation that would normally be here if we had
- |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
- |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam.
- |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus.
- |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus.
- |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum.
- |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi.
- |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes,
- |nascetur ridiculus mus.
- |""".stripMargin
- ),
- peopleTableCalc,
- oldestPeopleChartCalc
- ).render()
+ val oldestPeopleChartCalc = peopleOrderedByAge
+ .visualize(oldestPeopleChart): data =>
+ oldestPeopleChart.withData(
+ Seq(
+ Serie(
+ "Person",
+ data = data.take(5).map(person => Datum(person.name, person.age))
+ )
+ )
+ )
- session.waitTillUserClosesSession()
+ Seq(
+ Paragraph(
+ text = """
+ |The spark notebooks can use the `visualise` extension method over a dataframe/dataset. It will cache the dataset by
+ |saving it as a file under /tmp. The `Recalculate` button refreshes the dataset (re-runs it). In this example, the
+ |data are random and so are different each time the `Recalculate` is pressed.
+ |""".stripMargin,
+ style = Map("margin" -> "32px")
+ ),
+ // just make it look a bit more like a proper notebook by adding some fake maths
+ MathJax(
+ expression = """
+ |The following is total nonsense but it simulates some explanation that would normally be here if we had
+ |a proper notebook. When \(a \ne 0\), there are two solutions to \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\)
+ |Aenean vel velit a lacus lacinia pulvinar. Morbi eget ex et tellus aliquam molestie sit amet eu diam.
+ |Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus enim, tempor non efficitur et, rutrum efficitur metus.
+ |Nulla scelerisque, mauris sit amet accumsan iaculis, elit ipsum suscipit lorem, sed fermentum nunc purus non tellus.
+ |Aenean congue accumsan tempor. \(x = {-b \pm \sqrt{b^2-4ac} \over 2a}.\) maecenas vitae commodo tortor. Aliquam erat volutpat. Etiam laoreet malesuada elit sed vestibulum.
+ |Etiam consequat congue fermentum. Vivamus dapibus scelerisque ipsum eu tempus. Integer non pulvinar nisi.
+ |Morbi ultrices sem quis nisl convallis, ac cursus nunc condimentum. Orci varius natoque penatibus et magnis dis parturient montes,
+ |nascetur ridiculus mus.
+ |""".stripMargin,
+ style = Map("margin" -> "32px")
+ ),
+ peopleTableCalc,
+ oldestPeopleChartCalc
+ )
+ Controller.noModel(components).render().run()
object SparkNotebook:
private val names = Array("Andy", "Kostas", "Alex", "Andreas", "George", "Jack")
diff --git a/project/build.properties b/project/build.properties
index abbbce5d..04267b14 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.9.8
+sbt.version=1.9.9
diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala
index 4deef557..c41159da 100644
--- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala
+++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJax.scala
@@ -2,6 +2,7 @@ package org.terminal21.client.components.mathjax
import org.terminal21.client.components.UiElement.HasStyle
import org.terminal21.client.components.{Keys, UiElement}
+import org.terminal21.collections.TypedMap
sealed trait MathJaxElement extends UiElement
@@ -11,9 +12,12 @@ case class MathJax(
key: String = Keys.nextKey,
// expression should be like """ text \( asciimath \) text""", i.e. """When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\)"""
expression: String = """fill in the expression as per https://asciimath.org/""",
- style: Map[String, Any] = Map.empty // Note: some of the styles are ignored by mathjax lib
+ style: Map[String, Any] = Map.empty, // Note: some of the styles are ignored by mathjax lib
+ dataStore: TypedMap = TypedMap.Empty
) extends MathJaxElement
- with HasStyle[MathJax]:
+ with HasStyle:
+ type This = MathJax
override def withStyle(v: Map[String, Any]): MathJax = copy(style = v)
def withKey(k: String) = copy(key = k)
def withExpression(e: String) = copy(expression = e)
+ override def withDataStore(ds: TypedMap): MathJax = copy(dataStore = ds)
diff --git a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala
index 5946f253..e9747117 100644
--- a/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala
+++ b/terminal21-mathjax/src/main/scala/org/terminal21/client/components/mathjax/MathJaxLib.scala
@@ -6,6 +6,6 @@ import io.circe.*
import org.terminal21.client.components.{ComponentLib, UiElement}
object MathJaxLib extends ComponentLib:
- import org.terminal21.client.components.StdElementEncoding.given
+ import org.terminal21.client.json.StdElementEncoding.given
override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
case n: MathJaxElement => n.asJson.mapObject(o => o.add("type", "MathJax".asJson))
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala
index 4a68b032..97d595a5 100644
--- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/NivoLib.scala
@@ -1,12 +1,11 @@
package org.terminal21.client.components
+import io.circe.*
import io.circe.generic.auto.*
import io.circe.syntax.*
-import io.circe.*
import org.terminal21.client.components.nivo.NEJson
-import org.terminal21.client.components.{ComponentLib, UiElement}
object NivoLib extends ComponentLib:
- import org.terminal21.client.components.StdElementEncoding.given
+ import org.terminal21.client.json.StdElementEncoding.given
override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
case n: NEJson => n.asJson.mapObject(o => o.add("type", "Nivo".asJson))
diff --git a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala
index 8816f500..1782eec5 100644
--- a/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala
+++ b/terminal21-nivo/src/main/scala/org/terminal21/client/components/nivo/NivoElement.scala
@@ -2,9 +2,10 @@ package org.terminal21.client.components.nivo
import org.terminal21.client.components.UiElement.HasStyle
import org.terminal21.client.components.{Keys, UiElement}
+import org.terminal21.collections.TypedMap
-sealed trait NEJson extends UiElement
-sealed trait NivoElement[A <: UiElement] extends NEJson with HasStyle[A]
+sealed trait NEJson extends UiElement
+sealed trait NivoElement extends NEJson with HasStyle
/** https://nivo.rocks/line/
*/
@@ -28,11 +29,14 @@ case class ResponsiveLine(
pointBorderColor: Map[String, String] = Map("from" -> "serieColor"),
pointLabelYOffset: Int = -12,
useMesh: Boolean = true,
- legends: Seq[Legend] = Nil
-) extends NivoElement[ResponsiveLine]:
+ legends: Seq[Legend] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends NivoElement:
+ type This = ResponsiveLine
override def withStyle(v: Map[String, Any]): ResponsiveLine = copy(style = v)
def withKey(v: String) = copy(key = v)
def withData(data: Seq[Serie]) = copy(data = data)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://nivo.rocks/bar/
*/
@@ -56,8 +60,11 @@ case class ResponsiveBar(
axisBottom: Option[Axis] = Some(Axis(legend = "y", legendOffset = 36)),
axisLeft: Option[Axis] = Some(Axis(legend = "x", legendOffset = -40)),
legends: Seq[Legend] = Nil,
- ariaLabel: String = "Chart Label"
-) extends NivoElement[ResponsiveBar]:
+ ariaLabel: String = "Chart Label",
+ dataStore: TypedMap = TypedMap.Empty
+) extends NivoElement:
+ type This = ResponsiveBar
override def withStyle(v: Map[String, Any]): ResponsiveBar = copy(style = v)
def withKey(v: String) = copy(key = v)
def withData(data: Seq[Seq[BarDatum]]) = copy(data = data)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
diff --git a/terminal21-server-app/src/main/resources/logback.xml b/terminal21-server-app/src/main/resources/logback.xml
new file mode 100644
index 00000000..17c2c6ad
--- /dev/null
+++ b/terminal21-server-app/src/main/resources/logback.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala
new file mode 100644
index 00000000..f550ca07
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Dependencies.scala
@@ -0,0 +1,8 @@
+package org.terminal21.server
+
+import functions.fibers.FiberExecutor
+import org.terminal21.serverapp.{ServerSideApp, ServerSideSessionsBeans}
+import org.terminal21.serverapp.bundled.AppManagerBeans
+
+class Dependencies(val fiberExecutor: FiberExecutor, val apps: Seq[ServerSideApp]) extends ServerBeans with ServerSideSessionsBeans with AppManagerBeans:
+ override def dependencies: Dependencies = this
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala
similarity index 80%
rename from terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala
rename to terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala
index 70e8be52..0d0e42a3 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/Terminal21Server.scala
+++ b/terminal21-server-app/src/main/scala/org/terminal21/server/Terminal21Server.scala
@@ -6,15 +6,17 @@ import io.helidon.webserver.WebServer
import io.helidon.webserver.http.HttpRouting
import org.slf4j.LoggerFactory
import org.terminal21.config.Config
+import org.terminal21.serverapp.ServerSideApp
+import org.terminal21.serverapp.bundled.DefaultApps
import java.net.InetAddress
object Terminal21Server:
- private val logger = LoggerFactory.getLogger(getClass)
- def start(port: Option[Int] = None): Unit =
+ private val logger = LoggerFactory.getLogger(getClass)
+ def start(port: Option[Int] = None, apps: Seq[ServerSideApp] = Nil, defaultApps: Seq[ServerSideApp] = DefaultApps.All): Unit =
FiberExecutor.withFiberExecutor: executor =>
val portV = port.getOrElse(Config.Default.port)
- val dependencies = new Dependencies(executor)
+ val dependencies = new Dependencies(executor, apps ++ defaultApps)
val routesBuilder = HttpRouting.builder()
Routes.register(dependencies, routesBuilder)
Routes.static(routesBuilder)
@@ -27,6 +29,7 @@ object Terminal21Server:
.build
.start
+ dependencies.appManager.start()
if !server.isRunning then throw new IllegalStateException("Server failed to start")
try
logger.info(s"Terminal 21 Server started. Please open http://localhost:$portV/ui for the user interface")
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala
new file mode 100644
index 00000000..e057414c
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideApp.scala
@@ -0,0 +1,8 @@
+package org.terminal21.serverapp
+
+import org.terminal21.server.Dependencies
+
+trait ServerSideApp:
+ def name: String
+ def description: String
+ def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala
new file mode 100644
index 00000000..5f0ed820
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/ServerSideSessions.scala
@@ -0,0 +1,49 @@
+package org.terminal21.serverapp
+
+import functions.fibers.FiberExecutor
+import org.terminal21.client.ConnectedSession
+import org.terminal21.client.components.ComponentLib
+import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding}
+import org.terminal21.config.Config
+import org.terminal21.model.SessionOptions
+import org.terminal21.server.service.ServerSessionsService
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+class ServerSideSessions(sessionsService: ServerSessionsService, executor: FiberExecutor):
+ case class ServerSideSessionBuilder(
+ id: String,
+ name: String,
+ componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding),
+ sessionOptions: SessionOptions = SessionOptions.Defaults
+ ):
+ def andLibraries(libraries: ComponentLib*): ServerSideSessionBuilder = copy(componentLibs = componentLibs ++ libraries)
+ def andOptions(sessionOptions: SessionOptions) = copy(sessionOptions = sessionOptions)
+
+ def connect[R](f: ConnectedSession => R): R =
+ val config = Config.Default
+ val serverUrl = s"http://${config.host}:${config.port}"
+
+ val session = sessionsService.createSession(id, name, sessionOptions)
+ val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs)
+ val isStopped = new AtomicBoolean(false)
+
+ def terminate(): Unit =
+ isStopped.set(true)
+
+ val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate)
+ sessionsService.notifyMeOnSessionEvents(session): event =>
+ executor.submit:
+ connectedSession.fireEvent(event)
+ true
+ try
+ f(connectedSession)
+ finally
+ if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session)
+
+ def withNewSession(id: String, name: String): ServerSideSessionBuilder = ServerSideSessionBuilder(id, name)
+
+trait ServerSideSessionsBeans:
+ def sessionsService: ServerSessionsService
+ def fiberExecutor: FiberExecutor
+ lazy val serverSideSessions = new ServerSideSessions(sessionsService, fiberExecutor)
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala
new file mode 100644
index 00000000..75ab80bf
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/AppManager.scala
@@ -0,0 +1,86 @@
+package org.terminal21.serverapp.bundled
+
+import functions.fibers.FiberExecutor
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.std.{Header1, Paragraph, Span}
+import org.terminal21.model.SessionOptions
+import org.terminal21.server.Dependencies
+import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions}
+
+class AppManager(serverSideSessions: ServerSideSessions, fiberExecutor: FiberExecutor, apps: Seq[ServerSideApp], dependencies: Dependencies):
+ def start(): Unit =
+ fiberExecutor.submit:
+ serverSideSessions
+ .withNewSession("app-manager", "Terminal 21")
+ .andOptions(SessionOptions(alwaysOpen = true))
+ .connect: session =>
+ given ConnectedSession = session
+ new AppManagerPage(apps, startApp).run()
+
+ private def startApp(app: ServerSideApp): Unit =
+ fiberExecutor.submit:
+ app.createSession(serverSideSessions, dependencies)
+
+class AppManagerPage(apps: Seq[ServerSideApp], startApp: ServerSideApp => Unit)(using session: ConnectedSession):
+ case class ManagerModel(startApp: Option[ServerSideApp] = None)
+
+ def run(): Unit =
+ eventsIterator.foreach(_ => ())
+
+ private case class TableView(clicked: Option[ServerSideApp], columns: Seq[UiElement])
+ private def appRows(events: Events): Seq[TableView] = apps.map: app =>
+ val link = Link(s"app-${app.name}", text = app.name)
+ TableView(
+ if events.isClicked(link) then Some(app) else None,
+ Seq(
+ link,
+ Text(text = app.description)
+ )
+ )
+
+ def components(model: ManagerModel, events: Events): MV[ManagerModel] =
+ val appsMv = appRows(events)
+ val appsTable = QuickTable(
+ key = "apps-table",
+ caption = Some("Apps installed on the server, click one to run it."),
+ rows = appsMv.map(tv => tv.columns)
+ ).withHeaders("App Name", "Description")
+ val startApp = appsMv.map(_.clicked).find(_.nonEmpty).flatten
+ MV(
+ model.copy(startApp = startApp),
+ Seq(
+ Header1(text = "Terminal 21 Manager"),
+ Paragraph(text = "Here you can run all the installed apps on the server."),
+ appsTable,
+ Paragraph().withChildren(
+ Span(text = "Have a question? Please ask at "),
+ Link(
+ key = "discussion-board-link",
+ text = "terminal21's discussion board ",
+ href = "https://github.com/kostaskougios/terminal21-restapi/discussions",
+ color = Some("teal.500"),
+ isExternal = Some(true)
+ ).withChildren(ExternalLinkIcon(mx = Some("2px")))
+ )
+ )
+ )
+
+ def controller: Controller[ManagerModel] =
+ Controller(components)
+
+ def eventsIterator: Iterator[ManagerModel] =
+ controller
+ .render(ManagerModel())
+ .iterator
+ .map(_.model)
+ .tapEach: m =>
+ for app <- m.startApp do startApp(app)
+
+trait AppManagerBeans:
+ def serverSideSessions: ServerSideSessions
+ def fiberExecutor: FiberExecutor
+ def apps: Seq[ServerSideApp]
+ def dependencies: Dependencies
+ lazy val appManager = new AppManager(serverSideSessions, fiberExecutor, apps, dependencies)
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala
new file mode 100644
index 00000000..101c49ba
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/DefaultApps.scala
@@ -0,0 +1,7 @@
+package org.terminal21.serverapp.bundled
+
+object DefaultApps:
+ val All = Seq(
+ new ServerStatusApp,
+ new SettingsApp
+ )
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala
new file mode 100644
index 00000000..89b2ab26
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/ServerStatusApp.scala
@@ -0,0 +1,149 @@
+package org.terminal21.serverapp.bundled
+
+import functions.fibers.FiberExecutor
+import io.circe.Json
+import org.terminal21.client.*
+import org.terminal21.client.components.*
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.std.Paragraph
+import org.terminal21.model.{ClientEvent, Session}
+import org.terminal21.server.Dependencies
+import org.terminal21.server.model.SessionState
+import org.terminal21.server.service.ServerSessionsService
+import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions}
+
+class ServerStatusApp extends ServerSideApp:
+ override def name: String = "Server Status"
+ override def description: String = "Status of the server."
+
+ override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit =
+ serverSideSessions
+ .withNewSession("server-status", "Server Status")
+ .connect: session =>
+ new ServerStatusPage(serverSideSessions, dependencies.sessionsService)(using session, dependencies.fiberExecutor).run()
+
+class ServerStatusPage(
+ serverSideSessions: ServerSideSessions,
+ sessionsService: ServerSessionsService
+)(using appSession: ConnectedSession, fiberExecutor: FiberExecutor):
+ case class StatusModel(runtime: Runtime, sessions: Seq[Session])
+ val initModel = StatusModel(Runtime.getRuntime, sessionsService.allSessions)
+
+ case class Ticker(sessions: Seq[Session]) extends ClientEvent
+
+ def run(): Unit =
+ fiberExecutor.submit:
+ while !appSession.isClosed do
+ Thread.sleep(2000)
+ appSession.fireEvents(Ticker(sessionsService.allSessions))
+
+ try controller.render(initModel).iterator.lastOption
+ catch case t: Throwable => t.printStackTrace()
+
+ private def toMb(v: Long) = s"${v / (1024 * 1024)} MB"
+ private val xs = Some("2xs")
+
+ def controller: Controller[StatusModel] =
+ Controller(components)
+
+ def components(model: StatusModel, events: Events): MV[StatusModel] =
+ val newModel = events.event match
+ case Ticker(sessions) => model.copy(sessions = sessions)
+ case _ => model
+
+ MV(
+ newModel,
+ Box().withChildren(
+ jvmTable(newModel.runtime, events),
+ sessionsTable(newModel.sessions, events)
+ )
+ )
+
+ private val jvmTableE = QuickTable(key = "jvmTable", caption = Some("JVM"))
+ .withHeaders("Property", "Value", "Actions")
+ private val gcButton = Button(key = "gc-button", size = xs, text = "Run GC")
+
+ def jvmTable(runtime: Runtime, events: Events): UiElement =
+ if events.isClicked(gcButton) then System.gc()
+ jvmTableE.withRows(
+ Seq(
+ Seq("Free Memory", toMb(runtime.freeMemory()), ""),
+ Seq("Max Memory", toMb(runtime.maxMemory()), ""),
+ Seq("Total Memory", toMb(runtime.totalMemory()), gcButton),
+ Seq("Available processors", runtime.availableProcessors(), "")
+ )
+ )
+
+ private val sessionsTableE =
+ QuickTable(
+ key = "sessions-table",
+ caption = Some("All sessions")
+ ).withHeaders("Id", "Name", "Is Open", "Actions")
+
+ def sessionsTable(sessions: Seq[Session], events: Events): UiElement =
+ sessionsTableE.withRows(
+ sessions.map: session =>
+ Seq(Text(text = session.id), Text(text = session.name), if session.isOpen then CheckIcon() else NotAllowedIcon(), actionsFor(session, events))
+ )
+
+ private def actionsFor(session: Session, events: Events): UiElement =
+ if session.isOpen then
+ val closeButton = Button(key = s"close-${session.id}", text = "Close", size = xs)
+ .withLeftIcon(SmallCloseIcon())
+ if events.isClicked(closeButton) then sessionsService.terminateAndRemove(session)
+ val viewButton = Button(key = s"view-${session.id}", text = "View State", size = xs)
+ .withLeftIcon(ChatIcon())
+ if events.isClicked(viewButton) then
+ serverSideSessions
+ .withNewSession(session.id + "-server-state", s"Server State:${session.id}")
+ .connect: sSession =>
+ new ViewServerStatePage(using sSession).runFor(sessionsService.sessionStateOf(session))
+
+ Box().withChildren(
+ closeButton,
+ Text(text = " "),
+ viewButton
+ )
+ else NotAllowedIcon()
+
+class ViewServerStatePage(using session: ConnectedSession):
+
+ private val firstStyle = Map("margin" -> "24px")
+ def jsonToTable(depth: Int, j: Json): UiElement =
+ j.fold(
+ jsonNull = Text(text = "null"),
+ jsonBoolean = b => Text(text = b.toString),
+ jsonNumber = n => Text(text = n.toString),
+ jsonString = s => Text(text = s),
+ jsonArray = arr => if arr.isEmpty then Text(text = "") else Box().withChildren(arr.map(a => jsonToTable(depth + 1, a))*),
+ jsonObject = o => {
+ val keyValues = o.toList
+ if keyValues.isEmpty then Text(text = "")
+ else
+ Table(style = if depth == 1 then firstStyle else Map.empty).withChildren(
+ Tbody(children = keyValues.map: (k, v) =>
+ Tr()
+ .withBg("blackAlpha.500")
+ .withChildren(
+ Td(text = k),
+ Td().withChildren(jsonToTable(depth + 1, v))
+ ))
+ )
+ }
+ )
+
+ def runFor(state: SessionState): Unit =
+ val sj = state.serverJson
+
+ val components = Seq(
+ QuickTabs()
+ .withTabs("Hierarchy", "Json")
+ .withTabPanelsSimple(
+ Box().withChildren(sj.elements.map(j => jsonToTable(1, j))*),
+ Paragraph().withChildren(
+ sj.elements.map(e => Text(text = e.toString))*
+ )
+ )
+ )
+ session.render(components)
+ session.leaveSessionOpenAfterExiting()
diff --git a/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala
new file mode 100644
index 00000000..124022c3
--- /dev/null
+++ b/terminal21-server-app/src/main/scala/org/terminal21/serverapp/bundled/SettingsApp.scala
@@ -0,0 +1,28 @@
+package org.terminal21.serverapp.bundled
+
+import org.terminal21.client.components.*
+import org.terminal21.client.components.frontend.ThemeToggle
+import org.terminal21.client.*
+import org.terminal21.server.Dependencies
+import org.terminal21.serverapp.{ServerSideApp, ServerSideSessions}
+
+class SettingsApp extends ServerSideApp:
+ override def name = "Settings"
+
+ override def description = "Terminal21 Settings"
+
+ override def createSession(serverSideSessions: ServerSideSessions, dependencies: Dependencies): Unit =
+ serverSideSessions
+ .withNewSession("frontend-settings", "Settings")
+ .connect: session =>
+ given ConnectedSession = session
+ new SettingsPage().run()
+
+class SettingsPage(using session: ConnectedSession):
+ val themeToggle = ThemeToggle()
+ def run() =
+ controller.render(()).iterator.lastOption
+
+ def components = Seq(themeToggle)
+
+ def controller = Controller.noModel(components)
diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala
new file mode 100644
index 00000000..5db136d1
--- /dev/null
+++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/ServerSideSessionsTest.scala
@@ -0,0 +1,52 @@
+package org.terminal21.serverapp
+
+import functions.fibers.FiberExecutor
+import org.mockito.Mockito
+import org.mockito.Mockito.{verify, when}
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatestplus.mockito.MockitoSugar.*
+import org.terminal21.model.{CommonModelBuilders, SessionOptions}
+import org.terminal21.model.CommonModelBuilders.session
+import org.terminal21.server.service.ServerSessionsService
+
+class ServerSideSessionsTest extends AnyFunSuiteLike with BeforeAndAfterAll:
+ val executor = FiberExecutor()
+ override protected def afterAll(): Unit = executor.shutdown()
+
+ test("creates session"):
+ new App:
+ val s = session()
+ when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s)
+ serverSideSessions
+ .withNewSession(s.id, s.name)
+ .connect: session =>
+ session.leaveSessionOpenAfterExiting()
+
+ verify(sessionsService).createSession(s.id, s.name, SessionOptions.Defaults)
+
+ test("terminates session before exiting"):
+ new App:
+ val s = session()
+ when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s)
+ serverSideSessions
+ .withNewSession(s.id, s.name)
+ .connect: _ =>
+ ()
+
+ verify(sessionsService).terminateSession(s)
+
+ test("registers to receive events"):
+ new App:
+ val s = session()
+ when(sessionsService.createSession(s.id, s.name, SessionOptions.Defaults)).thenReturn(s)
+ serverSideSessions
+ .withNewSession(s.id, s.name)
+ .connect: _ =>
+ ()
+
+ verify(sessionsService).notifyMeOnSessionEvents(s)
+
+ class App:
+ val sessionsService = mock[ServerSessionsService]
+ val serverSideSessions = new ServerSideSessions(sessionsService, executor)
diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala
new file mode 100644
index 00000000..70ea67bb
--- /dev/null
+++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/AppManagerPageTest.scala
@@ -0,0 +1,63 @@
+//package org.terminal21.serverapp.bundled
+//
+//import org.mockito.Mockito
+//import org.mockito.Mockito.when
+//import org.scalatest.funsuite.AnyFunSuiteLike
+//import org.scalatestplus.mockito.MockitoSugar.mock
+//import org.terminal21.client.components.*
+//import org.terminal21.client.components.chakra.{Link, Text}
+//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock}
+//import org.terminal21.serverapp.ServerSideApp
+//import org.scalatest.matchers.should.Matchers.*
+//import org.terminal21.model.CommandEvent
+//
+//class AppManagerPageTest extends AnyFunSuiteLike:
+// def mockApp(name: String, description: String) =
+// val app = mock[ServerSideApp]
+// when(app.name).thenReturn(name)
+// when(app.description).thenReturn(description)
+// app
+//
+// class App(apps: ServerSideApp*):
+// given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+// var startedApp: Option[ServerSideApp] = None
+// val page = new AppManagerPage(apps, app => startedApp = Some(app))
+// val model = page.ManagerModel()
+// def allComponents = page.components.flatMap(_.flat)
+//
+// test("renders app links"):
+// new App(mockApp("app1", "the-app1-desc")):
+// allComponents
+// .collect:
+// case l: Link if l.text == "app1" => l
+// .size should be(1)
+//
+// test("renders app description"):
+// new App(mockApp("app1", "the-app1-desc")):
+// allComponents
+// .collect:
+// case t: Text if t.text == "the-app1-desc" => t
+// .size should be(1)
+//
+// test("renders the discussions link"):
+// new App():
+// allComponents
+// .collect:
+// case l: Link if l.href == "https://github.com/kostaskougios/terminal21-restapi/discussions" => l
+// .size should be(1)
+//
+// test("starts app when app link is clicked"):
+// val app = mockApp("app1", "the-app1-desc")
+// new App(app):
+// val eventsIt = page.eventsIterator
+// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.sessionClosed)
+// eventsIt.toList
+// startedApp should be(Some(app))
+//
+// test("resets startApp state on other events"):
+// val app = mockApp("app1", "the-app1-desc")
+// new App(app):
+// val other = allComponents.find(_.key == "discussion-board-link").get
+// val eventsIt = page.controller.render().handledEventsIterator
+// session.fireEvents(CommandEvent.onClick(page.appRows.head.head), CommandEvent.onClick(other), CommandEvent.sessionClosed)
+// eventsIt.toList.map(_.model).last.startApp should be(None)
diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala
new file mode 100644
index 00000000..6a6bd0f3
--- /dev/null
+++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/ServerStatusPageTest.scala
@@ -0,0 +1,69 @@
+//package org.terminal21.serverapp.bundled
+//
+//import org.mockito.Mockito.when
+//import org.scalatest.funsuite.AnyFunSuiteLike
+//import org.scalatest.matchers.should.Matchers.*
+//import org.scalatestplus.mockito.MockitoSugar.mock
+//import org.terminal21.client.components.chakra.{Button, CheckIcon, NotAllowedIcon}
+//import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given}
+//import org.terminal21.model.CommonModelBuilders.session
+//import org.terminal21.model.{CommandEvent, CommonModelBuilders, Session}
+//import org.terminal21.server.service.ServerSessionsService
+//import org.terminal21.serverapp.ServerSideSessions
+//
+//class ServerStatusPageTest extends AnyFunSuiteLike:
+// class App:
+// given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+// val sessionsService = mock[ServerSessionsService]
+// val serverSideSessions = mock[ServerSideSessions]
+// val session1 = session(id = "session1")
+// when(sessionsService.allSessions).thenReturn(Seq(session1))
+// val page = new ServerStatusPage(serverSideSessions, sessionsService)
+//
+// test("Close button for a session"):
+// new App:
+// page.sessionsTable.flat
+// .collectFirst:
+// case b: Button if b.text == "Close" => b
+// .isEmpty should be(false)
+//
+// test("View state button for a session"):
+// new App:
+// page.sessionsTable.flat
+// .collectFirst:
+// case b: Button if b.text == "View State" => b
+// .isEmpty should be(false)
+//
+// test("When session is open, a CheckIcon is displayed"):
+// new App:
+// page.sessionsTable.flat
+// .collectFirst:
+// case i: CheckIcon => i
+// .isEmpty should be(false)
+//
+// test("When session is closed, a NotAllowedIcon is displayed"):
+// new App:
+// import page.given
+// val table = page.sessionsTable
+// val m = page.initModel.copy(sessions = Seq(session(isOpen = false)))
+// table
+// .fireModelChangeRender(m)
+// .flat
+// .collectFirst:
+// case i: NotAllowedIcon => i
+// .isEmpty should be(false)
+//
+// test("sessions are rendered when Ticker event is fired"):
+// new App:
+// val it = page.controller.render().handledEventsIterator
+// private val sessions2 = Seq(session(id = "s2", name = "session 2"))
+// private val sessions3 = Seq(session(id = "s3", name = "session 3"))
+// connectedSession.fireEvents(
+// page.Ticker(sessions2),
+// page.Ticker(sessions3),
+// CommandEvent.sessionClosed
+// )
+// val handledEvents = it.toList
+// handledEvents.head.model.sessions should be(Seq(session(id = "session1")))
+// handledEvents(1).model.sessions should be(sessions2)
+// handledEvents(2).model.sessions should be(sessions3)
diff --git a/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala
new file mode 100644
index 00000000..5a88f916
--- /dev/null
+++ b/terminal21-server-app/src/test/scala/org/terminal21/serverapp/bundled/SettingsPageTest.scala
@@ -0,0 +1,14 @@
+package org.terminal21.serverapp.bundled
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.{*, given}
+
+class SettingsPageTest extends AnyFunSuiteLike:
+ class App:
+ given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+ val page = new SettingsPage
+
+ test("Should render the ThemeToggle component"):
+ new App:
+ page.components should contain(page.themeToggle)
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala
new file mode 100644
index 00000000..3a1fe334
--- /dev/null
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/client/components/AnyElement.scala
@@ -0,0 +1,6 @@
+package org.terminal21.client.components
+
+/** Base trait for any renderable element that has a key
+ */
+trait AnyElement:
+ def key: String
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala
new file mode 100644
index 00000000..a3830e1e
--- /dev/null
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/SEList.scala
@@ -0,0 +1,76 @@
+package org.terminal21.collections
+
+import java.util.concurrent.CountDownLatch
+
+class SEList[A]:
+ @volatile private var currentNode: NormalNode[A] = NormalNode(None, EndNode)
+
+ private val atLeastOneIterator = new CountDownLatch(1)
+
+ /** @return
+ * A new iterator that only reads elements that are added before the iterator is created.
+ */
+ def iterator: SEBlockingIterator[A] =
+ atLeastOneIterator.countDown()
+ new SEBlockingIterator(currentNode)
+
+ def waitUntilAtLeast1IteratorWasCreated(): Unit = atLeastOneIterator.await()
+
+ /** Add a poison pill to terminate all iterators.
+ */
+ def poisonPill(): Unit =
+ synchronized:
+ currentNode.valueAndNext = (None, PoisonPillNode)
+ currentNode.latch.countDown()
+
+ /** Adds an item that will be visible to all iterators that were created before this item was added.
+ * @param item
+ * the item
+ */
+ def add(item: A): Unit =
+ val cn = synchronized:
+ val cn = currentNode
+ if cn.valueAndNext._2 == PoisonPillNode then throw new IllegalStateException("Can't add items when the list has been poisoned.")
+ val n = NormalNode(None, currentNode.valueAndNext._2)
+ currentNode.valueAndNext = (Some(item), n)
+ currentNode = n
+ cn
+ cn.latch.countDown()
+
+class SEBlockingIterator[A](@volatile var currentNode: NormalNode[A]) extends Iterator[A]:
+ /** @return
+ * true if hasNext & next() will return immediately with the next value. This won't block.
+ */
+ def isNextAvailable: Boolean = currentNode.hasValue
+
+ /** @return
+ * true if there is a next() but blocks otherwise till next() becomes available or we are at the end of the iterator.
+ */
+ override def hasNext: Boolean =
+ currentNode.waitValue()
+ val v = currentNode.valueAndNext._2
+ if v == PoisonPillNode then false else true
+
+ /** @return
+ * the next element or blocks until the next element becomes available
+ */
+ override def next(): A =
+ if hasNext then
+ val v = currentNode.value
+ currentNode = currentNode.next
+ v
+ else throw new NoSuchElementException("next() called but there is no next element. The SEList has been poisoned and we reached the PoisonPill")
+
+sealed trait Node[+A]
+case object EndNode extends Node[Nothing]
+case object PoisonPillNode extends Node[Nothing]
+
+case class NormalNode[A](@volatile var valueAndNext: (Option[A], Node[A])) extends Node[A]:
+ val latch = new CountDownLatch(1)
+ def waitValue(): Unit = latch.await()
+
+ def hasValue: Boolean = valueAndNext._1.nonEmpty
+ def value: A = valueAndNext._1.get
+ def next: NormalNode[A] = valueAndNext._2 match
+ case nn: NormalNode[A] @unchecked => nn
+ case _ => throw new NoSuchElementException("next should be called only if hasValue is true")
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala
new file mode 100644
index 00000000..52f05908
--- /dev/null
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/collections/TypedMap.scala
@@ -0,0 +1,31 @@
+package org.terminal21.collections
+
+import scala.reflect.{ClassTag, classTag}
+
+type TMMap = Map[TypedMapKey[_], Any]
+
+class TypedMap(protected val m: TMMap):
+ def +[A](kv: (TypedMapKey[A], A)): TypedMap = new TypedMap(m + kv)
+ def apply[A](k: TypedMapKey[A]): A = m(k).asInstanceOf[A]
+ def get[A](k: TypedMapKey[A]): Option[A] = m.get(k).asInstanceOf[Option[A]]
+ def getOrElse[A](k: TypedMapKey[A], default: => A) = m.getOrElse(k, default).asInstanceOf[A]
+ def keys: Iterable[TypedMapKey[_]] = m.keys
+
+ override def hashCode() = m.hashCode()
+ override def equals(obj: Any) = obj match
+ case tm: TypedMap => m == tm.m
+ case _ => false
+
+ def contains[A](k: TypedMapKey[A]) = m.contains(k)
+ override def toString = s"TypedMap(${m.keys.mkString(", ")})"
+
+object TypedMap:
+ val Empty = new TypedMap(Map.empty)
+ def apply(kv: (TypedMapKey[_], Any)*) =
+ val m = Map(kv*)
+ new TypedMap(m)
+
+trait TypedMapKey[A: ClassTag]:
+ type Of = A
+
+ override def toString = s"${getClass.getSimpleName}[${classTag[A].runtimeClass.getSimpleName}]"
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala
index 216d6fe2..55590a8f 100644
--- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/CommandEvent.scala
@@ -1,9 +1,46 @@
package org.terminal21.model
+import io.circe.*
+import io.circe.generic.auto.*
+import io.circe.syntax.*
+import org.terminal21.client.components.AnyElement
+
+/** These are the events as they arrive from the server
+ */
sealed trait CommandEvent:
def key: String
+ def isSessionClosed: Boolean
+
+object CommandEvent:
+ def onClick(receivedBy: AnyElement): OnClick = OnClick(receivedBy.key)
+ def onChange(receivedBy: AnyElement, value: String): OnChange = OnChange(receivedBy.key, value)
+ def onChange(receivedBy: AnyElement, value: Boolean): OnChange = OnChange(receivedBy.key, value.toString)
+ def sessionClosed: SessionClosed = SessionClosed("-")
+
+ given Encoder[CommandEvent] =
+ case c: OnClick => c.asJson.mapObject(_.add("type", "OnClick".asJson))
+ case c: OnChange => c.asJson.mapObject(_.add("type", "OnChange".asJson))
+ case sc: SessionClosed => sc.asJson.mapObject(_.add("type", "SessionClosed".asJson))
+ case x => throw new IllegalStateException(s"$x should never be send as json")
+
+ given Decoder[CommandEvent] = o =>
+ o.get[String]("type") match
+ case Right("OnClick") => o.as[OnClick]
+ case Right("OnChange") => o.as[OnChange]
+ case Right("SessionClosed") => o.as[SessionClosed]
+ case x => throw new IllegalStateException(s"got unexpected $x")
+
+case class OnClick(key: String) extends CommandEvent:
+ override def isSessionClosed: Boolean = false
+
+case class OnChange(key: String, value: String) extends CommandEvent:
+ override def isSessionClosed: Boolean = false
-case class OnClick(key: String) extends CommandEvent
-case class OnChange(key: String, value: String) extends CommandEvent
+case class SessionClosed(key: String) extends CommandEvent:
+ override def isSessionClosed: Boolean = true
-case class SessionClosed(key: String) extends CommandEvent
+/** Extend this to send your own messages
+ */
+trait ClientEvent extends CommandEvent:
+ override def key = "client-event"
+ override def isSessionClosed: Boolean = false
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala
index 1165974e..dd05660d 100644
--- a/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/Session.scala
@@ -1,5 +1,5 @@
package org.terminal21.model
-case class Session(id: String, name: String, secret: String, isOpen: Boolean):
+case class Session(id: String, name: String, secret: String, isOpen: Boolean, options: SessionOptions):
def hideSecret: Session = copy(secret = "***")
- def close: Session = copy(isOpen=false)
+ def close: Session = copy(isOpen = false)
diff --git a/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala
new file mode 100644
index 00000000..fdba9954
--- /dev/null
+++ b/terminal21-server-client-common/src/main/scala/org/terminal21/model/SessionOptions.scala
@@ -0,0 +1,7 @@
+package org.terminal21.model
+
+case class SessionOptions(closeTabWhenTerminated: Boolean = true, alwaysOpen: Boolean = false)
+
+object SessionOptions:
+ val Defaults = SessionOptions()
+ val LeaveOpenWhenTerminated = SessionOptions(closeTabWhenTerminated = false)
diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala
new file mode 100644
index 00000000..85236f71
--- /dev/null
+++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/SEListTest.scala
@@ -0,0 +1,91 @@
+package org.terminal21.collections
+
+import functions.fibers.FiberExecutor
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+
+class SEListTest extends AnyFunSuiteLike:
+ val executor = FiberExecutor()
+
+ test("when empty, it.hasNext should wait"):
+ val l = SEList[Int]()
+ val it = l.iterator
+ l.poisonPill()
+ it.toList should be(Nil)
+
+ test("when empty with 2 iterators"):
+ val l = SEList[Int]()
+ val it1 = l.iterator
+ val it2 = l.iterator
+ l.poisonPill()
+ it1.toList should be(Nil)
+ it2.toList should be(Nil)
+
+ test("with 1 item"):
+ val l = SEList[Int]()
+ val it = l.iterator
+ l.add(1)
+ l.poisonPill()
+ it.toList should be(List(1))
+
+ test("with 2 items"):
+ val l = SEList[Int]()
+ val it = l.iterator
+ l.add(1)
+ l.add(2)
+ l.poisonPill()
+ it.toList should be(List(1, 2))
+
+ test("with 2 items and 2 iterators"):
+ val l = SEList[Int]()
+ val it1 = l.iterator
+ val it2 = l.iterator
+ l.add(1)
+ l.add(2)
+ l.poisonPill()
+ it1.toList should be(List(1, 2))
+ it2.toList should be(List(1, 2))
+
+ test("iterator after added items"):
+ val l = SEList[Int]()
+ l.add(1)
+ val it = l.iterator
+ l.add(2)
+ l.poisonPill()
+ it.toList should be(List(2))
+
+ test("hasNext & next()"):
+ val l = SEList[Int]()
+ val it = l.iterator
+ l.add(1)
+ l.add(2)
+ l.poisonPill()
+ it.hasNext should be(true)
+ it.next() should be(1)
+ it.hasNext should be(true)
+ it.next() should be(2)
+ it.hasNext should be(false)
+ an[NoSuchElementException] should be thrownBy (it.next())
+
+ test("it.isNextAvailable"):
+ val l = SEList[Int]()
+ val it = l.iterator
+ it.isNextAvailable should be(false)
+ l.add(1)
+ it.isNextAvailable should be(true)
+
+ test("multiple iterators and multi threading"):
+ val l = SEList[Int]()
+ val iterators = for _ <- 1 to 1000 yield
+ val it = l.iterator
+ executor.submit:
+ it.toList
+
+ for i <- 1 to 100 do
+ Thread.sleep(1)
+ l.add(i)
+
+ l.poisonPill()
+
+ val expected = (1 to 100).toList
+ for f <- iterators do f.get() should be(expected)
diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala
new file mode 100644
index 00000000..504427c8
--- /dev/null
+++ b/terminal21-server-client-common/src/test/scala/org/terminal21/collections/TypedMapTest.scala
@@ -0,0 +1,62 @@
+package org.terminal21.collections
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+
+class TypedMapTest extends AnyFunSuiteLike:
+ object IntKey extends TypedMapKey[Int]
+ object StringKey extends TypedMapKey[String]
+
+ test("apply"):
+ val m = TypedMap.Empty + (IntKey -> 5) + (StringKey -> "x")
+ m(IntKey) should be(5)
+ m(StringKey) should be("x")
+
+ test("construct"):
+ val m = TypedMap(IntKey -> 5, StringKey -> "x")
+ m(IntKey) should be(5)
+ m(StringKey) should be("x")
+
+ test("keys"):
+ val m = TypedMap(IntKey -> 5, StringKey -> "x")
+ m.keys.toSet should be(Set(IntKey, StringKey))
+
+ test("get"):
+ val m = TypedMap.Empty + (IntKey -> 5) + (StringKey -> "x")
+ m.get(IntKey) should be(Some(5))
+ m.get(StringKey) should be(Some("x"))
+
+ test("getOrElse when key not available"):
+ TypedMap.Empty.getOrElse(IntKey, 2) should be(2)
+
+ test("getOrElse when key available"):
+ (TypedMap.Empty + (IntKey -> 5)).getOrElse(IntKey, 2) should be(5)
+
+ test("contains key positive"):
+ (TypedMap.Empty + (IntKey -> 5)).contains(IntKey) should be(true)
+
+ test("contains key negative"):
+ TypedMap.Empty.contains(IntKey) should be(false)
+
+ test("get key negative"):
+ TypedMap.Empty.get(IntKey) should be(None)
+
+ test("equals positive"):
+ val m1 = TypedMap.Empty + (IntKey -> 5)
+ val m2 = TypedMap.Empty + (IntKey -> 5)
+ m1 should be(m2)
+
+ test("equals negative"):
+ val m1 = TypedMap.Empty + (IntKey -> 5)
+ val m2 = TypedMap.Empty + (IntKey -> 6)
+ m1 should not be m2
+
+ test("hashCode positive"):
+ val m1 = TypedMap.Empty + (IntKey -> 5)
+ val m2 = TypedMap.Empty + (IntKey -> 5)
+ m1.hashCode should be(m2.hashCode)
+
+ test("hashCode negative"):
+ val m1 = TypedMap.Empty + (IntKey -> 5)
+ val m2 = TypedMap.Empty + (IntKey -> 6)
+ m1.hashCode should not be m2.hashCode
diff --git a/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala b/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala
index a78db392..c5cd69de 100644
--- a/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala
+++ b/terminal21-server-client-common/src/test/scala/org/terminal21/model/CommonModelBuilders.scala
@@ -1,5 +1,11 @@
package org.terminal21.model
object CommonModelBuilders:
- def session(id: String = "session-id", name: String = "session-name", secret: String = "session-secret", isOpen: Boolean = true) =
- Session(id, name, secret, isOpen)
+ def session(
+ id: String = "session-id",
+ name: String = "session-name",
+ secret: String = "session-secret",
+ isOpen: Boolean = true,
+ sessionOptions: SessionOptions = SessionOptions.Defaults
+ ) =
+ Session(id, name, secret, isOpen, sessionOptions)
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala b/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala
index f0168f76..544af7d6 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala
+++ b/terminal21-server/src/main/scala/org/terminal21/server/Routes.scala
@@ -12,7 +12,7 @@ import java.nio.file.Path
object Routes:
private val logger = LoggerFactory.getLogger(getClass)
- def register(dependencies: Dependencies, rb: HttpRouting.Builder): Unit =
+ def register(dependencies: ServerBeans, rb: HttpRouting.Builder): Unit =
import dependencies.*
SessionsServiceReceiverFactory.newJsonSessionsServiceHelidonRoutes(sessionsService).routes(rb)
@@ -34,7 +34,7 @@ object Routes:
rb.register("/ui", staticContent)
rb.register("/web", publicContent)
- def ws(dependencies: Dependencies): WsRouting.Builder =
+ def ws(dependencies: ServerBeans): WsRouting.Builder =
val b = WsRouting.builder
b.endpoint("/ui/sessions", dependencies.sessionsWebSocket)
.endpoint("/api/command-ws", dependencies.commandWebSocket.commandWebSocketListener.listener)
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala b/terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala
similarity index 59%
rename from terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala
rename to terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala
index c799e449..ddb34dda 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/Dependencies.scala
+++ b/terminal21-server/src/main/scala/org/terminal21/server/ServerBeans.scala
@@ -4,4 +4,4 @@ import functions.fibers.FiberExecutor
import org.terminal21.server.service.{CommandWebSocketBeans, ServerSessionsServiceBeans}
import org.terminal21.server.ui.SessionsWebSocketBeans
-class Dependencies(val fiberExecutor: FiberExecutor) extends ServerSessionsServiceBeans with SessionsWebSocketBeans with CommandWebSocketBeans
+trait ServerBeans extends ServerSessionsServiceBeans with SessionsWebSocketBeans with CommandWebSocketBeans
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala
index d8749662..f17e6137 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala
+++ b/terminal21-server/src/main/scala/org/terminal21/server/service/ServerSessionsService.scala
@@ -31,13 +31,19 @@ class ServerSessionsService extends SessionsService:
override def terminateSession(session: Session): Unit =
val state = sessions.getOrElse(session, throw new IllegalArgumentException(s"Session ${session.id} doesn't exist"))
+ if session.options.alwaysOpen then throw new IllegalArgumentException("Can't terminate a session that should be always open")
state.eventsNotificationRegistry.notifyAll(SessionClosed("-"))
sessions -= session
sessions += session.close -> state.close
sessionChangeNotificationRegistry.notifyAll(allSessions)
+ if (session.options.closeTabWhenTerminated) removeSession(session.close)
- override def createSession(id: String, name: String): Session =
- val s = Session(id, name, UUID.randomUUID().toString, true)
+ def terminateAndRemove(session: Session): Unit =
+ terminateSession(session)
+ removeSession(session.close)
+
+ override def createSession(id: String, name: String, sessionOptions: SessionOptions): Session =
+ val s = Session(id, name, UUID.randomUUID().toString, true, sessionOptions)
logger.info(s"Creating session $s")
sessions.keys.toList.foreach(s => if s.id == id then sessions.remove(s))
val state = SessionState(ServerJson.Empty, new NotificationRegistry)
@@ -56,14 +62,15 @@ class ServerSessionsService extends SessionsService:
val newV = oldV.withNewState(newStateJson)
sessions += session -> newV
sessionStateChangeNotificationRegistry.notifyAll((session, newV, None))
- logger.info(s"Session $session new state $newStateJson")
+ logger.debug(s"Session $session new state $newStateJson")
override def changeSessionJsonState(session: Session, change: ServerJson): Unit =
- val oldV = sessions(session)
- val newV = oldV.withNewState(oldV.serverJson.include(change))
- sessions += session -> newV
- sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change)))
- logger.info(s"Session $session change $change")
+ ???
+// val oldV = sessions(session)
+// val newV = oldV.withNewState(oldV.serverJson.include(change))
+// sessions += session -> newV
+// sessionStateChangeNotificationRegistry.notifyAll((session, newV, Some(change)))
+// logger.debug(s"Session $session change $change")
def triggerUiEvent(event: UiEvent): Unit =
val e = event match
@@ -79,4 +86,4 @@ class ServerSessionsService extends SessionsService:
state.eventsNotificationRegistry.add(listener)
trait ServerSessionsServiceBeans:
- val sessionsService: ServerSessionsService = new ServerSessionsService
+ lazy val sessionsService: ServerSessionsService = new ServerSessionsService
diff --git a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala
index e78a3544..0a7507dd 100644
--- a/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala
+++ b/terminal21-server/src/main/scala/org/terminal21/server/ui/SessionsWebSocket.scala
@@ -30,37 +30,37 @@ class SessionsWebSocket(sessionsService: ServerSessionsService) extends WsListen
private def sendSessionState(wsSession: WsSession, session: Session, sessionState: SessionState): Unit =
val response = StateWsResponse(session.hideSecret, sessionState.serverJson).asJson.noSpaces
- logger.info(s"$wsSession: Sending session state response $response")
+ logger.debug(s"$wsSession: Sending session state response $response")
wsSession.send(response, true)
private def sendSessionStateChange(wsSession: WsSession, session: Session, change: ServerJson): Unit =
val response = StateChangeWsResponse(session.hideSecret, change).asJson.noSpaces
- logger.info(s"$wsSession: Sending session change state response $response")
+ logger.debug(s"$wsSession: Sending session change state response $response")
wsSession.send(response, true)
private def sendSessions(wsSession: WsSession, allSessions: Seq[Session]): Unit =
val sessions = allSessions.map(_.hideSecret).sortBy(_.name)
val json = SessionsWsResponse(sessions).asJson.noSpaces
- logger.info(s"$wsSession: Sending sessions $json")
+ logger.debug(s"$wsSession: Sending sessions $json")
wsSession.send(json, true)
override def onMessage(wsSession: WsSession, text: String, last: Boolean): Unit =
- logger.info(s"$wsSession: Received json: $text , last = $last")
+ logger.debug(s"$wsSession: Received json: $text , last = $last")
errorLogger.logErrors:
WsRequest.decoder(text) match
case Right(WsRequest("sessions", None)) =>
continuouslyRespond(wsSession)
- logger.info(s"$wsSession: sessions processed successfully")
+ logger.debug(s"$wsSession: sessions processed successfully")
case Right(WsRequest("session-full-refresh", Some(SessionFullRefresh(sessionId)))) =>
logger.info(s"$wsSession: session-full-refresh requested, sending full session data for $sessionId")
val session = sessionsService.sessionById(sessionId)
val sessionState = sessionsService.sessionStateOf(session)
sendSessionState(wsSession, session, sessionState)
case Right(WsRequest(eventName, Some(event: UiEvent))) =>
- logger.info(s"$wsSession: Received event $eventName = $event")
+ logger.debug(s"$wsSession: Received event $eventName = $event")
sessionsService.triggerUiEvent(event)
case Right(WsRequest("ping", None)) =>
- logger.info(s"$wsSession: ping received")
+ logger.debug(s"$wsSession: ping received")
case Right(WsRequest("close-session", Some(CloseSession(sessionId)))) =>
val session = sessionsService.sessionById(sessionId)
sessionsService.terminateSession(session)
diff --git a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala
index ddd96db5..201327c7 100644
--- a/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala
+++ b/terminal21-server/src/test/scala/org/terminal21/server/service/ServerSessionsServiceTest.scala
@@ -1,29 +1,28 @@
package org.terminal21.server.service
-import io.circe.Json
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers.*
-import org.terminal21.model.{OnChange, OnClick, SessionClosed}
+import org.terminal21.model.{OnChange, OnClick, SessionClosed, SessionOptions}
import org.terminal21.server.json
import org.terminal21.ui.std.StdExportsBuilders.serverJson
class ServerSessionsServiceTest extends AnyFunSuiteLike:
test("sessionById"):
new App:
- val session = createSession
+ val session = createSession()
serverSessionsService.setSessionJsonState(session, serverJson())
serverSessionsService.sessionById(session.id) should be(session)
test("sessionStateOf"):
new App:
- val session = createSession
+ val session = createSession()
val sj = serverJson()
serverSessionsService.setSessionJsonState(session, sj)
serverSessionsService.sessionStateOf(session).serverJson should be(sj)
test("removeSession"):
new App:
- val session = createSession
+ val session = createSession()
serverSessionsService.setSessionJsonState(session, serverJson())
serverSessionsService.removeSession(session)
an[IllegalArgumentException] should be thrownBy:
@@ -31,7 +30,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
test("removeSession notifies listeners"):
new App:
- val session = createSession
+ val session = createSession()
serverSessionsService.setSessionJsonState(session, serverJson())
var listenerCalled = 0
serverSessionsService.notifyMeWhenSessionsChange: sessions =>
@@ -45,14 +44,28 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
test("terminateSession marks session as closed"):
new App:
- val session = createSession
+ val session = createSession(SessionOptions.LeaveOpenWhenTerminated)
serverSessionsService.setSessionJsonState(session, serverJson())
serverSessionsService.terminateSession(session)
serverSessionsService.sessionById(session.id).isOpen should be(false)
+ test("terminateSession doesn't terminate a session that should always be open"):
+ new App:
+ val session = createSession(SessionOptions(alwaysOpen = true))
+ serverSessionsService.setSessionJsonState(session, serverJson())
+ an[IllegalArgumentException] should be thrownBy:
+ serverSessionsService.terminateSession(session)
+
+ test("terminateSession removes session if marked to be deleted when terminated"):
+ new App:
+ val session = createSession(SessionOptions(closeTabWhenTerminated = true))
+ serverSessionsService.setSessionJsonState(session, serverJson())
+ serverSessionsService.terminateSession(session)
+ serverSessionsService.allSessions should be(Nil)
+
test("terminateSession notifies session listeners"):
new App:
- val session = createSession
+ val session = createSession()
serverSessionsService.setSessionJsonState(session, serverJson())
var eventCalled = false
serverSessionsService.notifyMeOnSessionEvents(session): event =>
@@ -64,17 +77,18 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
test("terminateSession notifies sessions listeners"):
new App:
- val session = createSession
+ val session = createSession()
serverSessionsService.setSessionJsonState(session, serverJson())
var listenerCalled = 0
serverSessionsService.notifyMeWhenSessionsChange: sessions =>
listenerCalled match
case 0 => sessions should be(Seq(session))
case 1 => sessions should be(Seq(session.close))
+ case 2 => sessions should be(Nil)
listenerCalled += 1
true
serverSessionsService.terminateSession(session)
- listenerCalled should be(2)
+ listenerCalled should be(3)
test("createSession notifies listeners"):
new App:
@@ -86,44 +100,12 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
listenerCalled += 1
true
- createSession
+ createSession()
listenerCalled should be(2)
- test("changeSessionJsonState changes session's state"):
- new App:
- val session = createSession
- val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v")))
- serverSessionsService.setSessionJsonState(session, sj1)
- val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v")))
- serverSessionsService.changeSessionJsonState(session, sj2)
- serverSessionsService.sessionStateOf(session).serverJson should be(
- sj1.include(sj2)
- )
-
- test("changeSessionJsonState notifies listeners"):
- new App:
- val session = createSession
- val sj1 = serverJson(elements = Map("e1" -> Json.fromString("e1v")))
- serverSessionsService.setSessionJsonState(session, sj1)
- val sj2 = serverJson(elements = Map("e2" -> Json.fromString("e2v")))
- var called = 0
- serverSessionsService.notifyMeWhenSessionChanges: (s, sessionState, sjOption) =>
- called match
- case 0 =>
- s should be(session)
- sjOption should be(None)
- case 1 =>
- s should be(session)
- sjOption should be(Some(sj2))
-
- called += 1
- true
- serverSessionsService.changeSessionJsonState(session, sj2)
- called should be(2)
-
test("triggerUiEvent notifies listeners for clicks"):
new App:
- val session = createSession
+ val session = createSession()
var called = false
serverSessionsService.notifyMeOnSessionEvents(session): e =>
called = true
@@ -135,7 +117,7 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
test("triggerUiEvent notifies listeners for change"):
new App:
- val session = createSession
+ val session = createSession()
var called = false
serverSessionsService.notifyMeOnSessionEvents(session): e =>
called = true
@@ -146,5 +128,5 @@ class ServerSessionsServiceTest extends AnyFunSuiteLike:
called should be(true)
class App:
- val serverSessionsService = new ServerSessionsService
- def createSession = serverSessionsService.createSession("test", "Test")
+ val serverSessionsService = new ServerSessionsService
+ def createSession(options: SessionOptions = SessionOptions.Defaults) = serverSessionsService.createSession("test", "Test", options)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala
new file mode 100644
index 00000000..ed6d2ce4
--- /dev/null
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/Cached.scala
@@ -0,0 +1,62 @@
+package org.terminal21.sparklib
+
+import org.apache.commons.io.FileUtils
+import org.apache.spark.sql.SparkSession
+import org.terminal21.client.*
+import org.terminal21.client.components.UiElement
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.sparklib.calculations.SparkCalculation.TriggerRedraw
+import org.terminal21.sparklib.calculations.{ReadWriter, SparkCalculation}
+import org.terminal21.sparklib.util.Environment
+
+import java.io.File
+
+class Cached[OUT: ReadWriter](val name: String, outF: => OUT)(using spark: SparkSession):
+ private val rw = implicitly[ReadWriter[OUT]]
+ private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations"
+ private val targetDir = s"$rootFolder/$name"
+
+ def isCached: Boolean = new File(targetDir).exists()
+
+ def cachePath: String = targetDir
+
+ private def cache[A](reader: => A, writer: => A): A =
+ if isCached then reader
+ else writer
+
+ def invalidateCache(): Unit =
+ FileUtils.deleteDirectory(new File(targetDir))
+ out = None
+
+ private def calculateOnce: OUT =
+ cache(
+ rw.read(spark, targetDir), {
+ val ds = outF
+ rw.write(targetDir, ds)
+ ds
+ }
+ )
+
+ @volatile private var out = Option.empty[OUT]
+ private def startCalc(session: ConnectedSession): Unit =
+ fiberExecutor.submit:
+ out = Some(calculateOnce)
+ session.fireEvent(TriggerRedraw)
+
+ def get: Option[OUT] = out
+
+ def visualize(dataUi: UiElement & HasStyle)(
+ toUi: OUT => UiElement & HasStyle
+ )(using
+ SparkSession
+ )(using session: ConnectedSession, events: Events) =
+ val sc = new SparkCalculation[OUT](s"spark-calc-$name", dataUi, toUi, this)
+
+ if events.isClicked(sc.recalc) then
+ invalidateCache()
+ startCalc(session)
+ else if events.isInitialRender then startCalc(session)
+ sc
+
+object Cached:
+ def apply[OUT: ReadWriter](name: String)(outF: => OUT)(using spark: SparkSession): Cached[OUT] = new Cached(name, outF)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala
deleted file mode 100644
index 2ee3e933..00000000
--- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/CalculationsExtensions.scala
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.terminal21.sparklib
-
-import functions.fibers.FiberExecutor
-import org.apache.spark.sql.SparkSession
-import org.terminal21.client.ConnectedSession
-import org.terminal21.client.components.UiElement.HasStyle
-import org.terminal21.client.components.{Keys, UiElement}
-import org.terminal21.sparklib.calculations.{ReadWriter, StdUiSparkCalculation}
-
-extension [OUT: ReadWriter](ds: OUT)
- def visualize(name: String, dataUi: UiElement with HasStyle[_])(
- toUi: OUT => UiElement & HasStyle[_]
- )(using
- session: ConnectedSession,
- executor: FiberExecutor,
- spark: SparkSession
- ) =
- val ui = new StdUiSparkCalculation[OUT](Keys.nextKey, name, dataUi):
- override protected def whenResultsReady(results: OUT): Unit =
- try updateUi(toUi(results))
- catch case t: Throwable => t.printStackTrace()
- super.whenResultsReady(results)
- override def nonCachedCalculation: OUT = ds
-
- ui.run()
- ui
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala
index f7306ac1..26ee8ca3 100644
--- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/SparkSessions.scala
@@ -1,10 +1,6 @@
package org.terminal21.sparklib
import org.apache.spark.sql.SparkSession
-import org.terminal21.client.components.ComponentLib
-import org.terminal21.client.{ConnectedSession, Sessions}
-
-import scala.util.Using
object SparkSessions:
def newSparkSession(
@@ -20,24 +16,3 @@ object SparkSessions:
.config("spark.driver.bindAddress", bindAddress)
.config("spark.ui.enabled", sparkUiEnabled)
.getOrCreate()
-
- /** Will create a terminal21 session and use the provided spark session
- * @param spark
- * the spark session, will be closed before this call returns. Use #newSparkSession to quickly create one.
- * @param id
- * the id of the terminal21 session
- * @param name
- * the name of the terminal21 session
- * @param f
- * the code to run
- * @tparam R
- * if f returns some value, this will be returned by the method
- * @return
- * whatever f returns
- */
- def newTerminal21WithSparkSession[R](spark: SparkSession, id: String, name: String, componentLibs: ComponentLib*)(
- f: (SparkSession, ConnectedSession) => R
- ): R =
- Sessions.withNewSession(id, name, componentLibs: _*): terminal21Session =>
- Using.resource(spark): _ =>
- f(spark, terminal21Session)
diff --git a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala
index fc7ed440..dcb9582b 100644
--- a/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala
+++ b/terminal21-spark/src/main/scala/org/terminal21/sparklib/calculations/SparkCalculation.scala
@@ -1,14 +1,13 @@
package org.terminal21.sparklib.calculations
-import functions.fibers.FiberExecutor
-import org.apache.commons.io.FileUtils
import org.apache.spark.sql.SparkSession
-import org.terminal21.client.ConnectedSession
+import org.terminal21.client.components.*
import org.terminal21.client.components.UiElement.HasStyle
-import org.terminal21.client.components.{CachedCalculation, StdUiCalculation, UiComponent, UiElement}
-import org.terminal21.sparklib.util.Environment
-
-import java.io.File
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.*
+import org.terminal21.collections.TypedMap
+import org.terminal21.model.ClientEvent
+import org.terminal21.sparklib.Cached
/** A UI component that takes a spark calculation (i.e. a spark query) that results in a Dataset. It caches the results by storing them as parquet into the tmp
* folder/spark-calculations/$name. Next time the calculation runs it reads the cache if available. A button should allow the user to clear the cache and rerun
@@ -16,39 +15,45 @@ import java.io.File
*
* Because the cache is stored in the disk, it is available even if the jvm running the code restarts. This allows the user to run and rerun their code without
* having to rerun the spark calculation.
- *
- * Subclass this to create your own UI for a spark calculation, see StdUiSparkCalculation below.
*/
-trait SparkCalculation[OUT: ReadWriter](name: String)(using executor: FiberExecutor, spark: SparkSession) extends CachedCalculation[OUT] with UiComponent:
- private val rw = implicitly[ReadWriter[OUT]]
- private val rootFolder = s"${Environment.tmpDirectory}/spark-calculations"
- private val targetDir = s"$rootFolder/$name"
-
- def isCached: Boolean = new File(targetDir).exists()
- def cachePath: String = targetDir
-
- private def cache[A](reader: => A, writer: => A): A =
- if isCached then reader
- else writer
-
- override def invalidateCache(): Unit =
- FileUtils.deleteDirectory(new File(targetDir))
-
- private def calculateOnce(f: => OUT): OUT =
- cache(
- rw.read(spark, targetDir), {
- val ds = f
- rw.write(targetDir, ds)
- ds
- }
+case class SparkCalculation[OUT: ReadWriter](
+ key: String,
+ dataUi: UiElement with HasStyle,
+ toUi: OUT => UiElement & HasStyle,
+ cached: Cached[OUT],
+ dataStore: TypedMap = TypedMap.Empty
+)(using
+ spark: SparkSession,
+ session: ConnectedSession,
+ events: Events
+) extends UiComponent:
+ def name = cached.name
+ override type This = SparkCalculation[OUT]
+ override def withKey(key: String): This = copy(key = key)
+ override def withDataStore(ds: TypedMap): This = copy(dataStore = ds)
+
+ val recalc = Button(s"recalc-button-$name", text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon()))
+
+ override def rendered: Seq[UiElement] =
+ val header = Box(
+ s"recalc-box-$name",
+ bg = "green",
+ p = 4,
+ children = Seq(
+ HStack().withChildren(
+ Text(text = name),
+ if events.isClicked(recalc) then Badge(text = "Recalculating...")
+ else if events.isInitialRender then Badge(text = "Initializing...")
+ else recalc
+ )
+ )
)
+ val ui = cached.get
+ .map: ds =>
+ toUi(ds)
+ .getOrElse(dataUi)
- override protected def calculation(): OUT = calculateOnce(nonCachedCalculation)
+ Seq(header, ui)
-abstract class StdUiSparkCalculation[OUT: ReadWriter](
- val key: String,
- name: String,
- dataUi: UiElement with HasStyle[_]
-)(using session: ConnectedSession, executor: FiberExecutor, spark: SparkSession)
- extends SparkCalculation[OUT](name)
- with StdUiCalculation[OUT](name, dataUi)
+object SparkCalculation:
+ object TriggerRedraw extends ClientEvent
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala
deleted file mode 100644
index 13c7aadd..00000000
--- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/calculations/StdUiSparkCalculationTest.scala
+++ /dev/null
@@ -1,99 +0,0 @@
-package org.terminal21.sparklib.calculations
-
-import org.apache.spark.sql.{Dataset, Encoder, SparkSession}
-import org.scalatest.concurrent.Eventually
-import org.scalatest.funsuite.AnyFunSuiteLike
-import org.scalatest.matchers.should.Matchers.*
-import org.scalatest.time.{Millis, Span}
-import org.terminal21.client.components.Keys
-import org.terminal21.client.components.chakra.*
-import org.terminal21.client.{ConnectedSession, ConnectedSessionMock, given}
-import org.terminal21.sparklib.SparkSessions
-
-import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
-import scala.util.Using
-
-class StdUiSparkCalculationTest extends AnyFunSuiteLike with Eventually:
- given PatienceConfig = PatienceConfig(scaled(Span(3000, Millis)))
-
- test("calculates the correct result"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val calc = new TestingCalculation
- calc.run().get().collect().toList should be(List(2))
-
- test("whenResultsNotReady"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val called = new AtomicBoolean(false)
- val calc = new TestingCalculation:
- override protected def whenResultsNotReady(): Unit =
- called.set(true)
- calc.run().get()
- called.get() should be(true)
-
- test("whenResultsReady"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val called = new AtomicBoolean(false)
- val calc = new TestingCalculation:
- override protected def whenResultsReady(results: Dataset[Int]): Unit =
- results.collect().toList should be(List(2))
- called.set(true)
-
- calc.run()
- eventually:
- called.get() should be(true)
-
- test("whenResultsReady called even when cached"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val called = new AtomicInteger(0)
- val calc = new TestingCalculation:
- override protected def whenResultsReady(results: Dataset[Int]): Unit =
- results.collect().toList should be(List(2))
- called.incrementAndGet()
-
- calc.run().get()
- calc.run()
- eventually:
- called.get() should be(2)
-
- test("caches results"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val calc = new TestingCalculation
- calc.run().get().collect().toList should be(List(2))
- calc.run().get().collect().toList should be(List(2))
- calc.calcCalledTimes.get() should be(1)
-
- test("refresh button invalidates cache and runs calculations"):
- Using.resource(SparkSessions.newSparkSession()): spark =>
- import spark.implicits.*
- given session: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- given SparkSession = spark
- val calc = new TestingCalculation
- calc.run().get().collect().toList should be(List(2))
- session.click(calc.recalc)
- eventually:
- calc.calcCalledTimes.get() should be(2)
-
-class TestingCalculation(using session: ConnectedSession, spark: SparkSession, enc: Encoder[Int])
- extends StdUiSparkCalculation[Dataset[Int]](Keys.nextKey, "testing-calc", Box()):
- val calcCalledTimes = new AtomicInteger(0)
- invalidateCache()
-
- override def nonCachedCalculation: Dataset[Int] =
- import spark.implicits.*
- calcCalledTimes.incrementAndGet()
- Seq(2).toDS
diff --git a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala
index ec6d1a2a..db717804 100644
--- a/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala
+++ b/terminal21-spark/src/test/scala/org/terminal21/sparklib/endtoend/SparkBasics.scala
@@ -5,68 +5,87 @@ import org.apache.spark.sql.{Dataset, SparkSession}
import org.terminal21.client.components.*
import org.terminal21.client.components.chakra.*
import org.terminal21.client.components.nivo.*
-import org.terminal21.client.{*, given}
+import org.terminal21.client.*
import org.terminal21.sparklib.*
import org.terminal21.sparklib.endtoend.model.CodeFile
import org.terminal21.sparklib.endtoend.model.CodeFile.scanSourceFiles
+import scala.util.Using
+
@main def sparkBasics(): Unit =
- SparkSessions.newTerminal21WithSparkSession(SparkSessions.newSparkSession(), "spark-basics", "Spark Basics", NivoLib): (spark, session) =>
- given ConnectedSession = session
- given SparkSession = spark
- import scala3encoders.given
- import spark.implicits.*
+ Using.resource(SparkSessions.newSparkSession()): spark =>
+ Sessions
+ .withNewSession("spark-basics", "Spark Basics")
+ .andLibraries(NivoLib)
+ .connect: session =>
+ given ConnectedSession = session
+ given SparkSession = spark
+ import scala3encoders.given
+ import spark.implicits.*
- val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp")
+ val sourceFileCached = Cached("Code files"):
+ sourceFiles().limit(3)
+ val sortedSourceFilesDS = Cached("Sorted files"):
+ sortedSourceFiles(sourceFiles()).limit(3)
+ val sortedSourceFilesDFCached = Cached("Sorted files DF"):
+ sourceFiles()
+ .sort($"createdDate".asc, $"numOfWords".asc)
+ .toDF()
+ .limit(4)
- val sortedFilesTable = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords")
- val codeFilesTable = QuickTable().headers(headers: _*).caption("Unsorted files")
+ val sourceFilesSortedByNumOfLinesCached = Cached("Biggest Code Files"):
+ sourceFiles()
+ .sort($"numOfLines".desc)
- val sortedSourceFilesDS = sortedSourceFiles(sourceFiles())
- val sortedCalc = sortedSourceFilesDS.visualize("Sorted files", sortedFilesTable): results =>
- val tableRows = results.take(3).toList.map(_.toData)
- sortedFilesTable.rows(tableRows)
+ println(s"Cached dir: ${sourceFileCached.cachePath}")
+ def components(events: Events) =
+ given Events = events
- val codeFilesCalculation = sourceFiles().visualize("Code files", codeFilesTable): results =>
- val dt = results.take(3).toList
- codeFilesTable.rows(dt.map(_.toData))
+ val headers = Seq("id", "name", "path", "numOfLines", "numOfWords", "createdDate", "timestamp")
+ val sortedFilesTable = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords")
+ val codeFilesTable = QuickTable().withHeaders(headers: _*).caption("Unsorted files")
- val sortedFilesTableDF = QuickTable().headers(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF")
- val sortedCalcAsDF = sourceFiles()
- .sort($"createdDate".asc, $"numOfWords".asc)
- .toDF()
- .visualize("Sorted files DF", sortedFilesTableDF): results =>
- val tableRows = results.take(4).toList
- sortedFilesTableDF.rows(tableRows.toUiTable)
+ val sortedCalc = sortedSourceFilesDS.visualize(sortedFilesTable): results =>
+ val tableRows = results.collect().map(_.toData).toList
+ sortedFilesTable.withRows(tableRows)
- val chart = ResponsiveLine(
- data = Seq(
- Serie(
- "Scala",
- data = Seq(
- Datum("plane", 262),
- Datum("helicopter", 26),
- Datum("boat", 43)
- )
- )
- ),
- axisBottom = Some(Axis(legend = "Class", legendOffset = 36)),
- axisLeft = Some(Axis(legend = "Count", legendOffset = -40)),
- legends = Seq(Legend())
- )
+ val codeFilesCalculation = sourceFileCached.visualize(codeFilesTable): results =>
+ val dt = results.collect().toList
+ codeFilesTable.withRows(dt.map(_.toData))
- val sourceFileChart = sortedSourceFilesDS.visualize("Biggest Code Files", chart): results =>
- val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList
- chart.withData(Seq(Serie("Scala", data = data)))
+ val sortedFilesTableDF = QuickTable().withHeaders(headers: _*).caption("Files sorted by createdDate and numOfWords ASC and as DF")
+ val sortedCalcAsDF = sortedSourceFilesDFCached
+ .visualize(sortedFilesTableDF): results =>
+ val tableRows = results.collect().toList
+ sortedFilesTableDF.withRows(tableRows.toUiTable)
- Seq(
- codeFilesCalculation,
- sortedCalc,
- sortedCalcAsDF,
- sourceFileChart
- ).render()
+ val chart = ResponsiveLine(
+ data = Seq(
+ Serie(
+ "Scala",
+ data = Nil
+ )
+ ),
+ axisBottom = Some(Axis(legend = "Class", legendOffset = 36)),
+ axisLeft = Some(Axis(legend = "Number of Lines", legendOffset = -40)),
+ legends = Seq(Legend())
+ )
+
+ val sourceFileChart = sourceFilesSortedByNumOfLinesCached
+ .visualize(chart): results =>
+ val data = results.take(10).map(cf => Datum(StringUtils.substringBeforeLast(cf.name, ".scala"), cf.numOfLines)).toList
+ chart.withData(Seq(Serie("Scala", data = data)))
+ Seq(
+ codeFilesCalculation,
+ sortedCalc,
+ sortedCalcAsDF,
+ sourceFileChart
+ )
- session.waitTillUserClosesSession()
+ Controller
+ .noModel(components)
+ .render()
+ .run()
def sourceFiles()(using spark: SparkSession) =
import scala3encoders.given
diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala
index 5ff4371b..2c672740 100644
--- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala
+++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/ServerJson.scala
@@ -1,18 +1,11 @@
package org.terminal21.ui.std
import io.circe.Json
+import org.slf4j.LoggerFactory
case class ServerJson(
- rootKeys: Seq[String],
- elements: Map[String, Json],
- keyTree: Map[String, Seq[String]]
-):
- def include(j: ServerJson): ServerJson =
- ServerJson(
- rootKeys,
- elements ++ j.elements,
- keyTree ++ j.keyTree
- )
+ elements: Seq[Json]
+)
object ServerJson:
- val Empty = ServerJson(Nil, Map.empty, Map.empty)
+ val Empty = ServerJson(Nil)
diff --git a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala
index 8809d7da..98e09b85 100644
--- a/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala
+++ b/terminal21-ui-std-exports/src/main/scala/org/terminal21/ui/std/SessionsService.scala
@@ -1,11 +1,11 @@
package org.terminal21.ui.std
-import org.terminal21.model.Session
+import org.terminal21.model.{Session, SessionOptions}
/** //> exported
*/
trait SessionsService:
- def createSession(id: String, name: String): Session
+ def createSession(id: String, name: String, sessionOptions: SessionOptions): Session
def terminateSession(session: Session): Unit
def setSessionJsonState(session: Session, state: ServerJson): Unit
diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala
deleted file mode 100644
index 4a2f7dbf..00000000
--- a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/ServerJsonTest.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.terminal21.ui.std
-
-import io.circe.Json
-import org.scalatest.funsuite.AnyFunSuiteLike
-import org.scalatest.matchers.should.Matchers.*
-
-class ServerJsonTest extends AnyFunSuiteLike:
- test("include"):
- val j1 = ServerJson(Seq("k1"), Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(2)), Map("k1" -> Seq("k2", "k3")))
- val j2 = ServerJson(Nil, Map("k2" -> Json.fromInt(3)), Map("k2" -> Seq("k4")))
- j1.include(j2) should be(
- ServerJson(
- Seq("k1"),
- Map("k1" -> Json.fromInt(1), "k2" -> Json.fromInt(3)),
- Map("k1" -> Seq("k2", "k3"), "k2" -> Seq("k4"))
- )
- )
diff --git a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala
index 68f693e6..6daa5518 100644
--- a/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala
+++ b/terminal21-ui-std-exports/src/test/scala/org/terminal21/ui/std/StdExportsBuilders.scala
@@ -4,11 +4,5 @@ import io.circe.Json
object StdExportsBuilders:
def serverJson(
- rootKeys: Seq[String] = Nil,
- elements: Map[String, Json] = Map.empty,
- keyTree: Map[String, Seq[String]] = Map.empty
- ) = ServerJson(
- rootKeys,
- elements,
- keyTree
- )
+ elements: Seq[Json] = Nil
+ ) = ServerJson(elements)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
index e693d5c5..f05d20cc 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/ConnectedSession.scala
@@ -1,30 +1,41 @@
package org.terminal21.client
-import io.circe.*
-import io.circe.generic.auto.*
-import org.slf4j.LoggerFactory
-import org.terminal21.client.components.UiElement.{HasChildren, HasEventHandler, allDeep}
-import org.terminal21.client.components.{UiComponent, UiElement, UiElementEncoding}
-import org.terminal21.client.internal.EventHandlers
+import io.circe.{Json, JsonNumber, JsonObject}
+import org.terminal21.client.components.UiElement.HasChildren
+import org.terminal21.client.components.chakra.Box
+import org.terminal21.client.components.{UiComponent, UiElement}
+import org.terminal21.client.json.UiElementEncoding
+import org.terminal21.collections.SEList
import org.terminal21.model.*
import org.terminal21.ui.std.{ServerJson, SessionsService}
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.{CountDownLatch, TimeUnit}
import scala.annotation.tailrec
-import scala.collection.concurrent.TrieMap
+/** A session connected to the terminal21 server.
+ *
+ * @param session
+ * the session
+ * @param encoding
+ * json encoder for UiElements
+ * @param serverUrl
+ * the url of the server
+ * @param sessionsService
+ * the service to talk to the server
+ * @param onCloseHandler
+ * gets notified when the user closes the session
+ */
class ConnectedSession(val session: Session, encoding: UiElementEncoding, val serverUrl: String, sessionsService: SessionsService, onCloseHandler: () => Unit):
- private val logger = LoggerFactory.getLogger(getClass)
- private val handlers = new EventHandlers(this)
+ @volatile private var events = SEList[CommandEvent]()
def uiUrl: String = serverUrl + "/ui"
- def clear(): Unit =
- render()
- handlers.clear()
- modifiedElements.clear()
- def addEventHandler(key: String, handler: EventHandler): Unit = handlers.addEventHandler(key, handler)
+ /** Clears all UI elements and event handlers.
+ */
+ def clear(): Unit =
+ events.poisonPill()
+ events = SEList()
private val exitLatch = new CountDownLatch(1)
@@ -36,7 +47,8 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se
private val leaveSessionOpen = new AtomicBoolean(false)
- /** Doesn't close the session upon exiting. In the UI the session seems active but events are not working because the event handlers are not available.
+ /** Doesn't close the session upon exiting. In the UI the session seems active but events are not working because the event handlers are not available. Useful
+ * when we need to let the user read through some data. But no interaction is possible anymore between the user and the code.
*/
def leaveSessionOpenAfterExiting(): Unit =
leaveSessionOpen.set(true)
@@ -56,62 +68,75 @@ class ConnectedSession(val session: Session, encoding: UiElementEncoding, val se
*/
def isClosed: Boolean = exitLatch.getCount == 0
- def click(e: UiElement): Unit = fireEvent(OnClick(e.key))
+ def fireClickEvent(e: UiElement): Unit = fireEvent(CommandEvent.onClick(e))
+ def fireChangeEvent(e: UiElement, newValue: String): Unit = fireEvent(CommandEvent.onChange(e, newValue))
+ def fireSessionClosedEvent(): Unit = fireEvent(CommandEvent.sessionClosed)
- private[client] def fireEvent(event: CommandEvent): Unit =
+ /** @return
+ * A new event iterator. There can be many event iterators on the same time and each of them iterates events only from after the time it was created. The
+ * iterator blocks while waiting to receive an event.
+ */
+ def eventIterator: Iterator[CommandEvent] = events.iterator
+
+ /** Waits until at least 1 event iterator was created for the current page. Useful for testing purposes if i.e. one thread runs the main loop and gets an
+ * eventIterator at some point and an other thread needs to fire events.
+ */
+ def waitUntilAtLeast1EventIteratorWasCreated(): Unit = events.waitUntilAtLeast1IteratorWasCreated()
+
+ def fireEvents(events: CommandEvent*): Unit = for e <- events do fireEvent(e)
+
+ def fireEvent(event: CommandEvent): Unit =
+ events.add(event)
event match
case SessionClosed(_) =>
+ events.poisonPill()
exitLatch.countDown()
onCloseHandler()
case _ =>
- handlers.getEventHandler(event.key) match
- case Some(handlers) =>
- for handler <- handlers do
- (event, handler) match
- case (_: OnClick, h: OnClickEventHandler) => h.onClick()
- case (onChange: OnChange, h: OnChangeEventHandler) => h.onChange(onChange.value)
- case (onChange: OnChange, h: OnChangeBooleanEventHandler) => h.onChange(onChange.value.toBoolean)
- case x => logger.error(s"Unknown event handling combination : $x")
- case None =>
- logger.warn(s"There is no event handler for event $event")
-
- def render(es: UiElement*): Unit =
- handlers.registerEventHandlers(es)
+
+ /** Normally this method shouldn't be called directly. Terminates any previous event iterators, clears the UI and renders the UiElements.
+ * @param es
+ * the UiElements to be rendered.
+ */
+ def render(es: Seq[UiElement]): Unit =
+ clear()
val j = toJson(es)
sessionsService.setSessionJsonState(session, j)
- def renderChanges(es: UiElement*): Unit =
- for e <- es do modified(e)
- val j = toJson(es)
- sessionsService.changeSessionJsonState(session, j)
-
- private def toJson(elements: Seq[UiElement]): ServerJson =
- val flat = elements.flatMap(_.flat)
- val sj = ServerJson(
- elements.map(_.key),
- flat
- .map: el =>
- (
- el.key,
- el match
- case e: UiComponent => encoding.uiElementEncoder(e).deepDropNullValues
- case e: HasChildren[_] => encoding.uiElementEncoder(e.noChildren).deepDropNullValues
- case e => encoding.uiElementEncoder(e).deepDropNullValues
- )
- .toMap,
- flat
- .map: e =>
- (
- e.key,
- e match
- case e: UiComponent => e.rendered.map(_.key)
- case e: HasChildren[_] => e.children.map(_.key)
- case _ => Nil
- )
- .toMap
+ /** Normally this method shouldn't be called directly. Renders updates to existing elements
+ * @param es
+ * a seq of updated elements, all these should already have been rendered before (but not necessarily their children)
+ */
+ private[client] def renderChanges(es: Seq[UiElement]): Unit =
+ if !isClosed && es.nonEmpty then
+ val j = toJson(es)
+ sessionsService.setSessionJsonState(session, j) // TODO:changeSessionJsonState
+
+ private def nullEmptyKeysAndDropNulls(j: Json): Json =
+ val folder = new Json.Folder[Json] {
+ def onNull: Json = Json.Null
+ def onBoolean(value: Boolean): Json = Json.fromBoolean(value)
+ def onNumber(value: JsonNumber): Json = Json.fromJsonNumber(value)
+ def onString(value: String): Json = Json.fromString(value)
+ def onArray(value: Vector[Json]): Json =
+ Json.fromValues(value.collect {
+ case v if !v.isNull => v.foldWith(this)
+ })
+ def onObject(value: JsonObject): Json =
+ Json.fromJsonObject(
+ value
+ .filter:
+ case ("key", v) => !v.asString.contains("")
+ case (_, v) => !v.isNull
+ .mapValues(_.foldWith(this))
+ )
+ }
+
+ j.foldWith(folder)
+
+ private def toJson(elementsUn: Seq[UiElement]): ServerJson =
+ val elements = elementsUn.map(_.substituteComponents)
+ val sj = ServerJson(
+ elements.map(e => nullEmptyKeysAndDropNulls(encoding.uiElementEncoder(e)))
)
sj
- private val modifiedElements = TrieMap.empty[String, UiElement]
- def modified(e: UiElement): Unit =
- modifiedElements += e.key -> e
- def currentState[A <: UiElement](e: A): A = modifiedElements.getOrElse(e.key, e).asInstanceOf[A]
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala
new file mode 100644
index 00000000..1c6e8d4d
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Controller.scala
@@ -0,0 +1,217 @@
+package org.terminal21.client
+
+import org.terminal21.client.Events.InitialRender
+import org.terminal21.client.collections.EventIterator
+import org.terminal21.client.components.{OnChangeBooleanEventHandler, OnChangeEventHandler, UiElement}
+import org.terminal21.client.components.OnClickEventHandler.CanHandleOnClickEvent
+import org.terminal21.model.{ClientEvent, CommandEvent, OnChange, OnClick}
+
+/** The initial function passed on to a controller in order to create the MVC iterator.
+ * @tparam M
+ * the type of the model
+ */
+type ModelViewFunction[M] = (M, Events) => MV[M]
+
+/** Controller manages the changes in the model by receiving events. Also the rendering of the view (which is UiElements).
+ *
+ * @tparam M
+ * the type of the model
+ */
+class Controller[M](
+ eventIteratorFactory: => Iterator[CommandEvent],
+ renderChanges: Seq[UiElement] => Unit,
+ modelViewFunction: ModelViewFunction[M]
+):
+ /** Sends the initialModel along with an InitialRender event to the modelViewFunction and renders the resulting UI.
+ * @param initialModel
+ * the initial state of the model
+ * @return
+ * a RenderedController. Call run() or iterator on that.
+ */
+ def render(initialModel: M): RenderedController[M] =
+ val mv = modelViewFunction(initialModel, Events.Empty)
+ renderChanges(mv.view)
+ new RenderedController(eventIteratorFactory, renderChanges, modelViewFunction, mv)
+
+trait NoModelController:
+ this: Controller[Unit] =>
+ def render(): RenderedController[Unit] = render(())
+
+object Controller:
+ /** Call this for a full blown model-view-controller
+ * @param modelViewFunction
+ * a function (M, Events) => MV[M] which should process the events and render Seq[UiElement]
+ * @param session
+ * the ConnectedSession
+ * @tparam M
+ * the type of the model
+ * @return
+ * Controller[M], call render(initialModel) and then iterator or run()
+ */
+ def apply[M](modelViewFunction: ModelViewFunction[M])(using session: ConnectedSession): Controller[M] =
+ new Controller(session.eventIterator, session.renderChanges, modelViewFunction)
+
+ /** Call this id you just want to render some information UI that won't receive events.
+ * @param component
+ * a single component (and it's children) to be rendered
+ * @param session
+ * ConnectedSession
+ * @return
+ * the controller.
+ */
+ def noModel(component: UiElement)(using session: ConnectedSession): Controller[Unit] with NoModelController = noModel(Seq(component))
+
+ /** Call this id you just want to render some information UI that won't receive events.
+ * @param components
+ * components to be rendered
+ * @param session
+ * ConnectedSession
+ * @return
+ * the controller.
+ */
+ def noModel(components: Seq[UiElement])(using session: ConnectedSession): Controller[Unit] with NoModelController =
+ new Controller[Unit](session.eventIterator, session.renderChanges, (_, _) => MV((), components)) with NoModelController
+
+ /** Call this if you have no model but still want to receive events. I.e. a form with just an "Ok" button
+ *
+ * @param materializer
+ * a function that will be called initially to render the UI and whenever there is an event to render any changes to the UI
+ * @param session
+ * ConnectedSession
+ * @return
+ * the controller.
+ */
+ def noModel(materializer: Events => Seq[UiElement])(using session: ConnectedSession): Controller[Unit] with NoModelController =
+ new Controller[Unit](session.eventIterator, session.renderChanges, (_, events) => MV((), materializer(events))) with NoModelController
+
+class RenderedController[M](
+ eventIteratorFactory: => Iterator[CommandEvent],
+ renderChanges: Seq[UiElement] => Unit,
+ materializer: ModelViewFunction[M],
+ initialMv: MV[M]
+):
+ /** @return
+ * A new event iterator. There can be many event iterators on the same time and each of them iterates events only from after the time it was created. The
+ * iterator blocks while waiting to receive an event.
+ *
+ * Normally a single iterator is required and most of the time it is better done by the #run() method below.
+ */
+ def iterator: EventIterator[MV[M]] = new EventIterator[MV[M]](
+ eventIteratorFactory
+ .takeWhile(!_.isSessionClosed)
+ .scanLeft(initialMv): (mv, e) =>
+ val events = Events(e)
+ val newMv = materializer(mv.model, events)
+ if mv.view != newMv.view then renderChanges(newMv.view)
+ newMv
+ .flatMap: mv =>
+ // make sure we read the last MV change when terminating
+ if mv.terminate then Seq(mv.copy(terminate = false), mv) else Seq(mv)
+ .takeWhile(!_.terminate)
+ )
+
+ /** Gets an iterator and run the event processing.
+ * @return
+ * The last value of the model or None if the user closed the session.
+ */
+ def run(): Option[M] = iterator.lastOption.map(_.model)
+
+/** Wraps an event and has useful methods to process it.
+ * @param event
+ * CommandEvent (like clicks, changed values, ClientEvent etc)
+ */
+case class Events(event: CommandEvent):
+ def isClicked(e: UiElement): Boolean = event match
+ case OnClick(key) => key == e.key
+ case _ => false
+
+ /** If an element is clicked this results in Some(value), otherwise None
+ * @param e
+ * the element
+ * @param value
+ * the value
+ * @tparam V
+ * value type
+ * @return
+ * Some(value) if e is clicked, None if not
+ */
+ def ifClicked[V](e: UiElement & CanHandleOnClickEvent, value: => V): Option[V] = if isClicked(e) then Some(value) else None
+
+ /** @param e
+ * an editable element (like input)
+ * @param default
+ * the default value
+ * @return
+ * the new value of the editable (as received by an OnChange event) or the default value if the element's value didn't change
+ */
+ def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent, default: String): String = changedValue(e).getOrElse(default)
+
+ /** @param e
+ * an editable element (like input) that can receive OnChange events
+ * @return
+ * Some(newValue) if the element received an OnChange event, None if not
+ */
+ def changedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Option[String] = event match
+ case OnChange(key, value) if key == e.key => Some(value)
+ case _ => None
+
+ /** @param e
+ * an editable element (like input)
+ * @return
+ * true if the value of the element has changed, false if not
+ */
+ def isChangedValue(e: UiElement & OnChangeEventHandler.CanHandleOnChangeEvent): Boolean =
+ event match
+ case OnChange(key, _) => key == e.key
+ case _ => false
+
+ /** @param e
+ * an editable element with boolean value (like checkbox)
+ * @param default
+ * the value to return if the element wasn't changed
+ * @return
+ * the element's changed value or the default if the element didn't change
+ */
+ def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent, default: Boolean): Boolean =
+ changedBooleanValue(e).getOrElse(default)
+
+ /** @param e
+ * an editable element with boolean value (like checkbox)
+ * @return
+ * Some(value) if the element changed or None if not
+ */
+ def changedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Option[Boolean] = event match
+ case OnChange(key, value) if key == e.key => Some(value.toBoolean)
+ case _ => None
+
+ /** @param e
+ * an editable element with boolean value (like checkbox)
+ * @return
+ * true if the element changed or false if not
+ */
+ def isChangedBooleanValue(e: UiElement & OnChangeBooleanEventHandler.CanHandleOnChangeEvent): Boolean =
+ event match
+ case OnChange(key, _) => key == e.key
+ case _ => false
+
+ def isInitialRender: Boolean = event == InitialRender
+
+object Events:
+ case object InitialRender extends ClientEvent
+
+ val Empty = Events(InitialRender)
+
+/** The ModelViewFunction should return this, which contains the changes to the model, the changed view and if the event iteration should terminate.
+ * @param model
+ * the value of the model after processing the event
+ * @param view
+ * the value of the view after processing the event
+ * @param terminate
+ * if true, the event iteration will terminate
+ * @tparam M
+ * the type of the model
+ */
+case class MV[M](model: M, view: Seq[UiElement], terminate: Boolean = false)
+
+object MV:
+ def apply[M](model: M, view: UiElement): MV[M] = MV(model, Seq(view))
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala
deleted file mode 100644
index 1705b951..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/EventHandler.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.terminal21.client
-
-import org.terminal21.client.components.UiElement
-
-trait EventHandler
-
-trait OnClickEventHandler extends EventHandler:
- def onClick(): Unit
-
-object OnClickEventHandler:
- trait CanHandleOnClickEvent[A <: UiElement]:
- this: A =>
- def onClick(h: OnClickEventHandler)(using session: ConnectedSession): A =
- session.addEventHandler(key, h)
- this
-
-trait OnChangeEventHandler extends EventHandler:
- def onChange(newValue: String): Unit
-
-object OnChangeEventHandler:
- trait CanHandleOnChangeEvent[A <: UiElement]:
- this: A =>
- def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): A =
- session.addEventHandler(key, h)
- this
-
-trait OnChangeBooleanEventHandler extends EventHandler:
- def onChange(newValue: Boolean): Unit
-
-object OnChangeBooleanEventHandler:
- trait CanHandleOnChangeEvent[A <: UiElement]:
- this: A =>
- def onChange(h: OnChangeBooleanEventHandler)(using session: ConnectedSession): A =
- session.addEventHandler(key, h)
- this
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala
index 74a3af9b..b1f69c85 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Globals.scala
@@ -2,4 +2,5 @@ package org.terminal21.client
import functions.fibers.FiberExecutor
-given fiberExecutor: FiberExecutor = FiberExecutor()
+given FiberExecutor = FiberExecutor()
+val fiberExecutor = implicitly[FiberExecutor]
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
index 5b29b5fd..23d62381 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/Sessions.scala
@@ -4,38 +4,51 @@ import functions.fibers.FiberExecutor
import functions.helidon.transport.HelidonTransport
import io.helidon.webclient.api.WebClient
import io.helidon.webclient.websocket.WsClient
-import org.terminal21.client.components.{ComponentLib, StdElementEncoding, UiElementEncoding}
+import org.terminal21.client.components.ComponentLib
+import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding}
import org.terminal21.config.Config
+import org.terminal21.model.SessionOptions
import org.terminal21.ui.std.SessionsServiceCallerFactory
import java.util.concurrent.atomic.AtomicBoolean
object Sessions:
- def withNewSession[R](id: String, name: String, componentLibs: ComponentLib*)(f: ConnectedSession => R): R =
- val config = Config.Default
- val serverUrl = s"http://${config.host}:${config.port}"
- val client = WebClient.builder
- .baseUri(serverUrl)
- .build
- val transport = new HelidonTransport(client)
- val sessionsService = SessionsServiceCallerFactory.newHelidonJsonSessionsService(transport)
- val session = sessionsService.createSession(id, name)
- val wsClient = WsClient.builder
- .baseUri(s"ws://${config.host}:${config.port}")
- .build
-
- val isStopped = new AtomicBoolean(false)
- def terminate(): Unit =
- isStopped.set(true)
-
- val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs)
- val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate)
- FiberExecutor.withFiberExecutor: executor =>
- val listener = new ClientEventsWsListener(wsClient, connectedSession, executor)
- listener.start()
-
- try {
- f(connectedSession)
- } finally
- if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session)
- listener.close()
+ case class SessionBuilder(
+ id: String,
+ name: String,
+ componentLibs: Seq[ComponentLib] = Seq(StdElementEncoding),
+ sessionOptions: SessionOptions = SessionOptions.Defaults
+ ):
+ def andLibraries(libraries: ComponentLib*): SessionBuilder = copy(componentLibs = componentLibs ++ libraries)
+ def andOptions(sessionOptions: SessionOptions) = copy(sessionOptions = sessionOptions)
+
+ def connect[R](f: ConnectedSession => R): R =
+ val config = Config.Default
+ val serverUrl = s"http://${config.host}:${config.port}"
+ val client = WebClient.builder
+ .baseUri(serverUrl)
+ .build
+ val transport = new HelidonTransport(client)
+ val sessionsService = SessionsServiceCallerFactory.newHelidonJsonSessionsService(transport)
+ val session = sessionsService.createSession(id, name, sessionOptions)
+ val wsClient = WsClient.builder
+ .baseUri(s"ws://${config.host}:${config.port}")
+ .build
+
+ val isStopped = new AtomicBoolean(false)
+
+ def terminate(): Unit =
+ isStopped.set(true)
+
+ val encoding = new UiElementEncoding(Seq(StdElementEncoding) ++ componentLibs)
+ val connectedSession = ConnectedSession(session, encoding, serverUrl, sessionsService, terminate)
+ FiberExecutor.withFiberExecutor: executor =>
+ val listener = new ClientEventsWsListener(wsClient, connectedSession, executor)
+ listener.start()
+
+ try f(connectedSession)
+ finally
+ if !isStopped.get() && !connectedSession.isLeaveSessionOpen then sessionsService.terminateSession(session)
+ listener.close()
+
+ def withNewSession(id: String, name: String): SessionBuilder = SessionBuilder(id, name)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala
new file mode 100644
index 00000000..d8df35e0
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/collections/EventIterator.scala
@@ -0,0 +1,21 @@
+package org.terminal21.client.collections
+
+import org.terminal21.client.ConnectedSession
+
+import scala.collection.AbstractIterator
+
+class EventIterator[A](it: Iterator[A]) extends AbstractIterator[A]:
+ override def hasNext: Boolean = it.hasNext
+ override def next(): A = it.next()
+
+ def lastOption: Option[A] =
+ var last = Option.empty[A]
+ while hasNext do last = Some(next())
+ last
+
+ def lastOptionOrNoneIfSessionClosed(using session: ConnectedSession) =
+ val v = lastOption
+ if session.isClosed then None else v
+
+object EventIterator:
+ def apply[A](items: A*): EventIterator[A] = new EventIterator(Iterator(items*))
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala
deleted file mode 100644
index 98a95d8d..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/CachedCalculation.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.terminal21.client.components
-
-import functions.fibers.{Fiber, FiberExecutor}
-
-abstract class CachedCalculation[OUT](using executor: FiberExecutor) extends Calculation[OUT]:
- def isCached: Boolean
- def invalidateCache(): Unit
- def nonCachedCalculation: OUT
- override def reCalculate(): Fiber[OUT] =
- invalidateCache()
- super.reCalculate()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala
deleted file mode 100644
index 30c933e0..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Calculation.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.terminal21.client.components
-
-import functions.fibers.{Fiber, FiberExecutor}
-
-import java.util.concurrent.CountDownLatch
-
-trait Calculation[OUT](using executor: FiberExecutor):
- protected def calculation(): OUT
- protected def whenResultsNotReady(): Unit = ()
- protected def whenResultsReady(results: OUT): Unit = ()
-
- def reCalculate(): Fiber[OUT] = run()
-
- def onError(t: Throwable): Unit =
- t.printStackTrace()
-
- def run(): Fiber[OUT] =
- val refreshInOrder = new CountDownLatch(1)
- executor.submit:
- try
- executor.submit:
- try whenResultsNotReady()
- finally refreshInOrder.countDown()
-
- val out = calculation()
- refreshInOrder.await()
- executor.submit:
- whenResultsReady(out)
- out
- catch
- case t: Throwable =>
- onError(t)
- throw t
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala
new file mode 100644
index 00000000..9388995c
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/EventHandler.scala
@@ -0,0 +1,18 @@
+package org.terminal21.client.components
+
+trait EventHandler
+
+object OnClickEventHandler:
+ trait CanHandleOnClickEvent:
+ this: UiElement =>
+ if key.isEmpty then throw new IllegalStateException(s"clickables must have a stable key. Error occurred on $this")
+
+object OnChangeEventHandler:
+ trait CanHandleOnChangeEvent:
+ this: UiElement =>
+ if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this")
+
+object OnChangeBooleanEventHandler:
+ trait CanHandleOnChangeEvent:
+ this: UiElement =>
+ if key.isEmpty then throw new IllegalStateException(s"changeable must have a stable key. Error occurred on $this")
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala
index 4ef4ce23..97aa99c1 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/Keys.scala
@@ -1,7 +1,8 @@
package org.terminal21.client.components
+import org.terminal21.client.components.UiElement.HasChildren
+
import java.util.concurrent.atomic.AtomicInteger
object Keys:
- private val keyId = new AtomicInteger(0)
- def nextKey: String = s"key${keyId.incrementAndGet()}"
+ def nextKey: String = ""
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala
deleted file mode 100644
index 44c7b279..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/StdUiCalculation.scala
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.terminal21.client.components
-
-import functions.fibers.FiberExecutor
-import org.terminal21.client.ConnectedSession
-import org.terminal21.client.components.UiElement.HasStyle
-import org.terminal21.client.components.chakra.*
-
-import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
-
-/** Creates a standard UI for a calculation which may take time. While the calculation runs, the UI is grayed out, including the dataUi component. When the
- * calculation completes, it allows for updating the dataUi component.
- * @tparam OUT
- * the return value of the calculation.
- */
-trait StdUiCalculation[OUT](
- name: String,
- dataUi: UiElement with HasStyle[_]
-)(using session: ConnectedSession, executor: FiberExecutor)
- extends Calculation[OUT]
- with UiComponent:
- private val running = new AtomicBoolean(false)
- private val currentUi = new AtomicReference(dataUi)
-
- protected def updateUi(dataUi: UiElement & HasStyle[_]) = currentUi.set(dataUi)
-
- lazy val badge = Badge()
- lazy val recalc = Button(text = "Recalculate", size = Some("sm"), leftIcon = Some(RepeatIcon())).onClick: () =>
- if running.compareAndSet(false, true) then
- try
- reCalculate()
- finally running.set(false)
-
- override lazy val rendered: Seq[UiElement] =
- val header = Box(
- bg = "green",
- p = 4,
- children = Seq(
- HStack(children = Seq(Text(text = name), badge, recalc))
- )
- )
- Seq(header, dataUi)
-
- override def onError(t: Throwable): Unit =
- session.renderChanges(
- badge.withText(s"Error: ${t.getMessage}").withColorScheme(Some("red")),
- dataUi,
- recalc.withIsDisabled(None)
- )
- super.onError(t)
-
- override protected def whenResultsNotReady(): Unit =
- session.renderChanges(
- badge.withText("Calculating").withColorScheme(Some("purple")),
- currentUi.get().withStyle(dataUi.style + ("filter" -> "grayscale(100%)")),
- recalc.withIsDisabled(Some(true))
- )
- super.whenResultsNotReady()
-
- override protected def whenResultsReady(results: OUT): Unit =
- val newDataUi = currentUi.get().withStyle(dataUi.style - "filter")
- session.renderChanges(
- badge.withText("Ready").withColorScheme(None),
- newDataUi,
- recalc.withIsDisabled(Some(false))
- )
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala
index c1213662..473f992b 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiComponent.scala
@@ -1,7 +1,5 @@
package org.terminal21.client.components
-import org.terminal21.client.components.UiElement.HasChildren
-
/** A UiComponent is a UI element that is composed of a seq of other ui elements
*/
trait UiComponent extends UiElement:
@@ -9,3 +7,5 @@ trait UiComponent extends UiElement:
// keys of any sub-elements the component has.
def rendered: Seq[UiElement]
override def flat = Seq(this) ++ rendered.flatMap(_.flat)
+
+ protected def subKey(suffix: String): String = key + "-" + suffix
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
index 00bb95b0..468022a1 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElement.scala
@@ -1,42 +1,45 @@
package org.terminal21.client.components
-import org.terminal21.client.{ConnectedSession, EventHandler}
+import org.terminal21.client.components.UiElement.HasChildren
+import org.terminal21.client.components.chakra.Box
+import org.terminal21.collections.{TypedMap, TypedMapKey}
+
+abstract class UiElement extends AnyElement:
+ type This <: UiElement
-trait UiElement:
def key: String
- def flat: Seq[UiElement] = Seq(this)
+ def withKey(key: String): This
+ def findKey(key: String): UiElement = flat.find(_.key == key).get
- def render()(using session: ConnectedSession): Unit =
- session.render(this)
+ def dataStore: TypedMap
+ def withDataStore(ds: TypedMap): This
+ def store[V](key: TypedMapKey[V], value: V): This = withDataStore(dataStore + (key -> value))
+ def storedValue[V](key: TypedMapKey[V]): V = dataStore(key)
- /** Renders any changes for this element and it's children (if any). The element must previously have been added to the session.
+ /** @return
+ * this element along all it's children flattened
*/
- def renderChanges()(using session: ConnectedSession): Unit =
- session.renderChanges(this)
+ def flat: Seq[UiElement] = Seq(this)
-object UiElement:
- def allDeep(elements: Seq[UiElement]): Seq[UiElement] =
- elements ++ elements
- .collect:
- case hc: HasChildren[_] => allDeep(hc.children)
- .flatten
+ def substituteComponents: UiElement =
+ this match
+ case c: UiComponent => Box(key = c.key, text = "", children = c.rendered.map(_.substituteComponents), dataStore = c.dataStore)
+ case ch: HasChildren => ch.withChildren(ch.children.map(_.substituteComponents)*)
+ case _ => this
- trait Current[A <: UiElement]:
- this: UiElement =>
- def current(using session: ConnectedSession): A = session.currentState(this.asInstanceOf[A])
+ def toSimpleString: String = s"${getClass.getSimpleName}($key)"
- trait HasChildren[A <: UiElement]:
- this: A =>
+object UiElement:
+ trait HasChildren:
+ this: UiElement =>
def children: Seq[UiElement]
- override def flat: Seq[UiElement] = Seq(this) ++ children.flatMap(_.flat)
- def withChildren(cn: UiElement*): A
- def noChildren: A = withChildren()
- def addChildren(cn: UiElement*): A = withChildren(children ++ cn: _*)
+ override def flat: Seq[UiElement] = Seq(this) ++ children.flatMap(_.flat)
+ def withChildren(cn: UiElement*): This
+ def noChildren: This = withChildren()
+ def addChildren(cn: UiElement*): This = withChildren(children ++ cn: _*)
- trait HasEventHandler:
- def defaultEventHandler(session: ConnectedSession): EventHandler
-
- trait HasStyle[A <: UiElement]:
+ trait HasStyle:
+ this: UiElement =>
def style: Map[String, Any]
- def withStyle(v: Map[String, Any]): A
- def withStyle(vs: (String, Any)*): A = withStyle(vs.toMap)
+ def withStyle(v: Map[String, Any]): This
+ def withStyle(vs: (String, Any)*): This = withStyle(vs.toMap)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
index ef601436..6705d250 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/ChakraElement.scala
@@ -1,8 +1,8 @@
package org.terminal21.client.components.chakra
-import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle}
-import org.terminal21.client.components.{Keys, UiElement}
-import org.terminal21.client.{ConnectedSession, OnChangeBooleanEventHandler, OnChangeEventHandler, OnClickEventHandler}
+import org.terminal21.client.components.*
+import org.terminal21.client.components.UiElement.{HasChildren, HasStyle}
+import org.terminal21.collections.TypedMap
sealed trait CEJson extends UiElement
@@ -10,7 +10,7 @@ sealed trait CEJson extends UiElement
* https://github.com/kostaskougios/terminal21-restapi/blob/main/examples/src/main/scala/tests/ChakraComponents.scala and it's related scala files under
* https://github.com/kostaskougios/terminal21-restapi/tree/main/examples/src/main/scala/tests/chakra
*/
-sealed trait ChakraElement[A <: ChakraElement[A]] extends CEJson with HasStyle[A] with Current[A]
+sealed trait ChakraElement extends CEJson with HasStyle
/** https://chakra-ui.com/docs/components/button
*/
@@ -27,9 +27,11 @@ case class Button(
isDisabled: Option[Boolean] = None,
isLoading: Option[Boolean] = None,
isAttached: Option[Boolean] = None,
- spacing: Option[String] = None
-) extends ChakraElement[Button]
- with OnClickEventHandler.CanHandleOnClickEvent[Button]:
+ spacing: Option[String] = None,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with OnClickEventHandler.CanHandleOnClickEvent:
+ type This = Button
override def withStyle(v: Map[String, Any]): Button = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
@@ -37,12 +39,15 @@ case class Button(
def withVariant(v: Option[String]) = copy(variant = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
def withLeftIcon(v: Option[UiElement]) = copy(leftIcon = v)
+ def withLeftIcon(v: UiElement) = copy(leftIcon = Some(v))
def withRightIcon(v: Option[UiElement]) = copy(rightIcon = v)
+ def withRightIcon(v: UiElement) = copy(rightIcon = Some(v))
def withIsActive(v: Option[Boolean]) = copy(isActive = v)
def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v)
def withIsLoading(v: Option[Boolean]) = copy(isLoading = v)
def withIsAttached(v: Option[Boolean]) = copy(isAttached = v)
def withSpacing(v: Option[String]) = copy(spacing = v)
+ override def withDataStore(ds: TypedMap): Button = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/button
*/
@@ -56,9 +61,11 @@ case class ButtonGroup(
border: Option[String] = None,
borderColor: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[ButtonGroup]
- with HasChildren[ButtonGroup]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = ButtonGroup
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]): ButtonGroup = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -69,6 +76,7 @@ case class ButtonGroup(
def withHeight(v: Option[String]) = copy(height = v)
def withBorder(v: Option[String]) = copy(border = v)
def withBorderColor(v: Option[String]) = copy(borderColor = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/box
*/
@@ -81,18 +89,21 @@ case class Box(
color: String = "",
style: Map[String, Any] = Map.empty,
as: Option[String] = None,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Box]
- with HasChildren[Box]:
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withText(v: String) = copy(text = v)
- def withBg(v: String) = copy(bg = v)
- def withW(v: String) = copy(w = v)
- def withP(v: Int) = copy(p = v)
- def withColor(v: String) = copy(color = v)
- def withAs(v: Option[String]) = copy(as = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Box
+ override def withChildren(cn: UiElement*): Box = copy(children = cn)
+ override def withStyle(v: Map[String, Any]): Box = copy(style = v)
+ def withKey(v: String): Box = copy(key = v)
+ def withText(v: String): Box = copy(text = v)
+ def withBg(v: String): Box = copy(bg = v)
+ def withW(v: String): Box = copy(w = v)
+ def withP(v: Int): Box = copy(p = v)
+ def withColor(v: String): Box = copy(color = v)
+ def withAs(v: Option[String]): Box = copy(as = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/stack
*/
@@ -101,28 +112,38 @@ case class HStack(
spacing: Option[String] = None,
align: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[HStack]
- with HasChildren[HStack]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = HStack
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSpacing(v: Option[String]) = copy(spacing = v)
+ def withSpacing(v: String) = copy(spacing = Some(v))
def withAlign(v: Option[String]) = copy(align = v)
+ def withAlign(v: String) = copy(align = Some(v))
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class VStack(
key: String = Keys.nextKey,
spacing: Option[String] = None,
align: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[VStack]
- with HasChildren[VStack]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = VStack
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSpacing(v: Option[String]) = copy(spacing = v)
+ def withSpacing(v: String) = copy(spacing = Some(v))
def withAlign(v: Option[String]) = copy(align = v)
+ def withAlign(v: String) = copy(align = Some(v))
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class SimpleGrid(
key: String = Keys.nextKey,
@@ -131,9 +152,11 @@ case class SimpleGrid(
spacingY: Option[String] = None,
columns: Int = 2,
children: Seq[UiElement] = Nil,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SimpleGrid]
- with HasChildren[SimpleGrid]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = SimpleGrid
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -141,38 +164,43 @@ case class SimpleGrid(
def withSpacingX(v: Option[String]) = copy(spacingX = v)
def withSpacingY(v: Option[String]) = copy(spacingY = v)
def withColumns(v: Int) = copy(columns = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/editable
*/
case class Editable(
key: String = Keys.nextKey,
defaultValue: String = "",
- value: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Editable]
- with HasEventHandler
- with HasChildren[Editable]
- with OnChangeEventHandler.CanHandleOnChangeEvent[Editable]:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler =
- newValue => session.modified(copy(value = newValue))
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withDefaultValue(v: String) = copy(defaultValue = v)
- def withValue(v: String) = copy(value = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnChangeEventHandler.CanHandleOnChangeEvent:
+ type This = Editable
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withDefaultValue(v: String) = copy(defaultValue = v)
+ override def withDataStore(ds: TypedMap): Editable = copy(dataStore = ds)
-case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditablePreview]:
+case class EditablePreview(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement:
+ type This = EditablePreview
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableInput]:
+case class EditableInput(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement:
+ type This = EditableInput
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[EditableTextarea]:
+case class EditableTextarea(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement:
+ type This = EditableTextarea
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/form-control
*/
@@ -180,13 +208,16 @@ case class FormControl(
key: String = Keys.nextKey,
as: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[FormControl]
- with HasChildren[FormControl]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = FormControl
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withAs(v: String) = copy(as = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/form-control
*/
@@ -194,13 +225,16 @@ case class FormLabel(
key: String = Keys.nextKey,
text: String,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[FormLabel]
- with HasChildren[FormLabel]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = FormLabel
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/form-control
*/
@@ -208,13 +242,16 @@ case class FormHelperText(
key: String = Keys.nextKey,
text: String,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[FormHelperText]
- with HasChildren[FormHelperText]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = FormHelperText
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/input
*/
@@ -224,55 +261,65 @@ case class Input(
placeholder: String = "",
size: String = "md",
variant: Option[String] = None,
- value: String = "",
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Input]
- with HasEventHandler
- with OnChangeEventHandler.CanHandleOnChangeEvent[Input]:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withType(v: String) = copy(`type` = v)
- def withPlaceholder(v: String) = copy(placeholder = v)
- def withSize(v: String) = copy(size = v)
- def withVariant(v: Option[String]) = copy(variant = v)
- def withValue(v: String) = copy(value = v)
+ defaultValue: String = "",
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with OnChangeEventHandler.CanHandleOnChangeEvent:
+ type This = Input
+ override def withStyle(v: Map[String, Any]): Input = copy(style = v)
+ def withKey(v: String): Input = copy(key = v)
+ def withType(v: String): Input = copy(`type` = v)
+ def withPlaceholder(v: String): Input = copy(placeholder = v)
+ def withSize(v: String): Input = copy(size = v)
+ def withVariant(v: Option[String]): Input = copy(variant = v)
+ def withDefaultValue(v: String): Input = copy(defaultValue = v)
+ override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds)
case class InputGroup(
key: String = Keys.nextKey,
size: String = "md",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[InputGroup]
- with HasChildren[InputGroup]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = InputGroup
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSize(v: String) = copy(size = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class InputLeftAddon(
key: String = Keys.nextKey,
text: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[InputLeftAddon]
- with HasChildren[InputLeftAddon]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = InputLeftAddon
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class InputRightAddon(
key: String = Keys.nextKey,
text: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[InputRightAddon]
- with HasChildren[InputRightAddon]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = InputRightAddon
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/checkbox
*/
@@ -282,17 +329,16 @@ case class Checkbox(
defaultChecked: Boolean = false,
isDisabled: Boolean = false,
style: Map[String, Any] = Map.empty,
- checkedV: Option[Boolean] = None
-) extends ChakraElement[Checkbox]
- with HasEventHandler
- with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Checkbox]:
- def checked: Boolean = checkedV.getOrElse(defaultChecked)
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean)))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withText(v: String) = copy(text = v)
- def withDefaultChecked(v: Boolean) = copy(defaultChecked = v)
- def withIsDisabled(v: Boolean) = copy(isDisabled = v)
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with OnChangeBooleanEventHandler.CanHandleOnChangeEvent:
+ type This = Checkbox
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withText(v: String) = copy(text = v)
+ def withDefaultChecked(v: Boolean) = copy(defaultChecked = v)
+ def withIsDisabled(v: Boolean) = copy(isDisabled = v)
+ override def withDataStore(ds: TypedMap): Checkbox = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/radio
*/
@@ -301,30 +347,32 @@ case class Radio(
value: String,
text: String = "",
colorScheme: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Radio]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Radio
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withValue(v: String) = copy(value = v)
def withText(v: String) = copy(text = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class RadioGroup(
key: String = Keys.nextKey,
defaultValue: String = "",
- valueV: Option[String] = None,
- style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[RadioGroup]
- with HasEventHandler
- with HasChildren[RadioGroup]
- with OnChangeEventHandler.CanHandleOnChangeEvent[RadioGroup]:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(valueV = Some(newValue)))
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def value: String = valueV.getOrElse(defaultValue)
- def withKey(v: String) = copy(key = v)
- def withDefaultValue(v: String) = copy(defaultValue = v)
+ style: Map[String, Any] = Map.empty,
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnChangeEventHandler.CanHandleOnChangeEvent:
+ type This = RadioGroup
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withDefaultValue(v: String) = copy(defaultValue = v)
+ override def withDataStore(ds: TypedMap): RadioGroup = copy(dataStore = ds)
case class Center(
key: String = Keys.nextKey,
@@ -334,9 +382,11 @@ case class Center(
w: Option[String] = None,
h: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Center]
- with HasChildren[Center]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Center
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -345,6 +395,7 @@ case class Center(
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Circle(
key: String = Keys.nextKey,
@@ -354,9 +405,11 @@ case class Circle(
w: Option[String] = None,
h: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Circle]
- with HasChildren[Circle]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Circle
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -365,6 +418,7 @@ case class Circle(
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Square(
key: String = Keys.nextKey,
@@ -374,9 +428,11 @@ case class Square(
w: Option[String] = None,
h: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Square]
- with HasChildren[Square]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Square
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -385,6 +441,7 @@ case class Square(
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -394,14 +451,17 @@ case class AddIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AddIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AddIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -411,14 +471,17 @@ case class ArrowBackIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowBackIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowBackIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -428,14 +491,17 @@ case class ArrowDownIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowDownIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowDownIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -445,14 +511,17 @@ case class ArrowForwardIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowForwardIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowForwardIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -462,14 +531,17 @@ case class ArrowLeftIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowLeftIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowLeftIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -479,14 +551,17 @@ case class ArrowRightIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowRightIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowRightIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -496,14 +571,17 @@ case class ArrowUpIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowUpIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowUpIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -513,14 +591,17 @@ case class ArrowUpDownIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ArrowUpDownIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ArrowUpDownIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -530,14 +611,17 @@ case class AtSignIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AtSignIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AtSignIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -547,14 +631,17 @@ case class AttachmentIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AttachmentIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AttachmentIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -564,14 +651,17 @@ case class BellIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[BellIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = BellIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -581,14 +671,17 @@ case class CalendarIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[CalendarIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = CalendarIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -598,14 +691,17 @@ case class ChatIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ChatIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ChatIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -615,14 +711,17 @@ case class CheckIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[CheckIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = CheckIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -632,14 +731,17 @@ case class CheckCircleIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[CheckCircleIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = CheckCircleIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -649,14 +751,17 @@ case class ChevronDownIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ChevronDownIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ChevronDownIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -666,14 +771,17 @@ case class ChevronLeftIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ChevronLeftIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ChevronLeftIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -683,14 +791,17 @@ case class ChevronRightIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ChevronRightIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ChevronRightIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -700,14 +811,17 @@ case class ChevronUpIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ChevronUpIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ChevronUpIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -717,14 +831,17 @@ case class CloseIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[CloseIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = CloseIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -734,14 +851,17 @@ case class CopyIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[CopyIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = CopyIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -751,14 +871,17 @@ case class DeleteIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[DeleteIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = DeleteIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -768,14 +891,17 @@ case class DownloadIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[DownloadIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = DownloadIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -785,14 +911,17 @@ case class DragHandleIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[DragHandleIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = DragHandleIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -802,14 +931,17 @@ case class EditIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[EditIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = EditIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -819,14 +951,17 @@ case class EmailIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[EmailIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = EmailIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -834,16 +969,21 @@ case class ExternalLinkIcon(
key: String = Keys.nextKey,
w: Option[String] = None,
h: Option[String] = None,
+ mx: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ExternalLinkIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ExternalLinkIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ def withMx(v: Option[String]) = copy(mx = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -853,14 +993,17 @@ case class HamburgerIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[HamburgerIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = HamburgerIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -870,14 +1013,17 @@ case class InfoIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[InfoIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = InfoIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -887,14 +1033,17 @@ case class InfoOutlineIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[InfoOutlineIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = InfoOutlineIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -904,14 +1053,17 @@ case class LinkIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[LinkIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = LinkIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -921,14 +1073,17 @@ case class LockIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[LockIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = LockIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -938,14 +1093,17 @@ case class MinusIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[MinusIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = MinusIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -955,14 +1113,17 @@ case class MoonIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[MoonIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = MoonIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -972,14 +1133,17 @@ case class NotAllowedIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[NotAllowedIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = NotAllowedIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -989,14 +1153,17 @@ case class PhoneIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[PhoneIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = PhoneIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1006,14 +1173,17 @@ case class PlusSquareIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[PlusSquareIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = PlusSquareIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1023,14 +1193,17 @@ case class QuestionIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[QuestionIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = QuestionIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1040,14 +1213,17 @@ case class QuestionOutlineIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[QuestionOutlineIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = QuestionOutlineIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1057,14 +1233,17 @@ case class RepeatIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[RepeatIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = RepeatIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1074,14 +1253,17 @@ case class RepeatClockIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[RepeatClockIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = RepeatClockIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1091,14 +1273,17 @@ case class SearchIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SearchIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SearchIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1108,14 +1293,17 @@ case class Search2Icon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Search2Icon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Search2Icon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1125,14 +1313,17 @@ case class SettingsIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SettingsIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SettingsIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1142,14 +1333,17 @@ case class SmallAddIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SmallAddIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SmallAddIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1159,14 +1353,17 @@ case class SmallCloseIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SmallCloseIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SmallCloseIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1176,14 +1373,17 @@ case class SpinnerIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SpinnerIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SpinnerIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1193,14 +1393,17 @@ case class StarIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[StarIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = StarIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1210,14 +1413,17 @@ case class SunIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[SunIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = SunIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1227,14 +1433,17 @@ case class TimeIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[TimeIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = TimeIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1244,14 +1453,17 @@ case class TriangleDownIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[TriangleDownIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = TriangleDownIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1261,14 +1473,17 @@ case class TriangleUpIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[TriangleUpIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = TriangleUpIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1278,14 +1493,17 @@ case class UnlockIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[UnlockIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = UnlockIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1295,14 +1513,17 @@ case class UpDownIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[UpDownIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = UpDownIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1312,14 +1533,17 @@ case class ViewIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ViewIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ViewIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1329,14 +1553,17 @@ case class ViewOffIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[ViewOffIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = ViewOffIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1346,14 +1573,17 @@ case class WarningIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[WarningIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = WarningIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
*/
@@ -1363,14 +1593,17 @@ case class WarningTwoIcon(
h: Option[String] = None,
boxSize: Option[String] = None,
color: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[WarningTwoIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = WarningTwoIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withW(v: Option[String]) = copy(w = v)
def withH(v: Option[String]) = copy(h = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/textarea
*/
@@ -1380,19 +1613,20 @@ case class Textarea(
placeholder: String = "",
size: String = "md",
variant: Option[String] = None,
- value: String = "",
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Textarea]
- with HasEventHandler
- with OnChangeEventHandler.CanHandleOnChangeEvent[Textarea]:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withType(v: String) = copy(`type` = v)
- def withPlaceholder(v: String) = copy(placeholder = v)
- def withSize(v: String) = copy(size = v)
- def withVariant(v: Option[String]) = copy(variant = v)
- def withValue(v: String) = copy(value = v)
+ defaultValue: String = "",
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with OnChangeEventHandler.CanHandleOnChangeEvent:
+ type This = Textarea
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withType(v: String) = copy(`type` = v)
+ def withPlaceholder(v: String) = copy(placeholder = v)
+ def withSize(v: String) = copy(size = v)
+ def withVariant(v: Option[String]) = copy(variant = v)
+ def withDefaultValue(v: String) = copy(defaultValue = v)
+ override def withDataStore(ds: TypedMap): Textarea = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/switch
*/
@@ -1402,59 +1636,63 @@ case class Switch(
defaultChecked: Boolean = false,
isDisabled: Boolean = false,
style: Map[String, Any] = Map.empty,
- checkedV: Option[Boolean] = None
-) extends ChakraElement[Switch]
- with HasEventHandler
- with OnChangeBooleanEventHandler.CanHandleOnChangeEvent[Switch]:
- def checked: Boolean = checkedV.getOrElse(defaultChecked)
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(checkedV = Some(newValue.toBoolean)))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withText(v: String) = copy(text = v)
- def withDefaultChecked(v: Boolean) = copy(defaultChecked = v)
- def withIsDisabled(v: Boolean) = copy(isDisabled = v)
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with OnChangeBooleanEventHandler.CanHandleOnChangeEvent:
+ type This = Switch
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withText(v: String) = copy(text = v)
+ def withDefaultChecked(v: Boolean) = copy(defaultChecked = v)
+ def withIsDisabled(v: Boolean) = copy(isDisabled = v)
+ override def withDataStore(ds: TypedMap): Switch = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/select
*/
case class Select(
key: String = Keys.nextKey,
placeholder: String = "",
- value: String = "",
+ defaultValue: String = "",
bg: Option[String] = None,
color: Option[String] = None,
borderColor: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Select]
- with HasEventHandler
- with HasChildren[Select]
- with OnChangeEventHandler.CanHandleOnChangeEvent[Select]:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = newValue))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- override def withChildren(cn: UiElement*) = copy(children = cn)
- def withKey(v: String) = copy(key = v)
- def withPlaceholder(v: String) = copy(placeholder = v)
- def withValue(v: String) = copy(value = v)
- def withBg(v: Option[String]) = copy(bg = v)
- def withColor(v: Option[String]) = copy(color = v)
- def withBorderColor(v: Option[String]) = copy(borderColor = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnChangeEventHandler.CanHandleOnChangeEvent:
+ type This = Select
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ def withKey(v: String) = copy(key = v)
+ def withPlaceholder(v: String) = copy(placeholder = v)
+ def withDefaultValue(v: String) = copy(defaultValue = v)
+ def withBg(v: Option[String]) = copy(bg = v)
+ def withColor(v: Option[String]) = copy(color = v)
+ def withBorderColor(v: Option[String]) = copy(borderColor = v)
+ override def withDataStore(ds: TypedMap): Select = copy(dataStore = ds)
case class Option_(
key: String = Keys.nextKey,
value: String,
text: String = "",
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Option_]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Option_
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withValue(v: String) = copy(value = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/table/usage
*/
-case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty)
- extends ChakraElement[TableContainer]
- with HasChildren[TableContainer]:
+case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = TableContainer
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withRowStringData(data: Seq[Seq[String]]): TableContainer = withRowData(data.map(_.map(c => Text(text = c))))
@@ -1474,6 +1712,7 @@ case class TableContainer(key: String = Keys.nextKey, children: Seq[UiElement] =
this
override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Table(
key: String = Keys.nextKey,
@@ -1481,88 +1720,114 @@ case class Table(
size: String = "md",
colorScheme: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Table]
- with HasChildren[Table]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Table
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withVariant(v: String) = copy(variant = v)
def withSize(v: String) = copy(size = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty) extends ChakraElement[TableCaption]:
+case class TableCaption(key: String = Keys.nextKey, text: String = "", style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement:
+ type This = TableCaption
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty)
- extends ChakraElement[Thead]
- with HasChildren[Thead]:
+case class Thead(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = Thead
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty)
- extends ChakraElement[Tbody]
- with HasChildren[Tbody]:
+case class Tbody(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = Tbody
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty)
- extends ChakraElement[Tfoot]
- with HasChildren[Tfoot]:
+case class Tfoot(key: String = Keys.nextKey, children: Seq[UiElement] = Nil, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = Tfoot
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Tr(
key: String = Keys.nextKey,
children: Seq[UiElement] = Nil,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Tr]
- with HasChildren[Tr]:
+ bg: Option[String] = None,
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Tr
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ def withBg(v: Option[String]) = copy(bg = v)
+ def withBg(v: String) = copy(bg = Some(v))
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Th(
key: String = Keys.nextKey,
text: String = "",
isNumeric: Boolean = false,
children: Seq[UiElement] = Nil,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Th]
- with HasChildren[Th]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Th
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
def withIsNumeric(v: Boolean) = copy(isNumeric = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Td(
key: String = Keys.nextKey,
text: String = "",
isNumeric: Boolean = false,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Td]
- with HasChildren[Td]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Td
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
def withIsNumeric(v: Boolean) = copy(isNumeric = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/menu/usage
*/
-case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil)
- extends ChakraElement[Menu]
- with HasChildren[Menu]:
+case class Menu(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = Menu
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class MenuButton(
key: String = Keys.nextKey,
@@ -1570,39 +1835,49 @@ case class MenuButton(
size: Option[String] = None,
colorScheme: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[MenuButton]
- with HasChildren[MenuButton]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = MenuButton
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
def withSize(v: Option[String]) = copy(size = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil)
- extends ChakraElement[MenuList]
- with HasChildren[MenuList]:
+case class MenuList(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, children: Seq[UiElement] = Nil, dataStore: TypedMap = TypedMap.Empty)
+ extends ChakraElement
+ with HasChildren:
+ type This = MenuList
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class MenuItem(
key: String = Keys.nextKey,
style: Map[String, Any] = Map.empty,
text: String = "",
- children: Seq[UiElement] = Nil
-) extends ChakraElement[MenuItem]
- with HasChildren[MenuItem]
- with OnClickEventHandler.CanHandleOnClickEvent[MenuItem]:
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withText(v: String) = copy(text = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnClickEventHandler.CanHandleOnClickEvent:
+ type This = MenuItem
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap): MenuItem = copy(dataStore = ds)
-case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends ChakraElement[MenuDivider]:
+case class MenuDivider(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends ChakraElement:
+ type This = MenuDivider
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Badge(
key: String = Keys.nextKey,
@@ -1611,9 +1886,11 @@ case class Badge(
variant: Option[String] = None,
size: String = "md",
children: Seq[UiElement] = Nil,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Badge]
- with HasChildren[Badge]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Badge
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
@@ -1621,6 +1898,7 @@ case class Badge(
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
def withVariant(v: Option[String]) = copy(variant = v)
def withSize(v: String) = copy(size = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/image/usage
*
@@ -1634,14 +1912,17 @@ case class Image(
alt: String = "",
boxSize: Option[String] = None,
borderRadius: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Image]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Image
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSrc(v: String) = copy(src = v)
def withAlt(v: String) = copy(alt = v)
def withBoxSize(v: Option[String]) = copy(boxSize = v)
def withBorderRadius(v: Option[String]) = copy(borderRadius = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/text
*/
@@ -1655,8 +1936,10 @@ case class Text(
align: Option[String] = None,
casing: Option[String] = None,
decoration: Option[String] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Text]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Text
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
@@ -1667,93 +1950,118 @@ case class Text(
def withAlign(v: Option[String]) = copy(align = v)
def withCasing(v: Option[String]) = copy(casing = v)
def withDecoration(v: Option[String]) = copy(decoration = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Code(
key: String = Keys.nextKey,
text: String = "",
colorScheme: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Code]
- with HasChildren[Code]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Code
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class UnorderedList(
key: String = Keys.nextKey,
spacing: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[UnorderedList]
- with HasChildren[UnorderedList]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = UnorderedList
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSpacing(v: Option[String]) = copy(spacing = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class OrderedList(
key: String = Keys.nextKey,
spacing: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[OrderedList]
- with HasChildren[OrderedList]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = OrderedList
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withSpacing(v: Option[String]) = copy(spacing = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class ListItem(
key: String = Keys.nextKey,
text: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[ListItem]
- with HasChildren[ListItem]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = ListItem
def withText(v: String) = copy(text = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Alert(
key: String = Keys.nextKey,
status: String = "error", // error, success, warning, info
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Alert]
- with HasChildren[Alert]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Alert
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withStatus(v: String) = copy(status = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class AlertIcon(
key: String = Keys.nextKey,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AlertIcon]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AlertIcon
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class AlertTitle(
key: String = Keys.nextKey,
text: String = "Alert!",
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AlertTitle]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AlertTitle
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class AlertDescription(
key: String = Keys.nextKey,
text: String = "Something happened!",
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[AlertDescription]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = AlertDescription
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/progress
*/
@@ -1764,8 +2072,10 @@ case class Progress(
size: Option[String] = None,
hasStripe: Option[Boolean] = None,
isIndeterminate: Option[Boolean] = None,
- style: Map[String, Any] = Map.empty
-) extends ChakraElement[Progress]:
+ style: Map[String, Any] = Map.empty,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement:
+ type This = Progress
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withColorScheme(v: Option[String]) = copy(colorScheme = v)
@@ -1773,6 +2083,7 @@ case class Progress(
def withValue(v: Int) = copy(value = v)
def withHasStripe(v: Option[Boolean]) = copy(hasStripe = v)
def withIsIndeterminate(v: Option[Boolean]) = copy(isIndeterminate = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Tooltip(
key: String = Keys.nextKey,
@@ -1782,9 +2093,11 @@ case class Tooltip(
hasArrow: Option[Boolean] = None,
fontSize: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this"))
-) extends ChakraElement[Tooltip]
- with HasChildren[Tooltip]:
+ children: Seq[UiElement] = Seq(Text("use tooltip.withContent() to set this")),
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Tooltip
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withContent(cn: UiElement) = withChildren(cn)
def withKey(v: String) = copy(key = v)
@@ -1795,6 +2108,7 @@ case class Tooltip(
override def noChildren = copy(children = Nil)
override def withChildren(cn: UiElement*): Tooltip =
if cn.size != 1 then throw new IllegalArgumentException("tooltip takes 1 only child") else copy(children = cn)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** see https://chakra-ui.com/docs/components/tabs
*/
@@ -1807,9 +2121,11 @@ case class Tabs(
size: Option[String] = None,
isFitted: Option[Boolean] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Tabs]
- with HasChildren[Tabs]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Tabs
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
@@ -1818,18 +2134,22 @@ case class Tabs(
def withSize(v: Option[String]) = copy(size = v)
def withAlign(v: Option[String]) = copy(align = v)
def withIsFitted(v: Option[Boolean]) = copy(isFitted = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** see https://chakra-ui.com/docs/components/tabs
*/
case class TabList(
key: String = Keys.nextKey,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[TabList]
- with HasChildren[TabList]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = TabList
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** see https://chakra-ui.com/docs/components/tabs
*/
@@ -1837,45 +2157,57 @@ case class Tab(
key: String = Keys.nextKey,
text: String = "tab.text",
isDisabled: Option[Boolean] = None,
- _selected: Map[String, Any] = Map.empty,
- _hover: Map[String, Any] = Map.empty,
- _active: Map[String, Any] = Map.empty,
+ _selected: Option[Map[String, Any]] = None,
+ _hover: Option[Map[String, Any]] = None,
+ _active: Option[Map[String, Any]] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Tab]
- with HasChildren[Tab]:
- def withKey(v: String) = copy(key = v)
- def withText(v: String) = copy(text = v)
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v)
- def withSelected(v: Map[String, Any]) = copy(_selected = v)
- def withHover(v: Map[String, Any]) = copy(_hover = v)
- def withActive(v: Map[String, Any]) = copy(_active = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Tab
+ def withKey(v: String) = copy(key = v)
+ def withText(v: String) = copy(text = v)
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withIsDisabled(v: Option[Boolean]) = copy(isDisabled = v)
+ def withSelected(v: Map[String, Any]) = copy(_selected = Some(v))
+ def withSelected(v: Option[Map[String, Any]]) = copy(_selected = v)
+ def withHover(v: Map[String, Any]) = copy(_hover = Some(v))
+ def withHover(v: Option[Map[String, Any]]) = copy(_hover = v)
+ def withActive(v: Map[String, Any]) = copy(_active = Some(v))
+ def withActive(v: Option[Map[String, Any]]) = copy(_active = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** see https://chakra-ui.com/docs/components/tabs
*/
case class TabPanels(
key: String = Keys.nextKey,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[TabPanels]
- with HasChildren[TabPanels]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = TabPanels
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** see https://chakra-ui.com/docs/components/tabs
*/
case class TabPanel(
key: String = Keys.nextKey,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[TabPanel]
- with HasChildren[TabPanel]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = TabPanel
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/breadcrumb
*/
@@ -1887,9 +2219,11 @@ case class Breadcrumb(
fontSize: Option[String] = None,
pt: Option[Int] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Breadcrumb]
- with HasChildren[Breadcrumb]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = Breadcrumb
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
@@ -1898,6 +2232,7 @@ case class Breadcrumb(
def withFontWeight(v: Option[String]) = copy(fontWeight = v)
def withFontSize(v: Option[String]) = copy(fontSize = v)
def withPt(v: Option[Int]) = copy(pt = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/breadcrumb
*/
@@ -1905,13 +2240,16 @@ case class BreadcrumbItem(
key: String = Keys.nextKey,
isCurrentPage: Option[Boolean] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[BreadcrumbItem]
- with HasChildren[BreadcrumbItem]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren:
+ type This = BreadcrumbItem
def withKey(v: String) = copy(key = v)
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withIsCurrentPage(v: Option[Boolean]) = copy(isCurrentPage = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
/** https://chakra-ui.com/docs/components/breadcrumb
*/
@@ -1920,29 +2258,39 @@ case class BreadcrumbLink(
text: String = "breadcrumblink.text",
href: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[BreadcrumbLink]
- with HasChildren[BreadcrumbLink]
- with OnClickEventHandler.CanHandleOnClickEvent[BreadcrumbLink]:
- def withKey(v: String) = copy(key = v)
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withHref(v: Option[String]) = copy(href = v)
- def withText(v: String) = copy(text = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnClickEventHandler.CanHandleOnClickEvent:
+ type This = BreadcrumbLink
+ def withKey(v: String) = copy(key = v)
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withHref(v: Option[String]) = copy(href = v)
+ def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap): BreadcrumbLink = copy(dataStore = ds)
case class Link(
key: String = Keys.nextKey,
text: String = "link.text",
href: String = "#",
isExternal: Option[Boolean] = None,
+ color: Option[String] = None,
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends ChakraElement[Link]
- with HasChildren[Link]
- with OnClickEventHandler.CanHandleOnClickEvent[Link]:
- def withKey(v: String) = copy(key = v)
- override def withChildren(cn: UiElement*) = copy(children = cn)
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withIsExternal(v: Option[Boolean]) = copy(isExternal = v)
- def withHref(v: String) = copy(href = v)
- def withText(v: String) = copy(text = v)
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends ChakraElement
+ with HasChildren
+ with OnClickEventHandler.CanHandleOnClickEvent:
+ type This = Link
+ def withKey(v: String) = copy(key = v)
+ override def withChildren(cn: UiElement*) = copy(children = cn)
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withIsExternal(v: Option[Boolean]) = copy(isExternal = v)
+ def withIsExternal(v: Boolean) = copy(isExternal = Some(v))
+ def withHref(v: String) = copy(href = v)
+ def withText(v: String) = copy(text = v)
+ def withColor(v: String) = copy(color = Some(v))
+ def withColor(v: Option[String]) = copy(color = v)
+ override def withDataStore(ds: TypedMap): Link = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala
new file mode 100644
index 00000000..2bb9508e
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickFormControl.scala
@@ -0,0 +1,30 @@
+package org.terminal21.client.components.chakra
+
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiComponent, UiElement}
+import org.terminal21.collections.TypedMap
+
+case class QuickFormControl(
+ key: String = Keys.nextKey,
+ style: Map[String, Any] = Map.empty,
+ label: Option[String] = None,
+ inputGroup: Seq[UiElement] = Nil,
+ helperText: Option[String] = None,
+ dataStore: TypedMap = TypedMap.Empty
+) extends UiComponent
+ with HasStyle:
+ type This = QuickFormControl
+ lazy val rendered: Seq[UiElement] =
+ val ch: Seq[UiElement] =
+ label.map(l => FormLabel(key = subKey("label"), text = l)).toSeq ++
+ Seq(InputGroup(key = subKey("ig")).withChildren(inputGroup*)) ++
+ helperText.map(h => FormHelperText(key = subKey("helper"), text = h))
+ Seq(FormControl(key = subKey("fc"), style = style).withChildren(ch: _*))
+
+ def withLabel(label: String): QuickFormControl = copy(label = Some(label))
+ def withInputGroup(ig: UiElement*): QuickFormControl = copy(inputGroup = ig)
+ def withHelperText(text: String): QuickFormControl = copy(helperText = Some(text))
+
+ override def withStyle(v: Map[String, Any]): QuickFormControl = copy(style = v)
+ override def withKey(key: String): QuickFormControl = copy(key = key)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala
index a3394b89..04072176 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTable.scala
@@ -2,6 +2,7 @@ package org.terminal21.client.components.chakra
import org.terminal21.client.components.UiElement.HasStyle
import org.terminal21.client.components.{Keys, UiComponent, UiElement}
+import org.terminal21.collections.TypedMap
case class QuickTable(
key: String = Keys.nextKey,
@@ -10,39 +11,75 @@ case class QuickTable(
size: String = "mg",
style: Map[String, Any] = Map.empty,
caption: Option[String] = None,
- headers: Seq[UiElement] = Nil,
- rows: Seq[Seq[UiElement]] = Nil
+ headers: Seq[Any] = Nil,
+ rows: Seq[Seq[Any]] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
) extends UiComponent
- with HasStyle[QuickTable]:
- def withKey(v: String) = copy(key = v)
- def withVariant(v: String) = copy(variant = v)
- def withColorScheme(v: String) = copy(colorScheme = v)
- def withSize(v: String) = copy(size = v)
- def withCaption(v: Option[String]) = copy(caption = v)
- def withHeaders(v: Seq[UiElement]) = copy(headers = v)
- def withRows(v: Seq[Seq[UiElement]]) = copy(rows = v)
+ with HasStyle:
+ type This = QuickTable
+ def withKey(v: String) = copy(key = v)
+ def withVariant(v: String) = copy(variant = v)
+ def withColorScheme(v: String) = copy(colorScheme = v)
+ def withSize(v: String) = copy(size = v)
+ def withCaption(v: Option[String]) = copy(caption = v)
+ def withCaption(v: String) = copy(caption = Some(v))
override lazy val rendered: Seq[UiElement] =
- val head = Thead(key = key + "-th", children = Seq(Tr(children = headers.map(h => Th(children = Seq(h))))))
+ val head = Thead(
+ key = subKey("thead"),
+ children = Seq(
+ Tr(
+ key = subKey("thead-tr"),
+ children = headers.zipWithIndex.map: (h, i) =>
+ Th(
+ key = subKey(s"thead-tr-th-$i"),
+ children = Seq(
+ h match
+ case u: UiElement => u
+ case c => Text(text = c.toString)
+ )
+ )
+ )
+ )
+ )
val body = Tbody(
- key = key + "-tb",
- children = rows.map: row =>
- Tr(children = row.map(c => Td(children = Seq(c))))
+ key = subKey("tb"),
+ children = rows.zipWithIndex.map: (row, i) =>
+ Tr(
+ key = subKey(s"tb-tr-$i"),
+ children = row.zipWithIndex.map: (c, i) =>
+ Td(
+ key = subKey(s"tb-th-$i"),
+ children = Seq(
+ c match
+ case u: UiElement => u
+ case c => Text(text = c.toString)
+ )
+ )
+ )
)
val table = Table(
- key = key + "-t",
+ key = subKey("t"),
variant = variant,
colorScheme = Some(colorScheme),
size = size,
children = caption.map(text => TableCaption(text = text)).toSeq ++ Seq(head, body)
)
- val tableContainer = TableContainer(key = key + "-tc", style = style, children = Seq(table))
+ val tableContainer = TableContainer(key = subKey("tc"), style = style, children = Seq(table))
Seq(tableContainer)
- def headers(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h)))
- def headersElements(headers: UiElement*): QuickTable = copy(headers = headers)
- def rows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data.map(_.map(c => Text(text = c.toString))))
- def rowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data)
+ def withHeaders(headers: String*): QuickTable = copy(headers = headers.map(h => Text(text = h)))
+ def withHeadersElements(headers: UiElement*): QuickTable = copy(headers = headers)
+
+ /** @param data
+ * A mix of plain types or UiElement. If it is a UiElement, it will be rendered otherwise if it is anything else the `.toString` method will be used to
+ * render it.
+ * @return
+ * QuickTable
+ */
+ def withRows(data: Seq[Seq[Any]]): QuickTable = copy(rows = data)
+ def withRowsElements(data: Seq[Seq[UiElement]]): QuickTable = copy(rows = data)
def caption(text: String): QuickTable = copy(caption = Some(text))
override def withStyle(v: Map[String, Any]): QuickTable = copy(style = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala
new file mode 100644
index 00000000..90c4f963
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/chakra/QuickTabs.scala
@@ -0,0 +1,40 @@
+package org.terminal21.client.components.chakra
+
+import org.terminal21.client.components.UiElement.HasStyle
+import org.terminal21.client.components.{Keys, UiComponent, UiElement}
+import org.terminal21.collections.TypedMap
+
+case class QuickTabs(
+ key: String = Keys.nextKey,
+ style: Map[String, Any] = Map.empty,
+ tabs: Seq[String | Seq[UiElement]] = Nil,
+ tabPanels: Seq[Seq[UiElement]] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends UiComponent
+ with HasStyle:
+ type This = QuickTabs
+
+ def withTabs(tabs: String | Seq[UiElement]*): QuickTabs = copy(tabs = tabs)
+ def withTabPanels(tabPanels: Seq[UiElement]*): QuickTabs = copy(tabPanels = tabPanels)
+ def withTabPanelsSimple(tabPanels: UiElement*): QuickTabs = copy(tabPanels = tabPanels.map(e => Seq(e)))
+
+ override lazy val rendered =
+ Seq(
+ Tabs(key = subKey("tabs"), style = style).withChildren(
+ TabList(
+ key = subKey("tab-list"),
+ children = tabs.zipWithIndex.map:
+ case (name: String, idx) => Tab(key = s"$key-tab-$idx", text = name)
+ case (elements: Seq[UiElement], idx) => Tab(key = s"$key-tab-$idx", children = elements)
+ ),
+ TabPanels(
+ key = subKey("panels"),
+ children = tabPanels.zipWithIndex.map: (elements, idx) =>
+ TabPanel(key = s"$key-panel-$idx", children = elements)
+ )
+ )
+ )
+
+ override def withStyle(v: Map[String, Any]): QuickTabs = copy(style = v)
+ override def withKey(key: String): QuickTabs = copy(key = key)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala
deleted file mode 100644
index c0159356..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/extensions.scala
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.terminal21.client.components
-
-import org.terminal21.client.ConnectedSession
-
-extension (s: Seq[UiElement])
- def render()(using session: ConnectedSession): Unit =
- session.render(s: _*)
-
- def renderChanges()(using session: ConnectedSession): Unit =
- session.renderChanges(s: _*)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala
new file mode 100644
index 00000000..f4422ebf
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/frontend/FrontEndElement.scala
@@ -0,0 +1,11 @@
+package org.terminal21.client.components.frontend
+
+import org.terminal21.client.components.{Keys, UiElement}
+import org.terminal21.collections.TypedMap
+
+sealed trait FrontEndElement extends UiElement
+
+case class ThemeToggle(key: String = Keys.nextKey, dataStore: TypedMap = TypedMap.Empty) extends FrontEndElement:
+ override type This = ThemeToggle
+ override def withKey(key: String): ThemeToggle = copy(key = key)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala
index bf1b967c..1bfb5d3e 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdElement.scala
@@ -1,83 +1,101 @@
package org.terminal21.client.components.std
-import org.terminal21.client.components.UiElement.{Current, HasChildren, HasEventHandler, HasStyle}
-import org.terminal21.client.components.{Keys, UiElement}
-import org.terminal21.client.{ConnectedSession, OnChangeEventHandler}
+import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent
+import org.terminal21.client.components.UiElement.{HasChildren, HasStyle}
+import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement}
+import org.terminal21.collections.TypedMap
-sealed trait StdEJson extends UiElement
-sealed trait StdElement[A <: UiElement] extends StdEJson with HasStyle[A] with Current[A]
+sealed trait StdEJson extends UiElement
+sealed trait StdElement extends StdEJson with HasStyle
-case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Span]:
+case class Span(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Span
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty) extends StdElement[NewLine]:
+case class NewLine(key: String = Keys.nextKey, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = NewLine
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Em]:
+case class Em(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Em
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header1]:
+case class Header1(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header1
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header2]:
+case class Header2(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header2
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header3]:
+case class Header3(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header3
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header4]:
+case class Header4(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header4
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header5]:
+case class Header5(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header5
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
-case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty) extends StdElement[Header6]:
+case class Header6(key: String = Keys.nextKey, text: String, style: Map[String, Any] = Map.empty, dataStore: TypedMap = TypedMap.Empty) extends StdElement:
+ type This = Header6
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Paragraph(
key: String = Keys.nextKey,
- text: String = "paragraph.text",
+ text: String = "",
style: Map[String, Any] = Map.empty,
- children: Seq[UiElement] = Nil
-) extends StdElement[Paragraph]
- with HasChildren[Paragraph]:
+ children: Seq[UiElement] = Nil,
+ dataStore: TypedMap = TypedMap.Empty
+) extends StdElement
+ with HasChildren:
+ type This = Paragraph
override def withChildren(cn: UiElement*) = copy(children = cn)
override def withStyle(v: Map[String, Any]) = copy(style = v)
def withKey(v: String) = copy(key = v)
def withText(v: String) = copy(text = v)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
case class Input(
key: String = Keys.nextKey,
`type`: String = "text",
- defaultValue: Option[String] = None,
+ defaultValue: String = "",
style: Map[String, Any] = Map.empty,
- value: Option[String] = None
-) extends StdElement[Input]
- with HasEventHandler:
- override def defaultEventHandler(session: ConnectedSession): OnChangeEventHandler = newValue => session.modified(copy(value = Some(newValue)))
- override def withStyle(v: Map[String, Any]) = copy(style = v)
- def withKey(v: String) = copy(key = v)
- def withType(v: String) = copy(`type` = v)
- def withDefaultValue(v: Option[String]) = copy(defaultValue = v)
- def withValue(v: Option[String]) = copy(value = v)
-
- def onChange(h: OnChangeEventHandler)(using session: ConnectedSession): Input =
- session.addEventHandler(key, h)
- this
+ dataStore: TypedMap = TypedMap.Empty
+) extends StdElement
+ with CanHandleOnChangeEvent:
+ type This = Input
+ override def withStyle(v: Map[String, Any]) = copy(style = v)
+ def withKey(v: String) = copy(key = v)
+ def withType(v: String) = copy(`type` = v)
+ def withDefaultValue(v: String) = copy(defaultValue = v)
+ override def withDataStore(ds: TypedMap): Input = copy(dataStore = ds)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala
new file mode 100644
index 00000000..18ed771f
--- /dev/null
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/components/std/StdHttp.scala
@@ -0,0 +1,49 @@
+package org.terminal21.client.components.std
+
+import org.terminal21.client.components.OnChangeEventHandler.CanHandleOnChangeEvent
+import org.terminal21.client.components.{Keys, OnChangeEventHandler, UiElement}
+import org.terminal21.collections.TypedMap
+
+/** Elements mapping to Http functionality
+ */
+sealed trait StdHttp extends UiElement:
+ /** Each requestId will be processed only once per browser.
+ *
+ * I.e. lets say we have the Cookie(). If we add a cookie, we send it to the UI which in turn checks if it already set the cookie via the requestId. If it
+ * did, it skips it, if it didn't it sets the cookie.
+ *
+ * @return
+ * Should always be TransientRequest.newRequestId()
+ */
+ def requestId: String
+
+/** On the browser, https://github.com/js-cookie/js-cookie is used.
+ *
+ * Set a cookie on the browser.
+ */
+case class Cookie(
+ key: String = Keys.nextKey,
+ name: String = "cookie.name",
+ value: String = "cookie.value",
+ path: Option[String] = None,
+ expireDays: Option[Int] = None,
+ requestId: String = "cookie-set-req",
+ dataStore: TypedMap = TypedMap.Empty
+) extends StdHttp:
+ override type This = Cookie
+ override def withKey(key: String): Cookie = copy(key = key)
+ override def withDataStore(ds: TypedMap) = copy(dataStore = ds)
+
+/** Read a cookie value. The value, when read from the ui, it will reflect in `value` assuming the UI had the time to send the value back. Also the onChange
+ * handler will be called once with the value.
+ */
+case class CookieReader(
+ key: String = Keys.nextKey,
+ name: String = "cookie.name",
+ requestId: String = "cookie-read-req",
+ dataStore: TypedMap = TypedMap.Empty
+) extends StdHttp
+ with CanHandleOnChangeEvent:
+ type This = CookieReader
+ override def withDataStore(ds: TypedMap): CookieReader = copy(dataStore = ds)
+ override def withKey(key: String): CookieReader = copy(key = key)
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala
deleted file mode 100644
index 39ba715e..00000000
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/internal/EventHandlers.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.terminal21.client.internal
-
-import org.terminal21.client.{ConnectedSession, EventHandler}
-import org.terminal21.client.components.UiElement
-import org.terminal21.client.components.UiElement.{HasEventHandler, allDeep}
-
-class EventHandlers(session: ConnectedSession):
- private val eventHandlers = collection.concurrent.TrieMap.empty[String, List[EventHandler]]
-
- def registerEventHandlers(es: Seq[UiElement]): Unit = synchronized:
- val all = allDeep(es)
- val withEvents = all.collect:
- case h: HasEventHandler => h
-
- for e <- withEvents do addEventHandlerAtTheTop(e.key, e.defaultEventHandler(session))
-
- def addEventHandler(key: String, handler: EventHandler): Unit =
- val handlers = eventHandlers.getOrElse(key, Nil)
- eventHandlers += key -> (handlers :+ handler)
-
- def getEventHandler(key: String): Option[List[EventHandler]] = eventHandlers.get(key)
-
- private def addEventHandlerAtTheTop(key: String, handler: EventHandler): Unit =
- val handlers = eventHandlers.getOrElse(key, Nil)
- eventHandlers += key -> (handler :: handlers)
-
- def clear(): Unit = synchronized:
- eventHandlers.clear()
diff --git a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala
similarity index 61%
rename from terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala
rename to terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala
index 04993b10..2a6ff9a0 100644
--- a/terminal21-ui-std/src/main/scala/org/terminal21/client/components/UiElementEncoding.scala
+++ b/terminal21-ui-std/src/main/scala/org/terminal21/client/json/UiElementEncoding.scala
@@ -1,10 +1,13 @@
-package org.terminal21.client.components
+package org.terminal21.client.json
import io.circe.*
import io.circe.generic.auto.*
import io.circe.syntax.*
import org.terminal21.client.components.chakra.{Box, CEJson, ChakraElement}
-import org.terminal21.client.components.std.{StdEJson, StdElement}
+import org.terminal21.client.components.frontend.FrontEndElement
+import org.terminal21.client.components.std.{StdEJson, StdElement, StdHttp}
+import org.terminal21.client.components.{ComponentLib, UiComponent, UiElement}
+import org.terminal21.collections.TypedMap
class UiElementEncoding(libs: Seq[ComponentLib]):
given uiElementEncoder: Encoder[UiElement] =
@@ -29,9 +32,12 @@ object StdElementEncoding extends ComponentLib:
)
Json.obj(vs: _*)
+ given Encoder[TypedMap] = _ => Json.Null
+
override def toJson(using Encoder[UiElement]): PartialFunction[UiElement, Json] =
- case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson))
- case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson))
- case c: UiComponent =>
- val b: ChakraElement[Box] = Box(key = c.key, text = "")
- b.asJson.mapObject(o => o.add("type", "Chakra".asJson))
+ case std: StdEJson => std.asJson.mapObject(o => o.add("type", "Std".asJson))
+ case c: CEJson => c.asJson.mapObject(o => o.add("type", "Chakra".asJson))
+ case std: StdHttp => std.asJson.mapObject(o => o.add("type", "Std".asJson))
+ case fe: FrontEndElement => fe.asJson.mapObject(o => o.add("type", "FrontEnd".asJson))
+ case _: UiComponent =>
+ throw new IllegalStateException("substitute all components before serializing")
diff --git a/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala b/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala
deleted file mode 100644
index 5d6da43c..00000000
--- a/terminal21-ui-std/src/test/scala/generator/GenerateIconsCode.scala
+++ /dev/null
@@ -1,82 +0,0 @@
-package generator
-
-@main def generateIconsCode() =
- val icons = Seq(
- "AddIcon",
- "ArrowBackIcon",
- "ArrowDownIcon",
- "ArrowForwardIcon",
- "ArrowLeftIcon",
- "ArrowRightIcon",
- "ArrowUpIcon",
- "ArrowUpDownIcon",
- "AtSignIcon",
- "AttachmentIcon",
- "BellIcon",
- "CalendarIcon",
- "ChatIcon",
- "CheckIcon",
- "CheckCircleIcon",
- "ChevronDownIcon",
- "ChevronLeftIcon",
- "ChevronRightIcon",
- "ChevronUpIcon",
- "CloseIcon",
- "CopyIcon",
- "DeleteIcon",
- "DownloadIcon",
- "DragHandleIcon",
- "EditIcon",
- "EmailIcon",
- "ExternalLinkIcon",
- "HamburgerIcon",
- "InfoIcon",
- "InfoOutlineIcon",
- "LinkIcon",
- "LockIcon",
- "MinusIcon",
- "MoonIcon",
- "NotAllowedIcon",
- "PhoneIcon",
- "PlusSquareIcon",
- "QuestionIcon",
- "QuestionOutlineIcon",
- "RepeatIcon",
- "RepeatClockIcon",
- "SearchIcon",
- "Search2Icon",
- "SettingsIcon",
- "SmallAddIcon",
- "SmallCloseIcon",
- "SpinnerIcon",
- "StarIcon",
- "SunIcon",
- "TimeIcon",
- "TriangleDownIcon",
- "TriangleUpIcon",
- "UnlockIcon",
- "UpDownIcon",
- "ViewIcon",
- "ViewOffIcon",
- "WarningIcon",
- "WarningTwoIcon"
- )
-
- println("// ------------------------ SCALA CODE -------------------------------------")
- for clz <- icons do println(s"""
- |/** generated by generateIconsCode() , https://chakra-ui.com/docs/components/icon
- | */
- |case class $clz(
- | key: String = Keys.nextKey,
- | @volatile var w: Option[String] = None,
- | @volatile var h: Option[String] = None,
- | @volatile var boxSize: Option[String] = None,
- | @volatile var color: Option[String] = None
- |) extends ChakraElement
- |
- |""".stripMargin)
- println("// ------------------------ SCALA USING CODE -------------------------------------")
- println(icons.map(i => s"$i()").mkString(","))
- println("// ------------------------ TSX CODE -------------------------------------")
- println(s"import { ${icons.mkString(",")} } from '@chakra-ui/icons';")
- for clz <- icons do println(s"$clz: (b:any) => (<$clz {...b}/>),")
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala
deleted file mode 100644
index 9fea22ac..00000000
--- a/terminal21-ui-std/src/test/scala/org/terminal21/client/CalculationTest.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.terminal21.client
-
-import functions.fibers.FiberExecutor
-import org.scalatest.funsuite.AnyFunSuiteLike
-import org.scalatest.matchers.should.Matchers.*
-
-import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
-import org.scalatest.concurrent.Eventually.*
-import org.terminal21.client.components.Calculation
-
-class CalculationTest extends AnyFunSuiteLike:
- given executor: FiberExecutor = FiberExecutor()
- def testCalc(i: Int) = i + 1
- def testCalcString(i: Int): String = (i + 10).toString
-
- class Calc extends Calculation[Int]:
- val whenResultsNotReadyCalled = new AtomicBoolean(false)
- val whenResultsReadyValue = new AtomicInteger(-1)
- override protected def whenResultsNotReady(): Unit = whenResultsNotReadyCalled.set(true)
- override protected def whenResultsReady(results: Int): Unit = whenResultsReadyValue.set(results)
- override protected def calculation() = 2
-
- test("calculates"):
- val calc = new Calc
- calc.run().get() should be(2)
-
- test("calls whenResultsNotReady"):
- val calc = new Calc
- calc.run()
- eventually:
- calc.whenResultsNotReadyCalled.get() should be(true)
-
- test("calls whenResultsReady"):
- val calc = new Calc
- calc.run()
- eventually:
- calc.whenResultsReadyValue.get() should be(2)
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala
index 6323b0c7..49bd33d6 100644
--- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionMock.scala
@@ -1,7 +1,7 @@
package org.terminal21.client
import org.mockito.Mockito.mock
-import org.terminal21.client.components.{StdElementEncoding, UiElementEncoding}
+import org.terminal21.client.json.{StdElementEncoding, UiElementEncoding}
import org.terminal21.model.CommonModelBuilders.session
import org.terminal21.ui.std.SessionsService
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
index 5b26595c..3eabb616 100644
--- a/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ConnectedSessionTest.scala
@@ -12,52 +12,30 @@ import org.terminal21.ui.std.ServerJson
class ConnectedSessionTest extends AnyFunSuiteLike:
- test("default event handlers are invoked before user handlers"):
+ test("event iterator"):
given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
- val editable = Editable()
- editable.onChange: newValue =>
- editable.current.value should be(newValue)
-
- connectedSession.render(editable)
- connectedSession.fireEvent(OnChange(editable.key, "new value"))
-
- test("to server json"):
- val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock
-
- val p1 = Paragraph(text = "p1")
- val span1 = Span(text = "span1")
- connectedSession.render(p1.withChildren(span1))
- connectedSession.render()
- verify(sessionService).setSessionJsonState(
- connectedSession.session,
- ServerJson(
- Seq(p1.key),
- Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)),
- Map(p1.key -> Seq(span1.key), span1.key -> Nil)
+ val editable = Editable(key = "ed")
+ val it = connectedSession.eventIterator
+ val event1 = OnChange(editable.key, "v1")
+ val event2 = OnChange(editable.key, "v2")
+ connectedSession.fireEvent(event1)
+ connectedSession.fireEvent(event2)
+ connectedSession.clear()
+ it.toList should be(
+ List(
+ event1,
+ event2
)
)
- test("renderChanges changes state on server"):
+ test("to server json"):
val (sessionService, connectedSession) = ConnectedSessionMock.newConnectedSessionAndSessionServiceMock
-
- val p1 = Paragraph(text = "p1")
- val span1 = Span(text = "span1")
- connectedSession.render(p1)
- connectedSession.renderChanges(p1.withChildren(span1))
- verify(sessionService).changeSessionJsonState(
+ val span1 = Span("sk", text = "span1")
+ val p1 = Paragraph("pk", text = "p1").withChildren(span1)
+ connectedSession.render(Seq(p1.withChildren(span1)))
+ verify(sessionService).setSessionJsonState(
connectedSession.session,
ServerJson(
- Seq(p1.key),
- Map(p1.key -> encoder(p1.withChildren()), span1.key -> encoder(span1)),
- Map(p1.key -> Seq(span1.key), span1.key -> Nil)
+ Seq(encoder(p1).deepDropNullValues)
)
)
-
- test("renderChanges updates current version of component"):
- given connectedSession: ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
-
- val p1 = Paragraph(text = "p1")
- val span1 = Span(text = "span1")
- connectedSession.render(p1)
- connectedSession.renderChanges(p1.withChildren(span1))
- p1.current.children should be(Seq(span1))
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala
new file mode 100644
index 00000000..36f71d0c
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/ControllerTest.scala
@@ -0,0 +1,128 @@
+package org.terminal21.client
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.components.UiElement
+import org.terminal21.client.components.chakra.*
+import org.terminal21.client.components.std.Input
+import org.terminal21.collections.SEList
+import org.terminal21.model.{CommandEvent, OnChange, OnClick}
+
+class ControllerTest extends AnyFunSuiteLike:
+ val button = Button("b1")
+ val buttonClick = OnClick(button.key)
+ val input = Input("i1")
+ val inputChange = OnChange(input.key, "new-value")
+ val checkbox = Checkbox("c1")
+ val checkBoxChange = OnChange(checkbox.key, "true")
+ given ConnectedSession = ConnectedSessionMock.newConnectedSessionMock
+
+ def newController[M](
+ events: Seq[CommandEvent],
+ mvFunction: ModelViewFunction[M],
+ renderChanges: Seq[UiElement] => Unit = _ => ()
+ ): Controller[M] =
+ val seList = SEList[CommandEvent]()
+ val it = seList.iterator
+ events.foreach(e => seList.add(e))
+ seList.add(CommandEvent.sessionClosed)
+ new Controller(it, renderChanges, mvFunction)
+
+ test("model updated"):
+ def components(m: Int, events: Events) = MV(m + 1, Box())
+ newController(Seq(buttonClick), components).render(0).iterator.map(_.model).toList should be(List(1, 2))
+
+ test("model when terminated"):
+ def components(m: Int, events: Events) =
+ MV(100, Seq(Box()), terminate = true)
+ newController(Seq(buttonClick), components).render(0).iterator.map(_.model).toList should be(List(100))
+
+ test("view updated"):
+ def components(m: Int, events: Events) = MV(m + 1, Box(text = m.toString))
+ newController(Seq(buttonClick), components).render(0).iterator.map(_.view).toList should be(Seq(Seq(Box(text = "0")), Seq(Box(text = "1"))))
+
+ test("renderChanges() not invoked if no UI changed"):
+ def components(m: Int, events: Events) =
+ MV(
+ m + 1,
+ Box().withChildren(button, input, checkbox)
+ )
+
+ var rendered = List.empty[Seq[UiElement]]
+ def renderChanges(es: Seq[UiElement]) =
+ rendered = rendered :+ es
+
+ newController(Seq(buttonClick, checkBoxChange, inputChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3, 4))
+ rendered.size should be(1)
+
+ test("renderChanges() invoked if UI changed"):
+
+ def components(m: Int, events: Events) =
+ MV(
+ m + 1,
+ Box(text = s"m=$m").withChildren(button, input, checkbox)
+ )
+
+ var rendered = List.empty[Seq[UiElement]]
+
+ def renderChanges(es: Seq[UiElement]) =
+ rendered = rendered :+ es
+
+ newController(Seq(buttonClick, checkBoxChange), components, renderChanges).render(0).iterator.map(_.model).toList should be(List(1, 2, 3))
+ rendered.size should be(3)
+
+ test("events isClicked positive"):
+ def components(m: Boolean, events: Events) = MV(events.isClicked(button), button)
+ newController(Seq(buttonClick), components).render(false).iterator.lastOption.map(_.model) should be(Some(true))
+
+ test("events isClicked negative"):
+ def components(m: Boolean, events: Events) = MV(events.isClicked(button), button)
+ newController(Seq(checkBoxChange), components).render(false).iterator.lastOption.map(_.model) should be(Some(false))
+
+ test("events changedValue positive"):
+ def components(m: String, events: Events) = MV(events.changedValue(input, "x"), button)
+ newController(Seq(inputChange), components).render("").iterator.lastOption.map(_.model) should be(Some("new-value"))
+
+ test("events changedValue negative"):
+ def components(m: String, events: Events) = MV(events.changedValue(input, "x"), button)
+ newController(Seq(buttonClick), components).render("").iterator.lastOption.map(_.model) should be(Some("x"))
+
+ test("events changedBooleanValue positive"):
+ def components(m: Boolean, events: Events) = MV(events.changedBooleanValue(checkbox, false), button)
+ newController(Seq(checkBoxChange), components).render(false).iterator.lastOption.map(_.model) should be(Some(true))
+
+ test("events changedBooleanValue negative"):
+ def components(m: Boolean, events: Events) = MV(events.changedBooleanValue(checkbox, false), button)
+ newController(Seq(buttonClick), components).render(false).iterator.lastOption.map(_.model) should be(Some(false))
+
+ test("poc"):
+ case class Person(id: Int, name: String)
+ def personComponent(person: Person, events: Events): MV[Person] =
+ val nameInput = Input(s"person-${person.id}", defaultValue = person.name)
+ val component = Box()
+ .withChildren(
+ Text(text = "Name"),
+ nameInput
+ )
+ MV(
+ person.copy(
+ name = events.changedValue(nameInput, person.name)
+ ),
+ component
+ )
+
+ def peopleComponent(people: Seq[Person], events: Events): MV[Seq[Person]] =
+ val peopleComponents = people.map(p => personComponent(p, events))
+ val component = QuickTable("people")
+ .withRows(peopleComponents.map(p => Seq(p.view)))
+ MV(peopleComponents.map(_.model), component)
+
+ val p1 = Person(10, "person 1")
+ val p2 = Person(20, "person 2")
+ val people = Seq(p1, p2)
+ val mv = newController(Seq(OnChange("person-10", "changed p10")), peopleComponent)
+ .render(people)
+ .iterator
+ .lastOption
+ .get
+ mv.model should be(Seq(p1.copy(name = "changed p10"), p2))
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala
new file mode 100644
index 00000000..53c5891f
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/collections/EventIteratorTest.scala
@@ -0,0 +1,28 @@
+package org.terminal21.client.collections
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.ConnectedSessionMock
+import org.terminal21.model.{CommandEvent, SessionClosed}
+
+class EventIteratorTest extends AnyFunSuiteLike:
+ test("works as normal iterator"):
+ EventIterator(1, 2, 3).toList should be(List(1, 2, 3))
+
+ test("works as normal iterator when empty"):
+ EventIterator().toList should be(Nil)
+
+ test("lastOption when available"):
+ EventIterator(1, 2, 3).lastOption should be(Some(3))
+
+ test("lastOption when not available"):
+ EventIterator().lastOption should be(None)
+
+ test("lastOptionOrNoneIfSessionClosed when session open"):
+ val session = ConnectedSessionMock.newConnectedSessionMock
+ EventIterator(1, 2).lastOptionOrNoneIfSessionClosed(using session) should be(Some(2))
+
+ test("lastOptionOrNoneIfSessionClosed when session closed"):
+ val session = ConnectedSessionMock.newConnectedSessionMock
+ session.fireEvent(CommandEvent.sessionClosed)
+ EventIterator(1, 2).lastOptionOrNoneIfSessionClosed(using session) should be(None)
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala
new file mode 100644
index 00000000..eaa94f86
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/components/UiElementTest.scala
@@ -0,0 +1,28 @@
+package org.terminal21.client.components
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.scalatest.matchers.should.Matchers.*
+import org.terminal21.client.components.chakra.{Box, QuickTable, Text}
+import org.terminal21.client.components.std.Paragraph
+
+class UiElementTest extends AnyFunSuiteLike:
+ test("flat"):
+ val box = Box(key = "k1").withChildren(Text(key = "k2"), Text(key = "k3"))
+ box.flat should be(
+ Seq(box, Text(key = "k2"), Text(key = "k3"))
+ )
+ test("findKey"):
+ Box(key = "k1").withChildren(Text(key = "k2"), Text(key = "k3")).findKey("k3") should be(Text(key = "k3"))
+
+ test("substituteComponents when not component"):
+ val e = Text()
+ e.substituteComponents should be(e)
+
+ test("substituteComponents when component"):
+ val e = QuickTable(key = "k1")
+ e.substituteComponents should be(Box("k1", children = e.rendered))
+
+ test("substituteComponents when children are component"):
+ val t = QuickTable(key = "k1")
+ val e = Paragraph(key = "p1").withChildren(t)
+ e.substituteComponents should be(Paragraph(key = "p1").withChildren(Box("k1", children = t.rendered)))
diff --git a/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala
new file mode 100644
index 00000000..594c22ec
--- /dev/null
+++ b/terminal21-ui-std/src/test/scala/org/terminal21/client/json/UiElementEncodingTest.scala
@@ -0,0 +1,12 @@
+package org.terminal21.client.json
+
+import org.scalatest.funsuite.AnyFunSuiteLike
+import org.terminal21.client.components.chakra.Button
+import org.scalatest.matchers.should.Matchers.*
+
+class UiElementEncodingTest extends AnyFunSuiteLike:
+ val encoding = new UiElementEncoding(Seq(StdElementEncoding))
+ test("dataStore"):
+ val b = Button(key = "b")
+ val j = encoding.uiElementEncoder(b).deepDropNullValues
+ j.hcursor.downField("Button").downField("dataStore").failed should be(true)