Skip to content

Commit eb32324

Browse files
Feature/active field and errors (#46)
* Webpack index file source fixed. * Error construction updated to add origin. Focus and Blur events now set ActiveFieldId. * Field becomes touched on focus. RecalculateDependentFormState -> RecalculateDependentFormStatuses. * Missing return types added. * OnFocus and OnBlur moved into base-dom-field. onFocus and onBlur props are now called if they are defined. * Missing return types defined in core-field.
1 parent 9894a60 commit eb32324

File tree

9 files changed

+122
-35
lines changed

9 files changed

+122
-35
lines changed

packages/simplr-forms-dom/src/abstractions/base-dom-field.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ export interface BaseDomFieldState extends BaseFieldState {
77

88
export abstract class BaseDomField<TProps extends FieldProps, TState extends BaseDomFieldState>
99
extends BaseField<TProps, TState> {
10+
protected OnFocus = (event: React.FocusEvent<HTMLInputElement>) => {
11+
const props = this.props as FieldProps;
12+
if (props.onFocus != null) {
13+
props.onFocus(event);
14+
}
15+
16+
this.Focus();
17+
}
18+
19+
protected OnBlur = (event: React.FocusEvent<HTMLInputElement>) => {
20+
const props = this.props as FieldProps;
21+
if (props.onBlur != null) {
22+
props.onBlur(event);
23+
}
24+
25+
this.Blur();
26+
}
27+
1028
public abstract renderField(): JSX.Element | null;
1129

1230
public render() {

packages/simplr-forms-dom/src/components/form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export class Form extends BaseForm<FormProps, {}> {
3333
return;
3434
}
3535

36+
// Persist synthetic event, because it's passed into another method
3637
event.persist();
3738

39+
// Pass onSubmit result to FormStore for further processing
3840
const result = this.props.onSubmit(event, this.FormStore);
3941
this.FormStore.SubmitForm(result);
4042
}

packages/simplr-forms-dom/src/components/text.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export class Text extends BaseDomField<TextProps, BaseDomFieldState> {
6666
value={this.Value}
6767
onChange={this.OnChangeHandler}
6868
disabled={this.IsDisabled}
69+
onFocus={this.OnFocus}
70+
onBlur={this.OnBlur}
6971
/>;
7072
}
7173
}

packages/simplr-forms/src/abstractions/core-field.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
6969

7070
protected StoreEventSubscription: actionEmitter.EventSubscription;
7171

72-
componentWillMount() {
72+
componentWillMount(): void {
7373
// props.name MUST have a proper value
7474
if (this.props.name == null || this.props.name === "") {
7575
throw new Error("simplr-forms: A proper field name must be given (undefined and empty string are not valid).");
@@ -85,7 +85,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
8585
this.registerFieldInFormStore();
8686
}
8787

88-
componentWillReceiveProps(nextProps: CoreFieldProps) {
88+
componentWillReceiveProps(nextProps: CoreFieldProps): void {
8989
// Check if field name has not been changed
9090
if (this.props.name !== nextProps.name) {
9191
throw new Error(`simplr-forms: Field name must be constant`);
@@ -94,7 +94,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
9494
this.FormStore.UpdateFieldProps(this.FieldId, nextProps);
9595
}
9696

97-
componentWillUnmount() {
97+
componentWillUnmount(): void {
9898
if (this.StoreEventSubscription != null) {
9999
this.StoreEventSubscription.remove();
100100
}
@@ -112,7 +112,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
112112
/**
113113
* Is field currently controlled.
114114
*/
115-
protected get IsControlled() {
115+
protected get IsControlled(): boolean {
116116
return false;
117117
}
118118

@@ -138,7 +138,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
138138
return value;
139139
}
140140

141-
protected ProcessValueFromStore(value: FieldValue) {
141+
protected ProcessValueFromStore(value: FieldValue): FieldValue {
142142
return this.FormatValue(value);
143143
}
144144

@@ -177,11 +177,11 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
177177
);
178178
}
179179

180-
protected ChildrenToRender() {
180+
protected ChildrenToRender(): void {
181181
throw new Error("simplr-forms: Not implemented. Needs to filter out Validators, Modifiers and Normalizers.");
182182
}
183183

184-
protected OnStoreUpdated() {
184+
protected OnStoreUpdated(): void {
185185
const newFormState = this.FormStore.GetState();
186186
const newFieldState = this.FormStore.GetField(this.FieldId);
187187

@@ -202,7 +202,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
202202
}
203203
}
204204

205-
protected OnValueChange(newValue: FieldValue, processValue: boolean = true) {
205+
protected OnValueChange(newValue: FieldValue, processValue: boolean = true): void {
206206
// Noop if the component is controlled from outside
207207
if (this.IsControlled) {
208208
return;
@@ -215,6 +215,14 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
215215
this.FormStore.UpdateFieldValue(this.FieldId, newValue);
216216
}
217217

218+
protected Focus(): void {
219+
this.FormStore.SetActiveField(this.FieldId);
220+
}
221+
222+
protected Blur(): void {
223+
this.FormStore.SetActiveField(undefined);
224+
}
225+
218226
/**
219227
* ========================
220228
* Abstract methods
@@ -251,7 +259,7 @@ export abstract class CoreField<TProps extends CoreFieldProps, TState extends Co
251259
/**
252260
* Registers a field in FormStore or throws if the field was already registered
253261
*/
254-
private registerFieldInFormStore() {
262+
private registerFieldInFormStore(): void {
255263
if (this.FormStore.HasField(this.FieldId)) {
256264
throw new Error(`simplr-forms: Duplicate field id '${this.FieldId}'`);
257265
}

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

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from "react";
22
import * as Immutable from "immutable";
3-
import { recordify } from "typed-immutable-record";
3+
import { recordify, TypedRecord } from "typed-immutable-record";
44
import { ActionEmitter } from "action-emitter";
55

66
import * as Actions from "../actions/form-store";
@@ -25,7 +25,7 @@ import {
2525
} from "../contracts/form-store";
2626
import { FieldsGroupStateRecord } from "../contracts/fields-group";
2727
import { ConstructFormError } from "../utils/form-error-helpers";
28-
import { FormError, FormErrorRecord } from "../contracts/error";
28+
import { FormError, FormErrorRecord, FormErrorOrigin } from "../contracts/error";
2929

3030
export class FormStore extends ActionEmitter {
3131
constructor(formId: string) {
@@ -194,7 +194,12 @@ export class FormStore extends ActionEmitter {
194194
Touched: true
195195
} as FieldState));
196196

197-
return this.RecalculateDependentFormState(state);
197+
state.Form = state.Form.merge({
198+
SuccessfullySubmitted: false,
199+
Error: undefined
200+
} as FormState);
201+
202+
return this.RecalculateDependentFormStatuses(state);
198203
});
199204

200205
this.emit(new Actions.ValueChanged(fieldId, newValue));
@@ -235,7 +240,7 @@ export class FormStore extends ActionEmitter {
235240
Validating: false
236241
} as FieldState));
237242

238-
return this.RecalculateDependentFormState(state);
243+
return this.RecalculateDependentFormStatuses(state);
239244
});
240245
} catch (error) {
241246
// Skip validation if the value has changed again
@@ -244,7 +249,7 @@ export class FormStore extends ActionEmitter {
244249
return;
245250
}
246251

247-
const formError = ConstructFormError(error);
252+
const formError = ConstructFormError(error, FormErrorOrigin.Validation);
248253
if (formError == null) {
249254
throw Error(error);
250255
}
@@ -256,11 +261,42 @@ export class FormStore extends ActionEmitter {
256261
Error: recordify<FormError, FormErrorRecord>(formError!)
257262
} as FieldState));
258263

259-
return this.RecalculateDependentFormState(state);
264+
return this.RecalculateDependentFormStatuses(state);
260265
});
261266
}
262267
}
263268

