From 77bcf87cd110af9112f9ae01a766169854bf7622 Mon Sep 17 00:00:00 2001 From: efim Date: Sun, 16 Jul 2023 15:07:46 +0000 Subject: [PATCH] feat(15): phone validation htmx inline making phone input as a fragment that accepts value and error initial value from overall state, initial error empty string some contitional classes and hidden error message wowy, endpoint that takes POST request, extracts value and resends the rendered fragment. cool --- 15-multi-step-form/build.sbt | 3 +- .../src/main/resources/public/out.css | 26 +++-- .../src/main/resources/templates/index.html | 13 ++- .../src/main/resources/templates/step1.html | 109 ++++++++++++------ .../src/main/scala/multistepform/Routes.scala | 75 ++++++++++-- 5 files changed, 162 insertions(+), 64 deletions(-) diff --git a/15-multi-step-form/build.sbt b/15-multi-step-form/build.sbt index ebb5ed3..2140cbb 100644 --- a/15-multi-step-form/build.sbt +++ b/15-multi-step-form/build.sbt @@ -11,6 +11,7 @@ lazy val multiStepForm = (project in file(".")) libraryDependencies ++= Seq( "com.lihaoyi" %% "cask" % "0.9.1", "com.lihaoyi" %% "mainargs" % "0.5.0", - "org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE" + "org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE", + "com.googlecode.libphonenumber" % "libphonenumber" % "8.13.16" ) ) diff --git a/15-multi-step-form/src/main/resources/public/out.css b/15-multi-step-form/src/main/resources/public/out.css index b8895df..52e2faf 100644 --- a/15-multi-step-form/src/main/resources/public/out.css +++ b/15-multi-step-form/src/main/resources/public/out.css @@ -572,10 +572,6 @@ video { grid-row: span 2 / span 2; } -.row-span-full { - grid-row: 1 / -1; -} - .row-start-1 { grid-row-start: 1; } @@ -655,6 +651,10 @@ video { display: inline-grid; } +.contents { + display: contents; +} + .hidden { display: none; } @@ -777,14 +777,6 @@ video { grid-template-columns: repeat(4, auto); } -.grid-rows-5 { - grid-template-rows: repeat(5, minmax(0, 1fr)); -} - -.grid-rows-3 { - grid-template-rows: repeat(3, minmax(0, 1fr)); -} - .flex-row { flex-direction: row; } @@ -879,6 +871,11 @@ video { border-color: rgb(255 255 255 / var(--tw-border-opacity)); } +.border-strawberry-red { + --tw-border-opacity: 1; + border-color: hsl(354 84% 57% / var(--tw-border-opacity)); +} + .\!bg-light-blue { --tw-bg-opacity: 1 !important; background-color: hsl(206 94% 87% / var(--tw-bg-opacity)) !important; @@ -1063,6 +1060,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.text-strawberry-red { + --tw-text-opacity: 1; + color: hsl(354 84% 57% / var(--tw-text-opacity)); +} + .underline { text-decoration-line: underline; } diff --git a/15-multi-step-form/src/main/resources/templates/index.html b/15-multi-step-form/src/main/resources/templates/index.html index 417fba0..cbeaeaa 100644 --- a/15-multi-step-form/src/main/resources/templates/index.html +++ b/15-multi-step-form/src/main/resources/templates/index.html @@ -33,13 +33,14 @@
-
+
- loading... + loading...
diff --git a/15-multi-step-form/src/main/resources/templates/step1.html b/15-multi-step-form/src/main/resources/templates/step1.html index 617c27e..d7a569c 100644 --- a/15-multi-step-form/src/main/resources/templates/step1.html +++ b/15-multi-step-form/src/main/resources/templates/step1.html @@ -126,42 +126,81 @@

Please provide your name, email address, and phone number.

- + + + +
+ + +
+ +
- - - - - +
+ + +
Please enter valid phone number
+ +
+
diff --git a/15-multi-step-form/src/main/scala/multistepform/Routes.scala b/15-multi-step-form/src/main/scala/multistepform/Routes.scala index 367bb2d..5f777ba 100644 --- a/15-multi-step-form/src/main/scala/multistepform/Routes.scala +++ b/15-multi-step-form/src/main/scala/multistepform/Routes.scala @@ -8,7 +8,11 @@ import java.util.UUID import scala.jdk.CollectionConverters._ import multistepform.Models.Answers import scala.annotation.internal.requiresCapability +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource +import com.google.i18n.phonenumbers.PhoneNumberUtil + import java.net.URLDecoder +import scala.util.Try case class Routes()(implicit cc: castor.Context, log: cask.Logger) extends cask.Routes { @@ -103,9 +107,10 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger) // and that is not nice val userAnswers = Sessions.sessionReplies.getOrElse(id, Answers(id)) - val submittedData = URLDecoder.decode(request.text() , "UTF-8") + val submittedData = URLDecoder.decode(request.text(), "UTF-8") - val updatedAnswers = userAnswers.updateStep(stepNum, submittedData, nextStep) + val updatedAnswers = + userAnswers.updateStep(stepNum, submittedData, nextStep) Sessions.sessionReplies.update(id, updatedAnswers) @@ -124,25 +129,75 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger) ) } - /** - * This endpoint re-renders 'plan type inputs' - * so that togglng monthly\yearly could redraw their html - */ + @cask.post("/step1/phonenumber") + def validateStep1PhoneNumber(request: cask.Request) = { + val submittedData = URLDecoder.decode(request.text(), "UTF-8") + println( + s"getting data ${request.data} or ${request.text()} or $submittedData" + ) + val fieldValues = submittedData + .split("&") + .filterNot(_.isEmpty()) + .map { field => + println(s"looking at field $field") + val (name, value) = field.split("=").toList match { + case List(name, value) => name -> value + case name :: tail => name -> tail.headOption.getOrElse("") + case Nil => ??? + } + name -> value + } + .toMap + + val name = fieldValues.getOrElse("name", "") + val email = fieldValues.getOrElse("email", "") + val phone = fieldValues.getOrElse("phone", "") + + println(s"after parsing name=$name | email=$email | phone=$phone") + + val phoneUtils = PhoneNumberUtil.getInstance() + val phoneNum = Try( + phoneUtils.parse(phone, CountryCodeSource.UNSPECIFIED.name()) + ).toOption + val isPhoneValid = phoneNum.map(phoneUtils.isValidNumber(_)).getOrElse(false) + + val error = if (isPhoneValid) "" else "Please input valid phone number" + + val context = new Context() + context.setVariable("value", phone) + context.setVariable("error", error) + val inputDiv = + templateEngine.process("step1", Set("email-input").asJava, context) + + cask.Response( + inputDiv, + headers = Seq("Content-Type" -> "text/html;charset=UTF-8") + ) + } + + /** This endpoint re-renders 'plan type inputs' so that togglng monthly\yearly + * could redraw their html + */ @cask.post("/step2/planTypeInputs") def getPlanTypeInputs(sessionId: cask.Cookie, request: cask.Request) = { val id = sessionId.value - val submittedData = URLDecoder.decode(request.text() , "UTF-8") + val submittedData = URLDecoder.decode(request.text(), "UTF-8") println(s"requesting plan type inputs for $id and $request") Sessions.sessionReplies.get(id) match { case None => - cask.Response("Can't find answers for your session, please reload the page", 404) + cask.Response( + "Can't find answers for your session, please reload the page", + 404 + ) case Some(answers) => { // here changing yearly/monthly part of state based on passed checkbox value // and selected plan // only for purposes of rendering the fragment // not persisting, unless next or previous buttons are pressed - val withYearlyParamSetAndSelectedPlan = answers.step2.fromFormData(submittedData) - val updatedState = answers.copy(step2 = withYearlyParamSetAndSelectedPlan) + val withYearlyParamSetAndSelectedPlan = + answers.step2.fromFormData(submittedData) + val updatedState = + answers.copy(step2 = withYearlyParamSetAndSelectedPlan) val formData = Models.FormData(updatedState) val context = new Context() context.setVariable(formDataContextVarName, formData)