Skip to content

Commit 360c6d0

Browse files
Form Touched, UpdateFieldProps custom diffing added.
1 parent ebf96c2 commit 360c6d0

File tree

3 files changed

+222
-105
lines changed

3 files changed

+222
-105
lines changed
Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,82 @@
1-
{
2-
"name": "simplr-forms-core",
3-
"version": "4.0.0-pre-alpha.18",
4-
"description": "Shared simplr-forms logic.",
5-
"repository": "SimplrJS/simplr-forms",
6-
"homepage": "https://github.com/SimplrJS/simplr-forms",
7-
"main": "index.js",
8-
"types": "index.d.ts",
9-
"author": "simplrjs <[email protected]> (https://github.com/simplrjs)",
10-
"scripts": {
11-
"build": "webpack && npm run tslint",
12-
"watch": "webpack -w",
13-
"tslint": "tslint --config ./tslint.json --project . --exclude __tests__/**/* && echo TsLint test successfully passed.",
14-
"uglifyjs": "uglifyjs ./dist/simplr-forms-core.js -o ./dist/simplr-forms-core.min.js --compress --mangle",
15-
"release": "npm run build && npm run uglifyjs",
16-
"test": "jest",
17-
"test-watch": "jest --watchAll",
18-
"test-coverage": "npm test -- --coverage",
19-
"gulp-build": "tsc -p ./tools/tsconfig.gulp.json",
20-
"gulp-watch": "npm run gulp-build -- -w"
21-
},
22-
"license": "AGPL-3.0",
23-
"files": [
24-
"**/*.md",
25-
"*.js",
26-
"**/*.d.ts",
27-
"!*.config.js",
28-
"!gulpfile.js",
29-
"!node_modules/**"
30-
],
31-
"devDependencies": {
32-
"@types/chokidar": "^1.6.0",
33-
"@types/enzyme": "^2.7.9",
34-
"@types/gulp": "^4.0.2",
35-
"@types/jest": "^19.2.2",
36-
"@types/sinon": "^2.1.2",
37-
"@types/undertaker": "^0.12.28",
38-
"@types/vinyl-fs": "^2.4.5",
39-
"@types/webpack": "^2.2.15",
40-
"enzyme": "^2.8.2",
41-
"gulp": "github:gulpjs/gulp#4.0",
42-
"jest": "^19.0.2",
43-
"jest-enzyme": "^3.0.1",
44-
"on-build-webpack": "^0.1.0",
45-
"react-dom": "^15.5.4",
46-
"react-test-renderer": "^15.5.4",
47-
"simplr-mvdir": "0.0.1-beta.7",
48-
"sinon": "^2.1.0",
49-
"ts-jest": "^19.0.10",
50-
"ts-loader": "^2.0.3",
51-
"tslint": "^5.1.0",
52-
"typescript": "2.3.0",
53-
"uglify-js": "^2.8.22",
54-
"webpack": "^2.4.1"
55-
},
56-
"dependencies": {
57-
"@types/fbemitter": "^2.0.32",
58-
"@types/flux": "^3.0.1",
59-
"@types/prop-types": "^15.5.1",
60-
"@types/react": "^15.0.22",
61-
"@types/react-dom": "^15.5.0",
62-
"action-emitter": "^0.2.1",
63-
"fbemitter": "^2.1.1",
64-
"immutable": "^3.8.1",
65-
"prop-types": "^15.5.8",
66-
"react": "15.5.4",
67-
"react-dom": "^15.5.4",
68-
"typed-immutable-record": "^0.0.6"
69-
},
70-
"jest": {
71-
"setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
72-
"transform": {
73-
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
74-
},
75-
"testRegex": "/__tests__/.*\\.(test|spec).(ts|tsx|js)$",
76-
"moduleFileExtensions": [
77-
"ts",
78-
"tsx",
79-
"js"
80-
]
81-
}
82-
}
1+
{
2+
"name": "simplr-forms-core",
3+
"version": "4.0.0-pre-alpha.18",
4+
"description": "Shared simplr-forms logic.",
5+
"repository": "SimplrJS/simplr-forms",
6+
"homepage": "https://github.com/SimplrJS/simplr-forms",
7+
"main": "index.js",
8+
"types": "index.d.ts",
9+
"author": "simplrjs <[email protected]> (https://github.com/simplrjs)",
10+
"scripts": {
11+
"build": "webpack && npm run tslint",
12+
"watch": "webpack -w",
13+
"tslint": "tslint --config ./tslint.json --project . --exclude __tests__/**/* && echo TsLint test successfully passed.",
14+
"uglifyjs": "uglifyjs ./dist/simplr-forms-core.js -o ./dist/simplr-forms-core.min.js --compress --mangle",
15+
"release": "npm run build && npm run uglifyjs",
16+
"test": "jest",
17+
"test-watch": "jest --watchAll",
18+
"test-coverage": "npm test -- --coverage",
19+
"gulp-build": "tsc -p ./tools/tsconfig.gulp.json",
20+
"gulp-watch": "npm run gulp-build -- -w"
21+
},
22+
"license": "AGPL-3.0",
23+
"files": [
24+
"**/*.md",
25+
"*.js",
26+
"**/*.d.ts",
27+
"!*.config.js",
28+
"!gulpfile.js",
29+
"!node_modules/**"
30+
],
31+
"devDependencies": {
32+
"@types/chokidar": "^1.6.0",
33+
"@types/enzyme": "^2.7.9",
34+
"@types/gulp": "^4.0.2",
35+
"@types/jest": "^19.2.2",
36+
"@types/sinon": "^2.1.2",
37+
"@types/undertaker": "^0.12.28",
38+
"@types/vinyl-fs": "^2.4.5",
39+
"@types/webpack": "^2.2.15",
40+
"enzyme": "^2.8.2",
41+
"gulp": "github:gulpjs/gulp#4.0",
42+
"jest": "^19.0.2",
43+
"jest-enzyme": "^3.0.1",
44+
"on-build-webpack": "^0.1.0",
45+
"react-dom": "^15.5.4",
46+
"react-test-renderer": "^15.5.4",
47+
"simplr-mvdir": "0.0.1-beta.7",
48+
"sinon": "^2.1.0",
49+
"ts-jest": "^19.0.10",
50+
"ts-loader": "^2.0.3",
51+
"tslint": "^5.1.0",
52+
"typescript": "2.3.0",
53+
"uglify-js": "^2.8.22",
54+
"webpack": "^2.4.1"
55+
},
56+
"dependencies": {
57+
"@types/fbemitter": "^2.0.32",
58+
"@types/flux": "^3.0.1",
59+
"@types/prop-types": "^15.5.1",
60+
"@types/react": "^15.0.22",
61+
"@types/react-dom": "^15.5.0",
62+
"action-emitter": "^0.2.1",
63+
"fbemitter": "^2.1.1",
64+
"immutable": "^3.8.1",
65+
"prop-types": "^15.5.8",
66+
"react": "15.5.4",
67+
"react-dom": "^15.5.4",
68+
"typed-immutable-record": "^0.0.6"
69+
},
70+
"jest": {
71+
"setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
72+
"transform": {
73+
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
74+
},
75+
"testRegex": "/__tests__/.*\\.(test|spec).(ts|tsx|js)$",
76+
"moduleFileExtensions": [
77+
"ts",
78+
"tsx",
79+
"js"
80+
]
81+
}
82+
}