269+
public SetActiveField(fieldId: string | undefined): void {
270+
this.State = this.State.withMutations(state => {
271+
if (fieldId == null) {
272+
return state.Form.merge({
273+
ActiveFieldId: undefined
274+
} as FormState);
275+
}
276+
277+
const fieldState = this.State.Fields.get(fieldId);
278+
if (fieldState == null) {
279+
console.warn(`simplr-forms: Given field '${fieldId}' does not exist in form '${this.FormId}', `
280+
+ `therefore field cannot be focused. Form.ActiveFieldId was reset to an undefined.`);
281+
return state.Form.merge({
282+
ActiveFieldId: undefined
283+
} as FormState);
284+
}
285+
286+
state.Form = state.Form.merge({
287+
ActiveFieldId: fieldId
288+
} as FormState);
289+
290+
state.Fields = state.Fields.withMutations(fields => {
291+
fields.set(fieldId, fieldState.merge({
292+
Touched: true
293+
} as FieldState));
294+
});
295+
296+
return this.RecalculateDependentFormStatuses(state);
297+
});
298+
}
299+
264300
public async ValidateForm(validationPromise: Promise<never>): Promise<void> {
265301
const form = this.State.Form;
266302

@@ -288,10 +324,10 @@ export class FormStore extends ActionEmitter {
288324
Validating: false
289325
} as FormState);
290326

291-
return this.RecalculateDependentFormState(state);
327+
return this.RecalculateDependentFormStatuses(state);
292328
});
293329
} catch (error) {
294-
const formError = ConstructFormError(error);
330+
const formError = ConstructFormError(error, FormErrorOrigin.Validation);
295331
if (formError == null) {
296332
throw Error(error);
297333
}
@@ -302,7 +338,7 @@ export class FormStore extends ActionEmitter {
302338
Error: recordify<FormError, FormErrorRecord>(formError!)
303339
} as FormState);
304340

