Skip to content

Commit ec4c8e8

Browse files
committed
* improved the macro to cope with readonly calculated fields declared as FieldDescriptors in FormLayout
1 parent 40445a6 commit ec4c8e8

File tree

4 files changed

+65
-33
lines changed

4 files changed

+65
-33
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ flexibility of the form design.
1616
Currently it is available as a sonatype snapshot, add this to your build.sbt:
1717

1818
```
19-
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "core" % "0.0.13-SNAPSHOT"
19+
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "core" % "0.0.14-SNAPSHOT"
2020
```
2121

2222
If you want to use the materialui form-field descriptors, you'll need this as well:
2323
```
24-
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "extras" % "0.0.13-SNAPSHOT"
24+
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "extras" % "0.0.14-SNAPSHOT"
2525
```
2626

2727

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ def commonSettings: Project => Project =
33
_.enablePlugins(ScalaJSPlugin)
44
.settings(
55
organization := "com.github.torstenrudolf.scalajs-react-form-binder",
6-
version := "0.0.13-SNAPSHOT",
6+
version := "0.0.14-SNAPSHOT",
77
homepage := Some(url("https://github.com/torstenrudolf/scalajs-react-form-binder")),
88
licenses += ("MIT", url("https://opensource.org/licenses/MIT")),
99
scalaVersion := "2.11.8",

core/src/main/scala/torstenrudolf/scalajs/react/formbinder/Macros.scala

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ object Macros {
118118
$defaultFormValue.map((dmv: $dataModelTypeSymbol)=> Map[String, Any](..${dataModelFields.map(fn => (fn.asTerm.name.toString, q"dmv.${fn.asTerm.name}"))})).getOrElse(${getDefaultValuesFromCompanionObject(c)(dataModelCompanion)})
119119
""")
120120

121-
val formFieldDescriptors = formLayout.info.members.map(_.asTerm).filter(_.isAccessor)
121+
val formFieldDescriptors = formLayout.info.members.sorted
122+
.map(_.asTerm)
123+
.filter(_.isAccessor)
122124
.filter(_.asMethod.returnType.<:<(typeOf[FormFieldDescriptor[_]]))
123-
.toList
124125

125126
// check that targetFields and formFieldDescriptors match
126127
val missing = dataModelFields.map(_.name.toString).toSet.diff(formFieldDescriptors.map(_.name.toString).toSet)
@@ -130,8 +131,15 @@ object Macros {
130131

131132
// check types
132133
val nonMatchingFieldTypes = formFieldDescriptors.toSet.diff(
133-
formFieldDescriptors.filter(x =>
134-
List(dataModelFields.find(_.name == x.name).get.info) == x.info.resultType.typeArgs).toSet)
134+
formFieldDescriptors.filter(ffd =>
135+
ffd.info.resultType.typeArgs.size == 1 && {
136+
dataModelFields.find(_.name == ffd.name) match {
137+
case Some(targetField) => targetField.info == ffd.info.resultType.typeArgs.head
138+
case None => ffd.info.resultType.typeArgs.head == typeOf[Unit] // is calculated field
139+
}
140+
}
141+
).toSet
142+
)
135143

136144
if (nonMatchingFieldTypes.nonEmpty) {
137145
c.abort(
@@ -187,18 +195,21 @@ object Macros {
187195
globalTargetValidator.map(v => q"$v")
188196
.getOrElse(q"(d: $dataModelTypeSymbol) => torstenrudolf.scalajs.react.formbinder.ValidationResult.Success")
189197

190-
case class CompoundField(targetField: c.universe.Symbol,
191-
defaultValueExpr: c.universe.Tree,
198+
case class CompoundField(targetField: Option[c.universe.Symbol],
192199
transformedTargetFieldValidator: c.universe.Tree,
193200
name: String,
194201
termName: c.universe.TermName,
195202
formFieldDescriptor: c.universe.TermSymbol) {
196-
val fieldType = targetField.info
197-
val formFieldBindingValDef = q"val $termName: torstenrudolf.scalajs.react.formbinder.FormFieldBinding[${targetField.info}]"
203+
val hasTargetField = targetField.isDefined
204+
val fieldType = targetField.map(_.info).getOrElse(typeOf[Unit])
205+
val isString = fieldType =:= typeOf[String]
206+
val formFieldBindingValDef = q"val $termName: torstenrudolf.scalajs.react.formbinder.FormFieldBinding[$fieldType]"
207+
val defaultValueExpr = q"$targetFieldDefaultValues.get($name).asInstanceOf[Option[${fieldType}]]"
198208
}
199209

200-
val compoundFields = dataModelFields.zipWithIndex.map { case (f, idx) =>
201-
val fieldName = f.name.toString
210+
val compoundFields = formFieldDescriptors.zipWithIndex.map{ case (ffd, idx) =>
211+
val fieldName = ffd.name.toString
212+
val dataModelField = dataModelFields.find(_.name.toString == ffd.name.toString)
202213

203214
val validatorAndParamsOpt = targetFieldValidatorsInfo.get(fieldName)
204215

@@ -217,15 +228,15 @@ object Macros {
217228
.map(v => q"Some($v)").getOrElse(q"None")
218229

219230
CompoundField(
220-
targetField = f,
221-
defaultValueExpr = q"$targetFieldDefaultValues.get($fieldName).asInstanceOf[Option[${f.info}]]",
231+
targetField = dataModelField,
222232
transformedTargetFieldValidator = transformedTargetFieldValidator,
223233
name = fieldName,
224234
termName = TermName(fieldName),
225-
formFieldDescriptor = formFieldDescriptors.find(_.name.toString == fieldName).get
235+
formFieldDescriptor = ffd
226236
)
227237
}
228238

239+
229240
val newTree =
230241
q"""
231242
new torstenrudolf.scalajs.react.formbinder.Form[$dataModelTypeSymbol] with torstenrudolf.scalajs.react.formbinder.FormAPI[$dataModelTypeSymbol] {
@@ -237,25 +248,25 @@ object Macros {
237248
override var isInitializing: Boolean = true
238249

239250
private case class FieldBindingsHolder(..${compoundFields.map(_.formFieldBindingValDef)})
240-
private val fieldBindingsHolder = new FieldBindingsHolder(..${compoundFields.map(f => q"""torstenrudolf.scalajs.react.formbinder.FormFieldBinding[${f.fieldType}]($formLayout.${f.formFieldDescriptor}, ${f.defaultValueExpr}, ${f.targetField.info =:= typeOf[String]}, ${f.transformedTargetFieldValidator}, this, ${f.name})""")})
251+
private val fieldBindingsHolder = new FieldBindingsHolder(..${compoundFields.map(f => q"""torstenrudolf.scalajs.react.formbinder.FormFieldBinding[${f.fieldType}]($formLayout.${f.formFieldDescriptor}, ${f.defaultValueExpr}, ${f.isString}, ${f.transformedTargetFieldValidator}, this, ${f.name}, ${f.hasTargetField})""")})
241252

242253
val allFormFieldBindings: List[torstenrudolf.scalajs.react.formbinder.FormFieldBinding[_]] = ${compoundFields.map(f => q"fieldBindingsHolder.${f.termName}")}
243254

244255
isInitializing = false
245256
onChangeCB.runNow() // update default values
246257

247258
override def currentValueWithoutGlobalValidation: scala.util.Try[$dataModelTypeSymbol] = {
248-
val fieldValues = allFormFieldBindings.map(_.currentValidatedValue)
259+
val fieldValues = allFormFieldBindingsWithUnderlyingDataField.map(_.currentValidatedValue)
249260
if (fieldValues.forall(_.nonEmpty)) {
250-
val d = $dataModelCompanion.apply(..${compoundFields.map(f => q"fieldBindingsHolder.${f.termName}.currentValidatedValue.get")})
261+
val d = $dataModelCompanion.apply(..${compoundFields.filter(_.hasTargetField).map(f => q"${f.termName} = fieldBindingsHolder.${f.termName}.currentValidatedValue.get")})
251262
scala.util.Success(d)
252263
} else {
253264
scala.util.Failure(torstenrudolf.scalajs.react.formbinder.FormUninitialized)
254265
}
255266
}
256267

257268
override def setModelValue(newModelValue: $dataModelTypeSymbol): japgolly.scalajs.react.Callback = {
258-
${compoundFields.map(f =>
269+
${compoundFields.filter(_.hasTargetField).map(f =>
259270
q"""fieldBindingsHolder.${f.termName}.updateValue(newModelValue.${f.termName}) match {
260271
case scala.util.Success(cb) => cb
261272
case scala.util.Failure(e) => throw torstenrudolf.scalajs.react.formbinder.FormUninitialized
@@ -314,6 +325,8 @@ object FormField {
314325
$.modState(_.copy(currentValidationResult = Some(validationResult), showUnitializedError = showUnitializedErrorX)) >> CallbackTo(validationResult)
315326
}
316327

328+
def forceUpdate: Callback = $.forceUpdate
329+
317330
private def onChangeCB: Callback =
318331
$.state.zip($.props) >>= { case (state, props) => props.onChangeCB(currentValidatedValue) }
319332

@@ -379,14 +392,17 @@ case class FormFieldBinding[O](formFieldDescriptor: FormFieldDescriptor[O],
379392
private val isString: Boolean,
380393
transformedTargetFieldValidator: Option[(O, FormAPI[_]) => ValidationResult],
381394
private val parentForm: Form[_] with FormAPI[_],
382-
fieldName: String) {
395+
fieldName: String,
396+
hasTargetField: Boolean) {
383397

384398
def currentValidatedValue: Option[O] = formFieldBackend.flatMap(_.currentValidatedValue)
385399

386400
def currentValidationResult: Try[ValidationResult] = Try(formFieldBackend.get.currentValidationResult)
387401

388402
def validate(showUninitializedError: Boolean): Try[Callback] = Try(formFieldBackend.get.validate(showUninitializedError).void)
389403

404+
def forceUpdateComponent: Callback = formFieldBackend.map(_.forceUpdate).getOrElse(Callback.empty)
405+
390406
def updateValue(v: O): Try[Callback] = Try(formFieldBackend.get.updateRawValue(Some(v)))
391407

392408
def clear: Try[Callback] = Try(formFieldBackend.get.clear)
@@ -452,7 +468,16 @@ trait FormAPI[T] extends Form[T] {
452468
}
453469

454470
private def validateAllFields(showUninitializedError: Boolean): Try[Unit] =
455-
Try(allFormFieldBindings.map(_.validate(showUninitializedError = showUninitializedError).get).reduce {_ >> _}.runNow())
471+
// this validates the fields as well as triggers the field components to refresh
472+
Try(
473+
{
474+
allFormFieldBindingsWithUnderlyingDataField
475+
.map(_.validate(showUninitializedError = showUninitializedError).get) ++
476+
allFormFieldBindings.filter(f => !f.hasTargetField).map(_.forceUpdateComponent)
477+
}
478+
.reduce {_ >> _}
479+
.runNow()
480+
)
456481

457482
override def fullValidate: Callback = Callback {validate(showUninitializedError = true)}
458483

@@ -467,14 +492,17 @@ trait FormAPI[T] extends Form[T] {
467492
_validatedFormData
468493
}
469494

470-
private def allFieldValidationResults: List[ValidationResult] = allFormFieldBindings.map(
495+
private def allFieldValidationResults: List[ValidationResult] = allFormFieldBindingsWithUnderlyingDataField.map(
471496
_.currentValidationResult match {
472497
case Success(vr) => vr
473498
case _ => throw FormUninitialized
474499
})
475500

476501
protected def allFormFieldBindings: List[FormFieldBinding[_]]
477502

503+
protected def allFormFieldBindingsWithUnderlyingDataField: List[FormFieldBinding[_]] =
504+
allFormFieldBindings.filter(_.hasTargetField)
505+
478506
override def field[A](fd: torstenrudolf.scalajs.react.formbinder.FormFieldDescriptor[A]): ReactNode =
479507
fieldBinding(fd).formField
480508

@@ -490,7 +518,7 @@ trait FormAPI[T] extends Form[T] {
490518
override def clearAllFields: Callback = {
491519
// note: this calls FormAPI.onChangeCB and therefore FormAPI.validate N times (N = number of form fields)
492520
// this could become slow for big forms and we might need to improve this
493-
Try(allFormFieldBindings.map(_.clear.get).reduce {_ >> _}) match {
521+
Try(allFormFieldBindingsWithUnderlyingDataField.map(_.clear.get).reduce {_ >> _}) match {
494522
case Success(cb) => cb
495523
case Failure(e) => throw FormUninitialized
496524
}
@@ -501,12 +529,13 @@ trait FormAPI[T] extends Form[T] {
501529
case Failure(e) => throw FormUninitialized
502530
}
503531

504-
override def resetAllFields: Callback = Try(allFormFieldBindings.map(_.resetToDefault.get).reduce {_ >> _}) match {
505-
// note: this calls FormAPI.onChangeCB and therefore FormAPI.validate N times (N = number of form fields)
506-
// this could become slow for big forms and we might need to improve this
507-
case Success(cb) => cb
508-
case Failure(e) => throw FormUninitialized
509-
}
532+
override def resetAllFields: Callback =
533+
Try(allFormFieldBindingsWithUnderlyingDataField.map(_.resetToDefault.get).reduce {_ >> _}) match {
534+
// note: this calls FormAPI.onChangeCB and therefore FormAPI.validate N times (N = number of form fields)
535+
// this could become slow for big forms and we might need to improve this
536+
case Success(cb) => cb
537+
case Failure(e) => throw FormUninitialized
538+
}
510539

511540
override def setFieldValue[A](fd: torstenrudolf.scalajs.react.formbinder.FormFieldDescriptor[A], value: A): Callback =
512541
fieldBinding(fd).updateValue(value) match {

demo/src/main/scala/torstenrudolf/scalajs/react/formbinder/demo/components/SimpleMuiDemo.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ object SimpleMuiDemo {
88

99
// EXAMPLE:START
1010
import scala.scalajs.js
11+
import js.JSConverters._
1112
import japgolly.scalajs.react._
1213
import japgolly.scalajs.react.vdom.prefix_<^._
1314
import chandu0101.scalajs.react.components.materialui.{MuiRaisedButton, MuiTextField, MuiFlatButton}
@@ -38,6 +39,10 @@ object SimpleMuiDemo {
3839
val username = MuiTextField(floatingLabelText = "Username").asFormFieldDescriptor
3940
val password = MuiTextField(floatingLabelText = "Password", `type` = "password").asFormFieldDescriptor
4041
val age = MuiTextField(floatingLabelText = "Age", `type` = "number").asIntFormFieldDescriptor
42+
43+
val calculatedField = FormFieldDescriptor[Unit]((a: FormFieldArgs[Unit]) =>
44+
MuiTextField(floatingLabelText = "Calculated Field", disabled = true, value = (a.otherFieldValue(username) zip a.otherFieldValue(age)).map{case (un, age) => s"$un ($age yrs)"}.headOption.orUndefined)()
45+
)
4146
}
4247

4348
// this does the magic and binds the data model, validation rules and formlayout together
@@ -73,9 +78,7 @@ object SimpleMuiDemo {
7378
^.display.flex,
7479
^.flexDirection.column,
7580
^.marginBottom := 15.px,
76-
form.field(FormLayout.username),
77-
form.field(FormLayout.password),
78-
form.field(FormLayout.age)
81+
form.allFields
7982
),
8083
<.div(
8184
MuiFlatButton(label = "Override", onClick = (e: ReactEventH) => overrideFormData(form))(),

0 commit comments

Comments
 (0)