packages/simplr-forms-core/src/contracts/form.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface FormState {
2222
Pristine: boolean;
2323
Error?: FormError;
2424
Submitting: boolean;
25+
Touched: boolean;
2526
SuccessfullySubmitted: boolean;
2627
SubmitCallback?: () => void;
2728
ActiveFieldId?: string;

packages/simplr-forms-core/src/stores/form-store.ts

Lines changed: 139 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from "react";
12
import * as Immutable from "immutable";
23
import { recordify } from "typed-immutable-record";
34
import { ActionEmitter } from "action-emitter";
@@ -162,7 +163,70 @@ export class FormStore extends ActionEmitter {
162163
const propsRecord = recordify<FieldStateProps, FieldStatePropsRecord>(props);
163164
const fieldState = this.State.Fields.get(fieldId);
164165

165-
if (fieldState.Props == null || fieldState.Props.equals(propsRecord)) {
166+
if (fieldState.Props == null) {
167+
return;
168+
}
169+
170+
// Custom props diff, because Immutable.Reacord.equals always returns false
171+
// (because of children always changing?..)
172+
let changed = false;
173+
propsRecord.toSeq().forEach((value, key) => {
174+
if (key == null) {
175+
return;
176+
}
177+
const oldValue = fieldState.Props!.get(key);
178+
if (key !== "children") {
179+
if (!this.ObjectsEqualDeepCheck(oldValue, value)) {
180+
changed = true;
181+
return false;
182+
}
183+
} else {
184+
// Take old children
185+
const oldChildren = fieldState.Props!.get(key) as React.ReactNode | undefined;
186+
if (oldChildren != null) {
187+
// Take new children and convert them to an array with React.Children.toArray
188+
const newChildren = React.Children.toArray(value);
189+
190+
// For each oldChildren
191+
React.Children.forEach(oldChildren, (child: React.ReactChild, index) => {
192+
// If a child is a text component and no new children is equal to it
193+
if (typeof child === "string" && !newChildren.some(x => x === child)) {
194+
// Props have changed
195+
changed = true;
196+
return false;
197+
}
198+
199+
const childElement = child as React.ReactElement<any>;
200+
201+
// Try to find best a match for an old child in the newChildren array
202+
const newChildElement = newChildren.find(x => {
203+
// String case has been checked before
204+
if (typeof x !== "string") {
205+
const xElement = x as React.ReactElement<any>;
206+
// If type and key properties match
207+
// Children should be the same
208+
if (xElement.type === childElement.type &&
209+
xElement.key === childElement.key) {
210+
return true;
211+
}
212+
}
213+
// Return false explicitly by default
214+
return false;
215+
}) as React.ReactElement<any>;
216+
217+
// If newChildElement was found and its props are different
218+
if (newChildElement != null &&
219+
!this.ObjectsEqualDeepCheck(childElement.props, newChildElement.props)) {
220+
// Props have changed
221+
changed = true;
222+
return false;
223+
}
224+
});
225+
}
226+
}
227+
});
228+
229+
if (!changed) {
166230
return;
167231
}
168232

@@ -177,37 +241,29 @@ export class FormStore extends ActionEmitter {
177241
}
178242

179243
public UpdateFieldValue(fieldId: string, newValue: FieldValue): void {
244+
const fieldState = this.State.Fields.get(fieldId);
245+
if (fieldState.Value === newValue) {
246+
return;
247+
}
248+
180249
this.State = this.State.withMutations(state => {
181-
const fieldState = state.Fields.get(fieldId);
182-
const fieldPristine = (newValue === fieldState.InitialValue);
250+
const newPristine = (newValue === fieldState.InitialValue);
183251
state.Fields = state.Fields.set(fieldId, fieldState.merge({
184252
Value: newValue,
185-
Pristine: fieldPristine
253+
Pristine: newPristine,
254+
Touched: true
186255
} as FieldState));
187256

188-
if (!fieldPristine) {
189-
state.Form = state.Form.merge({
190-
Pristine: false
191-
} as FormState);
192-
} else {
193-
let allFieldsPristine = true;
194-
state.Fields.forEach(field => {
195-
if (field != null && !field.Pristine) {
196-
allFieldsPristine = false;
197-
return false;
198-
}
199-
});
200-
201-
state.Form = state.Form.merge({
202-
Pristine: allFieldsPristine
203-
});
204-
}
257+
state.Form = this.RecalculateDependentFormState(state, {
258+
Pristine: newPristine,
259+
Touched: true
260+
});
205261
});
206262

207263
this.emit(new Actions.ValueChanged(fieldId, newValue));
208264
}
209265

210-
public async ValidateiField(fieldId: string, validationPromise: Promise<never>): Promise<void> {
266+
public async ValidateField(fieldId: string, validationPromise: Promise<never>): Promise<void> {
211267
const field = this.State.Fields.get(fieldId);
212268
const fieldValue = field.Value;
213269

@@ -329,11 +385,12 @@ export class FormStore extends ActionEmitter {
329385
const fieldState = state.Fields.get(fieldId);
330386

331387
if (fieldState != null) {
388+
const oldValue = fieldState.Value;
332389
state.Fields = state.Fields.set(fieldId, fieldState.merge({
333390
Error: undefined,
334391
Value: fieldState.DefaultValue,
335392
Pristine: (fieldState.InitialValue === fieldState.DefaultValue),
336-
Touched: false
393+
Touched: oldValue !== fieldState.DefaultValue
337394
} as FieldState));
338395
}
339396
});
@@ -397,6 +454,7 @@ export class FormStore extends ActionEmitter {
397454
Pristine: true,
398455
SuccessfullySubmitted: false,
399456
ActiveFieldId: undefined,
457+
Touched: false,
400458
Error: undefined,
401459
SubmitCallback: undefined
402460
};
@@ -431,7 +489,65 @@ export class FormStore extends ActionEmitter {
431489
return formStoreObject;
432490
}
433491

492+
protected RecalculateDependentFormState(
493+
formStoreState: FormStoreStateRecord,
494+
newStatePartial: Partial<FormState>): FormStateRecord {
495+
let updater = {
496+
Pristine: true,
497+
Touched: false,
498+
...newStatePartial
499+
} as FormState;
500+
501+
// TODO: might build curried function for more efficient checking
502+
503+
this.State.Fields.forEach(field => {
504+
if (field != null) {
505+
if (updater.Pristine && !field.Pristine) {
506+
updater.Pristine = false;
507+
}
508+
if (!updater.Touched && field.Touched) {
509+
updater.Touched = true;
510+
}
511+
512+
// Short circuit if both Pristine and Touched are already resolved
513+
if (!updater.Pristine && updater.Touched) {
514+
return false;
515+
}
516+
}
517+
});
518+
519+
return formStoreState.Form.merge(updater);
520+
}
521+
434522
protected IsPromise<T>(value: any): value is Promise<T> {
435523
return value != null && value.then != null && value.catch != null;
436524
}
525+
526+
protected ObjectsEqualDeepCheck(obj: { [key: string]: any }, obj2: { [key: string]: any }) {
527+
if (typeof obj !== "object" && typeof obj !== typeof obj2) {
528+
console.warn("Object Deep Check: Variable given is not an object!");
529+
return false;
530+
}
531+
532+
if ((obj == null && obj2 != null) || (obj != null && obj2 == null)) {
533+
return false;
534+
} else if (obj == null && obj2 == null) {
535+
return true;
536+
}
537+
538+
if (Object.keys(obj2).length !== Object.keys(obj).length) {
539+
return false;
540+
}
541+
542+
for (let key in obj) {
543+
if (typeof obj[key] === "object" && typeof obj2[key] === "object") {
544+
if (!this.ObjectsEqualDeepCheck(obj[key], obj2[key])) {
545+
return false;
546+
}
547+
} else if (obj[key] !== obj2[key]) {
548+
return false;
549+
}
550+
}
551+
return true;
552+
}
437553
}

0 commit comments

Comments
 (0)