Skip to content

Commit

Permalink
feat: Validation delay for forms and fields. (#223)
Browse files Browse the repository at this point in the history
For a better user experience, wait to validate form fields until the user 'leaves' the field. This provides a more immediate feedback for the user on the value without distracting the user during input.

Also enables the form 'submit' and then marking all errors before disabling the button if invalid.
  • Loading branch information
Zoramite committed Aug 11, 2021
1 parent e7c0501 commit ffc24f7
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 240 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@types/lodash.merge": "^4.6.6",
"@types/marked": "^2.0.4",
"@types/mime-types": "^2.1.0",
"@types/node": "^16.4.13",
"@types/node": "^16.6.0",
"@types/nunjucks": "^3.1.5",
"@types/prosemirror-commands": "^1.0.4",
"@types/prosemirror-model": "^1.13.1",
Expand Down Expand Up @@ -96,7 +96,7 @@
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@blinkk/selective-edit": "^2.3.0",
"@blinkk/selective-edit": "^3.0.0",
"@toast-ui/editor": "^3.0.2",
"bent": "^7.3.12",
"codemirror": "^5.62.2",
Expand Down
13 changes: 10 additions & 3 deletions src/ts/editor/field/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,13 @@ export class MediaField
];
this.droppableUi.listeners.add('files', this.handleFiles.bind(this));

this.zoneToKey = {
path: 'path',
label: 'label',
this.zones = {
path: {
key: 'path',
},
label: {
key: 'label',
},
};
}

Expand Down Expand Up @@ -446,6 +450,9 @@ export class MediaField
type="text"
id="media-${this.uid}"
placeholder=${this.config.placeholder || ''}
@blur=${() => {
this.lostFocus('path');
}}
@input=${this.handleInput.bind(this)}
value=${value.path || ''}
/>
Expand Down
1 change: 1 addition & 0 deletions src/ts/editor/ui/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export class DialogModal extends Modal {
le__button: true,
le__modal__action: true,
'le__button--extreme': config.level === DialogActionLevel.Extreme,
'le__button--outline': config.level === DialogActionLevel.Primary,
'le__button--primary': config.level === DialogActionLevel.Primary,
'le__button--secondary': config.level === DialogActionLevel.Secondary,
};
Expand Down
31 changes: 27 additions & 4 deletions src/ts/editor/ui/parts/content/sectionFields.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ContentSectionPart, ContentSectionPartConfig} from './section';
import {DeepObject, TemplateResult} from '@blinkk/selective-edit';

import {EVENT_SAVE} from '../../../events';
import {EVENT_RENDER_COMPLETE, EVENT_SAVE} from '../../../events';
import {EditorFileData} from '../../../api';
import {StatePromiseKeys} from '../../../state';
import {UnknownField} from '../../../field/unknown';
Expand Down Expand Up @@ -35,9 +35,34 @@ export class FieldsPart extends ContentSectionPart {
this.selective.types.fields.DefaultCls = UnknownField as any;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleAction(evt: Event) {
this.isProcessing = true;
this.render();

// The first time the editor is marked for validation the values cannot be trusted.
// The render step needs to complete before the validation can be trusted.
if (!this.selective.markValidation) {
// Mark the selective editor for all field validation.
// For UX the validation is not run until the user interacts with a
// field or when they try to 'submit'.
this.selective.markValidation = true;

document.addEventListener(
EVENT_RENDER_COMPLETE,
() => {
this.handleAction(evt);
},
{
once: true,
}
);
this.render();
return;
}

if (this.selective.isClean || !this.selective.isValid) {
this.isProcessing = false;
this.render();
return;
}

Expand All @@ -46,8 +71,6 @@ export class FieldsPart extends ContentSectionPart {
// instead of overwritten.
value.data = this.selective.value;

this.isProcessing = true;
this.render();
this.config.state.saveFile(value as EditorFileData, false, () => {
this.isProcessing = false;
this.render();
Expand Down
31 changes: 28 additions & 3 deletions src/ts/editor/ui/parts/content/sectionRaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
TextAreaFieldConfig,
} from '@blinkk/selective-edit';

import {EVENT_SAVE} from '../../../events';
import {EVENT_RENDER_COMPLETE, EVENT_SAVE} from '../../../events';
import {EditorFileData} from '../../../api';
import {StatePromiseKeys} from '../../../state';

Expand Down Expand Up @@ -52,12 +52,37 @@ export class RawPart extends ContentSectionPart {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleAction(evt: Event) {
this.isProcessing = true;
this.render();

// The first time the editor is marked for validation the values cannot be trusted.
// The render step needs to complete before the validation can be trusted.
if (!this.selective.markValidation) {
// Mark the selective editor for all field validation.
// For UX the validation is not run until the user interacts with a
// field or when they try to 'submit'.
this.selective.markValidation = true;

document.addEventListener(
EVENT_RENDER_COMPLETE,
() => {
this.handleAction(evt);
},
{
once: true,
}
);

this.render();
return;
}

if (this.selective.isClean || !this.selective.isValid) {
this.isProcessing = false;
this.render();
return;
}

this.isProcessing = true;
this.render();
this.config.state.saveFile(
this.selective.value as EditorFileData,
true,
Expand Down

0 comments on commit ffc24f7

Please sign in to comment.