Add default model view input types and validation (#1397)

This commit is contained in:
Matt Irvine
2018-05-14 16:20:19 -07:00
committed by GitHub
parent 89c48bbe75
commit 9bd45cf66a
14 changed files with 363 additions and 118 deletions

View File

@@ -24,7 +24,8 @@ export class ItemDescriptor<T> {
export abstract class ComponentBase extends Disposable implements IComponent, OnDestroy, OnInit {
protected properties: { [key: string]: any; } = {};
protected _valid: boolean = true;
private _valid: boolean = true;
protected _validations: (() => boolean | Thenable<boolean>)[] = [];
private _eventQueue: IComponentEventArgs[] = [];
constructor(
protected _changeRef: ChangeDetectorRef) {
@@ -44,6 +45,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
protected baseInit(): void {
if (this.modelStore) {
this.modelStore.registerComponent(this);
this._validations.push(() => this.modelStore.validate(this));
}
}
@@ -67,6 +69,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
}
this.properties = properties;
this.layout();
this.validate();
}
protected getProperties<TPropertyBag>(): TPropertyBag {
@@ -84,6 +87,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
eventType: ComponentEventType.PropertiesChanged,
args: this.getProperties()
});
this.validate();
}
public get enabled(): boolean {
@@ -96,16 +100,6 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
return this._valid;
}
public setValid(valid: boolean): void {
if (this._valid !== valid) {
this._valid = valid;
this.fireEvent({
eventType: ComponentEventType.validityChanged,
args: valid
});
}
}
public registerEventHandler(handler: (event: IComponentEventArgs) => void): IDisposable {
if (this._eventQueue) {
while (this._eventQueue.length > 0) {
@@ -123,6 +117,21 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
this._eventQueue.push(event);
}
}
public validate(): Thenable<boolean> {
let validations = this._validations.map(validation => Promise.resolve(validation()));
return Promise.all(validations).then(values => {
let isValid = values.every(value => value === true);
if (this._valid !== isValid) {
this._valid = isValid;
this.fireEvent({
eventType: ComponentEventType.validityChanged,
args: this._valid
});
}
return isValid;
});
}
}
export abstract class ContainerBase<T> extends ComponentBase {
@@ -133,11 +142,17 @@ export abstract class ContainerBase<T> extends ComponentBase {
) {
super(_changeRef);
this.items = [];
this._validations.push(() => this.items.every(item => this.modelStore.getComponent(item.descriptor.id).valid));
}
/// IComponent container-related implementation
public addToContainer(componentDescriptor: IComponentDescriptor, config: any): void {
this.items.push(new ItemDescriptor(componentDescriptor, config));
this.modelStore.eventuallyRunOnComponent(componentDescriptor.id, component => component.registerEventHandler(event => {
if (event.eventType === ComponentEventType.validityChanged) {
this.validate();
}
}));
this._changeRef.detectChanges();
}

View File

@@ -52,15 +52,17 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
return undefined;
} else {
return {
content: nls.localize('invalidValueError', 'Invalid value'),
content: this._input.inputElement.validationMessage || nls.localize('invalidValueError', 'Invalid value'),
type: MessageType.ERROR
};
}
}
}
},
useDefaultValidation: true
};
this._input = new InputBox(this._inputContainer.nativeElement, this._commonService.contextViewService, inputOptions);
this._validations.push(() => !this._input.inputElement.validationMessage);
this._register(this._input);
this._register(attachInputBoxStyler(this._input, this._commonService.themeService));
@@ -74,6 +76,13 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
}
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
this._input.validate();
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
@@ -91,6 +100,10 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._input.inputElement.type = this.inputType;
if (this.inputType === 'number') {
this._input.inputElement.step = 'any';
}
this._input.value = this.value;
this._input.setAriaLabel(this.ariaLabel);
this._input.setPlaceHolder(this.placeHolder);
@@ -98,11 +111,8 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
if (this.width) {
this._input.width = this.width;
}
}
public setValid(valid: boolean): void {
super.setValid(valid);
this._input.validate();
this._input.inputElement.required = this.required;
this.validate();
}
// CSS-bound properties
@@ -146,4 +156,20 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
public set width(newValue: number) {
this.setPropertyFromUI<sqlops.InputBoxProperties, number>((props, value) => props.width = value, newValue);
}
public get inputType(): string {
return this.getPropertyOrDefault<sqlops.InputBoxProperties, string>((props) => props.inputType, 'text');
}
public set inputType(newValue: string) {
this.setPropertyFromUI<sqlops.InputBoxProperties, string>((props, value) => props.inputType = value, newValue);
}
public get required(): boolean {
return this.getPropertyOrDefault<sqlops.InputBoxProperties, boolean>((props) => props.required, false);
}
public set required(newValue: boolean) {
this.setPropertyFromUI<sqlops.InputBoxProperties, boolean>((props, value) => props.required = value, newValue);
}
}

