Skip to content

Commit 6e9cfaa

Browse files
committed
added first demo
1 parent 092fd04 commit 6e9cfaa

File tree

19 files changed

+824
-89
lines changed

19 files changed

+824
-89
lines changed

.gitignore

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
target/
1+
target
22
.cache
33
.classpath
44
.project
5-
.settings/
6-
.idea/
7-
*.txt
5+
.settings
6+
.idea
7+
*.txt
8+
node_modules
9+
build

README.md

Lines changed: 24 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,36 @@
11
# FormBinder
22

3-
FormBinder is a tool to bind reactjs form fields to a DataModel plus validation.
3+
FormBinder is a tool to bind
4+
[scalajs-react](https://github.com/japgolly/scalajs-react)
5+
form fields to a data model plus validation.
46

7+
My main goal was to provide a data binding between form fields and
8+
data model and a way to specify extra validation rules without loosing
9+
flexibility of the form design.
510

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-
...
11+
## Installation
4212

43-
// then define the form layout fields -- probably inside the component's Backend
44-
45-
object FormDescription extends FormBinder.FormDescription[Data] {
13+
Currently it is available as a sonatype snapshot, add this to your build.sbt:
4614

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)
15+
```
16+
libraryDependencies += "com.github.torstenrudolf.scalajs-react-form-binder" %%% "core" % "0.0.1-SNAPSHOT"
17+
```
6718

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-
)
19+
You also might need to add the resolver for the sonatype snapshot repo:
8720
```
21+
resolvers ++= Resolver.sonatypeRepo("snapshots")
22+
```
23+
24+
## Usage
25+
See demo: https://torstenrudolf.github.io/scalajs-react-form-binder
8826

27+
* define the data model as a `case class`
28+
* optionally define validation rules in an object
29+
* define the form fields in a FormLayout object
30+
* the matching of the field-names is type-safe as it is done at compile time via a macro
31+
* have full control over display of the form fields inside your `render` method
8932

9033
## TODO
9134

92-
* add prebuild FormFieldDescriptors for material-ui
35+
* add tests
36+
* add prebuild FormFieldDescriptors for material-ui fields

build.sbt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,49 @@ lazy val core = (project in file("core")).configure(commonSettings)
5050
</developers>
5151
)
5252

53-
lazy val root = (project in file("."))
54-
.aggregate(core)
55-
.configure(commonSettings, preventPublication)
53+
lazy val installExternalNPMDeps = TaskKey[Unit]("Execute the npm build command to build the external ui dependencies")
54+
def filesToWatchForExternalNPMDepsTask(workingDir: File) : Seq[File] = {
55+
Seq(workingDir / "package.json",
56+
workingDir / "webpack.config.js",
57+
workingDir / "webpack.config.prod.js",
58+
workingDir / "resources/jsBundles/index.js")
59+
}
60+
def installExternalNPMDeps(workingDir: File): Unit = {
61+
// use FileFunction.cached to run this task only if the package.json or webpack files have changed
62+
FileFunction.cached(workingDir / "build/cache",
63+
FilesInfo.lastModified, /* inStyle */
64+
FilesInfo.exists) /* outStyle */ {
65+
(inFiles: Set[File]) => {
66+
val installCommand = Seq("npm", "install")
67+
println(s"${installCommand.mkString(" ")}")
68+
Process(installCommand, workingDir) !!
69+
70+
val runCommand = Seq("npm", "run", "build")
71+
println(s"${runCommand.mkString(" ")}")
72+
Process(runCommand, workingDir) !!
5673

74+
Set(workingDir / "build/frontend-jsdeps.js")
75+
}
76+
}(filesToWatchForExternalNPMDepsTask(workingDir).toSet)
5777

78+
}
79+
80+
lazy val demo = project
81+
.dependsOn(core)
82+
.settings(
83+
libraryDependencies ++= Seq(
84+
"com.github.chandu0101.scalajs-react-components" %%% "core" % "0.5.0"
85+
),
86+
Seq(packageScalaJSLauncher, fastOptJS, fullOptJS) map { packageJSKey =>
87+
crossTarget in(Compile, packageJSKey) := baseDirectory.value / "build"
88+
},
89+
installExternalNPMDeps := installExternalNPMDeps(baseDirectory.value),
90+
watchSources <++= baseDirectory map { path => filesToWatchForExternalNPMDepsTask(path)},
91+
compile in Compile <<= (compile in Compile) dependsOn installExternalNPMDeps,
92+
cleanFiles <++= baseDirectory { base => Seq(base / "build", base / "node_modules") }
93+
)
94+
.configure(commonSettings, preventPublication)
5895

