From df6032fc9e4dbf0dc955bfb593df4f5b224b9adf Mon Sep 17 00:00:00 2001 From: Felix Dietze Date: Thu, 15 Dec 2022 19:30:02 +0100 Subject: [PATCH 1/5] wip --- build.sbt | 2 +- cypress/e2e/spec.cy.js | 20 +++++++ demo/src/main/scala/Main.scala | 4 ++ formidable/src/main/scala-2/Form-Scala2.scala | 53 ++++++++++++++++--- formidable/src/main/scala/FormConfig.scala | 4 +- formidable/src/main/scala/instances.scala | 9 +++- 6 files changed, 81 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index 65fe477..db1d50c 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ inThisBuild( val versions = new { val outwatch = "1.0.0-RC13" - val colibri = "0.7.8" + val colibri = "0.7.8+10-e7dd4fb2-SNAPSHOT" // https://github.com/cornerman/colibri/pull/275 val funPack = "0.3.2" val scalaTest = "3.2.12" } diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js index 589b73f..c99da2a 100644 --- a/cypress/e2e/spec.cy.js +++ b/cypress/e2e/spec.cy.js @@ -188,4 +188,24 @@ describe('Form interactions', () => { cy.get('.value').should('have.text', 'Cons(Cat(,4),Cons(Dog(,true),Nil))') }) }) + + it('backup entered values (sealed trait)', () => { + cy.get('.Pet').within(($form) => { + cy.get('select').select('Cat') + cy.contains('tr', 'name:').find('input[type="text"]').clear().type('Tiger') + cy.get('select').select('Dog') // select different case + cy.get('select').select('Cat') // back to previously selected case + cy.get('.value').should('have.text', 'Cat(Tiger,4)') // test default value + }) + }) + + it('backup entered values (Option[Int])', () => { + cy.get('.Option\\[Int\\]').within(($form) => { + cy.get('input[type="checkbox"]').check() + cy.get('input[type="text"]').clear().type('15') + cy.get('input[type="checkbox"]').uncheck() + cy.get('input[type="checkbox"]').check() + cy.get('.value').should('have.text', 'Some(15)') + }) + }) }) diff --git a/demo/src/main/scala/Main.scala b/demo/src/main/scala/Main.scala index ff9c908..a0bbb35 100644 --- a/demo/src/main/scala/Main.scala +++ b/demo/src/main/scala/Main.scala @@ -16,6 +16,9 @@ object Pet { case class Cat(name: String, legs: Int = 4) extends Pet } +case class Address(city: String, street: String) +case class Company(name: String, address: Option[Address]) + case class Tree(value: Int = 2, children: Seq[Tree]) sealed trait BinaryTree @@ -51,6 +54,7 @@ object Main extends Extras { formFrame[(Int, String, Option[Long])]("Tuple"), formFrame[Person]("Person"), formFrame[Pet]("Pet"), + formFrame[Company]("Company"), formFrame[Tree]("Tree"), formFrame[BinaryTree]("BinaryTree"), formFrame[GenericLinkedList[Pet]]("GenericLinkedList[Pet]"), diff --git a/formidable/src/main/scala-2/Form-Scala2.scala b/formidable/src/main/scala-2/Form-Scala2.scala index 565bd91..e5ba3d3 100644 --- a/formidable/src/main/scala-2/Form-Scala2.scala +++ b/formidable/src/main/scala-2/Form-Scala2.scala @@ -2,6 +2,7 @@ package formidable import outwatch._ import colibri.reactive._ +import scala.collection.mutable import magnolia1._ @@ -15,13 +16,23 @@ trait FormDerivation { override def default: T = ctx.construct(param => param.default.getOrElse(param.typeclass.default)) override def render(state: Var[T], config: FormConfig): VModifier = Owned.function { implicit owner => - val subStates: Var[Seq[Any]] = + println(s"product[${ctx.typeName.short}]: rendering") + val combinedFieldState: Var[Seq[Any]] = state.imap[Seq[Any]](seq => ctx.rawConstruct(seq))(_.asInstanceOf[Product].productIterator.toList) - subStates.sequence.map { subStates => + combinedFieldState.sequence.map { fieldStates => + println( + s"product[${ctx.typeName.short}]: state changed: ${ctx.parameters + .map(_.label) + .zip(fieldStates.map(_.now())) + .map { case (label, value) => + s"$label: $value" + } + .mkString(",")}", + ) config.labeledFormGroup( ctx.parameters - .zip(subStates) + .zip(fieldStates) .map { case (param, subState) => val subForm = ((s: Var[param.PType], c) => param.typeclass.render(s, c)) .asInstanceOf[(Var[Any], FormConfig) => VModifier] @@ -38,8 +49,17 @@ trait FormDerivation { defaultSubtype.typeclass.default } override def render(selectedValue: Var[T], config: FormConfig): VModifier = Owned.function { implicit owner => + println(s"sum[${ctx.typeName.short}]: rendering") + + val valueBackup = mutable.HashMap.empty[Subtype[Form, T], T].withDefault(_.typeclass.default) val selectedSubtype: Var[Subtype[Form, T]] = - selectedValue.imap[Subtype[Form, T]](subType => subType.typeclass.default)(value => ctx.split(value)(identity)) + selectedValue.imap[Subtype[Form, T]](subtype => valueBackup(subtype)) { value => + val subtype = ctx.split(value)(identity) + valueBackup(subtype) = value + subtype + } + + val subFormBackup = mutable.HashMap.empty[Subtype[Form, T], (Var[T], VModifier)] config.unionSubform( config.selectInput[Subtype[Form, T]]( @@ -47,9 +67,28 @@ trait FormDerivation { selectedValue = selectedSubtype, show = subtype => subtype.typeName.short, ), - subForm = selectedValue.map { value => - ctx.split(value) { sub => - VModifier.when(value.isInstanceOf[T])(sub.typeclass.asInstanceOf[Form[T]].render(selectedValue, config)) + subForm = selectedValue.map { newValue => + println(s"sum[${ctx.typeName.short}]: state changed: $newValue") + ctx.split(newValue) { subtype => + + val (formState, form) = subFormBackup.getOrElseUpdate( + key = subtype, + defaultValue = { + val formInstance = subtype.typeclass.asInstanceOf[Form[T]] + val state = Var(formInstance.default) + val form = formInstance.render(state, config) // TODO: this is lazy and always re-rendered.... + (state, form) + }, + ) + + println(subFormBackup.size) + + formState.set(newValue) + + VModifier( + VModifier.managedEval(formState.observable.unsafeForeach(selectedValue.set)), + form, + ) } }, ) diff --git a/formidable/src/main/scala/FormConfig.scala b/formidable/src/main/scala/FormConfig.scala index 2fd04a5..0e134d7 100644 --- a/formidable/src/main/scala/FormConfig.scala +++ b/formidable/src/main/scala/FormConfig.scala @@ -15,7 +15,9 @@ trait FormConfig { div(display.flex, VModifier.style("gap") := "0.5rem", subForms) def labeledFormGroup(subForms: Seq[(String, VModifier)]): VModifier = table( - subForms.map { case (label, subForm) => tr(td(b(label, ": "), verticalAlign := "top"), td(subForm)) } + subForms.map { case (label, subForm) => + tr(td(b(label, ": "), verticalAlign := "top"), td(subForm)) + } ) def formSequence(subForms: Seq[VModifier], addButton: VModifier): VModifier = diff --git a/formidable/src/main/scala/instances.scala b/formidable/src/main/scala/instances.scala index 8c0f5f2..1c2a248 100644 --- a/formidable/src/main/scala/instances.scala +++ b/formidable/src/main/scala/instances.scala @@ -58,10 +58,15 @@ package object formidable { implicit def optionForm[T: Form]: Form[Option[T]] = new Form[Option[T]] { def default = None def render(state: Var[Option[T]], config: FormConfig) = Owned { + var valueBackup: Option[T] = None val checkboxState = state.transformVar[Boolean](_.contramap { - case true => Some(Form[T].default) + case true => + valueBackup.orElse(Some(Form[T].default)) case false => None - })(_.map(_.isDefined)) + })(_.map { value => + value.foreach(some => valueBackup = Some(some)) + value.isDefined + }) config.withCheckbox( subForm = state.sequence.map( From 70cfab84a661390d33b5ef1d3b085003a44afc1e Mon Sep 17 00:00:00 2001 From: Felix Dietze Date: Thu, 15 Dec 2022 19:35:48 +0100 Subject: [PATCH 2/5] WIP: Main.scala --- demo/src/main/scala/Main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/scala/Main.scala b/demo/src/main/scala/Main.scala index a0bbb35..d649754 100644 --- a/demo/src/main/scala/Main.scala +++ b/demo/src/main/scala/Main.scala @@ -53,8 +53,8 @@ object Main extends Extras { formFrame[Vector[Int]]("Vector[Int]"), formFrame[(Int, String, Option[Long])]("Tuple"), formFrame[Person]("Person"), - formFrame[Pet]("Pet"), formFrame[Company]("Company"), + formFrame[Pet]("Pet"), formFrame[Tree]("Tree"), formFrame[BinaryTree]("BinaryTree"), formFrame[GenericLinkedList[Pet]]("GenericLinkedList[Pet]"), From f8a4cc3fe4813c203217b7ca540af5a5fd4139e3 Mon Sep 17 00:00:00 2001 From: Felix Dietze Date: Thu, 15 Dec 2022 15:05:50 -0600 Subject: [PATCH 3/5] wip --- README.md | 2 ++ cypress/e2e/spec.cy.js | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 732327f..a168683 100644 --- a/README.md +++ b/README.md @@ -35,5 +35,7 @@ Formidable defines a typeclass [`Form[T]`](formidable/src/main/scala/Form.scala) Automatic derivation is achieved using [Magnolia](https://github.com/softwaremill/magnolia). +It keeps backups of already entered data wherever it can, to help the user. + diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js index c99da2a..0924ef7 100644 --- a/cypress/e2e/spec.cy.js +++ b/cypress/e2e/spec.cy.js @@ -208,4 +208,27 @@ describe('Form interactions', () => { cy.get('.value').should('have.text', 'Some(15)') }) }) + + it('backup entered values (Option in case class)', () => { + cy.get('.Company').within(($form) => { + cy.contains('tr', 'address:').find('input[type="checkbox"]').check() + cy.contains('tr', 'city:').find('input[type="text"]').clear().type('Madrid') + cy.contains('tr', 'address:').find('input[type="checkbox"]').uncheck() + cy.contains('tr', 'address:').find('input[type="checkbox"]').check() + cy.get('.value').should('have.text', 'Company(,Some(Address(Madrid,)))') // test default value + }) + }) + + it.only('backup entered values (nested sum types)', () => { + cy.get('.GenericLinkedList\\[Pet\\]').within(($form) => { + cy.get('select').select('Cons') + cy.contains('tr', 'name:').find('input[type="text"]').clear().type('Dog') + cy.contains('tr', 'head:').within(($form) => { + cy.get('select').select('Dog') + cy.get('select').select('Cat') + }) + + cy.get('.value').should('have.text', 'Cons(Cat(Dog,4),Nil)') + }) + }) }) From da918840e2387fa115e7b253f26b2200052091e9 Mon Sep 17 00:00:00 2001 From: Felix Dietze Date: Thu, 15 Dec 2022 15:07:54 -0600 Subject: [PATCH 4/5] format --- formidable/src/main/scala-2/Form-Scala2.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/formidable/src/main/scala-2/Form-Scala2.scala b/formidable/src/main/scala-2/Form-Scala2.scala index e5ba3d3..1114713 100644 --- a/formidable/src/main/scala-2/Form-Scala2.scala +++ b/formidable/src/main/scala-2/Form-Scala2.scala @@ -28,7 +28,7 @@ trait FormDerivation { .map { case (label, value) => s"$label: $value" } - .mkString(",")}", + .mkString(",")}" ) config.labeledFormGroup( ctx.parameters @@ -70,7 +70,6 @@ trait FormDerivation { subForm = selectedValue.map { newValue => println(s"sum[${ctx.typeName.short}]: state changed: $newValue") ctx.split(newValue) { subtype => - val (formState, form) = subFormBackup.getOrElseUpdate( key = subtype, defaultValue = { From 80544ea3a77bf8a068576e5df00f80bb593e36b8 Mon Sep 17 00:00:00 2001 From: Felix Dietze Date: Thu, 15 Dec 2022 15:10:34 -0600 Subject: [PATCH 5/5] run all tests --- cypress/e2e/spec.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js index 0924ef7..da9b271 100644 --- a/cypress/e2e/spec.cy.js +++ b/cypress/e2e/spec.cy.js @@ -219,7 +219,7 @@ describe('Form interactions', () => { }) }) - it.only('backup entered values (nested sum types)', () => { + it('backup entered values (nested sum types)', () => { cy.get('.GenericLinkedList\\[Pet\\]').within(($form) => { cy.get('select').select('Cons') cy.contains('tr', 'name:').find('input[type="text"]').clear().type('Dog')