305-
return this.RecalculateDependentFormState(state);
341+
return this.RecalculateDependentFormStatuses(state);
306342
});
307343
}
308344
}
@@ -320,7 +356,7 @@ export class FormStore extends ActionEmitter {
320356
promise = result;
321357
} else {
322358
promise = new Promise<void>((resolve, reject) => {
323-
const error = ConstructFormError(result);
359+
const error = ConstructFormError(result, FormErrorOrigin.Submit);
324360
if (error !== undefined) {
325361
reject(result);
326362
return;
@@ -334,24 +370,34 @@ export class FormStore extends ActionEmitter {
334370
state.Form = state.Form.merge({
335371
Submitting: true
336372
} as FormState);
373+
return state;
337374
});
338-
339375
// Try submitting
340376
try {
341377
await promise;
342378
// No error and submitting -> false
343379
this.State = this.State.withMutations(state => {
344380
state.Form = state.Form.merge({
345381
Submitting: false,
382+
SuccessfullySubmitted: true,
346383
Error: undefined
347384
} as FormState);
385+
return state;
348386
});
349-
} catch (err) {
387+
} catch (caughtError) {
388+
// Set error origin
389+
const constructedError = ConstructFormError(caughtError, FormErrorOrigin.Submit);
390+
let error: FormErrorRecord;
391+
if (constructedError != null) {
392+
error = recordify<FormError, FormErrorRecord>(constructedError);
393+
}
394+
350395
// Error and submitting -> false
351396
this.State = this.State.withMutations(state => {
352397
state.Form = state.Form.merge({
353398
Submitting: false,
354-
Error: err
399+
SuccessfullySubmitted: false,
400+
Error: error
355401
} as FormState);
356402
});
357403
}
@@ -380,7 +426,7 @@ export class FormStore extends ActionEmitter {
380426
}
381427
});
382428

383-
return this.RecalculateDependentFormState(state);
429+
return this.RecalculateDependentFormStatuses(state);
384430
});
385431
}
386432

@@ -406,7 +452,7 @@ export class FormStore extends ActionEmitter {
406452
}
407453
});
408454

409-
return this.RecalculateDependentFormState(state);
455+
return this.RecalculateDependentFormStatuses(state);
410456
});
411457
}
412458

@@ -490,7 +536,7 @@ export class FormStore extends ActionEmitter {
490536
return formStoreObject;
491537
}
492538

493-
protected RecalculateDependentFormState(formStoreState: FormStoreStateRecord): FormStoreStateRecord {
539+
protected RecalculateDependentFormStatuses(formStoreState: FormStoreStateRecord): FormStoreStateRecord {
494540
let updater: FormStoreStateStatus = this.GetInitialStoreStatus();
495541

496542
// TODO: might build curried function for more efficient checking.
@@ -664,7 +710,7 @@ export class FormStore extends ActionEmitter {
664710
return result;
665711
}
666712

667-
protected RemoveValues<T>(array: T[], valuesToRemove: T[], concat: boolean = true) {
713+
protected RemoveValues<T>(array: T[], valuesToRemove: T[], concat: boolean = true): T[] {
668714
let result = concat ? array.concat() : array;
669715
for (const value of valuesToRemove) {
670716
let index;

packages/simplr-forms/src/utils/form-error-helpers.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import { FormError } from "../contracts/error";
1+
import { FormError, FormErrorOrigin } from "../contracts/error";
22

33
export function IsFormError(error: any): error is FormError {
44
return (error != null && (error as FormError).Message != null);
55
}
66

7-
export function ConstructFormError(error: any): FormError | undefined {
7+
export function ConstructFormError(error: any, origin: FormErrorOrigin): FormError | undefined {
88
if (IsFormError(error)) {
9+
if (error.Origin != null && error.Origin !== origin) {
10+
console.warn(`simplr-forms: Given error contains property Origin, which is reserved and is always set by FormStore.`);
11+
}
12+
error.Origin = origin;
913
return error;
1014
}
1115
if (typeof error === "string") {
1216
return {
13-
Message: error
17+
Message: error,
18+
Origin: origin
19+
};
20+
}
21+
if (error instanceof Error) {
22+
return {
23+
Message: error.message,
24+
Origin: origin
1425
};
1526
}
1627
return undefined;

packages/simplr-forms/src/utils/value-helpers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export function ProcessValue<TProcessor, TProcessedResult>(
2929
return value;
3030
}
3131

32-
const modifiers = components.filter(x => IsComponentOfType(x, processorTypeFunctionName));
32+
const processors = components.filter(x => IsComponentOfType(x, processorTypeFunctionName));
3333

34-
const renderedModifiers = RenderComponents<TProcessor>(modifiers);
34+
const renderedProcessors = RenderComponents<TProcessor>(processors);
3535

36-
for (const modifier of renderedModifiers) {
37-
value = process(modifier, value);
36+
for (const processor of renderedProcessors) {
37+
value = process(processor, value);
3838
}
3939
return value;
4040
}

packages/simplr-forms/tools/webpack.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const externalsResolver = [
5353

5454
module.exports = {
5555
entry: {
56-
index: "./src/index.ts",
56+
index: "./src/abstractions/index.ts",
5757
modifiers: "./src/modifiers/index.ts",
5858
normalizers: "./src/normalizers/index.ts",
5959
stores: "./src/stores/index.ts",

0 commit comments

Comments
 (0)