96+
lazy val root = (project in file("."))
97+
.aggregate(core)
98+
.configure(commonSettings, preventPublication)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ abstract class FormLayout[T] {
4545
/*
4646
4747
*/
48-
def onChange(newData: Option[T],
48+
def onChange(validatedData: Option[T],
4949
allFieldValidationResults: List[ValidationResult],
5050
globalFormValidationResult: ValidationResult): Callback
5151

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import scala.language.experimental.macros
33

44
package object formbinder {
55

6-
def bind[T](validatorObject: Any, formLayout: FormLayout[T]): Form[T] = macro Macros.generate[T] //Layout with Form[T] with FormProcs[T] = macro Macro.generate[T, Layout]
6+
def bind[DataModel](validatorObject: Any, formLayout: FormLayout[DataModel]): Form[DataModel] = macro Macros.generate[DataModel] //Layout with Form[T] with FormProcs[T] = macro Macro.generate[T, Layout]
77

88
}

demo/deploy.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/sh
2+
set -e
3+
4+
#Handy script to deploy demo-app to gh-pages
5+
6+
# get comment
7+
comment="$1"
8+
if [ "$comment" = "" ]; then echo "please provide commit message"; exit 23; fi
9+
10+
scriptFile=$(readlink -f "$0")
11+
projectPath=$(dirname "$scriptFile")/..
12+
13+
cd $projectPath
14+
15+
git checkout master
16+
git pull
17+
git checkout gh-pages
18+
git pull
19+
git rebase master
20+
21+
sbt clean
22+
23+
sbt "demo/fullOptJS"
24+
25+
ghPagesDir=$projectPath/"gh-pages"
26+
27+
mkdir -p ${ghPagesDir}
28+
29+
cp demo/index.html $projectPath
30+
31+
cp demo/build/demo-jsdeps.js ${ghPagesDir}
32+
cp demo/build/demo-opt.js ${ghPagesDir}
33+
cp demo/style.css ${ghPagesDir}
34+
35+
git add ${projectPath}
36+
37+
git commit -m "$comment"
38+
39+
echo "all you need to do is to push"

demo/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>scalajs-react-formbinder demo</title>
5+
6+
<meta http-equiv="Content-Type"/>
7+
8+
<script type="text/javascript" src="gh-pages/demo-opt.js"></script>
9+
<script type="text/javascript" src="gh-pages/demo-jsdeps.js"></script>
10+
<link rel="stylesheet" href="gh-pages/style.css">
11+
12+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
13+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/github.min.css">
14+
15+
</head>
16+
<body style="margin: 0px;">
17+
<div id="reactJSEntryHook"></div>
18+
<script>torstenrudolf.scalajs.react.formbinder.demo.App().main(document.getElementById('reactJSEntryHook'))</script>
19+
</body>
20+
</html>

demo/index_dev.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head><title>scalajs-react-formbinder demo</title>
4+
<meta http-equiv="Content-Type"/>
5+
<script type="text/javascript" src="./build/demo-fastopt.js"></script>
6+
<script type="text/javascript" src="./build/demo-jsdeps.js"></script>
7+
8+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
9+
10+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/github.min.css">
11+
12+
<link rel="stylesheet" href="style.css">
13+
14+
</head>
15+
<body style="margin: 0px;">
16+
<div id="reactJSEntryHook"></div>
17+
<script>torstenrudolf.scalajs.react.formbinder.demo.App().main(document.getElementById('reactJSEntryHook'))</script>
18+
</body>
19+
</html>

demo/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "scalajs-react-formbinder-demo",
3+
"version": "0.0.1",
4+
"description": "scalajs-react-formbinder demo",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/torstenrudolf/scalajs-react-formbinder.git"
8+
},
9+
"scripts": {
10+
"build-dev": "webpack --progress --colors",
11+
"build": "webpack --config webpack.config.prod.js"
12+
},
13+
"devDependencies": {
14+
"source-map-support": "v0.4.2",
15+
"webpack": "^1.9.10",
16+
"compression-webpack-plugin": "^0.2.0"
17+
},
18+
"dependencies": {
19+
"material-ui": "v0.15.4",
20+
"highlight.js": "8.9.1",
21+
"react": "^15.3.2",
22+
"react-dom": "^15.3.2",
23+
"react-tap-event-plugin": "^1.0.0"
24+
}
25+
}

demo/resources/jsBundles/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
window.ReactDOM = require('react-dom');
2+
window.React = require('react');
3+
4+
var injectTapEventPlugin = require('react-tap-event-plugin');
5+
injectTapEventPlugin();
6+
7+
window.mui = require("material-ui");
8+
window.mui.Styles = require("material-ui/styles");
9+
window.mui.SvgIcons = require('material-ui/svg-icons/index');
10+
11+
window.hljs = require("highlight.js");
12+
// require("highlight.js/styles/github.css");

0 commit comments

Comments
 (0)