View File

@@ -24,7 +24,7 @@ export interface IComponent {
setLayout?: (layout: any) => void;
setProperties?: (properties: { [key: string]: any; }) => void;
readonly valid?: boolean;
setValid(valid: boolean): void;
validate(): Thenable<boolean>;
}
export const COMPONENT_CONFIG = new InjectionToken<IComponentConfig>('component_config');
@@ -88,4 +88,12 @@ export interface IModelStore {
* @memberof IModelStore
*/
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T): Promise<T>;
/**
* Register a callback that will validate components when given a component ID
*/
registerValidationCallback(callback: (componentId: string) => Thenable<boolean>): void;
/**
* Run all validations for the given component and return the new validation value
*/
validate(component: IComponent): Thenable<boolean>;
}

View File

@@ -27,6 +27,7 @@ export class ModelStore implements IModelStore {
private _descriptorMappings: { [x: string]: IComponentDescriptor } = {};
private _componentMappings: { [x: string]: IComponent } = {};
private _componentActions: { [x: string]: Deferred<IComponent> } = {};
private _validationCallbacks: ((componentId: string) => Thenable<boolean>)[] = [];
constructor() {
}
@@ -66,6 +67,15 @@ export class ModelStore implements IModelStore {
}
}
registerValidationCallback(callback: (componentId: string) => Thenable<boolean>): void {
this._validationCallbacks.push(callback);
}
validate(component: IComponent): Thenable<boolean> {
let componentId = Object.entries(this._componentMappings).find(([id, mappedComponent]) => component === mappedComponent)[0];
return Promise.all(this._validationCallbacks.map(callback => callback(componentId))).then(validations => validations.every(validation => validation === true));
}
private addPendingAction<T>(componentId: string, action: (component: IComponent) => T): Promise<T> {
// We create a promise and chain it onto a tracking promise whose resolve method
// will only be called once the component is created

View File

@@ -39,9 +39,10 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
private _onEventEmitter = new Emitter<any>();
initializeModel(rootComponent: IComponentShape): void {
initializeModel(rootComponent: IComponentShape, validationCallback: (componentId: string) => Thenable<boolean>): void {
let descriptor = this.defineComponent(rootComponent);
this.rootDescriptor = descriptor;
this.modelStore.registerValidationCallback(validationCallback);
// Kick off the build by detecting changes to the model
this.changeRef.detectChanges();
}
@@ -91,10 +92,6 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
this.queueAction(componentId, (component) => component.setProperties(properties));
}
setValid(componentId: string, valid: boolean): void {
this.queueAction(componentId, (component) => component.setValid(valid));
}
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void {
this.modelStore.eventuallyRunOnComponent(componentId, action).catch(err => {
// TODO add error handling
@@ -113,4 +110,8 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
public get onEvent(): Event<IComponentEventArgs> {
return this._onEventEmitter.event;
}
public validate(componentId: string): Thenable<boolean> {
return new Promise(resolve => this.modelStore.eventuallyRunOnComponent(componentId, component => resolve(component.validate())));
}
}