"Fossies" - the Fresh Open Source Software Archive

Member "flow-4.0.8/flow-client/src/main/resources/META-INF/resources/frontend/form/Binder.ts" (26 Nov 2020, 8081 Bytes) of package /linux/www/vaadin-flow-4.0.8.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "Binder.ts": 4.0.5_vs_4.0.6.

    1 import {BinderNode} from "./BinderNode";
    2 import {
    3   _parent,
    4   AbstractModel,
    5   ModelConstructor
    6 } from "./Models";
    7 import {
    8   runValidator,
    9   ServerValidator,
   10   ValidationError,
   11   Validator,
   12   ValueError
   13 } from "./Validation";
   14 
   15 import {FieldStrategy, getDefaultFieldStrategy} from "./Field";
   16 
   17 const _submitting = Symbol('submitting');
   18 const _defaultValue = Symbol('defaultValue');
   19 const _value = Symbol('value');
   20 const _emptyValue = Symbol('emptyValue');
   21 const _onChange = Symbol('onChange');
   22 const _onSubmit = Symbol('onSubmit');
   23 const _validations = Symbol('validations');
   24 const _validating = Symbol('validating');
   25 const _validationRequestSymbol = Symbol('validationRequest');
   26 
   27 /**
   28  * A Binder controls all aspects of a single form. 
   29  * Typically it is used to get and set the form value, 
   30  * access the form model, validate, reset, and submit the form.
   31  * 
   32  * @param <T> is the type of the value that binds to a form
   33  * @param <M> is the type of the model that describes the structure of the value
   34  */
   35 export class Binder<T, M extends AbstractModel<T>> extends BinderNode<T, M> {
   36   private [_defaultValue]: T;
   37   private [_value]: T;
   38   private [_emptyValue]: T;
   39   private [_submitting]: boolean = false;
   40   private [_validating]: boolean = false;
   41   private [_validationRequestSymbol]: Promise<void> | undefined = undefined;
   42   private [_onChange]: (oldValue?: T) => void;
   43   private [_onSubmit]: (value: T) => Promise<T|void>;
   44 
   45   private [_validations]: Map<AbstractModel<any>, Map<Validator<any>, Promise<ReadonlyArray<ValueError<any>>>>> = new Map();
   46 
   47   /**
   48    * 
   49    * @param context The form view component instance to update.
   50    * @param Model The constructor (the class reference) of the form model. The Binder instantiates the top-level model
   51    * @param config The options object, which can be used to config the onChange and onSubmit callbacks.
   52    * 
   53    * ```
   54    * binder = new Binder(orderView, OrderModel);
   55    * or
   56    * binder = new Binder(orderView, OrderModel, {onSubmit: async (order) => {endpoint.save(order)}});
   57    * ```
   58    */
   59   constructor(
   60     public context: Element,
   61     Model: ModelConstructor<T, M>,
   62     config?: BinderConfiguration<T>
   63   ) {
   64     super(new Model({value: undefined}, 'value', false));
   65     this[_emptyValue] = (this.model[_parent] as {value: T}).value;
   66     // @ts-ignore
   67     this.model[_parent] = this;
   68 
   69     if (typeof (context as any).requestUpdate === 'function') {
   70       this[_onChange] = () => (context as any).requestUpdate();
   71     }
   72     this[_onChange] = config?.onChange || this[_onChange];
   73     this[_onSubmit] = config?.onSubmit || this[_onSubmit];
   74     this.read(this[_emptyValue]);
   75   }
   76 
   77   /**
   78    * The initial value of the form, before any fields are edited by the user.
   79    */
   80   get defaultValue() {
   81     return this[_defaultValue];
   82   }
   83 
   84   set defaultValue(newValue) {
   85     this[_defaultValue] = newValue;
   86   }
   87 
   88   /**
   89    * The current value of the form.
   90    */
   91   get value() {
   92     return this[_value];
   93   }
   94 
   95   set value(newValue) {
   96     if (newValue === this[_value]) {
   97       return;
   98     }
   99 
  100     const oldValue = this[_value];
  101     this[_value] = newValue;
  102     this.update(oldValue);
  103     this.updateValidation();
  104   }
  105 
  106   /**
  107    * Read the given value into the form and clear validation errors
  108    *
  109    * @param value Sets the argument as the new default
  110    * value before resetting, otherwise the previous default is used.
  111    */
  112   read(value: T) {
  113     this.defaultValue = value;
  114     if (
  115       // Skip when no value is set yet (e. g., invoked from constructor)
  116       this.value
  117       // Clear validation state, then proceed if update is needed
  118       && this.clearValidation()
  119       // When value is dirty, another update is coming from invoking the value
  120       // setter below, so we skip this one to prevent duplicate updates
  121       && this.value === value) {
  122       this.update(this.value);
  123     }
  124 
  125     this.value = this.defaultValue;
  126   }
  127 
  128   /**
  129    * Reset the form to the previous value
  130    */
  131   reset(){
  132     this.read(this[_defaultValue])
  133   }
  134 
  135   /**
  136    * Sets the form to empty value, as defined in the Model.
  137    */
  138   clear() {
  139     this.read(this[_emptyValue]);
  140   }
  141 
  142   /**
  143    * Submit the current form value to a predefined
  144    * onSubmit callback.
  145    * 
  146    * It's a no-op if the onSubmit callback is undefined.
  147    */
  148   async submit(): Promise<T|void>{
  149     if (this[_onSubmit] !== undefined) {
  150       return this.submitTo(this[_onSubmit]);
  151     }
  152   }
  153 
  154   /**
  155    * Submit the current form value to callback
  156    * 
  157    * @param endpointMethod the callback function
  158    */
  159   async submitTo(endpointMethod: (value: T) => Promise<T|void>): Promise<T|void> {
  160     const errors = await this.validate();
  161     if (errors.length) {
  162       throw new ValidationError(errors);
  163     }
  164 
  165     this[_submitting] = true;
  166     this.update(this.value);
  167     try {
  168       return await endpointMethod.call(this.context, this.value);
  169     } catch (error) {
  170       if (error.validationErrorData && error.validationErrorData.length) {
  171         const valueErrors: Array<ValueError<any>> = [];
  172         error.validationErrorData.forEach((data:any) => {
  173           const res = /Object of type '(.+)' has invalid property '(.+)' with value '(.+)', validation error: '(.+)'/.exec(data.message);
  174           const [property, value, message] = res ? res.splice(2) : [data.parameterName, undefined, data.message];
  175           valueErrors.push({ property, value, validator: new ServerValidator(message), message });
  176         });
  177         this.setErrorsWithDescendants(valueErrors);
  178         error = new ValidationError(valueErrors);
  179       }
  180       throw (error);
  181     } finally {
  182       this[_submitting] = false;
  183       this.defaultValue = this.value;
  184       this.update(this.value);
  185     }
  186   }
  187 
  188   async requestValidation<NT, NM extends AbstractModel<NT>>(model: NM, validator: Validator<NT>): Promise<ReadonlyArray<ValueError<NT>>> {
  189     let modelValidations: Map<Validator<NT>, Promise<ReadonlyArray<ValueError<NT>>>>;
  190     if (this[_validations].has(model)) {
  191       modelValidations = this[_validations].get(model) as Map<Validator<NT>, Promise<ReadonlyArray<ValueError<NT>>>>;
  192     } else {
  193       modelValidations = new Map();
  194       this[_validations].set(model, modelValidations);
  195     }
  196 
  197     await this.performValidation();
  198 
  199     if (modelValidations.has(validator)) {
  200       return modelValidations.get(validator) as Promise<ReadonlyArray<ValueError<NT>>>;
  201     }
  202 
  203     const promise = runValidator(model, validator);
  204     modelValidations.set(validator, promise);
  205     const valueErrors = await promise;
  206 
  207     modelValidations.delete(validator);
  208     if (modelValidations.size === 0) {
  209       this[_validations].delete(model);
  210     }
  211     if (this[_validations].size === 0) {
  212       this.completeValidation();
  213     }
  214 
  215     return valueErrors;
  216   }
  217 
  218   /**
  219    * Determines and returns the field directive strategy for the bound element. 
  220    * Override to customise the binding strategy for a component. 
  221    * The Binder extends BinderNode, see the inherited properties and methods below.
  222    * 
  223    * @param elm the bound element 
  224    */
  225   getFieldStrategy(elm: any): FieldStrategy {
  226     return getDefaultFieldStrategy(elm);
  227   }
  228 
  229   /**
  230    * Indicates the submitting status of the form.
  231    * True if the form was submitted, but the submit promise is not resolved yet.
  232    */
  233   get submitting() {
  234     return this[_submitting];
  235   }
  236 
  237   /**
  238    * Indicates the validating status of the form.
  239    * True when there is an ongoing validation.
  240    */
  241   get validating() {
  242     return this[_validating];
  243   }
  244 
  245   protected performValidation(): Promise<void> | void {
  246     if (!this[_validationRequestSymbol]) {
  247       this[_validating] = true;
  248       this[_validationRequestSymbol] = Promise.resolve().then(() => {
  249         this[_validationRequestSymbol] = undefined;
  250       });
  251     }
  252     return this[_validationRequestSymbol];
  253   }
  254 
  255   protected completeValidation() {
  256     this[_validating] = false;
  257   }
  258 
  259   protected update(oldValue: T) {
  260     if(this[_onChange]){
  261       this[_onChange].call(this.context, oldValue);
  262     }
  263   }
  264 }
  265 
  266 export interface BinderConfiguration<T>{
  267   onChange?: (oldValue?: T) => void,
  268   onSubmit?: (value: T) => Promise<T|void>
  269 }