Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions eclipse-scout-core/src/form/fields/BasicField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -88,13 +88,13 @@ export abstract class BasicField<TValue extends TModelValue, TModelValue = TValu
}
}

override acceptInput(whileTyping?: boolean) {
override acceptInput(whileTyping?: boolean): JQuery.Promise<void> | void {
if (this._displayTextModifiedTimeoutId !== null) {
// Cancel pending "acceptInput(true)" call (see _onDisplayTextModified) and execute it now
clearTimeout(this._displayTextModifiedTimeoutId);
this._displayTextModifiedTimeoutId = null;
}
super.acceptInput(whileTyping);
return super.acceptInput(whileTyping);
}

protected override _renderDisplayText() {
Expand Down
1 change: 1 addition & 0 deletions eclipse-scout-core/src/form/fields/FormField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,7 @@ export type ValidationResult = {
errorStatus?: Status;
field: FormField;
label: string;
promise?: JQuery.Promise<void>;
reveal: () => void;
visitResult?: TreeVisitResult;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {fields, FormField, InitModelOf, ObjectModel, ObjectWithType, scout, SomeRequired, Status, ValidationResult} from '../..';
import {fields, FormField, InitModelOf, ObjectModel, ObjectWithType, scout, SomeRequired, Status, ValidationResult, ValueField} from '../..';

export class FormFieldValidationResultProvider implements ObjectWithType {
declare model: FormFieldValidationResultProviderModel;
Expand All @@ -23,19 +23,28 @@ export class FormFieldValidationResultProvider implements ObjectWithType {
provide(errorStatus: Status): ValidationResult {
const validByErrorStatus = !errorStatus || errorStatus.isValid();
const validByMandatory = !this.field.mandatory || !this.field.empty;
const valid = validByErrorStatus && validByMandatory;
const validatePendingPromise = this._validatePendingPromise();
const valid = validByErrorStatus && validByMandatory && !validatePendingPromise;
return {
valid,
validByMandatory,
errorStatus,
field: this.field,
label: this.field.label,
promise: validatePendingPromise,
reveal: () => {
fields.selectAllParentTabsOf(this.field);
this.field.focus();
}
};
}

protected _validatePendingPromise(): JQuery.Promise<void> {
if (this.field instanceof ValueField && this.field.validatePending) {
return this.field.when('propertyChange:validatePending').then(() => undefined);
}
return null;
}
}

export interface FormFieldValidationResultProviderModel extends ObjectModel<FormFieldValidationResultProvider> {
Expand Down
117 changes: 93 additions & 24 deletions eclipse-scout-core/src/form/fields/ValueField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -8,7 +8,7 @@
* SPDX-License-Identifier: EPL-2.0
*/
import {
AbstractLayout, aria, arrays, EnumObject, focusUtils, FormField, InitModelOf, objects, ParsingFailedStatus, scout, Status, StatusSeverity, StatusType, strings, ValidationFailedStatus, ValueFieldEventMap, ValueFieldModel
AbstractLayout, App, aria, arrays, EnumObject, focusUtils, FormField, InitModelOf, objects, ParsingFailedStatus, scout, Status, StatusSeverity, StatusType, strings, ValidationFailedStatus, ValueFieldEventMap, ValueFieldModel
} from '../../index';
import $ from 'jquery';

Expand All @@ -29,6 +29,8 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
parser: ValueFieldParser<TValue>;
value: TValue;
validators: ValueFieldValidator<TValue>[];
validatePending: boolean;
protected _validatePendingCounter: number;
protected _updateDisplayTextPending: boolean;

constructor() {
Expand All @@ -45,6 +47,7 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
this.value = null;
this.validators = [];
this.validators.push(this._validateValue.bind(this));
this._validatePendingCounter = 0;
this._updateDisplayTextPending = false;
this.$clearIcon = null;

Expand All @@ -53,11 +56,11 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend

static Clearable = {
/**
* The clear icon is showed when the field has text.
* The clear icon is shown when the field has text.
*/
ALWAYS: 'always',
/**
* The clear icon will be showed when the field is focused and has text.
* The clear icon will be shown when the field is focused and has text.
*/
FOCUSED: 'focused',
/**
Expand Down Expand Up @@ -150,26 +153,31 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
// executes this._setDisplayText(), which updates the value.
this._setProperty('displayText', displayText);
if (!whileTyping) {
this.parseAndSetValue(displayText);
let result = this.parseAndSetValue(displayText);
if (objects.isPromise(result)) {
return result.then(() => this._triggerAcceptInput(whileTyping));
}
}
// Display text may be formatted -> Use this.displayText
this._triggerAcceptInput(whileTyping);
}
}

parseAndSetValue(displayText: string) {
parseAndSetValue(displayText: string): JQuery.Promise<void> | void {
this.removeErrorStatus(ParsingFailedStatus);
let parsedValue;
try {
let event = this.trigger('parse', {
displayText: displayText
});
if (!event.defaultPrevented) {
let parsedValue = this.parseValue(displayText);
this.setValue(parsedValue);
parsedValue = this.parseValue(displayText);
}
} catch (error) {
this._parsingFailed(displayText, error);
return;
}
return this.setValue(parsedValue);
}

protected _parsingFailed(displayText: string, error: any) {
Expand Down Expand Up @@ -342,17 +350,23 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
}

/** @see ValueFieldModel.value */
setValue(value: TValue | TModelValue) {
setValue(value: TValue | TModelValue): JQuery.Promise<void> | void {
// Same code as in Widget#setProperty expect for the equals check
// -> _setValue has to be called even if the value is equal so that update display text will be executed
value = this._prepareProperty('value', value);
if (this.rendered) {
this._callRemoveProperty('value');
}
this._callSetProperty('value', value);
if (this.rendered) {
this._callRenderProperty('value');
let result = this._setValue(value);
let renderValue = () => {
if (this.rendered) {
this._callRenderProperty('value');
}
};
if (objects.isPromise(result)) {
return result.then(error => renderValue());
}
renderValue();
}

/**
Expand All @@ -371,7 +385,7 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
return value as TValue;
}

protected _setValue(value: TValue | TModelValue) {
protected _setValue(value: TValue | TModelValue): JQuery.Promise<void> | void {
// When widget is initialized with a given errorStatus and a value -> don't remove the error
// status. This is a typical case for Scout Classic: field has a ParsingFailedError and user
// hits reload.
Expand All @@ -388,13 +402,52 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
return;
}

let valueOrPromise: ValueFieldValidatorResult<TValue>;
try {
this.value = this.validateValue(typedValue);
valueOrPromise = this.validateValue(typedValue);
} catch (error) {
this._validationFailed(typedValue, error);
return;
}

if (objects.isPromise(valueOrPromise)) {
this._setValidatePending(true);
valueOrPromise.then(
validatedValue => {
if (this._validatePendingCounter === 1) {
this._validationSucceeded(validatedValue, oldValue);
}
this._setValidatePending(false);
},
// Don't use catch() to not catch errors that happen in _validationSucceeded to make sure it behaves the same as the synchronous implementation
error => {
if (this._validatePendingCounter === 1) {
this._validationFailed(typedValue, error);
}
this._setValidatePending(false);
}
).catch(error => {
// Error in succeeded or failed
// Just calling errorHandler.handle() does not create fatal errors if error is a string or a Status
let errorHandler = App.get().errorHandler;
errorHandler.analyzeError(error).then(errorInfo => errorHandler.handleErrorInfo({
...errorInfo,
showAsFatalError: true
}));
});
return this.when('propertyChange:validatePending').then(() => undefined);
}
this._validationSucceeded(valueOrPromise, oldValue);
}

protected _setValidatePending(validatePending: boolean) {
let counter = Math.max(validatePending ? this._validatePendingCounter + 1 : this._validatePendingCounter - 1, 0);
this._validatePendingCounter = counter;
this._setProperty('validatePending', counter > 0);
}

protected _validationSucceeded(newValue: TValue, oldValue: TValue) {
this.value = newValue;
this._updateDisplayText();
if (this._valueEquals(oldValue, this.value)) {
return;
Expand Down Expand Up @@ -485,24 +538,39 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend
}

/**
* @param the value to be validated
* Validates the given value using the {@link ValueFieldModel.validators}.
*
* @param value the value to be validated
* @returns the validated value
* @throws a message, a {@link Status} or an error if the validation fails
* @throws a message, a {@link Status} or an {@link Error} if the validation fails
*/
validateValue(value: TValue): TValue {
validateValue(value: TValue): ValueFieldValidatorResult<TValue> {
let defaultValidator = this._validateValue.bind(this);
this.validators.forEach(validator => {
value = validator(value, defaultValidator);
});
value = scout.nvl(value, null); // Ensure value is never undefined (necessary for updateSaveNeeded and should make it easier generally)

// Ensure value is never undefined (necessary for updateSaveNeeded and should make it easier in general)
let ensureNotUndefined = v => scout.nvl(v, null);
let validators = [...this.validators, ensureNotUndefined];

return this._validateValueImpl(value, validators, defaultValidator);
}

protected _validateValueImpl(value: TValue, validators: ValueFieldValidator<TValue>[], defaultValidator: ValueFieldValidator<TValue>): ValueFieldValidatorResult<TValue> {
for (let i = 0; i < validators.length; i++) {
const validator = validators[i];
const valueOrPromise = validator(value, defaultValidator);
if (objects.isPromise(valueOrPromise)) {
return valueOrPromise.then(result => this._validateValueImpl(result, validators.slice(i + 1), defaultValidator));
}
value = valueOrPromise as TValue;
}
return value;
}

/**
* @returns the validated value
* @throws a message, a {@link Status} or an error if the validation fails
* @throws a message, a {@link Status} or an {@link Error} if the validation fails
*/
protected _validateValue(value: TValue): TValue {
protected _validateValue(value: TValue): ValueFieldValidatorResult<TValue> {
if (typeof value === 'string' && value === '') {
// Convert empty string to null.
// Not using strings.nullIfEmpty is by purpose because it also removes white space characters which may not be desired here
Expand Down Expand Up @@ -704,6 +772,7 @@ export class ValueField<TValue extends TModelValue, TModelValue = TValue> extend

export type ValueFieldClearable = EnumObject<typeof ValueField.Clearable>;
export type ValueFieldMenuType = EnumObject<typeof ValueField.MenuType>;
export type ValueFieldValidator<TValue> = (value: TValue, defaultValidator?: ValueFieldValidator<TValue>) => TValue;
export type ValueFieldValidatorResult<TValue> = TValue | JQuery.Promise<TValue>;
export type ValueFieldValidator<TValue> = (value: TValue, defaultValidator?: ValueFieldValidator<TValue>) => ValueFieldValidatorResult<TValue>;
export type ValueFieldFormatter<TValue> = (value: TValue, defaultFormatter?: ValueFieldFormatter<TValue>) => string | JQuery.Promise<string>;
export type ValueFieldParser<TValue> = (displayText: string, defaultParser?: ValueFieldParser<TValue>) => TValue;
3 changes: 2 additions & 1 deletion eclipse-scout-core/src/form/fields/ValueFieldEventMap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -29,6 +29,7 @@ export interface ValueFieldEventMap<TValue> extends FormFieldEventMap {
'parse': ValueFieldParseEvent<TValue>;
'parseError': ValueFieldParseErrorEvent<TValue>;
'propertyChange:value': PropertyChangeEvent<TValue>;
'propertyChange:validatePending': PropertyChangeEvent<boolean>;
'propertyChange:clearable': PropertyChangeEvent<ValueFieldClearable>;
'propertyChange:displayText': PropertyChangeEvent<string>;
'propertyChange:formatter': PropertyChangeEvent<ValueFieldFormatter<TValue>>;
Expand Down
4 changes: 2 additions & 2 deletions eclipse-scout-core/src/form/fields/datefield/DateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import {
AddCellEditorFieldCssClassesOptions, aria, arrays, CellEditorPopup, CellEditorRenderedOptions, DateFieldEventMap, DateFieldModel, DateFormat, DateFormatAnalyzeInfo, DatePicker, DatePickerDateSelectEvent, DatePickerPopup,
DatePickerTouchPopup, DatePredictionFailedStatus, dates, DateTimeCompositeLayout, Device, Event, fields, focusUtils, FormField, HtmlComponent, InitModelOf, InputFieldKeyStrokeContext, keys, KeyStrokeContext, objects, ParsingFailedStatus,
Popup, Predicate, scout, Status, StatusType, strings, styles, TimePicker, TimePickerPopup, TimePickerTimeSelectEvent, TimePickerTouchPopup, ValueField, ValueFieldWithCellEditorRenderedCallback
Popup, Predicate, scout, Status, StatusType, strings, styles, TimePicker, TimePickerPopup, TimePickerTimeSelectEvent, TimePickerTouchPopup, ValueField, ValueFieldValidatorResult, ValueFieldWithCellEditorRenderedCallback
} from '../../../index';
import $ from 'jquery';

Expand Down Expand Up @@ -452,7 +452,7 @@ export class DateField extends ValueField<Date, Date | string> implements DateFi
return dates.ensure(value);
}

protected override _validateValue(value: Date): Date {
protected override _validateValue(value: Date): ValueFieldValidatorResult<Date> {
if (objects.isNullOrUndefined(value)) {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, Button, Device, Event, FileChooserButtonEventMap, FileChooserButtonModel, FileInput, FileInputChangeEvent, HtmlComponent, InitModelOf, scout, SingleLayout, strings, ValueField} from '../../../index';
import {
arrays, Button, Device, Event, FileChooserButtonEventMap, FileChooserButtonModel, FileInput, FileInputChangeEvent, HtmlComponent, InitModelOf, scout, SingleLayout, strings, ValueField, ValueFieldValidatorResult
} from '../../../index';

export class FileChooserButton extends ValueField<File> implements FileChooserButtonModel {
declare model: FileChooserButtonModel;
Expand Down Expand Up @@ -140,7 +142,7 @@ export class FileChooserButton extends ValueField<File> implements FileChooserBu
this.setValue(arrays.first(event.files));
}

protected override _validateValue(value: File): File {
protected override _validateValue(value: File): ValueFieldValidatorResult<File> {
this.fileInput.validateMaximumUploadSize(value);
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, FileChooserFieldBrowseKeyStroke, FileChooserFieldDeleteKeyStroke, FileChooserFieldEventMap, FileChooserFieldModel, FileInput, FileInputChangeEvent, InitModelOf, objects, scout, ValueField} from '../../../index';
import {
arrays, FileChooserFieldBrowseKeyStroke, FileChooserFieldDeleteKeyStroke, FileChooserFieldEventMap, FileChooserFieldModel, FileInput, FileInputChangeEvent, InitModelOf, objects, scout, ValueField, ValueFieldValidatorResult
} from '../../../index';

export class FileChooserField extends ValueField<File> implements FileChooserFieldModel {
declare model: FileChooserFieldModel;
Expand Down Expand Up @@ -135,7 +137,7 @@ export class FileChooserField extends ValueField<File> implements FileChooserFie
this.fileInput.browse();
}

protected override _validateValue(value: File): File {
protected override _validateValue(value: File): ValueFieldValidatorResult<File> {
this.fileInput.validateMaximumUploadSize(value);
return value;
}
Expand Down
Loading
Loading