Skip to content

Commit 26fdb26

Browse files
committed
* introduced FormBinder trait in order to declare form spec encapsulated in one place
1 parent 612f41e commit 26fdb26

File tree

8 files changed

+213
-48
lines changed

8 files changed

+213
-48
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.9-SNAPSHOT"
19+
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "core" % "0.0.10-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.9-SNAPSHOT"
24+
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "extras" % "0.0.10-SNAPSHOT"
2525
```
2626

2727

build.sbt

Lines changed: 5 additions & 2 deletions
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.9-SNAPSHOT",
6+
version := "0.0.10-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",
@@ -49,7 +49,10 @@ lazy val core = project
4949
libraryDependencies ++= Seq(
5050
"org.scala-lang" % "scala-compiler" % scalaVersion.value,
5151
"com.github.japgolly.scalajs-react" %%% "extra" % "0.11.3"
52-
)
52+
),
53+
libraryDependencies += "com.lihaoyi" %% "utest" % "0.4.5" % "test",
54+
testFrameworks += new TestFramework("utest.runner.Framework")
55+
// scalacOptions += "-Ymacro-debug-lite"
5356
)
5457

5558
lazy val extras = project
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package torstenrudolf.scalajs.react.formbinder
2+
import scala.language.experimental.macros
3+
import scala.annotation.compileTimeOnly
4+
5+
trait FormBinder {
6+
type DataModel
7+
8+
val formLayout: FormLayout[DataModel]
9+
10+
val validatorsHolder: Any
11+
12+
val defaultFormValue: Option[DataModel]
13+
14+
protected def formX: Form[DataModel] = macro Macros.generateOnFormBinder[DataModel]
15+
16+
// you need to override this with val form = formX in your implementation, macro expansion cannot happen here ;-(
17+
// todo: maybe this can be avoided by using macro annotation?
18+
val form: Form[DataModel]
19+
}

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

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import scala.util.{Failure, Success, Try}
1010
object Macros {
1111
// Thanks to https://github.com/Voltir/form.rx for the idea!
1212

13-
def getDefaultValuesFromCompanionObject(c: scala.reflect.macros.blackbox.Context)(companion: c.Expr[Any]): c.Expr[Map[String, Any]] = {
13+
def getDefaultValuesFromCompanionObject(c: scala.reflect.macros.blackbox.Context)(companion: c.Symbol): c.Expr[Map[String, Any]] = {
1414
import c.universe._
15-
val applyMethod = companion.actualType.decls.find(_.name == TermName("apply")).get.asMethod
15+
val applyMethod = companion.info.decls.find(_.name == TermName("apply")).get.asMethod
1616
// can handle only default parameters from the first parameter list
1717
// because subsequent parameter lists might depend on previous parameters
1818
val paramName2DefaultValue = applyMethod.paramLists.head.map(_.asTerm).zipWithIndex.flatMap { case (p, i) =>
@@ -35,60 +35,103 @@ object Macros {
3535
def generateWithoutDefault[T: c.WeakTypeTag](c: scala.reflect.macros.blackbox.Context)
3636
(formLayout: c.Expr[FormLayout[T]],
3737
validatorObject: c.Expr[Any]): c.Expr[Form[T]] = {
38-
generate[T](c)(formLayout, validatorObject, c.universe.reify(None))
38+
generateFromExplicitValues[T](c)(formLayout, validatorObject, c.universe.reify(None))
39+
.asInstanceOf[c.Expr[Form[T]]] // unnecessary, but idea showed error otherwise
3940
}
4041

4142
def generateWithDefault[T: c.WeakTypeTag](c: scala.reflect.macros.blackbox.Context)
4243
(formLayout: c.Expr[FormLayout[T]],
4344
validatorObject: c.Expr[Any],
4445
defaultModelValue: c.Expr[T]): c.Expr[Form[T]] = {
45-
generate[T](c)(formLayout, validatorObject, c.universe.reify(Some(defaultModelValue.splice)))
46+
import c.universe._
47+
generateFromExplicitValues[T](c)(formLayout, validatorObject, reify[Option[T]](Some(defaultModelValue.splice)))
48+
.asInstanceOf[c.Expr[Form[T]]] // unnecessary, but idea showed error otherwise
4649
}
4750

48-
def generate[DataModel: c.WeakTypeTag](c: scala.reflect.macros.blackbox.Context)
49-
(formLayout: c.Expr[FormLayout[DataModel]],
50-
validatorObject: c.Expr[Any],
51-
defaultModelValue: c.Expr[Option[DataModel]]): c.Expr[Form[DataModel]] =
51+
def generateOnFormBinder[DataModel: c.WeakTypeTag](c: scala.reflect.macros.blackbox.Context): c.Expr[Form[DataModel]] = {
52+
import c.universe._
53+
54+
val dataModelFields = c.prefix.actualType.members
55+
.find(m => m.isClass && m.name == TypeName("DataModel")).get.info.members
56+
.collect{case v: TermSymbol => v.asTerm}
57+
58+
val fields = c.prefix.actualType.members
59+
.find(m => m.isClass && m.name == TypeName("DataModel")).get.info.decls
60+
.collectFirst {
61+
case m: MethodSymbol if m.isPrimaryConstructor => m
62+
}.get.paramLists.head
63+
64+
val defaultModelValue = c.prefix.actualType.members.find(m => m.name == TermName("defaultFormValue")).get
65+
66+
val dataModelType = c.prefix.actualType.members.find(m => m.isClass && m.name == TypeName("DataModel")).get.asType
67+
val formLayout = c.prefix.actualType.members.find(m => m.isTerm && m.name == TermName("formLayout")).get.asTerm
68+
val validatorsHolder = c.prefix.actualType.members.find(m => m.isTerm && m.name == TermName("validatorsHolder")).get.asTerm
69+
val defaultFormValue = c.prefix.actualType.members.find(m => m.isTerm && m.name == TermName("defaultFormValue")).get.asTerm
70+
71+
c.Expr[Form[DataModel]](formImpl(c)(dataModelType, formLayout, validatorsHolder, c.Expr[Option[_]](q"$defaultFormValue")))
72+
73+
}
74+
75+
def generateFromExplicitValues[DataModel: c.WeakTypeTag](c: scala.reflect.macros.blackbox.Context)
76+
(formLayout: c.Expr[FormLayout[DataModel]],
77+
validatorsHolder: c.Expr[Any],
78+
defaultFormValue: c.Expr[Option[DataModel]]): c.Expr[Form[DataModel]] = {
79+
import c.universe._
80+
val dataModelTpe = weakTypeOf[DataModel]
81+
val dataModelCompanion = getCompanion(c)(dataModelTpe).symbol // had problems when using abstract classes
82+
c.Expr[Form[DataModel]](formImpl(c)(
83+
dataModelTpe.typeSymbol,
84+
formLayout.tree.symbol,
85+
validatorsHolder.tree.symbol,
86+
defaultFormValue))
87+
}
88+
89+
def formImpl(c: scala.reflect.macros.blackbox.Context)
90+
(dataModelTypeSymbol: c.universe.Symbol,
91+
formLayout: c.universe.Symbol,
92+
validatorsHolder: c.universe.Symbol,
93+
defaultFormValue: c.Expr[Option[_]]): c.universe.Tree =
5294
{
5395

5496
import c.universe._
5597

56-
val dataModelTpe = weakTypeOf[DataModel]
57-
val dataModelCompanion = getCompanion(c)(dataModelTpe) // had problems when using abstract classes
98+
val dataModelCompanion = dataModelTypeSymbol.companion
99+
100+
// val dataModelCompanion = getCompanion(c)(dataModelTypeSymbol.info) // had problems when using abstract classes
58101

59102

60103
//Collect all fields for the target type
61-
// val targetFields = dataModelCompanion.actualType.decls.find(_.name == TermName("apply")).get.asMethod.paramLists.head
62-
val targetFields = dataModelTpe.decls.collectFirst {
104+
// val targetFields = dataModelCompanion.actualType.decls.find(_.name == TermName("apply")).get.asMethod.paramLists.head
105+
val dataModelFields = dataModelTypeSymbol.info.decls.collectFirst {
63106
case m: MethodSymbol if m.isPrimaryConstructor => m
64107
}.get.paramLists.head
65108

66-
if (targetFields.exists(_.name.toString == "$global")) {
109+
if (dataModelFields.exists(_.name.toString == "$global")) {
67110
c.abort(c.enclosingPosition, s"You cannot name a field `$$global`. `$$global` is reserved methodName for the global model validator.")
68111
}
69112

70-
val targetFieldNames = targetFields.map(_.name.toString)
113+
val targetFieldNames = dataModelFields.map(_.name.toString)
71114

72115
val targetFieldDefaultValues: c.Expr[Map[String, Any]] =
73116
c.Expr[Map[String, Any]](
74117
q"""
75-
$defaultModelValue.map((dmv: $dataModelTpe)=> Map[String, Any](..${targetFields.map(fn => (fn.asTerm.name.toString, q"dmv.${fn.asTerm.name}"))})).getOrElse(${getDefaultValuesFromCompanionObject(c)(c.Expr[Any](q"$dataModelCompanion"))})
118+
$defaultFormValue.map((dmv: $dataModelTypeSymbol)=> Map[String, Any](..${dataModelFields.map(fn => (fn.asTerm.name.toString, q"dmv.${fn.asTerm.name}"))})).getOrElse(${getDefaultValuesFromCompanionObject(c)(dataModelCompanion)})
76119
""")
77120

78-
val formFieldDescriptors = formLayout.actualType.members.map(_.asTerm).filter(_.isAccessor)
121+
val formFieldDescriptors = formLayout.info.members.map(_.asTerm).filter(_.isAccessor)
79122
.filter(_.asMethod.returnType.<:<(typeOf[FormFieldDescriptor[_]]))
80123
.toList
81124

82125
// check that targetFields and formFieldDescriptors match
83-
val missing = targetFields.map(_.name.toString).toSet.diff(formFieldDescriptors.map(_.name.toString).toSet)
126+
val missing = dataModelFields.map(_.name.toString).toSet.diff(formFieldDescriptors.map(_.name.toString).toSet)
84127
if (missing.nonEmpty) {
85128
c.abort(c.enclosingPosition, s"The form layout is not fully defined: Missing formFieldDescriptors are:\n--- ${missing.mkString("\n--- ")}")
86129
}
87130

88131
// check types
89132
val nonMatchingFieldTypes = formFieldDescriptors.toSet.diff(
90133
formFieldDescriptors.filter(x =>
91-
List(targetFields.find(_.name == x.name).get.info) == x.accessed.info.typeArgs).toSet)
134+
List(dataModelFields.find(_.name == x.name).get.info) == x.accessed.info.typeArgs).toSet)
92135

93136
if (nonMatchingFieldTypes.nonEmpty) {
94137
c.abort(
@@ -98,7 +141,7 @@ object Macros {
98141
}
99142

100143
// collect all targetFieldValidators
101-
val targetFieldValidators = validatorObject.actualType.members.map(_.asTerm).filter(_.isMethod).map(_.asMethod)
144+
val targetFieldValidators = validatorsHolder.info.members.map(_.asTerm).filter(_.isMethod).map(_.asMethod)
102145
.filter(v => targetFieldNames.contains(v.name.toString))
103146

104147
val targetFieldValidatorsWrongType =
@@ -111,10 +154,10 @@ object Macros {
111154
if (!targetFieldValidators.forall { x =>
112155
x.paramLists.size == 1 ||
113156
x.paramLists.head.head.name == x.name ||
114-
x.paramLists.head.head.info == targetFields.find(_.name.toString == x.name.toString).get.info ||
157+
x.paramLists.head.head.info == dataModelFields.find(_.name.toString == x.name.toString).get.info ||
115158
x.paramLists.head.tail.forall {
116159
case p@q"Option[$t]" =>
117-
val correspondingField = targetFields.find(_.name.toString == p.name.toString)
160+
val correspondingField = dataModelFields.find(_.name.toString == p.name.toString)
118161
correspondingField.nonEmpty && t == correspondingField.get.info
119162
case _ => false
120163
}
@@ -127,22 +170,22 @@ object Macros {
127170

128171
val targetFieldValidatorsInfo = targetFieldValidators.map(x => (x.name.toString, (x, x.paramLists.head))).toMap
129172

130-
val globalTargetValidator = validatorObject.actualType.members.map(_.asTerm).filter(_.isMethod).map(_.asMethod)
173+
val globalTargetValidator = validatorsHolder.info.members.map(_.asTerm).filter(_.isMethod).map(_.asMethod)
131174
.find(_.name.toString == "$global")
132175

133176
// check for correct type
134177
if (!globalTargetValidator.forall(v => {
135178
v.returnType.<:<(typeOf[ValidationResult]) &&
136179
v.paramLists.size == 1 && v.paramLists.head.size == 1 &&
137-
v.paramLists.head.head.info == dataModelTpe
180+
v.paramLists.head.head.info.typeSymbol == dataModelTypeSymbol
138181
})) {
139182
c.abort(c.enclosingPosition,
140183
s"Type of global validator must match the target type. (def $$global(data: ${globalTargetValidator.get.accessed.info.typeArgs}): torstenrudolf.scalajs.react.formbinder.ValidationResult = ...)")
141184
}
142185

143186
val transformedGlobalTargetValidator =
144187
globalTargetValidator.map(v => q"$v")
145-
.getOrElse(q"(d: $dataModelTpe) => torstenrudolf.scalajs.react.formbinder.ValidationResult.Success")
188+
.getOrElse(q"(d: $dataModelTypeSymbol) => torstenrudolf.scalajs.react.formbinder.ValidationResult.Success")
146189

147190
case class CompoundField(targetField: c.universe.Symbol,
148191
defaultValueExpr: c.universe.Tree,
@@ -154,7 +197,7 @@ object Macros {
154197
val formFieldBindingValDef = q"val $termName: torstenrudolf.scalajs.react.formbinder.FormFieldBinding[${targetField.info}]"
155198
}
156199

157-
val compoundFields = targetFields.zipWithIndex.map { case (f, idx) =>
200+
val compoundFields = dataModelFields.zipWithIndex.map { case (f, idx) =>
158201
val fieldName = f.name.toString
159202

160203
val validatorAndParamsOpt = targetFieldValidatorsInfo.get(fieldName)
@@ -166,7 +209,7 @@ object Macros {
166209
val transformedTargetFieldValidator = validatorAndParamsOpt
167210
.map(v =>
168211
q"""(fieldValue: ${v._2.head.info}, parentForm: torstenrudolf.scalajs.react.formbinder.FormAPI[_]) => {
169-
$validatorObject.${v._1}(
212+
$validatorsHolder.${v._1}(
170213
fieldValue,
171214
..${paramListFieldDescriptors.map(d => q"parentForm.fieldBinding($d).currentValidatedValue")})
172215
}
@@ -185,10 +228,10 @@ object Macros {
185228

186229
val newTree =
187230
q"""
188-
new torstenrudolf.scalajs.react.formbinder.Form[$dataModelTpe] with torstenrudolf.scalajs.react.formbinder.FormAPI[$dataModelTpe] {
189-
override val formLayout: torstenrudolf.scalajs.react.formbinder.FormLayout[$dataModelTpe] = $formLayout
231+
new torstenrudolf.scalajs.react.formbinder.Form[$dataModelTypeSymbol] with torstenrudolf.scalajs.react.formbinder.FormAPI[$dataModelTypeSymbol] {
232+
override val formLayout: torstenrudolf.scalajs.react.formbinder.FormLayout[$dataModelTypeSymbol] = $formLayout
190233

191-
override def globalValidator(data: $dataModelTpe): torstenrudolf.scalajs.react.formbinder.ValidationResult =
234+
override def globalValidator(data: $dataModelTypeSymbol): torstenrudolf.scalajs.react.formbinder.ValidationResult =
192235
($transformedGlobalTargetValidator)(data)
193236

194237
override var isInitializing: Boolean = true
@@ -201,7 +244,7 @@ object Macros {
201244
isInitializing = false
202245
onChangeCB.runNow() // update default values
203246

204-
override def currentValueWithoutGlobalValidation: scala.util.Try[$dataModelTpe] = {
247+
override def currentValueWithoutGlobalValidation: scala.util.Try[$dataModelTypeSymbol] = {
205248
val fieldValues = allFormFieldBindings.map(_.currentValidatedValue)
206249
if (fieldValues.forall(_.nonEmpty)) {
207250
val d = $dataModelCompanion.apply(..${compoundFields.map(f => q"fieldBindingsHolder.${f.termName}.currentValidatedValue.get")})
@@ -211,9 +254,9 @@ object Macros {
211254
}
212255
}
213256

214-
override def setModelValue(newModelValue: $dataModelTpe): japgolly.scalajs.react.Callback = {
257+
override def setModelValue(newModelValue: $dataModelTypeSymbol): japgolly.scalajs.react.Callback = {
215258
${compoundFields.map(f =>
216-
q"""fieldBindingsHolder.${f.termName}.updateValue(newModelValue.${f.termName}) match {
259+
q"""fieldBindingsHolder.${f.termName}.updateValue(newModelValue.${f.termName}) match {
217260
case scala.util.Success(cb) => cb
218261
case scala.util.Failure(e) => throw torstenrudolf.scalajs.react.formbinder.FormUninitialized
219262
}""")}.foldLeft(japgolly.scalajs.react.Callback.empty)(_ >> _)
@@ -233,9 +276,8 @@ object Macros {
233276
}
234277
"""
235278
// println(show(newTree))
236-
c.Expr[Form[DataModel]](newTree)
279+
newTree
237280
}
238-
239281
}
240282

241283
object FormField {
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package torstenrudolf.scalajs.react
2+
import scala.annotation.compileTimeOnly
23
import scala.language.experimental.macros
34

45
package object formbinder {
56

6-
// not: currently scala macros dont support default values for arguments
7+
// note: currently scala macros dont support default values for arguments
78

89
def bindWithDefault[DataModel](formLayout: FormLayout[DataModel],
910
validatorObject: Any,
@@ -12,4 +13,5 @@ package object formbinder {
1213
def bind[DataModel](formLayout: FormLayout[DataModel],
1314
validatorObject: Any): Form[DataModel] = macro Macros.generateWithoutDefault[DataModel]
1415

16+
1517
}

demo/src/main/scala/torstenrudolf/scalajs/react/formbinder/demo/App.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import org.scalajs.dom
44

55
import scala.scalajs.js.annotation.JSExport
66
import japgolly.scalajs.react.extra.router._
7-
import torstenrudolf.scalajs.react.formbinder.demo.components.{InterdependentFieldsMuiDemo, SimpleDemo, SimpleMuiDemo}
7+
import torstenrudolf.scalajs.react.formbinder.demo.components.{FormBinderTraitDemo, InterdependentFieldsMuiDemo, SimpleDemo, SimpleMuiDemo}
88

99

1010
/**
@@ -28,6 +28,7 @@ object AppRouter {
2828
case object Simple extends AppPage
2929
case object SimpleMui extends AppPage
3030
case object ComplexMui extends AppPage
31+
case object FormBinderTrait extends AppPage
3132

3233
type AppRouterCtrl = RouterCtl[AppPage]
3334

@@ -39,10 +40,11 @@ object AppRouter {
3940
| staticRoute("#simple", Simple) ~> render(SimpleDemo())
4041
| staticRoute("#simple-mui", SimpleMui) ~> render(SimpleMuiDemo())
4142
| staticRoute("#complex-mui", ComplexMui) ~> render(InterdependentFieldsMuiDemo())
43+
| staticRoute("#formbindertrait", FormBinderTrait) ~> render(FormBinderTraitDemo())
4244
)
4345
.notFound(redirectToPage(Simple)(Redirect.Replace))
4446
.verify(// ensure all pages are configured and valid, see: https://github.com/japgolly/scalajs-react/blob/master/doc/ROUTER.md#a-spot-of-unsafeness
45-
Simple, SimpleMui, ComplexMui
47+
Simple, SimpleMui, ComplexMui, FormBinderTrait
4648
)
4749
.renderWith(layout)
4850
}
@@ -53,7 +55,8 @@ object AppRouter {
5355
def mainMenuItems = Vector(
5456
MenuItem("Simple", Simple),
5557
MenuItem("Material-UI", SimpleMui),
56-
MenuItem("Dependent Fields", ComplexMui)
58+
MenuItem("Dependent Fields", ComplexMui),
59+
MenuItem("FormBinder Trait", FormBinderTrait)
5760
)
5861

5962
def layout(c: RouterCtl[AppPage], r: Resolution[AppPage]) = {

0 commit comments

Comments
 (0)