diff --git a/build.sbt b/build.sbt index b24edcc..042e3e2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ val scala33 = "3.5.0" val tapirVersion = "1.11.1" -val laminarVersion = "17.0.0" +val laminarVersion = "17.1.0" inThisBuild( List( @@ -161,6 +161,7 @@ lazy val core = scalajsProject("core", false) .settings(scalacOptions ++= usedScalacOptions) .settings( libraryDependencies ++= Seq( + "io.github.cquiroz" %%% "scala-java-time" % "2.6.0", "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.7", "com.raquo" %%% "laminar" % laminarVersion, // "io.laminext" %%% "websocket" % laminarVersion, @@ -182,7 +183,7 @@ lazy val ui5 = scalajsProject("ui5", false) .dependsOn(core) .settings( libraryDependencies ++= Seq( - "be.doeraene" %%% "web-components-ui5" % "1.21.0" + "be.doeraene" %%% "web-components-ui5" % "2.0.0-RC1" ) ) diff --git a/examples/client/package-lock.json b/examples/client/package-lock.json index 4599565..54b8a68 100644 --- a/examples/client/package-lock.json +++ b/examples/client/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@ui5/webcomponents": "1.24.0", - "@ui5/webcomponents-fiori": "1.24.0", - "@ui5/webcomponents-icons": "1.24.0", + "@ui5/webcomponents": "2.0.1", + "@ui5/webcomponents-fiori": "2.0.1", + "@ui5/webcomponents-icons": "2.0.1", "highlight.js": "^11.8.0", "jsdom": "^9.9.0" }, @@ -453,9 +453,10 @@ } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", - "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", + "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.17.2", @@ -668,7 +669,8 @@ "node_modules/@sap-theming/theming-base-content": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@sap-theming/theming-base-content/-/theming-base-content-11.12.0.tgz", - "integrity": "sha512-kPHlziH8e6W8VjzljOiNjgBz81GuvC8WUAi7K6F5k+ZaRc1DUkDU12x9k6B0l4u9nPtprdZTse55r3PFGuELdQ==" + "integrity": "sha512-kPHlziH8e6W8VjzljOiNjgBz81GuvC8WUAi7K6F5k+ZaRc1DUkDU12x9k6B0l4u9nPtprdZTse55r3PFGuELdQ==", + "license": "Apache-2.0" }, "node_modules/@scala-js/vite-plugin-scalajs": { "version": "1.0.0", @@ -1143,6 +1145,7 @@ "version": "3.5.30", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.30.tgz", "integrity": "sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==", + "license": "MIT", "dependencies": { "@types/sizzle": "*" } @@ -1158,9 +1161,10 @@ } }, "node_modules/@types/openui5": { - "version": "1.124.0", - "resolved": "https://registry.npmjs.org/@types/openui5/-/openui5-1.124.0.tgz", - "integrity": "sha512-lRn2aXgmgScvQfmeVEnqWWQSG5mXBmVcyB7Llb/PqTgGNu16ykXUkRqYxfG9YE6I5GPWRDR46GGn1HZ5T5pOJQ==", + "version": "1.127.0", + "resolved": "https://registry.npmjs.org/@types/openui5/-/openui5-1.127.0.tgz", + "integrity": "sha512-MIauyuHgaNnN7PDMZ71vS9XqpVlo0tXL6EaMhdmNjQTMblm6IJxlmOz0UX1BkdJwGKIA2rVE+2WGz6xn7ndJaQ==", + "license": "MIT", "dependencies": { "@types/jquery": "~3.5.13", "@types/qunit": "^2.5.4" @@ -1169,92 +1173,103 @@ "node_modules/@types/qunit": { "version": "2.19.10", "resolved": "https://registry.npmjs.org/@types/qunit/-/qunit-2.19.10.tgz", - "integrity": "sha512-gVB+rxvxmbyPFWa6yjjKgcumWal3hyqoTXI0Oil161uWfo1OCzWZ/rnEumsx+6uVgrwPrCrhpQbLkzfildkSbg==" + "integrity": "sha512-gVB+rxvxmbyPFWa6yjjKgcumWal3hyqoTXI0Oil161uWfo1OCzWZ/rnEumsx+6uVgrwPrCrhpQbLkzfildkSbg==", + "license": "MIT" }, "node_modules/@types/sizzle": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" }, "node_modules/@ui5/webcomponents": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents/-/webcomponents-1.24.0.tgz", - "integrity": "sha512-ctGk8t7wrlF7ZukDrd5kjseSIiKvnJJCEDeLgZCHL0Sd5lTMmZAOpa9OQOLLTf2UXH6uTRavFGgm6zlEIkfXEg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents/-/webcomponents-2.0.1.tgz", + "integrity": "sha512-+TLTH7PUYbThx/47NIEJLrMo/NnaIUIbgTQnWEkPjWH92YGnCQQh/je6Ml4E+vkUVwcEGzb1fTDIB3alHhVctQ==", + "license": "Apache-2.0", "dependencies": { - "@ui5/webcomponents-base": "1.24.0", - "@ui5/webcomponents-icons": "1.24.0", - "@ui5/webcomponents-icons-business-suite": "1.24.0", - "@ui5/webcomponents-icons-tnt": "1.24.0", - "@ui5/webcomponents-localization": "1.24.0", - "@ui5/webcomponents-theming": "1.24.0" + "@ui5/webcomponents-base": "2.0.1", + "@ui5/webcomponents-icons": "2.0.1", + "@ui5/webcomponents-icons-business-suite": "2.0.1", + "@ui5/webcomponents-icons-tnt": "2.0.1", + "@ui5/webcomponents-localization": "2.0.1", + "@ui5/webcomponents-theming": "2.0.1" } }, "node_modules/@ui5/webcomponents-base": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-base/-/webcomponents-base-1.24.0.tgz", - "integrity": "sha512-8fWEUzFBCG6ovjaMI1G/h6bvg51iopCtjKWBK3UueVMiajdVWgidukW8M27NUlu4UfnTEp44eLA28F6v2XWCiQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-base/-/webcomponents-base-2.0.1.tgz", + "integrity": "sha512-jdoUmPbZxMTjyx4MOjCFrxz1OCV/dIBiB/4mas4BkiZLV+jYc/jrPSckdko39KR1m152pIN1n1RxtxWa3S1cPA==", + "license": "Apache-2.0", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.1.2", "lit-html": "^2.0.1" } }, "node_modules/@ui5/webcomponents-fiori": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-fiori/-/webcomponents-fiori-1.24.0.tgz", - "integrity": "sha512-zHsOA5WOFq7LyiLEJjgSvDUnjPHkJuG0JZfKQn7PjmSD30tLQywGqkaZ6mjqm+o3W3MvmdddnucCwos3CMDWeA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-fiori/-/webcomponents-fiori-2.0.1.tgz", + "integrity": "sha512-o+txrbWlY7TonPfw/5adUYma5imJBDDP0M4IuJnsfp8CiZEh0gKn+07C2M3PaIwcIaRmYnTfqa4A3mj8zyQqSg==", + "license": "Apache-2.0", "dependencies": { - "@ui5/webcomponents": "1.24.0", - "@ui5/webcomponents-base": "1.24.0", - "@ui5/webcomponents-icons": "1.24.0", - "@ui5/webcomponents-theming": "1.24.0", + "@ui5/webcomponents": "2.0.1", + "@ui5/webcomponents-base": "2.0.1", + "@ui5/webcomponents-icons": "2.0.1", + "@ui5/webcomponents-theming": "2.0.1", "@zxing/library": "^0.17.1" } }, "node_modules/@ui5/webcomponents-icons": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons/-/webcomponents-icons-1.24.0.tgz", - "integrity": "sha512-52ZNZC5/+/MuD+rzgxrk7I8BJhI0EKPro/pVwTt5I8D7geEbP6MO9/KPeDvq/EhHbyPEXCKZA2YP0AnTyciheA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons/-/webcomponents-icons-2.0.1.tgz", + "integrity": "sha512-dRSB3722pdQG3+ciKhGlJpU/nlnAQ63SgJ2PKXlIXvcbgc8BMRprasfz7Y7l9ZRsAMZZ2TVO3PUhLNMkZS6vlg==", + "license": "Apache-2.0", "dependencies": { - "@ui5/webcomponents-base": "1.24.0" + "@ui5/webcomponents-base": "2.0.1" } }, "node_modules/@ui5/webcomponents-icons-business-suite": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-business-suite/-/webcomponents-icons-business-suite-1.24.0.tgz", - "integrity": "sha512-YZY7uw4RPlrGqP7VySlDuaBJQ3SQtHlqIYFT3c8ZIKjD+IfiYTe46ha+56Ce7A2rrA/6bW0O8hp7N47EMu5w/A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-business-suite/-/webcomponents-icons-business-suite-2.0.1.tgz", + "integrity": "sha512-OQsLFFMlJWuj7ENFg7ZT1fiEpEqSWPnQ7I/EIIqPyBWjpgvDb84vzOv+c2Hou2DpNB/dGH0gAGmkWJnDHealYw==", + "license": "Apache-2.0", "dependencies": { - "@ui5/webcomponents-base": "1.24.0" + "@ui5/webcomponents-base": "2.0.1" } }, "node_modules/@ui5/webcomponents-icons-tnt": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-tnt/-/webcomponents-icons-tnt-1.24.0.tgz", - "integrity": "sha512-JavMGjBUNaBTnms9p+iqHcXZA0rSYSWl+zj+W1QOe5/e+YsbK/ms4Ipj6DjupV985XhNyRsN8qmLydTUdX90/Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-icons-tnt/-/webcomponents-icons-tnt-2.0.1.tgz", + "integrity": "sha512-/4QwEYZ0UfJRrRhG7o4kAjoFZ2Wy8bEThq+ZTV1afqJWCiPDcrKZKhPqIEOCHIE1y2TMjmo/w7UIZSIBGF81oQ==", + "license": "Apache-2.0", "dependencies": { - "@ui5/webcomponents-base": "1.24.0" + "@ui5/webcomponents-base": "2.0.1" } }, "node_modules/@ui5/webcomponents-localization": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-localization/-/webcomponents-localization-1.24.0.tgz", - "integrity": "sha512-qV764Olcgd8uT/kQnHYNZIiOrcsPeLUoij+Sv+WcL63dt8+d8ue3BOisaKmVDUSkyIsv0Rb28ZU9ne9l+7EeYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-localization/-/webcomponents-localization-2.0.1.tgz", + "integrity": "sha512-GHtpC9Ayo4Nha2nRgUfc6dnz8qAmj73ldCfWBh2arcDQ6VY7ZdI5d6mSTPQeNn9/5ZzvtDccJScN+IMO1z169g==", + "license": "Apache-2.0", "dependencies": { "@types/openui5": "^1.113.0", - "@ui5/webcomponents-base": "1.24.0" + "@ui5/webcomponents-base": "2.0.1" } }, "node_modules/@ui5/webcomponents-theming": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ui5/webcomponents-theming/-/webcomponents-theming-1.24.0.tgz", - "integrity": "sha512-boMrIIgU+UJApfz/4a8Hq9jFHrNT69zjXRzBtqWYttUAhr3Jl2mpdGNwVbcwU7YRL3+fxm86ngYnJ7EEwfEakQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ui5/webcomponents-theming/-/webcomponents-theming-2.0.1.tgz", + "integrity": "sha512-HdofJMU8xPDWReq3zjj6kW0HM3rvXfDm5lhIi3cFkqxZJN0nxY4nVCdIIrwzROxYNupeMmYvnj776B3GsAoAwQ==", + "license": "Apache-2.0", "dependencies": { "@sap-theming/theming-base-content": "11.12.0", - "@ui5/webcomponents-base": "1.24.0" + "@ui5/webcomponents-base": "2.0.1" } }, "node_modules/@zxing/library": { @@ -1753,6 +1768,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" } diff --git a/examples/client/package.json b/examples/client/package.json index e4ed609..49b5c92 100644 --- a/examples/client/package.json +++ b/examples/client/package.json @@ -11,9 +11,9 @@ }, "license": "MIT", "dependencies": { - "@ui5/webcomponents": "1.24.0", - "@ui5/webcomponents-fiori": "1.24.0", - "@ui5/webcomponents-icons": "1.24.0", + "@ui5/webcomponents": "2.0.1", + "@ui5/webcomponents-fiori": "2.0.1", + "@ui5/webcomponents-icons": "2.0.1", "highlight.js": "^11.8.0", "jsdom": "^9.9.0" }, diff --git a/examples/client/src/main/scala/HelloWorld.scala b/examples/client/src/main/scala/HelloWorld.scala index 2d1cce1..e1cf970 100644 --- a/examples/client/src/main/scala/HelloWorld.scala +++ b/examples/client/src/main/scala/HelloWorld.scala @@ -8,7 +8,7 @@ case class Sample(name: String, component: HtmlElement) object App extends App { - val sample = Var(samples.tree.component) + val sample = Var(samples.list.component) private def item(name: String) = SideNavigation.item( _.text := name, diff --git a/examples/client/src/main/scala/samples/EitherSample.scala b/examples/client/src/main/scala/samples/EitherSample.scala index be34723..52d9f63 100644 --- a/examples/client/src/main/scala/samples/EitherSample.scala +++ b/examples/client/src/main/scala/samples/EitherSample.scala @@ -22,7 +22,7 @@ val either = Sample( s"$item" ) }, - Form.renderVar(eitherVar) + eitherVar.asForm ) } ) diff --git a/examples/client/src/main/scala/samples/EnumSample.scala b/examples/client/src/main/scala/samples/EnumSample.scala index be2b0bf..13adb3b 100644 --- a/examples/client/src/main/scala/samples/EnumSample.scala +++ b/examples/client/src/main/scala/samples/EnumSample.scala @@ -33,7 +33,7 @@ val enums = Sample( s"$item" ) }, - Form.renderVar(eitherVar) + eitherVar.asForm ) } ) diff --git a/examples/client/src/main/scala/samples/ListElement.scala b/examples/client/src/main/scala/samples/ListElement.scala index 6f89a4a..e420390 100644 --- a/examples/client/src/main/scala/samples/ListElement.scala +++ b/examples/client/src/main/scala/samples/ListElement.scala @@ -4,15 +4,19 @@ import dev.cheleb.scalamigen.{*, given} import com.raquo.laminar.api.L.* -case class Person2(name: String, age: Int) +case class Person2(id: Int, name: String, age: Int) case class ListElement( ints: List[Person2] ) -val listPersonVar = Var(ListElement(List(1, 2, 3).map(Person2("Vlad", _)))) +val listPersonVar = Var( + ListElement(List(1, 2, 3).map(id => Person2(id, "Vlad", 20))) +) val listIntVar = Var(List(1, 2, 3)) +given (Person2 => Int) = _.id + val list = Sample( "List", div( @@ -21,12 +25,12 @@ val list = Sample( s"$item" ) }, - Form.renderVar(listPersonVar), + listPersonVar.asForm, child <-- listIntVar.signal.map { item => div( s"$item" ) - }, - Form.renderVar(listIntVar) + } + // Form.renderVar(listIntVar) ) ) diff --git a/examples/client/src/main/scala/samples/Persons.scala b/examples/client/src/main/scala/samples/Persons.scala index 96c809e..36383f5 100644 --- a/examples/client/src/main/scala/samples/Persons.scala +++ b/examples/client/src/main/scala/samples/Persons.scala @@ -7,10 +7,14 @@ import magnolia1.* import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.all.* +import samples.model.Password +import java.time.LocalDate // Define some models case class Person( name: String, + password: Password, + birthDate: LocalDate, fav: Pet, pet: Option[Pet], email: Option[String], @@ -34,6 +38,8 @@ given Defaultable[Pet] with val vlad = Person( "Vlad", + Password("not a password"), + LocalDate.of(1431, 11, 8), Pet("Batman", 666, House(2), 169), Some(Pet("Wolfy", 12, House(1), 42)), Some("vlad.dracul@gmail.com"), @@ -51,6 +57,6 @@ val person = Sample( s"$item" ) }, - Form.renderVar(personVar) + personVar.asForm ) ) diff --git a/examples/client/src/main/scala/samples/SimpleSample.scala b/examples/client/src/main/scala/samples/SimpleSample.scala index ee04458..4e00d6e 100644 --- a/examples/client/src/main/scala/samples/SimpleSample.scala +++ b/examples/client/src/main/scala/samples/SimpleSample.scala @@ -17,7 +17,7 @@ val simple = Sample( s"$item" ) }, - Form.renderVar(eitherVar) + eitherVar.asForm ) } ) diff --git a/examples/client/src/main/scala/samples/Tree.scala b/examples/client/src/main/scala/samples/Tree.scala index 4603f36..3c94f9e 100644 --- a/examples/client/src/main/scala/samples/Tree.scala +++ b/examples/client/src/main/scala/samples/Tree.scala @@ -110,7 +110,7 @@ val tree = Sample( child <-- treeVar2.signal .distinctByFn(Tree.isSameStructure) .map { item => - Form.renderVar(treeVar2) + treeVar2.asForm } ) } diff --git a/examples/client/src/main/scala/samples/Validation.scala b/examples/client/src/main/scala/samples/Validation.scala index ac1b557..c2f5a44 100644 --- a/examples/client/src/main/scala/samples/Validation.scala +++ b/examples/client/src/main/scala/samples/Validation.scala @@ -38,6 +38,6 @@ val validation = Sample( s"$item" ) }, - Form.renderVar(ironSampleVar) + ironSampleVar.asForm ) ) diff --git a/examples/client/src/main/scala/samples/model/LoginPassword.scala b/examples/client/src/main/scala/samples/model/LoginPassword.scala new file mode 100644 index 0000000..6047f73 --- /dev/null +++ b/examples/client/src/main/scala/samples/model/LoginPassword.scala @@ -0,0 +1,24 @@ +package samples.model + +import dev.cheleb.scalamigen.Form +import com.raquo.laminar.api.L.* +import dev.cheleb.scalamigen.WidgetFactory + +opaque type Password = String + +object Password: + def apply(password: String): Password = password + given Form[Password] with + override def render( + variable: Var[Password], + syncParent: () => Unit, + values: List[Password] = List.empty + )(using factory: WidgetFactory): HtmlElement = + factory.renderSecret + .amend( + value <-- variable.signal, + onInput.mapToValue --> { v => + variable.set(v) + syncParent() + } + ) diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala index 2cf205b..9474de5 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala @@ -6,6 +6,11 @@ import magnolia1.* import scala.util.Try import com.raquo.airstream.state.Var import org.scalajs.dom.HTMLDivElement +import org.scalajs.dom.HTMLElement +import com.raquo.laminar.nodes.ReactiveHtmlElement + +extension [A](v: Var[A]) + def asForm(using WidgetFactory, Form[A]) = Form.renderVar(v) trait Form[A] { self => @@ -62,9 +67,9 @@ object Form extends AutoDerivation[Form] { WidgetFactory )(using fa: Form[A] - ) = { + ): ReactiveHtmlElement[HTMLElement] = fa.render(v, syncParent) - } + def join[A]( caseClass: CaseClass[Typeclass, A] ): Form[A] = new Form[A] { @@ -150,3 +155,43 @@ object Form extends AutoDerivation[Form] { } } + +/** Use this form to render a string that can be converted to A, can be used for + * Opaque types. + */ +def stringForm[A](to: String => A) = new Form[A]: + override def render( + variable: Var[A], + syncParent: () => Unit, + values: List[A] = List.empty + )(using factory: WidgetFactory): HtmlElement = + factory.renderText.amend( + value <-- variable.signal.map(_.toString), + onInput.mapToValue.map(to) --> { v => + variable.set(v) + syncParent() + } + ) + +/** Form for a numeric type. + */ +def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] { + self => + override def fromString(s: String): Option[A] = + f(s).orElse(Some(zero)) + override def render( + variable: Var[A], + syncParent: () => Unit, + values: List[A] = List.empty + )(using factory: WidgetFactory): HtmlElement = + factory.renderNumeric + .amend( + value <-- variable.signal.map { str => + str.toString() + }, + onInput.mapToValue --> { v => + fromString(v).foreach(variable.set) + syncParent() + } + ) +} diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/LaminarWidgetFactory.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/LaminarWidgetFactory.scala index 0a38a2c..885cd28 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/LaminarWidgetFactory.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/LaminarWidgetFactory.scala @@ -7,25 +7,35 @@ import org.scalajs.dom.HTMLSelectElement /** This is raw laminar implementation of the widget factory. */ object LaminarWidgetFactory extends WidgetFactory: - def renderText: HtmlElement = input( + + override def renderDatePicker: HtmlElement = input( + tpe := "date" + ) + + override def renderSecret: HtmlElement = input( + tpe := "password" + ) + + override def renderText: HtmlElement = input( tpe := "text" ) - def renderLabel(required: Boolean, name: String): HtmlElement = span( + override def renderLabel(required: Boolean, name: String): HtmlElement = span( name ) - def renderNumeric: HtmlElement = input( + override def renderNumeric: HtmlElement = input( tpe := "number" ) - def renderButton: HtmlElement = button() - def renderLink(text: String, el: EventListener[?, ?]): HtmlElement = a( - text, - href := "#", - el - ) - def renderUL(id: String): HtmlElement = ul(idAttr := id) - def renderPanel(headerText: String): HtmlElement = div(headerText) + override def renderButton: HtmlElement = button() + override def renderLink(text: String, el: EventListener[?, ?]): HtmlElement = + a( + text, + href := "#", + el + ) + override def renderUL(id: String): HtmlElement = ul(idAttr := id) + override def renderPanel(headerText: String): HtmlElement = div(headerText) - def renderSelect(f: Int => Unit): HtmlElement = select( + override def renderSelect(f: Int => Unit): HtmlElement = select( onChange.map( _.target.asInstanceOf[HTMLSelectElement].selectedIndex ) --> { ds => @@ -33,7 +43,11 @@ object LaminarWidgetFactory extends WidgetFactory: } ) - def renderOption(label: String, idx: Int, isSelected: Boolean): HtmlElement = + override def renderOption( + label: String, + idx: Int, + isSelected: Boolean + ): HtmlElement = option( label, value := s"$idx", diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/WidgetFactory.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/WidgetFactory.scala index d2ed0e5..c460c8a 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/WidgetFactory.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/WidgetFactory.scala @@ -6,10 +6,17 @@ import com.raquo.laminar.modifiers.EventListener /** This is a trait that defines the interface for the widget factory. */ trait WidgetFactory: + + def renderDatePicker: HtmlElement + /** Render a text input, for strings. */ def renderText: HtmlElement + /** Render a password input, for secret strings. + */ + def renderSecret: HtmlElement + /** Render a label for a widget. */ def renderLabel(required: Boolean, name: String): HtmlElement diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/Forms.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala similarity index 76% rename from modules/core/src/main/scala/dev/cheleb/scalamigen/Forms.scala rename to modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala index 1538e59..d164a12 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/Forms.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala @@ -7,7 +7,8 @@ import com.raquo.laminar.api.L.* import scala.util.Try import com.raquo.airstream.state.Var -import com.raquo.laminar.api.L + +import java.time.LocalDate /** Default value for Int is 0. */ @@ -45,7 +46,7 @@ given [T, C](using fv: IronTypeValidator[T, C]): Form[IronType[T, C]] = variable: Var[IronType[T, C]], syncParent: () => Unit, values: List[IronType[T, C]] - )(using factory: WidgetFactory): L.HtmlElement = + )(using factory: WidgetFactory): HtmlElement = val errorVar = Var("") div( @@ -98,46 +99,6 @@ given Form[String] with } ) -/** Use this form to render a string that can be converted to A, can be used for - * Opaque types. - */ -def stringForm[A](to: String => A) = new Form[A]: - override def render( - variable: Var[A], - syncParent: () => Unit, - values: List[A] = List.empty - )(using factory: WidgetFactory): HtmlElement = - factory.renderText.amend( - value <-- variable.signal.map(_.toString), - onInput.mapToValue.map(to) --> { v => - variable.set(v) - syncParent() - } - ) - -/** Form for a numeric type. - */ -def numericForm[A](f: String => Option[A], zero: A): Form[A] = new Form[A] { - self => - override def fromString(s: String): Option[A] = - f(s).orElse(Some(zero)) - override def render( - variable: Var[A], - syncParent: () => Unit, - values: List[A] = List.empty - )(using factory: WidgetFactory): HtmlElement = - factory.renderNumeric - .amend( - value <-- variable.signal.map { str => - str.toString() - }, - onInput.mapToValue --> { v => - fromString(v).foreach(variable.set) - syncParent() - } - ) -} - given Form[Nothing] = new Form[Nothing] { override def render( variable: Var[Nothing], @@ -155,6 +116,8 @@ given Form[BigInt] = given Form[BigDecimal] = numericForm(str => Try(BigDecimal(str)).toOption, BigDecimal(0)) +//given + given eitherOf[L, R](using lf: Form[L], rf: Form[R], @@ -260,7 +223,7 @@ given optionOfA[A](using ) } -given listOfA[A](using fa: Form[A]): Form[List[A]] = +given listOfA[A, K](using fa: Form[A], idOf: A => K): Form[List[A]] = new Form[List[A]] { override def render( @@ -268,40 +231,32 @@ given listOfA[A](using fa: Form[A]): Form[List[A]] = syncParent: () => Unit, values: List[List[A]] = List.empty )(using factory: WidgetFactory): HtmlElement = - - def renderNewA( - index: Int, - initialAatIdx: (A, Int), - aSignalAt: Signal[(A, Int)] - ) = - val va = Var(initialAatIdx._1) - - val formOfA = - if (fa.isAnyRef) - fa.render(va, () => variable.update(_.updated(index, va.now()))) - else - fa.render(va, syncParent) - .amend( - onInput.mapToValue --> { v => - fa.fromString(v).foreach { v => - variable.update(_.updated(index, v)) - } - } - ) - - div( - idAttr := s"list-item-$index", + div( + children <-- variable.split(idOf)((id, initial, aVar) => { div( - formOfA + idAttr := s"list-item-$id", + div( + fa.render(aVar, syncParent) + ) ) - ) + }) + ) + } - factory - .renderUL("list-of-string") +given Form[LocalDate] = new Form[LocalDate] { + override def render( + variable: Var[LocalDate], + syncParent: () => Unit, + values: List[LocalDate] = List.empty + )(using factory: WidgetFactory): HtmlElement = + div( + factory.renderDatePicker .amend( - children <-- variable - .zoom(_.zipWithIndex)((a, b) => b.map(_._1)) - .signal - .split(_._2)(renderNewA) + value <-- variable.signal.map(_.toString), + onChange.mapToValue --> { v => + variable.set(LocalDate.parse(v)) + syncParent() + } ) - } + ) +} diff --git a/modules/ui5/src/main/scala/dev/cheleb/scalamigen/ui5/UI5WidgetFactory.scala b/modules/ui5/src/main/scala/dev/cheleb/scalamigen/ui5/UI5WidgetFactory.scala index 98b5efe..77e842b 100644 --- a/modules/ui5/src/main/scala/dev/cheleb/scalamigen/ui5/UI5WidgetFactory.scala +++ b/modules/ui5/src/main/scala/dev/cheleb/scalamigen/ui5/UI5WidgetFactory.scala @@ -8,6 +8,8 @@ import be.doeraene.webcomponents.ui5.configkeys.ListSeparator import be.doeraene.webcomponents.ui5.configkeys.TitleLevel import dev.cheleb.scalamigen.WidgetFactory +import com.raquo.laminar.api.L +import be.doeraene.webcomponents.ui5.configkeys.InputType.Password /** UI5WidgetFactory is a factory for [SAP UI5 * widgets](https://sap.github.io/ui5-webcomponents/). @@ -16,33 +18,43 @@ import dev.cheleb.scalamigen.WidgetFactory * bindings](https://github.com/sherpal/LaminarSAPUI5Bindings). */ object UI5WidgetFactory extends WidgetFactory: - def renderText: HtmlElement = Input( + + override def renderDatePicker: L.HtmlElement = DatePicker( + _.formatPattern := "yyyy-MM-dd" + ) + + override def renderSecret: L.HtmlElement = Input( + _.tpe := Password + ) + + override def renderText: HtmlElement = Input( _.showClearIcon := true ) - def renderLabel(required: Boolean, name: String): HtmlElement = Label( - _.required := required, - _.showColon := false + override def renderLabel(required: Boolean, name: String): HtmlElement = + Label( + _.required := required, + _.showColon := false // _.text := name - ).amend(name) + ).amend(name) - def renderNumeric: HtmlElement = Input( + override def renderNumeric: HtmlElement = Input( tpe := "number" ) - def renderButton: HtmlElement = Button() - def renderLink(text: String, el: EventListener[?, ?]): HtmlElement = + override def renderButton: HtmlElement = Button() + override def renderLink(text: String, el: EventListener[?, ?]): HtmlElement = Link(text, el) - def renderUL(id: String): HtmlElement = UList( + override def renderUL(id: String): HtmlElement = UList( _.id := id, width := "100%", _.noDataText := "No data", _.separators := ListSeparator.None ) - def renderPanel(headerText: String): HtmlElement = Panel( + override def renderPanel(headerText: String): HtmlElement = Panel( _.headerText := headerText, _.headerLevel := TitleLevel.H3 ) - def renderSelect(f: Int => Unit): HtmlElement = Select( + override def renderSelect(f: Int => Unit): HtmlElement = Select( _.events.onChange .map(_.detail.selectedOption.dataset) --> { ds => ds.get("idx").foreach(idx => f(idx.toInt)) @@ -50,7 +62,7 @@ object UI5WidgetFactory extends WidgetFactory: } ) - def renderOption( + override def renderOption( label: String, idx: Int, selected: Boolean diff --git a/scripts/build-env.sh b/scripts/build-env.sh index 7b1f8e8..33b1cd0 100644 --- a/scripts/build-env.sh +++ b/scripts/build-env.sh @@ -1,3 +1,2 @@ - # Generated file see build.sbt -SCALA_VERSION="3.4.1" +SCALA_VERSION="3.5.0"