Skip to content

Commit 092fd04

Browse files
committed
Initial commit
0 parents  commit 092fd04

File tree

10 files changed

+606
-0
lines changed

10 files changed

+606
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
target/
2+
.cache
3+
.classpath
4+
.project
5+
.settings/
6+
.idea/
7+
*.txt

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2016 TR
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# FormBinder
2+
3+
FormBinder is a tool to bind reactjs form fields to a DataModel plus validation.
4+
5+
6+
## Usage
7+
8+
```scala
9+
import FormBinder.{ValidationResult, Validator}
10+
11+
12+
case class Data(username: String = "",
13+
password1: String = "",
14+
password2: String = "")
15+
16+
// define validation rules in companion object
17+
// note: the field names must match to the data model
18+
object Data {
19+
20+
def username(username: String) = {
21+
if (username.isEmpty) ValidationResult.withError("Please specify username")
22+
else ValidationResult.success
23+
}
24+
25+
def password1(password1: String) = // simpler syntax
26+
Validator(password.isEmpty, "Please specify password")
27+
28+
def password2(password1: String, password2: Option[String]) = {
29+
// you can "inject" any other fields as Options
30+
if (password1.isEmpty) ValidationResult.withError("Please specify password")
31+
else if (password2.nonEmpty && password1 != password2.get) ValidationResult.withError("Must match password.")
32+
else ValidationResult.success
33+
}
34+
35+
// define global (not tied to a specific field) validation rules here
36+
def $global(data: Data) = {
37+
ValidationResult.success
38+
}
39+
40+
41+
...
42+
43+
// then define the form layout fields -- probably inside the component's Backend
44+
45+
object FormDescription extends FormBinder.FormDescription[Data] {
46+
47+
// field names must match the data model's field names
48+
val username = FormFieldDescriptor((a: FormFieldArgs[String]) =>
49+
MuiTextField(
50+
floatingLabelText = "Username",
51+
onChange = (e: ReactEventI) => a.onChangeHandler(e.target.value),
52+
errorText = a.currentValidationResult.errorMessage)()
53+
)
54+
val password1 = ...
55+
val password2 = ...
56+
57+
// define what to do after form data and/or form validation changes
58+
override def onChange(newData: Option[Data],
59+
allFieldValidationResults: List[ValidationResult],
60+
globalFormValidationResult: ValidationResult): Callback = {
61+
$.modState(_.copy(data = newData, globalValidationResult = globalFormValidationResult))
62+
}
63+
}
64+
65+
// this binds the data model, validation rules and form description together
66+
val form = FormBinder.bind[Data](FormDescription)
67+
68+
// use it like this:
69+
val handleSubmit: Callback = $.state >>= { (state: State) =>
70+
if (state.globalFormValidationResult.isValid) {
71+
// do something with state.data.get
72+
} else form.showUninitializedFieldErrors
73+
}
74+
75+
def render(state: State) = <.div(
76+
<.form(
77+
^.onSubmit --> handleSubmit,
78+
^.display.flex,
79+
^.flexDirection.column,
80+
loginForm.field(LoginFormDescription.username),
81+
loginForm.field(LoginFormDescription.password)
82+
),
83+
if (!state.globalValidationResult.isValid) {
84+
<.div(state.globalValidationResult.errorMessage)
85+
} else ""
86+
)
87+
```
88+
89+
90+
## TODO
91+
92+
* add prebuild FormFieldDescriptors for material-ui

