diff --git a/src/Dom/WebFormComponents.ts b/src/Dom/WebFormComponents.ts index bfdabcd..9ccf6bd 100755 --- a/src/Dom/WebFormComponents.ts +++ b/src/Dom/WebFormComponents.ts @@ -1,48 +1,47 @@ type control = 'businesslogic' | - 'text' | - 'number' | - 'integer' | - 'text-data' | // Input with datalist - 'textarea' | - 'select' | - 'checkbox' | - 'radio' | - 'range' | - 'password' | - 'email' | - 'tel' | - 'time' | - 'date' | - 'month' | - 'week' | - 'submit' | - 'submit-reset' | - 'output' | - 'output-array' | - 'output-meter' | + 'text' | + 'number' | + 'integer' | + 'text-data' | // Input with datalist + 'textarea' | + 'select' | + 'checkbox' | + 'radio' | + 'range' | + 'password' | + 'email' | + 'tel' | + 'time' | + 'date' | + 'month' | + 'week' | + 'submit' | + 'submit-reset' | + 'output' | + 'output-array' | + 'output-meter' | 'output-progress' export class WebFormComponents { - private webformComponents: string; - private groupName:string; + private groupName: string; - constructor(groupName?:string) { + constructor(groupName?: string) { this.webformComponents = ''; this.groupName = groupName ? groupName : ''; } - public compileWebformComponents():Element { - if(this.groupName !== '')this.webformComponents = this.groupComponents(this.webformComponents ,this.groupName); - return new DOMParser().parseFromString(this.webformComponents,'text/html').body.children[0]; + public compileWebformComponents(): Element { + if (this.groupName !== '') this.webformComponents = this.groupComponents(this.webformComponents, this.groupName); + return new DOMParser().parseFromString(this.webformComponents, 'text/html').body.children[0]; } - public getWebformComponents():string { + public getWebformComponents(): string { return this.webformComponents; } - public attachComponent(control:control, param?:string, options?:any): void { + public attachComponent(control: control, param?: string, options?: any): void { let component; let submit; let reset; @@ -256,12 +255,12 @@ export class WebFormComponents { */ // Wrap inside the group - if(this.groupName !== '') component = this.groupComponents(component,'form-group'); + if (this.groupName !== '') component = this.groupComponents(component, 'form-group'); this.webformComponents += component; } - public groupComponents(component:string,groupClass:string):string{ - return `
${component}
`; + public groupComponents(component: string, groupClass: string): string { + return `
${component}
`; } } diff --git a/src/Helpers/HelperFunctions.ts b/src/Helpers/HelperFunctions.ts index 44fbfb3..eb4118f 100755 --- a/src/Helpers/HelperFunctions.ts +++ b/src/Helpers/HelperFunctions.ts @@ -1,3 +1,6 @@ +import { WebForm } from "../interfaces"; + + export function isEmpty(obj:any) { for(let key in obj) { if(obj.hasOwnProperty(key)) @@ -18,4 +21,81 @@ export function isConstructor(f:any) { export function isUndefined(obj:any) { return obj === undefined; -} \ No newline at end of file +} + +export function log(message: any, isDebug = false): void { + if (isDebug) console.log(message); +} + +export function mapWebForm(formItem: any): WebForm { + let name = formItem.getAttribute('bl-name'); + let key = formItem.getAttribute('bl-token'); + let inputs = formItem.querySelectorAll('[bl-input]'); + let controls = formItem.querySelectorAll('[bl-control]'); + let outputs = formItem.querySelectorAll('[bl-output]'); + let param: string, type: string, tagName: string; + let el: Element; + let lbl_el: Element, desc_el: Element, err_el: Element; + + + let wf: WebForm = { + name: name, + inputs: {}, + controls: {}, + outputs: {} + }; + + if (formItem instanceof HTMLFormElement) { + wf.form_el = formItem; + } + + // Handling inputs + for(let i = 0; i < inputs.length; i++) { + param = inputs[i].getAttribute('bl-input'); + type = inputs[i].getAttribute('type'); + tagName = inputs[i].tagName; + el = inputs[i]; + lbl_el = formItem.querySelector('[bl-input-label=' + param + ']'); + desc_el = formItem.querySelector('[bl-input-description=' + param + ']'); + err_el = formItem.querySelector('[bl-input-error=' + param + ']'); + wf.inputs[param] = {'label_el': lbl_el, 'desc_el': desc_el, 'input_el': el, 'err_el': err_el}; + } + // Handling controls + for(let i = 0; i < controls.length; i++) { + param = controls[i].getAttribute('bl-control'); + type = controls[i].getAttribute('type'); + tagName = controls[i].tagName; + el = controls[i]; + wf.controls[param] = {'control_el': el}; + } + // Handling outputs + for(let i = 0; i < outputs.length; i++) { + param = outputs[i].getAttribute('bl-output'); + type = outputs[i].getAttribute('type'); + tagName = outputs[i].tagName; + el = outputs[i]; + lbl_el = formItem.querySelector('[bl-output-label=' + param + ']'); + desc_el = formItem.querySelector('[bl-output-description=' + param + ']'); + wf.outputs[param] = {'label_el': lbl_el, 'desc_el': desc_el, 'output_el': el}; + } + + return wf; +} + +//TODO refactor library, replace string component by html components +export function setRangeListeners(formInputs) { + const rangeGroup = formInputs.querySelectorAll('.range-group'); + + rangeGroup.forEach((rangeGroup) => { + const rangeInput = rangeGroup.querySelector('input[type="range"]'); + + rangeInput.addEventListener('input', (event) => { + let target = event.target + const min = target['min']; + const max = target['max']; + const val = target['value']; + + target['style'].backgroundSize = (val - min) * 100 / (max - min) + '% 100%' + }); + }); +} diff --git a/src/Validation/WebFormValidation.ts b/src/Validation/WebFormValidation.ts index 4cf1706..f11720a 100755 --- a/src/Validation/WebFormValidation.ts +++ b/src/Validation/WebFormValidation.ts @@ -1,5 +1,5 @@ -import { WebForm, WebFormErrors } from '../WebForm'; import * as helpers from '../Helpers/HelperFunctions'; +import { WebForm, WebFormErrors } from "../interfaces"; export interface validationMessages { @@ -17,23 +17,22 @@ export interface validationMessages { } export class WebFormValidation { - - private errors:WebFormErrors; - private defaultMessages:validationMessages; - private formElement:HTMLFormElement; - private webform:WebForm; + private errors: WebFormErrors; + private defaultMessages: validationMessages; + private formElement: HTMLFormElement; + private webform: WebForm; private customErrorMessages: boolean; - constructor(language:string = '', webform?:WebForm) { + constructor(language: string = '', webform?: WebForm) { this.formElement = new HTMLFormElement(); this.webform = webform; } - private validate():Boolean { - let valid:boolean = true; + private validate(): Boolean { + let valid: boolean = true; this.errors = {}; - if(this.formElement) { + if (this.formElement) { // Use native form validation this.formElement.customMessages = true; valid = this.formElement.checkValidity(); // Returns true if the element's value has no validity problems; false otherwise. Fires an invalid event at the element in the latter case. @@ -46,48 +45,47 @@ export class WebFormValidation { let willValidate = input.willValidate; // Returns true if the element will be validated when the form is submitted; false otherwise. let custom = false; let message = ''; - if(!this.customErrorMessages) { + if (!this.customErrorMessages) { message = input.validationMessage; // Returns the error message that would be shown to the user if the element was to be checked for validity. - } else if(input.validity.valueMissing) { + } else if (input.validity.valueMissing) { - } else if(input.validity.typeMismatch) { + } else if (input.validity.typeMismatch) { - } else if(input.validity.typeMismatch) { + } else if (input.validity.typeMismatch) { - } else if(input.validity.patternMismatch) { + } else if (input.validity.patternMismatch) { - } else if(input.validity.tooLong) { + } else if (input.validity.tooLong) { - } else if(input.validity.tooShort) { + } else if (input.validity.tooShort) { - } else if(input.validity.rangeUnderflow) { + } else if (input.validity.rangeUnderflow) { - } else if(input.validity.rangeOverflow) { + } else if (input.validity.rangeOverflow) { - } else if(input.validity.stepMismatch) { + } else if (input.validity.stepMismatch) { - } else if(input.validity.badInput) { + } else if (input.validity.badInput) { - } else if(input.validity.customError) { + } else if (input.validity.customError) { - } else if(input.validity.valid) { + } else if (input.validity.valid) { } else { //this.updateErrorMessage(el,''); } - if(custom)input.setCustomValidity(message); // Sets a custom error, so that the element would fail to validate. The given message is the message to be shown to the user when reporting the problem to the user. + if (custom) input.setCustomValidity(message); // Sets a custom error, so that the element would fail to validate. The given message is the message to be shown to the user when reporting the problem to the user. this.errors[el] = message; // this.updateErrorMessage(el,message); } - - if(!helpers.isEmpty(this.errors)) { + if (!helpers.isEmpty(this.errors)) { valid = false } } - if(!valid) { + if (!valid) { /* this.ValidationFailedListener.emit({ errors: this.errors @@ -98,7 +96,7 @@ export class WebFormValidation { return valid; } - private setErrorMessages(value:string) { + private setErrorMessages(value: string) { this.defaultMessages = { valueMissing: `${value} is required `, // Returns true if the element has no value but is a required field; false otherwise. typeMismatch: `Some text here ${value}`, // Returns true if the element's value is not in the correct syntax; false otherwise. @@ -113,4 +111,4 @@ export class WebFormValidation { valid: `Some text here ${value}` // Returns true if the element's value has no validity problems; false otherwise. } } -} \ No newline at end of file +} diff --git a/src/WebForm.ts b/src/WebForm.ts deleted file mode 100755 index 656d1e4..0000000 --- a/src/WebForm.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -export interface WebForm { - name: string; - form_el?:HTMLFormElement; - inputs?: { [id: string]: { - label_el?: Element; - desc_el?: Element; - input_el?: Element; - err_el?: Element; - }}; - controls?: { [id: string]: { - control_el?: Element; - }}; - outputs?: { [id: string]: { - label_el?: Element; - desc_el?: Element; - output_el?: Element; - }}; -} - -export interface WebFormErrors { - [id: string]: string; -} \ No newline at end of file diff --git a/src/classes/service-container.ts b/src/classes/service-container.ts new file mode 100644 index 0000000..7f63e13 --- /dev/null +++ b/src/classes/service-container.ts @@ -0,0 +1,26 @@ +// Create dictonary for keeping track of all webservice instances +import { JSDict } from "../Helpers/TypedDictionary"; +import { Webservice } from "./web-service"; + + +export class ServiceContainer { + private dict: any; + + constructor() { + this.dict = JSDict.Create(); + } + + public add(apiKey: string, webservice: Webservice): void { + if (!this.dict[apiKey]) { + this.dict[apiKey] = webservice; + } else { + console.warn('Webservice with apiKey: ' + apiKey + ' was allready added to Businesslogic.Webservices'); + } + } + + // TODO: Make this functions as a promise so you can be sure it is initialised + // Remember to have a timeout resolve 30secs + public get(apiKey: string): Webservice { + return this.dict[apiKey]; + } +} diff --git a/src/classes/web-service.ts b/src/classes/web-service.ts new file mode 100644 index 0000000..b0c4460 --- /dev/null +++ b/src/classes/web-service.ts @@ -0,0 +1,559 @@ +import * as helpers from "../Helpers/HelperFunctions"; +import { + DataChangedEvent, + ExecutedEvent, + SchemaReceivedEvent, + ValidationFailedEvent, + WebForm, + WebFormErrors +} from "../interfaces"; +import { TypedEvent } from "../Events/TypedEvent"; +import { log } from "../Helpers/HelperFunctions"; +import { WebServicesContainer } from "../index"; +import { http } from '../http'; + +declare global { + interface Window { + Ajv: any; + } +} + +const ajv: any = helpers.isConstructor(window.Ajv) ? new window.Ajv() : null; + +// Todo: Needs to be split into more logical parts +export class Webservice { + protected key: string; + private http: http; + protected webform: WebForm; + protected data: any; + private inputSchema: any; + private outputSchema: any; + private relatedData: any; + private cachedParams: any; + private errors: WebFormErrors; + private test: number; + private debug: boolean; + + public SchemaReceviedListener = new TypedEvent(); + public ValidationFailedListener = new TypedEvent(); + public DataChangedListener = new TypedEvent(); + public ExecutedListener = new TypedEvent(); + + constructor(key: string = '', debug = false, webform?: WebForm) { + this.key = key; + this.data = {}; + this.webform = webform; + this.http = new http(key); + this.errors = {}; + this.cachedParams = {}; + this.test = 1; + this.debug = debug; + + this.init(); + } + + private init(): void { + let vm = this; + + // Adds this webservice to the collection of WebServicesContainer + WebServicesContainer.add(this.key, this); + + this.describe().then(() => { + vm.initDataTypes(); + vm.setParamsFromCachedParams(); + if (this.webform) vm.enrichWebFormInputs(); + }).then(() => { + if (this.webform) vm.handleAssociatedWebform(); + }); + } + + private describe(): Promise { + let self = this; + + return new Promise((resolve: any, reject: any) => { + // Retrieve all relevant webservice metadata + if (self.inputSchema) { + resolve(''); + } else { + this.getWebserviceDocs().then((result) => { + this.SchemaReceviedListener.emit({ + inputSchema: self.inputSchema = result.expected_input || {}, + outputSchema: self.outputSchema = result.expected_output || {}, + relatedData: self.relatedData = result.available_data || {} + }); + + resolve(result); + }) + } + }); + } + + public assignWebForm(webform: WebForm): void { + this.webform = webform; + } + + private handleAssociatedWebform(): void { + for(let param in this.webform.inputs) { + let vm = this; + let value = (this.webform.inputs[param].input_el).value; + let type = vm.inputSchema.properties[param].type; + + vm.setParamFromWebform(param, value); + + this.webform.inputs[param].input_el.addEventListener('blur', function(e) { + vm.validateInput(param); + vm.webform.inputs[param].input_el.classList.add('touched'); + }); + this.webform.inputs[param].input_el.addEventListener('input', function(e) { + vm.validateInput(param); + vm.setParamFromWebform(param, this.value); + }); + // If there is no submit button we execute the form upon a valid input + if (this.webform.controls['submit'] === undefined) { + this.webform.inputs[param].input_el.addEventListener('change', function(e) { + if (vm.validate()) vm.execute(); + }) + } + } + for(let name in this.webform.controls) { + let vm = this; + + if (name == 'submit') { + this.webform.controls[name].control_el.addEventListener('click', function(e) { + if (vm.validate()) vm.execute(); + }) + } + if (name == 'reset') { + this.webform.controls[name].control_el.addEventListener('click', function(e) { + vm.clearParams() + }) + } + } + } + + public clearParams(): void { + // Todo: clear the results as well + if (this.inputSchema) { + let vm = this; + this.initDataTypes(); + this.correctDataTypes(); + for(let param in this.data) { + if (this.data.hasOwnProperty(param)) { + if (this.webform) { + (this.webform.inputs[param].input_el).value = this.data[param]; + } + } + } + this.validate(); + this.DataChangedListener.emit({data: vm.data}); + } + } + + public setParams(params: any = {}): void { + // Todo: make it wait for schema + if (this.inputSchema) { + let vm = this; + let dataChanged = false; + this.data = params; + for(let param in params) { + if (this.data.hasOwnProperty(param)) { + if (this.data[param] !== params[param]) { + if (this.webform) { + (this.webform.inputs[param].input_el).value = params[param]; + } + this.data[param] = params[param]; + dataChanged = true; + } + } + } + if (dataChanged) this.DataChangedListener.emit({data: vm.data}); + } else { + for(let param in params) { + if (params.hasOwnProperty(param)) { + this.cachedParams[param] = params[param]; + } + } + } + } + + public setParam(param: string, value: any): void { + // Todo: make it wait for schema + if (this.inputSchema) { + let vm = this; + let dataChanged = false; + if (this.data.hasOwnProperty(param)) { + if (this.data[param] !== value) { + if (this.webform) { + (this.webform.inputs[param].input_el).value = value; + } + this.data[param] = value; + dataChanged = true; + } + if (dataChanged) this.DataChangedListener.emit({data: vm.data}); + } + } else { + this.cachedParams[param] = value; + } + } + + public getParams(): void { + return this.data || this.cachedParams; + } + + private setParamFromWebform(param: string, value: any): void { + // Todo: make it wait for schema + if (this.inputSchema) { + let vm = this; + let dataChanged = false; + if (this.data.hasOwnProperty(param)) { + if (this.data[param] !== value) { + this.data[param] = value; + dataChanged = true; + } + } + if (dataChanged) this.DataChangedListener.emit({data: vm.data}); + } else { + this.cachedParams[param] = value; + } + } + + private setParamsFromCachedParams(): void { + if (!helpers.isEmpty(this.cachedParams)) { + this.setParams(this.cachedParams); + this.cachedParams = {}; + } + } + + public getValidationErrors(): WebFormErrors { + return this.errors; + } + + public execute(): Promise { + let vm = this; + //Wait for documents to get arround + return new Promise((resolve: any, reject: any) => { + let test: any; + this.describe().then(() => { + // Retrieve cached value + vm.setParamsFromCachedParams(); + }).then(() => { + // Correct data types + vm.correctDataTypes(); + + vm.http.makeRequest('POST', 'https://api.businesslogic.online/execute', vm.data) + .then(function(result) { + if (vm.webform) { + // If webservice was assigned to a webform print outputs to assigned elements + vm.handleWebformOutputs(result); + } + vm.ExecutedListener.emit(result); + test = result; + resolve(result); + }) + .catch(function(error) { + reject(error); + console.error('Augh, there was an error! ', error.status + ': ' + error.statusText); + }); + }); + }); + } + + private validate(): Boolean { + let valid: boolean = true; + this.errors = {}; + + if (ajv) ajv.validate(this.inputSchema, this.data); + + if (this.webform) { + + // Handle form validation when the webservice is associated with a webform + if (this.webform.form_el) { + // Use native form validation + this.webform.form_el.customMessages = true; + valid = this.webform.form_el.checkValidity(); + } else { + // Use input validation + let inputValid: boolean; + for(let el in this.webform.inputs) { + this.validateInput(el); + this.webform.inputs[el].input_el.classList.add('touched'); + } + if (!helpers.isEmpty(this.errors)) { + valid = false + } + } + } else { + //Handle pure data validation based on associated json schema + // TODO: Consider using AJV library for schema validation instead of form validation + // valid = ajv.validate(this.inputSchema, this.data); + } + + if (!valid) { + this.ValidationFailedListener.emit({ + errors: this.errors + }); + } + return valid; + } + + private validateInput(param: string): Boolean { + let input = this.webform.inputs[param].input_el; + let inputValid = true; + let message = ''; + if (!input.validity.valid) { + inputValid = false; + message = input.validationMessage; + this.errors[param] = message; + this.updateWebFormErrorMessage(param, message); + } else { + this.updateWebFormErrorMessage(param, ''); + } + // Todo: Implement custom validation with language support + // Todo: Implement custom validation with support for custom messages + + return inputValid; + } + + private updateWebFormErrorMessage(input: any, message: string): void { + if (this.webform && this.webform.inputs[input].err_el) { + this.webform.inputs[input].err_el.textContent = message; + } + } + + private getWebserviceDocs(): Promise { + let self = this; + + return new Promise((resolve: any, reject: any) => { + this.http.makeRequest('GET', 'https://api.businesslogic.online/describe') + .then(function(result) { + resolve(result); + log(result, self.debug); + }) + .catch(function(error) { + reject(error); + console.error('Augh, there was an error! ', error.status + ': ' + error.statusText); + }); + }); + } + + private enrichWebFormInputs(): void { + // Enrich input elements + for(let param in this.webform.inputs) { + for(let property in this.inputSchema.properties) { + if (param === property && this.inputSchema.properties.hasOwnProperty(property)) { + let definition = this.inputSchema.properties[property]; + let label = this.webform.inputs[property].label_el; + let description = this.webform.inputs[property].desc_el; + let type = this.inputSchema.properties[property].type; + let select: HTMLSelectElement; + let input = this.webform.inputs[property].input_el; + if (this.webform.inputs[property].input_el.tagName === 'SELECT') { + select = this.webform.inputs[property].input_el; + } + + // Set title and description + if (definition.title) label.innerHTML = definition.title; + if (definition.description) description.innerHTML = definition.description; + + // Set required + if (this.inputSchema.required.includes(property)) input.setAttribute('required', 'required'); + + // Set default to inputs (not select) + if (definition.default !== null) input.value = definition.default; + + // Set options for enum + // TODO: Consider switching to simple version of enum without dataobject + if (definition.enum) { + // Handle labels + let inputData: any; + let mappings: Array; + let labelObjName: string; + let labelFieldName: string; + let valueObjName: string; + let valueFieldName: string; + + // TODO: Revisit data mapping + if (definition.data_label_mapping) { + mappings = definition.data_label_mapping.split('.'); + labelFieldName = mappings.pop(); + labelObjName = mappings.pop(); + } + + if (definition.data_mapping) { + mappings = definition.data_mapping.split('.'); + valueFieldName = mappings.pop(); + valueObjName = mappings.pop(); + } + + inputData = this.relatedData[labelObjName || valueObjName] || null; + + // TODO: Consider support of optgroup + if (!!select) { + select.querySelectorAll('option').forEach((o) => { + if (o.getAttribute('bl-placeholder') === '') { + (o).value = ''; + } else { + o.remove(); + } + }); + for(let i = 0; i < definition.enum.length; i++) { + let option = document.createElement('option'); + option.text = inputData && inputData[i][labelFieldName] || definition.enum[i]; + option.value = inputData && inputData[i][labelObjName] || definition.enum[i]; + // Set defaults for select + if (definition.default !== null) { + if (option.value === String(definition.default)) { + option.setAttribute('selected', 'selected') + } + } + select.add(option); + } + } else { + // TODO: Consider alternative inputs for select with options + } + } + + // Set options for oneOf + if (definition.oneOf) { + // TODO: Consider support of optgroup + if (!!select) { + select.querySelectorAll('option').forEach((o) => { + if (o.getAttribute('bl-placeholder') === '') { + (o).value = ''; + } else { + o.remove(); + } + }); + for(let i = 0; i < definition.oneOf.length; i++) { + let option = document.createElement('option'); + option.text = definition.oneOf[i].title; + option.value = definition.oneOf[i].const; + // Set defaults for select + if (definition.default !== null) { + if (option.value === String(definition.default)) { + option.setAttribute('selected', 'selected') + } + } + select.add(option); + } + } else { + // TODO: Consider alternative inputs for select with options + } + } + + // Set extras from webservice definitions (input json schema) + switch(type) { + case 'string': + if (definition.minLength) input.minLength = definition.minLength; + if (definition.maxLength) input.maxLength = definition.maxLength; + if (definition.pattern) input.pattern = definition.pattern; + //if(definition.format) consider what to do with formats; + break; + case 'number': + if (definition.minimum) input.min = definition.minimum; + if (definition.maximum) input.max = definition.maximum; + if (definition.pattern) input.pattern = definition.pattern; + if (definition.multipleOf) input.step = definition.multipleOf; + //code block + break; + case 'integer': + if (definition.minimum) input.min = definition.minimum; + if (definition.maximum) input.max = definition.maximum; + if (definition.pattern) input.pattern = definition.pattern; + if (definition.multipleOf) input.step = definition.multipleOf; + //code block + break; + default: + //code block + } + } + } + } + + // Enrich output elements + // TODO: Consider other types of outputs like progress and meter tags + // TODO: Consider other types of outputs like charts + // TODO: Support array outputs + for(let param in this.webform.outputs) { + for(let property in this.outputSchema.properties) { + if (param === property && this.outputSchema.properties.hasOwnProperty(property)) { + let definition = this.outputSchema.properties[property]; + let label = this.webform.outputs[property].label_el; + let description = this.webform.outputs[property].desc_el; + let type = this.outputSchema.properties[property].type; + let output = this.webform.outputs[property].output_el; + + // Set title and description + if (definition.title) label.innerHTML = definition.title; + if (definition.description) description.innerHTML = definition.description; + + // Set default + if (definition.default) output.innerHTML = definition.default; + } + } + } + } + + private handleWebformOutputs(results: any = {}): void { + for(let result in results) { + for(let param in this.webform.outputs) { + if (result === param) { + if (Array.isArray(results[param])) { + let values = ''; + for(let field in results[param]) { + values += '
    '; + for(let entry in results[param][field]) { + values += '
  • ' + results[param][field][entry] + '
  • '; + } + values += '
'; + } + + // TODO: Handle array outputs with proper component from WebFormComponents + this.webform.outputs[param].output_el.innerHTML = values; + } else { + this.webform.outputs[param].output_el.innerHTML = results[param]; + } + } + } + } + } + + private correctDataTypes(): void { + if (this.inputSchema) { + for(let param in this.data) { + if (this.data.hasOwnProperty(param)) { + let value = this.data[param]; + if (this.inputSchema.properties.hasOwnProperty(param)) { + let type = this.inputSchema.properties[param].type; + let val: any; + switch(type) { + case 'string': + val = String(value); + break; + case 'number': + val = Number(value); + break; + case 'integer': + val = parseInt(value); + break; + default: + this.data[param] = String(value); + } + this.data[param] = val; + } else { + // Strip miscellaneous params + delete this.data[param]; + } + } + } + } + } + + private initDataTypes(): void { + if (this.inputSchema) { + for(let param in this.inputSchema.properties) { + this.data[param] = null; + } + } + } +} diff --git a/src/index.html b/src/index.html index c1fb9b9..5b08733 100755 --- a/src/index.html +++ b/src/index.html @@ -32,25 +32,25 @@ - +
diff --git a/src/index.ts b/src/index.ts old mode 100755 new mode 100644 index 361f07a..383d2cd --- a/src/index.ts +++ b/src/index.ts @@ -1,690 +1,21 @@ 'use strict'; +import { WebFormComponents } from './Dom/WebFormComponents'; +import { log, mapWebForm, setRangeListeners } from "./Helpers/HelperFunctions"; +import { ServiceContainer } from "./classes/service-container"; +import { Webservice } from "./classes/web-service"; -import {WebForm, WebFormErrors} from './WebForm'; -import {http} from './http'; -import {TypedEvent} from './Events/TypedEvent'; -import {WebFormComponents} from './Dom/WebFormComponents'; -import * as helpers from './Helpers/HelperFunctions'; -import {JSDict} from './Helpers/TypedDictionary'; - -declare global { - interface Window { - Ajv: any; - } -} - -const ajv: any = helpers.isConstructor(window.Ajv) ? new window.Ajv() : null; - - -export interface SchemaReceivedEvent { - inputSchema: any; - outputSchema: any; - relatedData: any; -} - -export interface ValidationFailedEvent { - errors: WebFormErrors; -} - -export interface DataChangedEvent { - data: any; -} - -export interface ExecutedEvent { - data: any; -} - -// Todo: Needs to be split into more logical parts - -export class Webservice { - protected key: string; - private http: http; - protected webform: WebForm; - protected data: any; - private inputSchema: any; - private outputSchema: any; - private relatedData: any; - private cachedParams: any; - private errors: WebFormErrors; - private test: number; - - public SchemaReceviedListener = new TypedEvent(); - public ValidationFailedListener = new TypedEvent(); - public DataChangedListener = new TypedEvent(); - public ExecutedListener = new TypedEvent(); - - constructor(key: string = '', webform?: WebForm) { - this.key = key; - this.data = {}; - this.webform = webform; - this.http = new http(key); - this.errors = {}; - this.cachedParams = {}; - this.test = 1; - - this.init(); - } - - private init(): void { - let vm = this; - - // Adds this webservice to the collection of Webservices - Webservices.add(this.key, this); - - this.describe().then(() => { - vm.initDataTypes(); - vm.setParamsFromCachedParams(); - if (this.webform) vm.enrichWebFormInputs(); - }).then(() => { - if (this.webform) vm.handleAssociatedWebform(); - }); - } - - private describe(): Promise { - let vm = this; - - return new Promise((resolve: any, reject: any) => { - // Retrieve all relevant webservice metadata - if (vm.inputSchema) { - resolve(''); - } else { - this.getWebserviceDocs().then((result) => { - this.SchemaReceviedListener.emit({ - inputSchema: vm.inputSchema = result.expected_input || {}, - outputSchema: vm.outputSchema = result.expected_output || {}, - relatedData: vm.relatedData = result.available_data || {} - }); - - resolve(result); - }) - } - }); - } - - public assignWebForm(webform: WebForm): void { - this.webform = webform; - } - - private handleAssociatedWebform(): void { - for (let param in this.webform.inputs) { - let vm = this; - let value = (this.webform.inputs[param].input_el).value; - let type = vm.inputSchema.properties[param].type; - - vm.setParamFromWebform(param, value); - - this.webform.inputs[param].input_el.addEventListener('blur', function (e) { - vm.validateInput(param); - vm.webform.inputs[param].input_el.classList.add('touched'); - }); - this.webform.inputs[param].input_el.addEventListener('input', function (e) { - vm.validateInput(param); - vm.setParamFromWebform(param, this.value); - }); - // If there is no submit button we execute the form upon a valid input - if (this.webform.controls['submit'] === undefined) { - this.webform.inputs[param].input_el.addEventListener('change', function (e) { - if (vm.validate()) vm.execute(); - }) - } - } - for (let name in this.webform.controls) { - let vm = this; - - if (name == 'submit') { - this.webform.controls[name].control_el.addEventListener('click', function (e) { - if (vm.validate()) vm.execute(); - }) - } - if (name == 'reset') { - this.webform.controls[name].control_el.addEventListener('click', function (e) { - vm.clearParams() - }) - } - } - } - - public clearParams(): void { - // Todo: clear the results as well - if (this.inputSchema) { - let vm = this; - this.initDataTypes(); - this.correctDataTypes(); - for (let param in this.data) { - if (this.data.hasOwnProperty(param)) { - if (this.webform) { - (this.webform.inputs[param].input_el).value = this.data[param]; - } - } - } - this.validate(); - this.DataChangedListener.emit({data: vm.data}); - } - } - - public setParams(params: any = {}): void { - // Todo: make it wait for schema - if (this.inputSchema) { - let vm = this; - let dataChanged = false; - this.data = params; - for (let param in params) { - if (this.data.hasOwnProperty(param)) { - if (this.data[param] !== params[param]) { - if (this.webform) { - (this.webform.inputs[param].input_el).value = params[param]; - } - this.data[param] = params[param]; - dataChanged = true; - } - } - } - if (dataChanged) this.DataChangedListener.emit({data: vm.data}); - } else { - for (let param in params) { - if (params.hasOwnProperty(param)) { - this.cachedParams[param] = params[param]; - } - } - } - } - - public setParam(param: string, value: any): void { - // Todo: make it wait for schema - if (this.inputSchema) { - let vm = this; - let dataChanged = false; - if (this.data.hasOwnProperty(param)) { - if (this.data[param] !== value) { - if (this.webform) { - (this.webform.inputs[param].input_el).value = value; - } - this.data[param] = value; - dataChanged = true; - } - if (dataChanged) this.DataChangedListener.emit({data: vm.data}); - } - } else { - this.cachedParams[param] = value; - } - } - - public getParams(): void { - return this.data || this.cachedParams; - } - - private setParamFromWebform(param: string, value: any): void { - // Todo: make it wait for schema - if (this.inputSchema) { - let vm = this; - let dataChanged = false; - if (this.data.hasOwnProperty(param)) { - if (this.data[param] !== value) { - this.data[param] = value; - dataChanged = true; - } - } - if (dataChanged) this.DataChangedListener.emit({data: vm.data}); - } else { - this.cachedParams[param] = value; - } - } - - private setParamsFromCachedParams(): void { - if (!helpers.isEmpty(this.cachedParams)) { - this.setParams(this.cachedParams); - this.cachedParams = {}; - } - } - - public getValidationErrors(): WebFormErrors { - return this.errors; - } - - public execute(): Promise { - let vm = this; - console.log(vm.data) - //Wait for documents to get arround - return new Promise((resolve: any, reject: any) => { - let test: any; - this.describe().then(() => { - // Retrieve cached value - vm.setParamsFromCachedParams(); - }).then(() => { - // Correct data types - vm.correctDataTypes(); - - vm.http.makeRequest('POST', 'https://api.businesslogic.online/execute', vm.data) - .then(function (result) { - if (vm.webform) { - // If webservice was assigned to a webform print outputs to assigned elements - vm.handleWebformOutputs(result); - } - vm.ExecutedListener.emit(result); - test = result; - resolve(result); - }) - .catch(function (error) { - reject(error); - console.error('Augh, there was an error! ', error.status + ': ' + error.statusText); - }); - }); - }); - } - - private validate(): Boolean { - let valid: boolean = true; - this.errors = {}; - - if (ajv) ajv.validate(this.inputSchema, this.data); - - if (this.webform) { - // Handle form validation when the webservice is associated with a webform - - if (this.webform.form_el) { - // Use native form validation - this.webform.form_el.customMessages = true; - valid = this.webform.form_el.checkValidity(); - } else { - // Use input validation - let inputValid: boolean; - for (let el in this.webform.inputs) { - this.validateInput(el); - this.webform.inputs[el].input_el.classList.add('touched'); - } - if (!helpers.isEmpty(this.errors)) { - valid = false - } - } - } else { - //Handle pure data validation based on associated json schema - // TODO: Consider using AJV library for schema validation instead of form validation - // valid = ajv.validate(this.inputSchema, this.data); - } - - if (!valid) { - this.ValidationFailedListener.emit({ - errors: this.errors - }); - } - return valid; - } - - private validateInput(param: string): Boolean { - let input = this.webform.inputs[param].input_el; - let inputValid = true; - let message = ''; - if (!input.validity.valid) { - inputValid = false; - message = input.validationMessage; - this.errors[param] = message; - this.updateWebFormErrorMessage(param, message); - } else { - this.updateWebFormErrorMessage(param, ''); - } - // Todo: Implement custom validation with language support - // Todo: Implement custom validation with support for custom messages - - return inputValid; - } - - private updateWebFormErrorMessage(input: any, message: string): void { - if (this.webform && this.webform.inputs[input].err_el) { - this.webform.inputs[input].err_el.textContent = message; - } - } - - private getWebserviceDocs(): Promise { - return new Promise((resolve: any, reject: any) => { - this.http.makeRequest('GET', 'https://api.businesslogic.online/describe') - .then(function (result) { - resolve(result); - log(result); - }) - .catch(function (error) { - reject(error); - console.error('Augh, there was an error! ', error.status + ': ' + error.statusText); - }); - }); - } - - private enrichWebFormInputs(): void { - // Enrich input elements - for (let param in this.webform.inputs) { - for (let property in this.inputSchema.properties) { - if (param === property && this.inputSchema.properties.hasOwnProperty(property)) { - let definition = this.inputSchema.properties[property]; - let label = this.webform.inputs[property].label_el; - let description = this.webform.inputs[property].desc_el; - let type = this.inputSchema.properties[property].type; - let select: HTMLSelectElement; - let input = this.webform.inputs[property].input_el; - if (this.webform.inputs[property].input_el.tagName === 'SELECT') { - select = this.webform.inputs[property].input_el; - } - - // Set title and description - if (definition.title) label.innerHTML = definition.title; - if (definition.description) description.innerHTML = definition.description; - - // Set required - if (this.inputSchema.required.includes(property)) input.setAttribute('required', 'required'); - - // Set default to inputs (not select) - if (definition.default !== null) input.value = definition.default; - - // Set options for enum - // TODO: Consider switching to simple version of enum without dataobject - if (definition.enum) { - // Handle labels - let inputData: any; - let mappings: Array; - let labelObjName: string; - let labelFieldName: string; - let valueObjName: string; - let valueFieldName: string; - - // TODO: Revisit data mapping - if (definition.data_label_mapping) { - mappings = definition.data_label_mapping.split('.'); - labelFieldName = mappings.pop(); - labelObjName = mappings.pop(); - } - - if (definition.data_mapping) { - mappings = definition.data_mapping.split('.'); - valueFieldName = mappings.pop(); - valueObjName = mappings.pop(); - } - - inputData = this.relatedData[labelObjName || valueObjName] || null; - - // TODO: Consider support of optgroup - if (!!select) { - select.querySelectorAll('option').forEach((o) => { - if (o.getAttribute('bl-placeholder') === '') { - (o).value = ''; - } else { - o.remove(); - } - }); - for (let i = 0; i < definition.enum.length; i++) { - let option = document.createElement('option'); - option.text = inputData && inputData[i][labelFieldName] || definition.enum[i]; - option.value = inputData && inputData[i][labelObjName] || definition.enum[i]; - // Set defaults for select - if (definition.default !== null) { - if (option.value === String(definition.default)) { - option.setAttribute('selected', 'selected') - } - } - select.add(option); - } - } else { - // TODO: Consider alternative inputs for select with options - } - } - - // Set options for oneOf - if (definition.oneOf) { - // TODO: Consider support of optgroup - if (!!select) { - select.querySelectorAll('option').forEach((o) => { - if (o.getAttribute('bl-placeholder') === '') { - (o).value = ''; - } else { - o.remove(); - } - }); - for (let i = 0; i < definition.oneOf.length; i++) { - let option = document.createElement('option'); - option.text = definition.oneOf[i].title; - option.value = definition.oneOf[i].const; - // Set defaults for select - if (definition.default !== null) { - if (option.value === String(definition.default)) { - option.setAttribute('selected', 'selected') - } - } - select.add(option); - } - } else { - // TODO: Consider alternative inputs for select with options - } - } - - // Set extras from webservice definitions (input json schema) - switch (type) { - case 'string': - if (definition.minLength) input.minLength = definition.minLength; - if (definition.maxLength) input.maxLength = definition.maxLength; - if (definition.pattern) input.pattern = definition.pattern; - //if(definition.format) consider what to do with formats; - break; - case 'number': - if (definition.minimum) input.min = definition.minimum; - if (definition.maximum) input.max = definition.maximum; - if (definition.pattern) input.pattern = definition.pattern; - if (definition.multipleOf) input.step = definition.multipleOf; - //code block - break; - case 'integer': - if (definition.minimum) input.min = definition.minimum; - if (definition.maximum) input.max = definition.maximum; - if (definition.pattern) input.pattern = definition.pattern; - if (definition.multipleOf) input.step = definition.multipleOf; - //code block - break; - default: - //code block - } - } - } - } - - // Enrich output elements - // TODO: Consider other types of outputs like progress and meter tags - // TODO: Consider other types of outputs like charts - // TODO: Support array outputs - for (let param in this.webform.outputs) { - for (let property in this.outputSchema.properties) { - if (param === property && this.outputSchema.properties.hasOwnProperty(property)) { - let definition = this.outputSchema.properties[property]; - let label = this.webform.outputs[property].label_el; - let description = this.webform.outputs[property].desc_el; - let type = this.outputSchema.properties[property].type; - let output = this.webform.outputs[property].output_el; - - // Set title and description - if (definition.title) label.innerHTML = definition.title; - if (definition.description) description.innerHTML = definition.description; - - // Set default - if (definition.default) output.innerHTML = definition.default; - } - } - } - } - - private handleWebformOutputs(results: any = {}): void { - for (let result in results) { - for (let param in this.webform.outputs) { - if (result === param) { - if (Array.isArray(results[param])) { - let values = ''; - for (let field in results[param]) { - values += '
    '; - for (let entry in results[param][field]) { - values += '
  • ' + results[param][field][entry] + '
  • '; - } - values += '
'; - } - - // TODO: Handle array outputs with proper component from WebFormComponents - this.webform.outputs[param].output_el.innerHTML = values; - } else { - this.webform.outputs[param].output_el.innerHTML = results[param]; - } - } - } - } - } - - private correctDataTypes(): void { - if (this.inputSchema) { - for (let param in this.data) { - if (this.data.hasOwnProperty(param)) { - let value = this.data[param]; - if (this.inputSchema.properties.hasOwnProperty(param)) { - let type = this.inputSchema.properties[param].type; - let val: any; - switch (type) { - case 'string': - val = String(value); - break; - case 'number': - val = Number(value); - break; - case 'integer': - val = parseInt(value); - break; - default: - this.data[param] = String(value); - } - this.data[param] = val; - } else { - // Strip miscellaneous params - delete this.data[param]; - } - } - } - } - } - - private initDataTypes(): void { - if (this.inputSchema) { - for (let param in this.inputSchema.properties) { - this.data[param] = null; - } - } - } -} - -export interface IDictionary { - [id: string]: Webservice; -} - -// Create dictonary for keeping track of all webservice instances -class ServiceContainer { - private dict: any; - - constructor() { - this.dict = JSDict.Create(); - } - - public add(apiKey: string, webservice: Webservice): void { - if (!this.dict[apiKey]) { - this.dict[apiKey] = webservice; - } else { - console.warn('Webservice with apiKey: ' + apiKey + ' was allready added to Businesslogic.Webservices'); - } - } - - // TODO: Make this functions as a promise so you can be sure it is initialised - // Remember to have a timeout resolve 30secs - public get(apiKey: string): Webservice { - return this.dict[apiKey]; - } -} - -let Webservices: ServiceContainer = new ServiceContainer(); +let WebServicesContainer: ServiceContainer = new ServiceContainer(); let debug: boolean; -function log(message: any): void { - if (debug) console.log(message); -} - -function mapWebForm(formItem: any): WebForm { - let name = formItem.getAttribute('bl-name'); - let key = formItem.getAttribute('bl-token'); - let inputs = formItem.querySelectorAll('[bl-input]'); - let controls = formItem.querySelectorAll('[bl-control]'); - let outputs = formItem.querySelectorAll('[bl-output]'); - let param: string, type: string, tagName: string; - let el: Element; - let lbl_el: Element, desc_el: Element, err_el: Element; - - - let wf: WebForm = { - name: name, - inputs: {}, - controls: {}, - outputs: {} - }; - - if (formItem instanceof HTMLFormElement) { - wf.form_el = formItem; - } - // Handling inputs - for (let i = 0; i < inputs.length; i++) { - param = inputs[i].getAttribute('bl-input'); - type = inputs[i].getAttribute('type'); - tagName = inputs[i].tagName; - el = inputs[i]; - lbl_el = formItem.querySelector('[bl-input-label=' + param + ']'); - desc_el = formItem.querySelector('[bl-input-description=' + param + ']'); - err_el = formItem.querySelector('[bl-input-error=' + param + ']'); - wf.inputs[param] = {'label_el': lbl_el, 'desc_el': desc_el, 'input_el': el, 'err_el': err_el}; - } - // Handling controls - for (let i = 0; i < controls.length; i++) { - param = controls[i].getAttribute('bl-control'); - type = controls[i].getAttribute('type'); - tagName = controls[i].tagName; - el = controls[i]; - wf.controls[param] = {'control_el': el}; - } - // Handling outputs - for (let i = 0; i < outputs.length; i++) { - param = outputs[i].getAttribute('bl-output'); - type = outputs[i].getAttribute('type'); - tagName = outputs[i].tagName; - el = outputs[i]; - lbl_el = formItem.querySelector('[bl-output-label=' + param + ']'); - desc_el = formItem.querySelector('[bl-output-description=' + param + ']'); - wf.outputs[param] = {'label_el': lbl_el, 'desc_el': desc_el, 'output_el': el}; - } - return wf; -} -//TODO refactor library, replace string component by html components -function setRangeListeners(formInputs) { - const rangeGroup = formInputs.querySelectorAll('.range-group'); - - rangeGroup.forEach((rangeGroup) => { - const rangeInput = rangeGroup.querySelector('input[type="range"]'); - - rangeInput.addEventListener('input', (event) => { - let target = event.target - const min = target['min']; - const max = target['max']; - const val = target['value']; - - target['style'].backgroundSize = (val - min) * 100 / (max - min) + '% 100%' - }); - }); -} - -export {Webservices}; - -(function () { +(function() { // See if we are in debug mode if (!!document.querySelector('script[bl-debug]')) debug = true; - log('Initialise businesslogic'); + log('Initialise businesslogic', debug); let formList = document.querySelectorAll('[bl-name]'); - for (let f = 0; f < formList.length; f++) { + for(let f = 0; f < formList.length; f++) { let ws: Webservice; let name = formList[f].getAttribute('bl-name'); let key = formList[f].getAttribute('bl-token'); @@ -697,13 +28,14 @@ export {Webservices}; let container: HTMLDivElement = document.createElement('div'); container.className = 'bl-form'; formList[f].appendChild(container); - ws = new Webservice(key); + ws = new Webservice(key, debug); + ws.SchemaReceviedListener.on((e) => { let inputs: WebFormComponents = new WebFormComponents('form-inputs'); let outputs: WebFormComponents = new WebFormComponents('form-outputs'); let signature: WebFormComponents = new WebFormComponents(); - for (let param in e.inputSchema.properties) { + for(let param in e.inputSchema.properties) { if (!e.inputSchema.properties.hasOwnProperty(param)) return; let type = e.inputSchema.properties[param].type; @@ -720,7 +52,7 @@ export {Webservices}; } else if (isRange) { inputs.attachComponent('range', param, e.inputSchema.properties[param]); } else { - switch (type) { + switch(type) { case 'number': inputs.attachComponent('number', param); break; @@ -750,7 +82,7 @@ export {Webservices}; container.appendChild(formInputs); - for (let param in e.outputSchema.properties) { + for(let param in e.outputSchema.properties) { if (!e.outputSchema.properties.hasOwnProperty(param)) return; let type = e.outputSchema.properties[param].type; let enumeration = e.outputSchema.properties[param].enum || e.outputSchema.properties[param].oneOf; @@ -759,7 +91,7 @@ export {Webservices}; outputs.attachComponent('select', param); } else { - switch (type) { + switch(type) { case 'number': outputs.attachComponent('output', param); break; @@ -787,13 +119,15 @@ export {Webservices}; ws.assignWebForm(wf); }); } else { - ws = new Webservice(key, mapWebForm(formList[f])); + ws = new Webservice(key, debug, mapWebForm(formList[f])); } - log('Creating form from termplate: ' + name); + log(`Creating form from termplate: ${name}`, debug); } })(); +export { WebServicesContainer }; + // TODO: Consider implementing datalist with input instead of select https://www.quackit.com/html/tags/html_datalist_tag.cfm // TODO: Consider supporting http://inorganik.github.io/countUp.js/ // TODO: Consider supporting https://nosir.github.io/cleave.js/ diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..8632c65 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,44 @@ +export interface SchemaReceivedEvent { + inputSchema: any; + outputSchema: any; + relatedData: any; +} + +export interface ValidationFailedEvent { + errors: WebFormErrors; +} + +export interface DataChangedEvent { + data: any; +} + +export interface ExecutedEvent { + data: any; +} + +export interface WebForm { + name: string; + form_el?:HTMLFormElement; + inputs?: { [id: string]: { + label_el?: Element; + desc_el?: Element; + input_el?: Element; + err_el?: Element; + }}; + controls?: { [id: string]: { + control_el?: Element; + }}; + outputs?: { [id: string]: { + label_el?: Element; + desc_el?: Element; + output_el?: Element; + }}; +} + +export interface WebFormErrors { + [id: string]: string; +} + +export interface IDictionary { + [id: string]: Webservice; +}