build.sbt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
def commonSettings: Project => Project =
3+
_.enablePlugins(ScalaJSPlugin)
4+
.settings(
5+
organization := "com.github.torstenrudolf.scalajs-react-form-binder",
6+
version := "0.0.1-SNAPSHOT",
7+
homepage := Some(url("https://github.com/torstenrudolf/scalajs-react-form-binder")),
8+
licenses += ("MIT", url("https://opensource.org/licenses/MIT")),
9+
scalaVersion := "2.11.8")
10+
11+
def preventPublication: Project => Project =
12+
_.settings(
13+
publishTo := Some(Resolver.file("Unused transient repository", target.value / "fakepublish")),
14+
publishArtifact := false)
15+
16+
17+
lazy val core = (project in file("core")).configure(commonSettings)
18+
.settings(
19+
name := "core",
20+
libraryDependencies ++= Seq(
21+
"org.scala-lang" % "scala-compiler" % scalaVersion.value,
22+
"com.github.japgolly.scalajs-react" %%% "extra" % "0.11.2"
23+
),
24+
publishTo <<= version { (v: String) =>
25+
val nexus = "https://oss.sonatype.org/"
26+
if (v.trim.endsWith("SNAPSHOT"))
27+
Some("snapshots" at nexus + "content/repositories/snapshots")
28+
else
29+
Some("releases" at nexus + "service/local/staging/deploy/maven2")
30+
},
31+
publishArtifact in Test := false,
32+
pomExtra :=
33+
<url>https://github.com/torstenrudolf/scalajs-react-form-binder</url>
34+
<licenses>
35+
<license>
36+
<name>MIT license</name>
37+
<url>http://www.opensource.org/licenses/mit-license.php</url>
38+
</license>
39+
</licenses>
40+
<scm>
41+
<url>git://github.com/torstenrudolf/scalajs-react-form-binder.git</url>
42+
<connection>scm:git://github.com/torstenrudolf/scalajs-react-form-binder.git</connection>
43+
</scm>
44+
<developers>
45+
<developer>
46+
<id>torstenrudolf</id>
47+
<name>Torsten Rudolf</name>
48+
<url>https://github.com/torstenrudolf</url>
49+
</developer>
50+
</developers>
51+
)
52+
53+
lazy val root = (project in file("."))
54+
.aggregate(core)
55+
.configure(commonSettings, preventPublication)
56+
57+
58+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package torstenrudolf.scalajs.react.formbinder
2+
3+
import japgolly.scalajs.react.{Callback, ReactNode}
4+
5+
trait Form[DataModel] {
6+
7+
def allFields: List[ReactNode]
8+
9+
def field[T](fd: FormFieldDescriptor[T]): ReactNode
10+
11+
def setFieldValue[T](fd: FormFieldDescriptor[T], value: T): Callback
12+
13+
def showUninitializedFieldErrors: Callback
14+
15+
def fieldValue[A](fd: FormFieldDescriptor[A]): Option[A]
16+
17+
}
18+
19+
20+
sealed trait FormFailure
21+
22+
case object FormUninitialized extends Throwable("Uninitialized Field") with FormFailure
23+
24+
case object FormProcessingFailure extends Throwable("Form is actively processing") with FormFailure
25+
26+
27+
case class FormFieldArgs[T](fieldName: String,
28+
currentValue: Option[T],
29+
currentValidationResult: ValidationResult,
30+
onChangeCB: (T) => Callback,
31+
// todo: Do they need to be functions?
32+
resetCB: () => Callback, // sets to defaultValue from DataModel if present
33+
clearCB: () => Callback,
34+
private val parentForm: Form[_] with FormAPI[_]) {
35+
36+
def otherFieldValue[T2](fd: FormFieldDescriptor[T2]): Option[T2] = parentForm.fieldValue(fd)
37+
38+
def clearOtherField[T2](fd: FormFieldDescriptor[T2]): Callback = parentForm.fieldBinding(fd).clear()
39+
}
40+
41+
case class FormFieldDescriptor[T](descr: (FormFieldArgs[T]) => ReactNode)
42+
43+
44+
abstract class FormLayout[T] {
45+
/*
46+
47+
*/
48+
def onChange(newData: Option[T],
49+
allFieldValidationResults: List[ValidationResult],
50+
globalFormValidationResult: ValidationResult): Callback
51+
52+
}

0 commit comments

Comments
 (0)