diff --git a/__mocks__/cosmiconfig.ts b/__mocks__/cosmiconfig.ts new file mode 100644 index 0000000..de7f103 --- /dev/null +++ b/__mocks__/cosmiconfig.ts @@ -0,0 +1,13 @@ +import { vol } from 'memfs' + +const loadConfigFile = filePath => { + const fileContent: string = vol.readFileSync(filePath, 'utf8') as string + const config = JSON.parse(fileContent) + return { config } +} + +const explorer = { loadSync: loadConfigFile } + +const cosmiconfig = jest.fn(() => explorer) + +module.exports = cosmiconfig diff --git a/__mocks__/fs.ts b/__mocks__/fs.ts new file mode 100644 index 0000000..8768c47 --- /dev/null +++ b/__mocks__/fs.ts @@ -0,0 +1,11 @@ +import { vol } from 'memfs' + +const fs = jest.requireActual('fs') + +function mockedWriteFile(path: string, content: string) { + vol.writeFileSync(path, content) +} + +jest.spyOn(fs, 'writeFile').mockImplementation(mockedWriteFile) + +module.exports = fs diff --git a/__mocks__/util.ts b/__mocks__/util.ts new file mode 100644 index 0000000..e557fa6 --- /dev/null +++ b/__mocks__/util.ts @@ -0,0 +1,13 @@ +const util = jest.requireActual('util') + +function promisify(fn) { + return (...args) => { + return new Promise(resolve => { + resolve(fn(...args)) + }) + } +} + +util.promisify = promisify + +module.exports = util diff --git a/dist/commands/generate.js b/dist/commands/generate.js index 93cad7e..fd0a18d 100644 --- a/dist/commands/generate.js +++ b/dist/commands/generate.js @@ -4,18 +4,25 @@ const tslib_1 = require('tslib') const command_1 = require('@oclif/command') const fs = require('fs') const html_ui_prototyper_1 = require('../html-ui-prototyper') +const printer_1 = require('../utils/printer') class Generate extends command_1.Command { run() { return tslib_1.__awaiter(this, void 0, void 0, function*() { - const { flags } = this.parse(Generate) - if (flags.features) { + const printer = new printer_1.default() + try { + const { flags } = this.parse(Generate) + if (!flags.features) throw new Error('Missing flag --features') const processResult = JSON.parse(flags.features) + if (processResult.features.length === 0) + throw new Error('No features found') const generator = new html_ui_prototyper_1.default( fs, flags.outputDir ) const result = yield generator.generate(processResult.features) - this.log(JSON.stringify(result)) + printer.printGeneratedFiles(result) + } catch (e) { + printer.printErrorMessage(e.message) } }) } diff --git a/dist/html-ui-prototyper.js b/dist/html-ui-prototyper.js index 1b3ad06..b59a7d2 100644 --- a/dist/html-ui-prototyper.js +++ b/dist/html-ui-prototyper.js @@ -4,8 +4,10 @@ const tslib_1 = require('tslib') const fs = require('fs') const util_1 = require('util') const path_1 = require('path') -const prettier = require('prettier') +const case_converter_1 = require('./utils/case-converter') +const format_html_1 = require('./utils/format-html') const cosmiconfig = require('cosmiconfig') +const { normalize } = require('normalize-diacritics') const widget_factory_1 = require('./widgets/widget-factory') class HtmlUIPrototyper { constructor(_fs = fs, _outputDir) { @@ -30,13 +32,13 @@ class HtmlUIPrototyper { } createHtmlFile(fileName, widgets) { return tslib_1.__awaiter(this, void 0, void 0, function*() { + fileName = yield normalize( + case_converter_1.convertCase(fileName, 'snake') + ) let content = widgets.reduce((result, widget) => { return result + widget.renderToString() }, '') - content = prettier.format(`
\n${content}
`, { - parser: 'html', - htmlWhitespaceSensitivity: 'ignore', - }) + content = format_html_1.formatHtml(`
${content}
`) const path = path_1.format({ dir: this._outputDir, name: fileName, diff --git a/dist/interfaces/app-config.d.ts b/dist/interfaces/app-config.d.ts index d56546e..9d65b8d 100644 --- a/dist/interfaces/app-config.d.ts +++ b/dist/interfaces/app-config.d.ts @@ -1,11 +1,25 @@ export interface AppConfig { widgets?: { - input?: WidgetConfig; + [key: string]: WidgetConfig; }; } export interface WidgetConfig { - opening: string; - closure?: string; - wrapperOpening?: string; - wrapperClosure?: string; + template?: string; + widget: { + opening: string; + closure?: string; + onePerValue?: boolean; + }; + valueWrapper?: { + opening: string; + closure: string; + }; + wrapper?: { + opening: string; + closure: string; + }; + label?: { + opening: string; + closure: string; + }; } diff --git a/dist/utils/case-converter.d.ts b/dist/utils/case-converter.d.ts new file mode 100644 index 0000000..fff91a0 --- /dev/null +++ b/dist/utils/case-converter.d.ts @@ -0,0 +1 @@ +export declare function convertCase(text: string, type: string): string; diff --git a/dist/utils/case-converter.js b/dist/utils/case-converter.js new file mode 100644 index 0000000..175a1a6 --- /dev/null +++ b/dist/utils/case-converter.js @@ -0,0 +1,30 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const case_1 = require('case') +var CaseType +;(function(CaseType) { + CaseType['CAMEL'] = 'camel' + CaseType['PASCAL'] = 'pascal' + CaseType['SNAKE'] = 'snake' + CaseType['KEBAB'] = 'kebab' +})(CaseType || (CaseType = {})) +function convertCase(text, type) { + switch ( + type + .toString() + .trim() + .toLowerCase() + ) { + case CaseType.CAMEL: + return case_1.camel(text) + case CaseType.PASCAL: + return case_1.pascal(text) + case CaseType.SNAKE: + return case_1.snake(text) + case CaseType.KEBAB: + return case_1.kebab(text) + default: + return text // do nothing + } +} +exports.convertCase = convertCase diff --git a/dist/utils/format-html.d.ts b/dist/utils/format-html.d.ts new file mode 100644 index 0000000..a12da9d --- /dev/null +++ b/dist/utils/format-html.d.ts @@ -0,0 +1 @@ +export declare function formatHtml(html: string): any; diff --git a/dist/utils/format-html.js b/dist/utils/format-html.js new file mode 100644 index 0000000..eb4b05f --- /dev/null +++ b/dist/utils/format-html.js @@ -0,0 +1,10 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const prettier = require('prettier') +function formatHtml(html) { + return prettier.format(html, { + parser: 'html', + htmlWhitespaceSensitivity: 'ignore', + }) +} +exports.formatHtml = formatHtml diff --git a/dist/utils/index.d.ts b/dist/utils/index.d.ts new file mode 100644 index 0000000..5a535f2 --- /dev/null +++ b/dist/utils/index.d.ts @@ -0,0 +1,3 @@ +export * from './case-converter'; +export * from './format-html'; +export * from './prop'; diff --git a/dist/utils/index.js b/dist/utils/index.js new file mode 100644 index 0000000..6d61023 --- /dev/null +++ b/dist/utils/index.js @@ -0,0 +1,6 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const tslib_1 = require('tslib') +tslib_1.__exportStar(require('./case-converter'), exports) +tslib_1.__exportStar(require('./format-html'), exports) +tslib_1.__exportStar(require('./prop'), exports) diff --git a/dist/utils/printer.d.ts b/dist/utils/printer.d.ts new file mode 100644 index 0000000..43df5df --- /dev/null +++ b/dist/utils/printer.d.ts @@ -0,0 +1,4 @@ +export default class Printer { + printGeneratedFiles(files: string[]): void; + printErrorMessage(message: string): void; +} diff --git a/dist/utils/printer.js b/dist/utils/printer.js new file mode 100644 index 0000000..c45877d --- /dev/null +++ b/dist/utils/printer.js @@ -0,0 +1,21 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const Table = require('cli-table3') +const colors = require('colors') +/* tslint:disable:no-console */ +class Printer { + printGeneratedFiles(files) { + const table = new Table({ + head: [colors.green('#'), colors.green('Generated files')], + }) + for (let i = 0; i < files.length; i++) { + const counter = i + 1 + table.push([counter, files[i]]) + } + console.log(table.toString()) + } + printErrorMessage(message) { + console.log(colors.red(message)) + } +} +exports.default = Printer diff --git a/dist/utils/prop.d.ts b/dist/utils/prop.d.ts index c409969..c34767e 100644 --- a/dist/utils/prop.d.ts +++ b/dist/utils/prop.d.ts @@ -1 +1 @@ -export declare function formatProperties(props: any, validProperties: string[]): string; +export declare function formatProperties(props: any, caseType?: string): string; diff --git a/dist/utils/prop.js b/dist/utils/prop.js index b4c1c4e..7f57fef 100644 --- a/dist/utils/prop.js +++ b/dist/utils/prop.js @@ -1,6 +1,7 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -function formatProperties(props, validProperties) { +const case_converter_1 = require('./case-converter') +function formatProperties(props, caseType = 'camel') { const translateProp = key => { switch (key) { case 'format': @@ -9,33 +10,15 @@ function formatProperties(props, validProperties) { return key } } - const getFormattedProp = key => { - let value = props[key] - const invalidIdPattern = /^\/\// - if (key === 'id') { - let newKey = key - // TODO: replace test wit str.match(pattern) - if (!invalidIdPattern.test(value)) { - const validIdPattern = /^#|~/ - const validClassPattern = /^\./ - if (validIdPattern.test(value)) { - value = value.toString().replace(validIdPattern, '') - } else if (validClassPattern.test(value)) { - newKey = 'class' - value = value.toString().replace(validClassPattern, '') - } - return `${translateProp(newKey)}="${value}"` - } - } - return `${translateProp(key)}="${value}"` - } - const formatValid = (result, prop) => { - return validProperties.includes(prop) - ? result + getFormattedProp(prop) + ' ' - : result + const getValueOf = key => + case_converter_1.convertCase(props[key].toString(), caseType) + const format = (result, key) => { + const value = getValueOf(key) + const htmlProp = translateProp(key) + return result + `${htmlProp}="${value}"` + ' ' } return Object.keys(props) - .reduce(formatValid, '') + .reduce(format, '') .trimRight() } exports.formatProperties = formatProperties diff --git a/dist/widgets/button.d.ts b/dist/widgets/button.d.ts index 906d5f2..53ef2a2 100644 --- a/dist/widgets/button.d.ts +++ b/dist/widgets/button.d.ts @@ -1,7 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; -export default class Button extends Widget { - private readonly VALID_PROPERTIES; - constructor(props: any, name?: string); - renderToString(): string; +import { WidgetConfig } from '../interfaces/app-config'; +import HtmlWidget from './html-widget'; +export default class Button extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; private getType; } diff --git a/dist/widgets/button.js b/dist/widgets/button.js index 3c35e32..33c5313 100644 --- a/dist/widgets/button.js +++ b/dist/widgets/button.js @@ -1,23 +1,23 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -class Button extends concordialang_ui_core_1.Widget { - constructor(props, name) { - super(props, name || '') - this.VALID_PROPERTIES = ['id', 'disabled', 'value'] +const html_widget_1 = require('./html-widget') +class Button extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + this.props.value = this.props.value || name } - renderToString() { - // const inputType = this.getType(this.props.datatype as string) - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - // return `` - return `` + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'disabled'] + props.type = this.getType(props.datatype) + props.value = props.value || this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } getType(datatype) { - return `type="${datatype || 'button'}"` + return datatype || 'button' } } exports.default = Button diff --git a/dist/widgets/checkbox.d.ts b/dist/widgets/checkbox.d.ts index b7204c6..9b4fc7a 100644 --- a/dist/widgets/checkbox.d.ts +++ b/dist/widgets/checkbox.d.ts @@ -1,6 +1,6 @@ -import { Widget } from 'concordialang-ui-core'; -export default class Checkbox extends Widget { - private readonly VALID_PROPERTIES; - constructor(props: any, name: string); - renderToString(): string; +import { WidgetConfig } from '../interfaces/app-config'; +import HtmlWidget from './html-widget'; +export default class Checkbox extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/checkbox.js b/dist/widgets/checkbox.js index f322947..1218466 100644 --- a/dist/widgets/checkbox.js +++ b/dist/widgets/checkbox.js @@ -1,23 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -class Checkbox extends concordialang_ui_core_1.Widget { - constructor(props, name) { - super(props, name) - this.VALID_PROPERTIES = ['value', 'required'] +const html_widget_1 = require('./html-widget') +class Checkbox extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) } - // TODO: remove \n - renderToString() { - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - if (properties) - return `
\n${ - this.name - }\n
` - return `
\n${this.name}\n
` + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value', 'required'] + props.type = 'checkbox' + props.name = props.value + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Checkbox diff --git a/dist/widgets/html-widget.d.ts b/dist/widgets/html-widget.d.ts new file mode 100644 index 0000000..8056e13 --- /dev/null +++ b/dist/widgets/html-widget.d.ts @@ -0,0 +1,11 @@ +import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; +export default abstract class HtmlWidget extends Widget { + private _config; + constructor(props: any, name: string, _config: WidgetConfig); + renderToString(): string; + protected abstract getFormattedProps(props: any): string; + private renderWidgetWithSingleValue; + private renderOneWidgetPerValue; + private renderWidgetWithMultipleValues; +} diff --git a/dist/widgets/html-widget.js b/dist/widgets/html-widget.js new file mode 100644 index 0000000..a4f01a4 --- /dev/null +++ b/dist/widgets/html-widget.js @@ -0,0 +1,95 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const concordialang_ui_core_1 = require('concordialang-ui-core') +const Mustache = require('mustache') +const prop_1 = require('../utils/prop') +const label_1 = require('./label') +const wrapper_1 = require('./wrapper') +class HtmlWidget extends concordialang_ui_core_1.Widget { + constructor(props, name, _config) { + super(props, name) + this._config = _config + } + renderToString() { + let main + if (Array.isArray(this.props.value)) { + main = this._config.widget.onePerValue + ? this.renderOneWidgetPerValue(this.props.value) + : this.renderWidgetWithMultipleValues(this.props.value) + } else { + main = this.renderWidgetWithSingleValue() + } + const widgetId = this.props.id ? this.props.id.toString() : undefined + const label = label_1.createLabel(this.name, widgetId, this._config) + return wrapper_1.wrap(label + main, this._config) + } + renderWidgetWithSingleValue() { + const props = this.getFormattedProps(this.props) + const config = { + widget: Object.assign({}, this._config.widget), + // Widgets like button may include {{value}} in the template + value: this.props.value, + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = + this._config.template || '{{&widget.opening}}{{&widget.closure}}' + return Mustache.render(template, config) + } + renderOneWidgetPerValue(values) { + // When rendering one widget per value, we must delete the "id" property + // to avoid multiple widgets with the same id. + delete this.props.id + const widgets = [] + for (const value of values) { + const props = this.getFormattedProps( + Object.assign({}, this.props, { value }) + ) + const config = { + widget: Object.assign({}, this._config.widget), + valueWrapper: Object.assign({}, this._config.valueWrapper), + value, + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = + this._config.template || + '{{&widget.opening}}{{&widget.closure}}{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgets.push(Mustache.render(template, config)) + } + return widgets.join(' ') + } + renderWidgetWithMultipleValues(values) { + const widgetValues = [] + for (const value of values) { + const props = prop_1.formatProperties({ value }) + const config = { + valueWrapper: this._config.valueWrapper + ? Object.assign({}, this._config.valueWrapper) + : { opening: '', closure: '' }, + value, + } + config.valueWrapper.opening = Mustache.render( + config.valueWrapper.opening, + { props } + ) + const template = + '{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgetValues.push(Mustache.render(template, config)) + } + delete this.props.value + const props = this.getFormattedProps(this.props) + const config = { + widget: Object.assign({}, this._config.widget), + values: widgetValues.join(' '), + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = '{{&widget.opening}}{{&values}}{{&widget.closure}}' + return Mustache.render(template, config) + } +} +exports.default = HtmlWidget diff --git a/dist/widgets/input.d.ts b/dist/widgets/input.d.ts index eb72d3a..d2669e0 100644 --- a/dist/widgets/input.d.ts +++ b/dist/widgets/input.d.ts @@ -1,10 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Input extends Widget { - private _config; - private readonly VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; - private wrap; +import HtmlWidget from './html-widget'; +export default class Input extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; private getType; } diff --git a/dist/widgets/input.js b/dist/widgets/input.js index 89e7db6..5f64209 100644 --- a/dist/widgets/input.js +++ b/dist/widgets/input.js @@ -1,43 +1,28 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -class Input extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name) - this._config = _config - this.VALID_PROPERTIES = [ +const html_widget_1 = require('./html-widget') +class Input extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + } + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = [ 'id', + 'type', + 'name', 'editable', 'minlength', 'maxlength', 'required', 'format', ] - } - renderToString() { - const inputType = this.getType(this.props.datatype) - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - const input = this._config.opening.replace( - '%s', - `${inputType} ${properties}` - ) - const inputClosure = this._config.closure || '' - const label = label_1.createLabel(this.name, this.props.id.toString()) - return this.wrap(label + input + inputClosure) - } - wrap(elements) { - if (this._config.wrapperOpening && this._config.wrapperClosure) - return ( - this._config.wrapperOpening + - elements + - this._config.wrapperClosure - ) - return elements + props.type = this.getType(props.datatype) + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } getType(datatype) { let typeProperty @@ -55,7 +40,7 @@ class Input extends concordialang_ui_core_1.Widget { default: typeProperty = 'text' } - return `type="${typeProperty}"` + return typeProperty } } exports.default = Input diff --git a/dist/widgets/label.d.ts b/dist/widgets/label.d.ts index d914e3a..013d063 100644 --- a/dist/widgets/label.d.ts +++ b/dist/widgets/label.d.ts @@ -1 +1,2 @@ -export declare function createLabel(name: string, id: string): string; +import { WidgetConfig } from '../interfaces/app-config'; +export declare function createLabel(widgetName: string, widgetId: string | undefined, widgetConfig: WidgetConfig): string; diff --git a/dist/widgets/label.js b/dist/widgets/label.js index 125f5ce..7ae3d93 100644 --- a/dist/widgets/label.js +++ b/dist/widgets/label.js @@ -1,10 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -function createLabel(name, id) { - const validIdPattern = /^(#|~|\d|\w).*/ - const labelFor = validIdPattern.test(id) - ? `for="${id.replace(/^#|~/, '')}"` - : '' - return `` +const Mustache = require('mustache') +const utils_1 = require('../utils') +function createLabel(widgetName, widgetId, widgetConfig) { + if (!widgetConfig.label) return '' + const idPattern = /^(#|~|\d|\w).*/ + const labelFor = + widgetId && widgetId.match(idPattern) + ? `for="${widgetId.replace(/^#|~/, '')}"` + : '' + widgetConfig.label.opening = Mustache.render(widgetConfig.label.opening, { + props: labelFor, + }) + return utils_1.formatHtml( + widgetConfig.label.opening + widgetName + widgetConfig.label.closure + ) } exports.createLabel = createLabel diff --git a/dist/widgets/radio.d.ts b/dist/widgets/radio.d.ts index 594884f..2cc48ed 100644 --- a/dist/widgets/radio.d.ts +++ b/dist/widgets/radio.d.ts @@ -1,6 +1,6 @@ -import { Widget } from 'concordialang-ui-core'; -export default class Radio extends Widget { - private readonly VALID_PROPERTIES; - constructor(props: any, name: string); - renderToString(): string; +import { WidgetConfig } from '../interfaces/app-config'; +import HtmlWidget from './html-widget'; +export default class Radio extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/radio.js b/dist/widgets/radio.js index c1c3c1d..86f1667 100644 --- a/dist/widgets/radio.js +++ b/dist/widgets/radio.js @@ -1,30 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -class Radio extends concordialang_ui_core_1.Widget { - constructor(props, name) { - super(props, name) - this.VALID_PROPERTIES = ['value'] +const html_widget_1 = require('./html-widget') +class Radio extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) } - // TODO: remove \n - renderToString() { - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - let inputs = [] - const label = label_1.createLabel(this.name, this.props.id.toString()) - const inputName = this.name.toLowerCase() - if (properties) { - for (let value of this.props.value) { - let input = `${value}` - inputs.push(input) - } - return `
\n${label + inputs.join('\n')}\n
` - } - return '
\n\n
' + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value'] + props.type = 'radio' + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Radio diff --git a/dist/widgets/select.d.ts b/dist/widgets/select.d.ts index 463e827..0d3dfd3 100644 --- a/dist/widgets/select.d.ts +++ b/dist/widgets/select.d.ts @@ -1,7 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; -export default class Select extends Widget { +import { WidgetConfig } from '../interfaces/app-config'; +import HtmlWidget from './html-widget'; +export default class Select extends HtmlWidget { private readonly VALID_PROPERTIES; - constructor(props: any, name: string); - renderToString(): string; - private getOptions; + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/select.js b/dist/widgets/select.js index fc0979c..00274ce 100644 --- a/dist/widgets/select.js +++ b/dist/widgets/select.js @@ -1,32 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -class Select extends concordialang_ui_core_1.Widget { - constructor(props, name) { - super(props, name) - this.VALID_PROPERTIES = ['id', 'required'] +const html_widget_1 = require('./html-widget') +class Select extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + this.VALID_PROPERTIES = [] } - // TODO: remove \n - renderToString() { - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - if (!properties) return '
\n\n
' - const options = this.getOptions() - const select = `\n` - const label = label_1.createLabel(this.name, this.props.id.toString()) - return `
\n${label + select}
` - } - getOptions() { - let options = [] - for (let value of this.props.value) { - let option = `` - options.push(option) - } - return options.join('\n') + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'name', 'required'] + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Select diff --git a/dist/widgets/widget-factory.d.ts b/dist/widgets/widget-factory.d.ts index 5cd426e..417a0cf 100644 --- a/dist/widgets/widget-factory.d.ts +++ b/dist/widgets/widget-factory.d.ts @@ -5,4 +5,8 @@ export default class WidgetFactory { constructor(_config: AppConfig); create(element: UiElement): Widget; private createInputElement; + private createRadioElement; + private createCheckboxElement; + private createSelectElement; + private createButtonElement; } diff --git a/dist/widgets/widget-factory.js b/dist/widgets/widget-factory.js index f188a64..f91f996 100644 --- a/dist/widgets/widget-factory.js +++ b/dist/widgets/widget-factory.js @@ -2,8 +2,8 @@ Object.defineProperty(exports, '__esModule', { value: true }) const lodash_1 = require('lodash') const button_1 = require('./button') -const input_1 = require('./input') const checkbox_1 = require('./checkbox') +const input_1 = require('./input') const radio_1 = require('./radio') const select_1 = require('./select') class WidgetFactory { @@ -15,20 +15,48 @@ class WidgetFactory { case 'textbox' /* TEXTBOX */: return this.createInputElement(element) case 'button' /* BUTTON */: - return new button_1.default(element.props, element.name) + return this.createButtonElement(element) case 'checkbox' /* CHECKBOX */: - return new checkbox_1.default(element.props, element.name) + return this.createCheckboxElement(element) case 'radio' /* RADIO */: - return new radio_1.default(element.props, element.name) + return this.createRadioElement(element) case 'select' /* SELECT */: - return new select_1.default(element.props, element.name) + return this.createSelectElement(element) default: throw new Error(`Invalid widget type: ${element.widget}`) } } createInputElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.input') + widgetConfig.label = + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') return new input_1.default(element.props, element.name, widgetConfig) } + createRadioElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.radio') + widgetConfig.label = + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') + return new radio_1.default(element.props, element.name, widgetConfig) + } + createCheckboxElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.checkbox') + widgetConfig.label = + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') + return new checkbox_1.default(element.props, element.name, widgetConfig) + } + createSelectElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.select') + widgetConfig.label = + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') + return new select_1.default(element.props, element.name, widgetConfig) + } + createButtonElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.button') + return new button_1.default(element.props, element.name, widgetConfig) + } } exports.default = WidgetFactory diff --git a/dist/widgets/wrapper.d.ts b/dist/widgets/wrapper.d.ts new file mode 100644 index 0000000..63e9a5b --- /dev/null +++ b/dist/widgets/wrapper.d.ts @@ -0,0 +1,2 @@ +import { WidgetConfig } from '../interfaces/app-config'; +export declare function wrap(elements: string, widgetConfig: WidgetConfig): string; diff --git a/dist/widgets/wrapper.js b/dist/widgets/wrapper.js new file mode 100644 index 0000000..fba6b8a --- /dev/null +++ b/dist/widgets/wrapper.js @@ -0,0 +1,13 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +function wrap(elements, widgetConfig) { + if (widgetConfig.wrapper) { + return ( + widgetConfig.wrapper.opening + + elements + + widgetConfig.wrapper.closure + ) + } + return elements +} +exports.wrap = wrap diff --git a/package-lock.json b/package-lock.json index b71ba09..c4c5148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -818,6 +818,11 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.134.tgz", "integrity": "sha512-2/O0khFUCFeDlbi7sZ7ZFRCcT812fAeOLm7Ev4KbwASkZ575TDrDcY7YyaoHdTOzKcNbfiwLYZqPmoC4wadrsw==" }, + "@types/mustache": { + "version": "0.8.32", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-0.8.32.tgz", + "integrity": "sha512-RTVWV485OOf4+nO2+feurk0chzHkSjkjALiejpHltyuMf/13fGymbbNNFrSKdSSUg1TIwzszXdWsVirxgqYiFA==" + }, "@types/node": { "version": "10.14.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz", @@ -1532,6 +1537,11 @@ "redeyed": "~2.1.0" } }, + "case": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.1.tgz", + "integrity": "sha512-N0rDB5ftMDKANGsIBRWPWcG0VIKtirgqcXb2vKFi66ySAjXVEwbfCN7ass1mkdXO8fbol3RfbWlQ9KyBX2F/Gg==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1614,6 +1624,16 @@ "restore-cursor": "^2.0.0" } }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -1822,6 +1842,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "optional": true + }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -5359,6 +5385,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "mustache": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", + "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" + }, "nan": { "version": "2.13.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", @@ -5443,6 +5474,11 @@ "which": "^1.3.0" } }, + "normalize-diacritics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-diacritics/-/normalize-diacritics-2.5.0.tgz", + "integrity": "sha512-E6A4zGUh1MIhFnNb0WolW+NqCHPyiKfuPRikNDOt1RuLjJ+7aIZU30d+h7sbZGNT78mbR12Y+0FNgUTY/7IWHw==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5514,8 +5550,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", diff --git a/package.json b/package.json index 1b193f6..4e8805f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "ts-jest": { "diagnostics": false } - } + }, + "testEnvironment": "node" }, "oclif": { "commands": "./dist/commands", @@ -73,9 +74,14 @@ "@oclif/command": "^1.5.12", "@oclif/config": "^1.12.12", "@types/lodash": "^4.14.134", + "@types/mustache": "^0.8.32", + "case": "^1.6.1", + "cli-table3": "^0.5.1", "concordialang-ui-core": "^0.2.3", "cosmiconfig": "^5.2.1", "lodash": "^4.17.11", + "mustache": "^3.0.1", + "normalize-diacritics": "^2.5.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 1f59803..5bff607 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -1,27 +1,33 @@ -import {Command, flags} from '@oclif/command' -import {ProcessResult} from 'concordialang-ui-core' +import { Command, flags } from '@oclif/command' +import { ProcessResult } from 'concordialang-ui-core' import * as fs from 'fs' import HtmlUIPrototyper from '../html-ui-prototyper' +import Printer from '../utils/printer' export default class Generate extends Command { + static description = 'Generate html files' - static description = 'Generate html files' + static flags = { + help: flags.help({ char: 'h' }), + features: flags.string({ description: 'processed features from ast', required: true }), + outputDir: flags.string({ description: 'location where output files will be saved', required: true }) + } - static flags = { - help: flags.help({char: 'h'}), - features: flags.string({description: 'processed features from ast', required: true}), - outputDir: flags.string({description: 'location where output files will be saved', required: true}) - } + async run() { + const printer: Printer = new Printer() + try { + const { flags } = this.parse(Generate) + if (!flags.features) throw new Error('Missing flag --features') - async run() { - const {flags} = this.parse(Generate) + const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult + if (processResult.features.length === 0) throw new Error('No features found') - if (flags.features) { - const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult - const generator = new HtmlUIPrototyper(fs, flags.outputDir) - const result = await generator.generate(processResult.features) - this.log(JSON.stringify(result)) - } - } + const generator = new HtmlUIPrototyper(fs, flags.outputDir) + const result: string[] = await generator.generate(processResult.features) + printer.printGeneratedFiles(result) + } catch (e) { + printer.printErrorMessage(e.message) + } + } } diff --git a/src/html-ui-prototyper.ts b/src/html-ui-prototyper.ts index b4a091a..69e10e9 100644 --- a/src/html-ui-prototyper.ts +++ b/src/html-ui-prototyper.ts @@ -2,8 +2,11 @@ import { Feature, Prototyper, Widget } from 'concordialang-ui-core' import * as fs from 'fs' import { promisify } from 'util' import { format } from 'path' -const prettier = require('prettier') +import { convertCase } from './utils/case-converter' +import { formatHtml } from './utils/format-html' + const cosmiconfig = require('cosmiconfig') +const { normalize } = require('normalize-diacritics') import WidgetFactory from './widgets/widget-factory' import { AppConfig } from './interfaces/app-config' @@ -26,11 +29,13 @@ export default class HtmlUIPrototyper implements Prototyper { } private async createHtmlFile(fileName: string, widgets: Widget[]): Promise { + fileName = await normalize(convertCase(fileName, 'snake')) + let content = widgets.reduce((result, widget) => { return result + widget.renderToString() }, '') - content = prettier.format(`
\n${content}
`, {parser: 'html', htmlWhitespaceSensitivity: 'ignore'}) + content = formatHtml(`
${content}
`) const path = format({ dir: this._outputDir, name: fileName, ext: '.html' }) await promisify(fs.writeFile)(path, content) diff --git a/src/interfaces/app-config.ts b/src/interfaces/app-config.ts index f52efca..83cdbb4 100644 --- a/src/interfaces/app-config.ts +++ b/src/interfaces/app-config.ts @@ -1,12 +1,27 @@ export interface AppConfig { + // TODO: add a property to config the widget properties case widgets?: { - input?: WidgetConfig + [key: string]: WidgetConfig, } } export interface WidgetConfig { - opening: string, - closure?: string, - wrapperOpening?: string, - wrapperClosure?: string + template?: string, + widget: { + opening: string, + closure?: string, + onePerValue?: boolean, + } + valueWrapper?: { + opening: string, + closure: string, + }, + wrapper?: { + opening: string, + closure: string, + } + label?: { + opening: string, + closure: string, + } } diff --git a/src/utils/case-converter.ts b/src/utils/case-converter.ts new file mode 100644 index 0000000..8f2dbbd --- /dev/null +++ b/src/utils/case-converter.ts @@ -0,0 +1,18 @@ +import { camel, pascal, snake, kebab } from 'case'; + +enum CaseType { + CAMEL = 'camel', + PASCAL = 'pascal', + SNAKE = 'snake', + KEBAB = 'kebab' +} + +export function convertCase(text: string, type: string): string { + switch (type.toString().trim().toLowerCase()) { + case CaseType.CAMEL: return camel(text); + case CaseType.PASCAL: return pascal(text); + case CaseType.SNAKE: return snake(text); + case CaseType.KEBAB: return kebab(text); + default: return text; // do nothing + } +} diff --git a/src/utils/format-html.ts b/src/utils/format-html.ts new file mode 100644 index 0000000..8f08182 --- /dev/null +++ b/src/utils/format-html.ts @@ -0,0 +1,5 @@ +const prettier = require('prettier') + +export function formatHtml(html: string) { + return prettier.format(html, { parser: 'html', htmlWhitespaceSensitivity: 'ignore' }) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..c1455ac --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './case-converter' +export * from './format-html' +export * from './prop' diff --git a/src/utils/printer.ts b/src/utils/printer.ts new file mode 100644 index 0000000..f538081 --- /dev/null +++ b/src/utils/printer.ts @@ -0,0 +1,22 @@ +const Table = require('cli-table3') +const colors = require('colors') + +/* tslint:disable:no-console */ +export default class Printer { + public printGeneratedFiles(files: string[]) { + const table = new Table({ + head: [colors.green('#'), colors.green('Generated files')] + }) + + for (let i = 0; i < files.length; i++) { + const counter = i + 1 + table.push([counter, files[i]]) + } + + console.log(table.toString()) + } + + public printErrorMessage(message: string) { + console.log(colors.red(message)) + } +} diff --git a/src/utils/prop.ts b/src/utils/prop.ts index 5741272..91fbd48 100644 --- a/src/utils/prop.ts +++ b/src/utils/prop.ts @@ -1,4 +1,6 @@ -export function formatProperties(props: any, validProperties: string[]): string { +import { convertCase } from './case-converter' + +export function formatProperties(props: any, caseType: string = 'camel'): string { const translateProp = (key: string) => { switch(key) { case 'format': return 'pattern'; @@ -6,35 +8,13 @@ export function formatProperties(props: any, validProperties: string[]): string } } - const getFormattedProp = (key: string) => { - let value = props[key] - const invalidIdPattern = /^\/\// - - if(key === 'id') { - let newKey = key - // TODO: replace test wit str.match(pattern) - if(!invalidIdPattern.test(value)) { - const validIdPattern = /^#|~/ - const validClassPattern = /^\./ - - if(validIdPattern.test(value)) { - value = value.toString().replace(validIdPattern, '') - } else if(validClassPattern.test(value)) { - newKey = 'class' - value = value.toString().replace(validClassPattern, '') - } - return `${translateProp(newKey)}="${value}"` - } - } - - return `${translateProp(key)}="${value}"` - } + const getValueOf = (key: string) => (convertCase(props[key].toString(), caseType)) - const formatValid = (result: string, prop: string) => { - return validProperties.includes(prop) - ? result + getFormattedProp(prop) + ' ' - : result + const format = (result: string, key: string) => { + const value = getValueOf(key) + const htmlProp = translateProp(key) + return result + `${htmlProp}="${value}"` + ' ' } - return Object.keys(props).reduce(formatValid, '').trimRight() + return Object.keys(props).reduce(format, '').trimRight() } diff --git a/src/widgets/button.ts b/src/widgets/button.ts index b036186..6cbdbb9 100644 --- a/src/widgets/button.ts +++ b/src/widgets/button.ts @@ -1,22 +1,29 @@ -import {Widget} from 'concordialang-ui-core' +import { pick } from 'lodash' -import {formatProperties} from '../utils/prop' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' -export default class Button extends Widget { - private readonly VALID_PROPERTIES = ['id', 'disabled', 'value'] +import HtmlWidget from './html-widget' - constructor(props: any, name?: string) { - super(props, name || '') - } +export default class Button extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) + this.props.value = this.props.value || name + } - public renderToString(): string { - // const inputType = this.getType(this.props.datatype as string) - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - // return `` - return `` - } + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'disabled'] - private getType(datatype: string): string { - return `type="${datatype || 'button'}"` - } + props.type = this.getType(props.datatype) + props.value = props.value || this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) + } + + private getType(datatype: string): string { + return datatype || 'button' + } } diff --git a/src/widgets/checkbox.ts b/src/widgets/checkbox.ts index 5e37c09..3f5df1b 100644 --- a/src/widgets/checkbox.ts +++ b/src/widgets/checkbox.ts @@ -1,18 +1,24 @@ -import {Widget} from 'concordialang-ui-core' +import { pick } from 'lodash' -import {formatProperties} from '../utils/prop' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' -export default class Checkbox extends Widget { - private readonly VALID_PROPERTIES = ['value', 'required'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string) { - super(props, name) +export default class Checkbox extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - // TODO: remove \n - public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - if (properties) return `
\n${this.name}\n
` - return `
\n${this.name}\n
` + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value', 'required'] + + props.type = 'checkbox' + props.name = props.value + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } } diff --git a/src/widgets/html-widget.ts b/src/widgets/html-widget.ts new file mode 100644 index 0000000..57c7c00 --- /dev/null +++ b/src/widgets/html-widget.ts @@ -0,0 +1,94 @@ +import { Widget } from 'concordialang-ui-core' +import { get } from 'lodash' +import * as Mustache from 'mustache' + +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' + +import { createLabel } from './label' +import { wrap } from './wrapper' + +export default abstract class HtmlWidget extends Widget { + constructor(props: any, name: string, private _config: WidgetConfig) { + super(props, name) + } + + public renderToString(): string { + let main: string + + if (Array.isArray(this.props.value)) { + main = this._config.widget.onePerValue + ? this.renderOneWidgetPerValue(this.props.value as string []) + : this.renderWidgetWithMultipleValues(this.props.value as string[]) + } else { + main = this.renderWidgetWithSingleValue() + } + + const widgetId = this.props.id ? this.props.id.toString() : undefined + const label: string = createLabel(this.name, widgetId, this._config) + return wrap(label + main, this._config) + } + + protected abstract getFormattedProps(props: any): string + + private renderWidgetWithSingleValue(): string { + const props = this.getFormattedProps(this.props) + const config = { + widget: { ...this._config.widget }, + // Widgets like button may include {{value}} in the template + value: this.props.value + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + const template = this._config.template || '{{&widget.opening}}{{&widget.closure}}' + return Mustache.render(template, config) + } + + private renderOneWidgetPerValue(values: string[]): string { + // When rendering one widget per value, we must delete the "id" property + // to avoid multiple widgets with the same id. + delete this.props.id + + const widgets: string[] = [] + + for (const value of values) { + const props = this.getFormattedProps({ ...this.props, value }) + const config = { + widget: { ...this._config.widget }, + valueWrapper: { ...this._config.valueWrapper }, + value + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + + const template = this._config.template || '{{&widget.opening}}{{&widget.closure}}{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgets.push(Mustache.render(template, config)) + } + + return widgets.join(' ') + } + + private renderWidgetWithMultipleValues(values: string[]): string { + const widgetValues: string[] = [] + for (const value of values) { + const props = formatProperties({ value }) + const config = { + valueWrapper: this._config.valueWrapper ? { ...this._config.valueWrapper } : { opening: '', closure: '' }, + value + } + + config.valueWrapper.opening = Mustache.render(config.valueWrapper.opening, { props }) + + const template = '{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgetValues.push(Mustache.render(template, config)) + } + + delete this.props.value + const props = this.getFormattedProps(this.props) + const config = { + widget: { ...this._config.widget }, + values: widgetValues.join(' ') + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + const template = '{{&widget.opening}}{{&values}}{{&widget.closure}}' + return Mustache.render(template, config) + } +} diff --git a/src/widgets/input.ts b/src/widgets/input.ts index 30c07f9..d831df3 100644 --- a/src/widgets/input.ts +++ b/src/widgets/input.ts @@ -1,9 +1,9 @@ -import { Widget } from 'concordialang-ui-core' -import { get } from 'lodash'; +import { pick } from 'lodash' import { WidgetConfig } from '../interfaces/app-config' import { formatProperties } from '../utils/prop' -import { createLabel } from './label' + +import HtmlWidget from './html-widget' const enum DataTypes { STRING = 'string', @@ -14,39 +14,34 @@ const enum DataTypes { DATETIME = 'datetime' } -export default class Input extends Widget { - private readonly VALID_PROPERTIES = ['id', 'editable', 'minlength', 'maxlength', 'required', 'format'] - - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name) +export default class Input extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - public renderToString(): string { - const inputType = this.getType(this.props.datatype as string) - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - const input = this._config.opening.replace('%s', `${ inputType } ${ properties }`) - const inputClosure = this._config.closure || '' - const label = createLabel(this.name, this.props.id.toString()) - return this.wrap(label + input + inputClosure) - } + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'name', 'editable', 'minlength', 'maxlength', 'required', 'format'] + + props.type = this.getType(props.datatype) + props.name = this.name + + const filteredProps = pick(props, VALID_PROPERTIES) - private wrap(elements: string): string { - if (this._config.wrapperOpening && this._config.wrapperClosure) - return this._config.wrapperOpening + elements + this._config.wrapperClosure - return elements + return formatProperties(filteredProps) } private getType(datatype: string): string { let typeProperty switch (datatype) { - case DataTypes.INTEGER: - case DataTypes.DOUBLE: typeProperty = 'number'; break - case DataTypes.TIME: typeProperty = 'time'; break - case DataTypes.DATETIME: typeProperty = 'datetime-local'; break - default: typeProperty = 'text' + case DataTypes.INTEGER: + case DataTypes.DOUBLE: typeProperty = 'number'; break + case DataTypes.TIME: typeProperty = 'time'; break + case DataTypes.DATETIME: typeProperty = 'datetime-local'; break + default: typeProperty = 'text' } - return `type="${typeProperty}"` + return typeProperty } } diff --git a/src/widgets/label.ts b/src/widgets/label.ts index 51f1061..798ffa7 100644 --- a/src/widgets/label.ts +++ b/src/widgets/label.ts @@ -1,5 +1,14 @@ -export function createLabel(name: string, id: string): string { - const validIdPattern = /^(#|~|\d|\w).*/ - const labelFor = (validIdPattern.test(id)) ? `for="${id.replace(/^#|~/ , '')}"` : '' - return `` +import * as Mustache from 'mustache' + +import { WidgetConfig } from '../interfaces/app-config' +import { formatHtml } from '../utils' + +export function createLabel(widgetName: string, widgetId: string | undefined, widgetConfig: WidgetConfig): string { + if (!widgetConfig.label) return '' + + const idPattern = /^(#|~|\d|\w).*/ + const labelFor = widgetId && widgetId.match(idPattern) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' + widgetConfig.label.opening = Mustache.render(widgetConfig.label.opening, { props: labelFor }) + + return formatHtml(widgetConfig.label.opening + widgetName + widgetConfig.label.closure) } diff --git a/src/widgets/radio.ts b/src/widgets/radio.ts index 60f104d..735e3fd 100644 --- a/src/widgets/radio.ts +++ b/src/widgets/radio.ts @@ -1,30 +1,24 @@ -import {Widget} from 'concordialang-ui-core' +import { pick } from 'lodash' -import {formatProperties} from '../utils/prop' -import {createLabel} from './label' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' -export default class Radio extends Widget { - private readonly VALID_PROPERTIES = ['value'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string) { - super(props, name) +export default class Radio extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - // TODO: remove \n - public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - let inputs: String[] = [] - const label = createLabel(this.name, this.props.id.toString()) - const inputName = this.name.toLowerCase() + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value'] - if (properties) { - for (let value of this.props.value as Array) { - let input = `${value}` - inputs.push(input) - } - return `
\n${label + inputs.join('\n')}\n
` - } - return '
\n\n
' + props.type = 'radio' + props.name = this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } } - diff --git a/src/widgets/select.ts b/src/widgets/select.ts index 1994f1e..113dd87 100644 --- a/src/widgets/select.ts +++ b/src/widgets/select.ts @@ -1,31 +1,25 @@ -import {Widget} from 'concordialang-ui-core' +import { pick } from 'lodash' -import {formatProperties} from '../utils/prop' -import {createLabel} from './label' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' -export default class Select extends Widget { - private readonly VALID_PROPERTIES = ['id', 'required'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string) { - super(props, name) - } +export default class Select extends HtmlWidget { + private readonly VALID_PROPERTIES = [] - // TODO: remove \n - public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - if (!properties) return '
\n\n
' - const options = this.getOptions() - const select = `\n` - const label = createLabel(this.name, this.props.id.toString()) - return `
\n${label + select}
` + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - private getOptions(): string { - let options: string[] = [] - for (let value of this.props.value as Array) { - let option = `` - options.push(option) - } - return options.join('\n') + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'name', 'required'] + + props.name = this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } } diff --git a/src/widgets/widget-factory.ts b/src/widgets/widget-factory.ts index 7bfcfa9..5e2bd0e 100644 --- a/src/widgets/widget-factory.ts +++ b/src/widgets/widget-factory.ts @@ -1,12 +1,13 @@ -import {UiElement, Widget} from 'concordialang-ui-core' -import { get } from 'lodash'; +import { UiElement, Widget } from 'concordialang-ui-core' +import { get } from 'lodash' + +import { AppConfig, WidgetConfig } from '../interfaces/app-config' import Button from './button' -import Input from './input' import Checkbox from './checkbox' +import Input from './input' import Radio from './radio' import Select from './select' -import { AppConfig, WidgetConfig } from '../interfaces/app-config' const enum Widgets { BUTTON = 'button', @@ -22,16 +23,40 @@ export default class WidgetFactory { create(element: UiElement): Widget { switch (element.widget) { case Widgets.TEXTBOX: return this.createInputElement(element) - case Widgets.BUTTON: return new Button(element.props, element.name) - case Widgets.CHECKBOX: return new Checkbox(element.props, element.name) - case Widgets.RADIO: return new Radio(element.props, element.name) - case Widgets.SELECT: return new Select(element.props, element.name) + case Widgets.BUTTON: return this.createButtonElement(element) + case Widgets.CHECKBOX: return this.createCheckboxElement(element) + case Widgets.RADIO: return this.createRadioElement(element) + case Widgets.SELECT: return this.createSelectElement(element) default: throw new Error(`Invalid widget type: ${element.widget}`) } } - private createInputElement(element: UiElement): any { + private createInputElement(element: UiElement): Input { const widgetConfig: WidgetConfig = get(this._config, 'widgets.input') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') return new Input(element.props, element.name, widgetConfig) } + + private createRadioElement(element: UiElement): Radio { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.radio') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') + return new Radio(element.props, element.name, widgetConfig) + } + + private createCheckboxElement(element: UiElement): Checkbox { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.checkbox') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') + return new Checkbox(element.props, element.name, widgetConfig) + } + + private createSelectElement(element: UiElement): Select { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.select') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') + return new Select(element.props, element.name, widgetConfig) + } + + private createButtonElement(element: UiElement): Button { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.button') + return new Button(element.props, element.name, widgetConfig) + } } diff --git a/src/widgets/wrapper.ts b/src/widgets/wrapper.ts new file mode 100644 index 0000000..54c6a75 --- /dev/null +++ b/src/widgets/wrapper.ts @@ -0,0 +1,8 @@ +import { WidgetConfig } from '../interfaces/app-config' + +export function wrap(elements: string, widgetConfig: WidgetConfig): string { + if (widgetConfig.wrapper) { + return widgetConfig.wrapper.opening + elements + widgetConfig.wrapper.closure + } + return elements +} diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index fd7d496..3e926c4 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -1,12 +1,99 @@ +import { fs as memfs, vol } from 'memfs' + import Generate from '../../src/commands/generate' +import Printer from '../../src/utils/printer' +import { completeAppConfig } from '../fixtures/app-config' +import { featureWithName } from '../fixtures/features' + +jest.mock('fs') +jest.mock('util') +jest.mock('../../src/utils/printer') describe('Generate', () => { + const CURRENT_DIR: string = process.cwd() + const OUTPUT_DIR = 'outputDir' + + const mockFiles = files => { vol.fromJSON(files, CURRENT_DIR) } + + beforeEach(() => { + (Printer as jest.Mock).mockClear() + vol.reset() + }) + + afterAll(() => { + require('fs').writeFile.mockRestore() + }) + + describe('with a complete app config', () => { + beforeEach(async () => { + mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) + vol.mkdirSync(OUTPUT_DIR) + + const features: string = JSON.stringify(featureWithName('Login de usuário')) + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) + + it('should save an html file', () => { + expect(require('fs').writeFile).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html`, expect.anything()) + expect(memfs.existsSync(`${OUTPUT_DIR}/login_de_usuario.html`)).toBe(true) + }) + + it('should list the generated file in the console', () => { + const printGeneratedeFiles = (Printer as jest.Mock).mock.instances[0].printGeneratedFiles + expect(printGeneratedeFiles).toBeCalledWith(['outputDir/login_de_usuario.html']) + }) + }) + + describe('without features', () => { + beforeEach(async () => { + mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) + vol.mkdirSync(OUTPUT_DIR) + + const features = '{ "features": [] }' + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) + + it('should show an error message', async () => { + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('No features found')) + }) + + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) + + describe('without the outputDir flag', () => { + beforeEach(async () => { + mockFiles({ 'concordialang-ui-html.json': completeAppConfig }) + + const features: string = JSON.stringify(featureWithName('Login de usuário')) + await Generate.run(['--features', features]) + }) + + it('should show an error message', async () => { + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('Missing required flag')) + }) + + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) + + describe('without an app config', () => { + beforeEach(async () => { + const features: string = JSON.stringify(featureWithName('Login de usuário')) + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) - it('should print a JSON content', async () => { - let spy = jest.spyOn(process.stdout, 'write'); - await Generate.run([]) // TODO: pass a parameter - // expect(spy).toBeCalledWith({}) - expect(spy).not.toBeCalled() // TODO: change this assertion - }) + it('should show an error message', async () => { + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('Config file not found')) + }) -}) \ No newline at end of file + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) +}) diff --git a/test/fixtures/app-config.ts b/test/fixtures/app-config.ts new file mode 100644 index 0000000..64f1a97 --- /dev/null +++ b/test/fixtures/app-config.ts @@ -0,0 +1,26 @@ +export const completeAppConfigObject = { + widgets: { + input: { + widget: { + opening: '' + }, + wrapper: { + opening: '
', + closure: '
' + } + }, + label: { + widget: { + opening: '' + } + }, + button: { + widget: { + opening: '' + } + } + } +} +export const completeAppConfig: string = JSON.stringify(completeAppConfigObject) diff --git a/test/fixtures/features.ts b/test/fixtures/features.ts new file mode 100644 index 0000000..6bf8bde --- /dev/null +++ b/test/fixtures/features.ts @@ -0,0 +1,32 @@ +export const featureWithName = (name) => ({ + features: [ + { + name, + position: 2, + uiElements: [ + { + name: "Nome de Usuário", + widget: "textbox", + position: 22, + props: { + id: "nome_usuario"} + }, + { + name: "Senha", + widget: "textbox", + position: 26, + props: { + id: "senha", + required: true + } + }, + { + name: "Entrar", + widget: "button", + position: 31, + props: {} + } + ] + } + ] +}) diff --git a/test/generator.spec.ts b/test/generator.spec.ts deleted file mode 100644 index d574429..0000000 --- a/test/generator.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Feature } from 'concordialang-ui-core' -import { minify } from 'html-minifier' -import { fs, vol } from 'memfs' -import { promisify } from 'util' - -import Generator from '../src/generator' - -describe('Generator', () => { - - const CURRENT_DIR: string = process.cwd() - - let generator: Generator | null - - beforeEach(() => { - vol.mkdirpSync(CURRENT_DIR) // Synchronize with the current fs structure - generator = new Generator(fs) // In-memory fs - }) - - afterEach(() => { - vol.reset() // Erase in-memory structure - generator = null - }) - - async function expectFeaturesToProduceHtml(features: Feature[], htmls: string[]): Promise { - if (! generator) { - generator = new Generator(fs) - } - const files: string[] = await generator.generate(features) - expect(files).toHaveLength(htmls.length) - // tslint:disable-next-line:forin - for (let i in files) { - await expectFileToHaveHtml(files[i], htmls[i]) - } - } - - async function expectFileToHaveHtml(filePath: string, html: string): Promise { - const expected: string = minify(html) - const readF = promisify(fs.readFile) - const content: string = await readF(filePath) - const produced: string = minify(content) - expect(produced).toEqual(expected) - } - - it('produces an HTML file from features', async () => { - const features: Feature[] = [ /* something here */ ] - const htmls: string[] = [ /* put the expected html here */]; - await expectFeaturesToProduceHtml(features, htmls) - }) - -}) diff --git a/test/html-ui-prototyper.spec.ts b/test/html-ui-prototyper.spec.ts new file mode 100644 index 0000000..698ae9a --- /dev/null +++ b/test/html-ui-prototyper.spec.ts @@ -0,0 +1,70 @@ +import { Feature } from 'concordialang-ui-core' +import { minify } from 'html-minifier' +import { fs, vol } from 'memfs' +import { promisify } from 'util' +const cosmiconfig = require('cosmiconfig') + +import HtmlUIPrototyper from '../src/html-ui-prototyper' + +jest.mock('cosmiconfig') + +describe('HtmlUIPrototyper', () => { + + const CURRENT_DIR: string = process.cwd() + let prototyper: HtmlUIPrototyper | null + const appConfig = { + widgets: { + input: {}, + label: {} + } + } + + beforeEach(() => { + vol.fromJSON({ + './concordialang-ui-html.json': JSON.stringify(appConfig) + }, CURRENT_DIR) + + const explorer = { + loadSync: () => ({ + config: vol.readFileSync(`${ CURRENT_DIR }/concordialang-ui-html.json`, 'utf8') + }) + } + cosmiconfig.mockReturnValue(explorer) + + prototyper = new HtmlUIPrototyper(fs, CURRENT_DIR) // In-memory fs + }) + + afterEach(() => { + vol.reset() // Erase in-memory structure + prototyper = null + }) + + async function expectFeaturesToProduceHtml(features: Feature[], htmls: string[]): Promise { + if (! prototyper) { + prototyper = new HtmlUIPrototyper(fs, CURRENT_DIR) + } + const files: string[] = await prototyper.generate(features) + expect(files).toHaveLength(htmls.length) + // tslint:disable-next-line:forin + for (let i in files) { + await expectFileToHaveHtml(files[i], htmls[i]) + } + } + + async function expectFileToHaveHtml(filePath: string, html: string): Promise { + const expected: string = minify(html) + const readF = promisify(fs.readFile) + const content: string = await readF(filePath) + const produced: string = minify(content) + expect(produced).toEqual(expected) + } + + // FIXME the content of app config file is a string. It should be an object. + // Maybe we will have to mock loadJson from cosmiconfig. + xit('produces an HTML file from features', async () => { + const features: Feature[] = [ { name: 'Test Feature', uiElements: [ { name: 'Name', widget: 'textbox', props: { id: 'name' }, position: 0 } ], position: 0 } ] + const htmls: string[] = [ /* put the expected html here */]; + await expectFeaturesToProduceHtml(features, htmls) + }) + +}) diff --git a/test/utils/format-properties.spec.ts b/test/utils/format-properties.spec.ts new file mode 100644 index 0000000..9c269a8 --- /dev/null +++ b/test/utils/format-properties.spec.ts @@ -0,0 +1,13 @@ +import { formatProperties } from '../../src/utils/prop' + +describe('formatProperties', () => { + const props = { + id: 'id', + name: 'name', + required: true + } + + it('produces a string with the properties', () => { + expect(formatProperties(props)).toEqual('id="id" name="name" required="true"') + }) +}) diff --git a/test/utils/printer.spec.ts b/test/utils/printer.spec.ts new file mode 100644 index 0000000..1e23600 --- /dev/null +++ b/test/utils/printer.spec.ts @@ -0,0 +1,37 @@ +import Printer from '../../src/utils/printer' + +describe('Printer', () => { + describe('printGeneratedFiles', () => { + let consoleOutputSpy + + const files: string[] = [ + 'file1.html', + 'file2.html', + 'file3.html' + ] + + beforeAll(() => { + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + new Printer().printGeneratedFiles(files) + }) + + it('should print all files', () => { + expect(consoleOutputSpy).toBeCalled + }) + }) + + describe('printErrorMessage', () => { + let consoleOutputSpy + + const message = 'Test error message' + + beforeAll(() => { + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + new Printer().printErrorMessage(message) + }) + + it('should print the error message', () => { + expect(consoleOutputSpy).toBeCalled + }) + }) +}) diff --git a/test/widgets/button.spec.ts b/test/widgets/button.spec.ts index d3c0d99..5e18367 100644 --- a/test/widgets/button.spec.ts +++ b/test/widgets/button.spec.ts @@ -1,20 +1,36 @@ -import { UiElement} from 'concordialang-ui-core' -import { Button } from '../../src/widgets/button' - +import { UiElement } from 'concordialang-ui-core' +import { WidgetConfig } from '../../src/interfaces/app-config' +import Button from '../../src/widgets/button' describe('Button', () => { - describe('renderToString', () => { - it('without properties', () => { - const b = new Button({}) - expect(b.renderToString()).toBe('') - }) + describe('renderToString', () => { + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Save', + widget: 'button', + position: 7, + props: { + id: 'save' + } + } + + widgetConfig = { + template: '{{&widget.opening}}{{value}}{{&widget.closure}}', + widget: { + opening: '' + } + } + }) - it('produces html from a button element', async () => { - const buttonUiElement: UiElement = { name: 'OK', widget: 'button', position: 30, props: {} } - const buttonWidget: Button = new Button(buttonUiElement.props, buttonUiElement.name) - const result = buttonWidget.renderToString() - expect(result).toEqual(``) - }) - }) + it('produces html from a button element', async () => { + const buttonWidget: Button = new Button(uiElement.props, uiElement.name, widgetConfig) + const result = buttonWidget.renderToString() + expect(result).toEqual('') + }) + }) }) diff --git a/test/widgets/checkbox.spec.ts b/test/widgets/checkbox.spec.ts index 84e1b1b..e28b75e 100644 --- a/test/widgets/checkbox.spec.ts +++ b/test/widgets/checkbox.spec.ts @@ -1,45 +1,61 @@ import { UiElement } from 'concordialang-ui-core' -import { Checkbox } from '../../src/widgets/checkbox' -describe('Checkbox', () => { +import { WidgetConfig } from '../../src/interfaces/app-config' +import Checkbox from '../../src/widgets/checkbox' +describe('Checkbox', () => { describe('renderToString', () => { - const defaultProps: UiElement = { - name: 'Web Developer', - widget: 'checkbox', - position: 16, - props: { - value: 'web_developer' - } - } + let uiElement: UiElement + let widgetConfig: WidgetConfig - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Checkbox(uiElement.props, uiElement.name) : - new Checkbox({}) - ) + beforeEach(() => { + uiElement = { + name: 'Web Developer Skills', + widget: 'checkbox', + position: 16, + props: { + value: ['html', 'css', 'javascript'] + } + } - it('without properties', () => { - const inputWidget: Checkbox = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) + widgetConfig = { + widget: { + opening: '', + onePerValue: true, + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } + } + }) - it('surrounds the input with a div', () => { - const inputWidget: Checkbox = subject() - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) + it('produces html from an input element with name', async () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) - it('produces html from an input element with name', async () => { - const inputWidget: Checkbox = subject(defaultProps) + it('produces a label for the input element', async () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Web Developer')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) - it('produces html from an input element without name', async () => { - const inputWidget: Checkbox = subject({ ...defaultProps, name: undefined }) + it('produces a wrapper for the input element', () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching('\n')) - }) + expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) + }) }) }) diff --git a/test/widgets/input.spec.ts b/test/widgets/input.spec.ts index 8096c03..040ab7b 100644 --- a/test/widgets/input.spec.ts +++ b/test/widgets/input.spec.ts @@ -1,54 +1,69 @@ import { UiElement } from 'concordialang-ui-core' -import { Input } from '../../src/widgets/input' + +import { WidgetConfig } from '../../src/interfaces/app-config' +import Input from '../../src/widgets/input' describe('Input', () => { + describe('renderToString', () => { + let uiElement: UiElement + let widgetConfig: WidgetConfig - describe('renderToString', () => { - const defaultProps: UiElement = { - name: 'Username', - widget: 'textbox', - position: 16, - props: { - id: 'username', - required: true, - maxlength: 20, - minlength: 10 + beforeEach(() => { + uiElement = { + name: 'Username', + widget: 'textbox', + position: 16, + props: { + id: 'username', + required: true, + maxlength: 20, + minlength: 10 + } } - } - - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Input(uiElement.props, uiElement.name) : - new Input({}) - ) - - it('without properties', () => { - const inputWidget: Input = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) - it('produces html from an input element with name', async () => { - const inputWidget: Input = subject(defaultProps) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) - }) + widgetConfig = { + widget: { + opening: '', + }, + wrapper: { + opening: '
', + closure: '
', + }, + label: { + opening: '' + } + } + }) - it('produces html from an input element without name', async () => { - const inputWidget: Input = subject({ ...defaultProps, name: undefined }) + it('produces html from an input element with name', async () => { + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) it('produces a label for the input element', async () => { - const inputWidget: Input = subject(defaultProps) + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) - it('surrounds the input with a div', () => { - const inputWidget: Input = subject() - const result = inputWidget.renderToString() + it('produces a wrapper for the input element', () => { + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) - }) + + describe('when the label is not defined', () => { + beforeEach(() => { + widgetConfig.label = undefined + }) + + it('does not produce a label for the input element', async () => { + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).not.toEqual(expect.stringContaining('label')) + }) + }) + }) }) diff --git a/test/widgets/prop.spec.ts b/test/widgets/prop.spec.ts deleted file mode 100644 index fccdacd..0000000 --- a/test/widgets/prop.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { formatProperties } from '../../src/widgets/prop' - -describe('formatProperties', () => { - it('creates a string with the valid properties', () => { - const props = { id: 'id', name: 'name', value: 'value' } - const validProperties = ['id', 'name'] - expect(formatProperties(props, validProperties)).toBe('id="id" name="name"') - }) -}) diff --git a/test/widgets/radio.spec.ts b/test/widgets/radio.spec.ts index fa9286e..63378b5 100644 --- a/test/widgets/radio.spec.ts +++ b/test/widgets/radio.spec.ts @@ -1,47 +1,61 @@ import { UiElement } from 'concordialang-ui-core' -import { Radio } from '../../src/widgets/radio' + +import { WidgetConfig } from '../../src/interfaces/app-config' +import Radio from '../../src/widgets/radio' describe('Radio', () => { describe('renderToString', () => { - const defaultProps: UiElement = { - name: 'Gender', - widget: 'radio', - position: 7, - props: { - id: 'gender', - value: ['Male', 'Female'] - } - } + let uiElement: UiElement + let widgetConfig: WidgetConfig - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Radio(uiElement.props, uiElement.name) : - new Radio({}) - ) + beforeEach(() => { + uiElement = { + name: 'Gender', + widget: 'radio', + position: 7, + props: { + id: 'gender', + value: ['Male', 'Female'] + } + } - it('without properties', () => { - const inputWidget: Radio = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) + widgetConfig = { + widget: { + opening: '', + onePerValue: true, + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } + } + }) - it('surrounds the input with a div', () => { - const inputWidget: Radio = subject() - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) + it('produces html from an radio element with name', async () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) - it('produces html from an input element with name', async () => { - const inputWidget: Radio = subject(defaultProps) + it('produces a label for the input element', async () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Male')) - expect(result).toEqual(expect.stringContaining('Female')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) - it('produces a label for the select element', async () => { - const inputWidget: Radio = subject(defaultProps) + it('produces a wrapper for the input element', () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) }) }) - diff --git a/test/widgets/select.spec.ts b/test/widgets/select.spec.ts index 637656c..c2a614d 100644 --- a/test/widgets/select.spec.ts +++ b/test/widgets/select.spec.ts @@ -1,53 +1,68 @@ import { UiElement } from 'concordialang-ui-core' -import { Select } from '../../src/widgets/select' + +import { WidgetConfig } from '../../src/interfaces/app-config' +import Select from '../../src/widgets/select' describe('Select', () => { describe('renderToString', () => { - const defaultProps: UiElement = { - name: 'Gender', - widget: 'select', - position: 7, - props: { - id: 'gender', - value: ['Male', 'Female'] + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Gender', + widget: 'select', + position: 7, + props: { + id: 'gender', + value: ['Male', 'Female'] + } + } + + widgetConfig = { + widget: { + opening: '', + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } } - } - - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Select(uiElement.props, uiElement.name) : - new Select({}) - ) - - it('without properties', () => { - const inputWidget: Select = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) - - it('surrounds the select with a div', () => { - const inputWidget: Select = subject() - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) - it('produces html from an select element with name', async () => { - const inputWidget: Select = subject(defaultProps) + it('produces html from an select element with name', async () => { + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) expect(result).toEqual(expect.stringContaining('')) - }) - - it('produces a label for the select element', async () => { - const inputWidget: Select = subject(defaultProps) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) }) it('produces the options for the select element', async () => { - const inputWidget: Select = subject(defaultProps) + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringContaining('')) expect(result).toEqual(expect.stringContaining('')) }) + + it('produces a label for the select element', async () => { + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('')) + }) + + it('produces a wrapper for the input element', () => { + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) + }) }) }) diff --git a/test/widgets/widget-factory.spec.ts b/test/widgets/widget-factory.spec.ts index 9354c1a..6f9ee03 100644 --- a/test/widgets/widget-factory.spec.ts +++ b/test/widgets/widget-factory.spec.ts @@ -1,54 +1,55 @@ import { UiElement } from 'concordialang-ui-core' -import WidgetFactory from '../../src/widgets/widget-factory' -import { Button } from '../../src/widgets/button'; -import { Input } from '../../src/widgets/input'; - +import Button from '../../src/widgets/button' +import Input from '../../src/widgets/input' +import WidgetFactory from '../../src/widgets/widget-factory' +import { completeAppConfigObject } from '../fixtures/app-config' describe('WidgetFactory', () => { - let widgetFactory: WidgetFactory = new WidgetFactory() - - describe('create', () => { - it('create button with valid properties', () => { - const buttonUiElement: UiElement = { - name: 'OK', - widget: 'button', - position: 30, - props: {} - } - - const buttonWidget = new Button(buttonUiElement.props, buttonUiElement.name) - - expect(widgetFactory.create(buttonUiElement)).toEqual(buttonWidget) - }) - - it('create input with valid properties', async () => { - const inputUiElement: UiElement = { - name: 'Username', - widget: 'textbox', - position: 16, - props: { - required: true, - maxlength: 20, - minlength: 10 - } - } - - const inputWidget = new Input(inputUiElement.props, inputUiElement.name) - - expect(widgetFactory.create(inputUiElement)).toEqual(inputWidget) - }) - - it('throw invalid widget error', async () => { - const inputUiElement: UiElement = { - widget: 'invalid', - position: 16, - props: {} - } - - expect(() => { - widgetFactory.create(inputUiElement) - }).toThrow(Error); - }) - }) + let widgetFactory: WidgetFactory = new WidgetFactory(completeAppConfigObject) + + describe('create', () => { + it('create button with valid properties', () => { + const buttonUiElement: UiElement = { + name: 'OK', + widget: 'button', + position: 30, + props: {} + } + const { button: buttonConfig } = completeAppConfigObject.widgets + const buttonWidget = new Button(buttonUiElement.props, buttonUiElement.name, buttonConfig) + + expect(widgetFactory.create(buttonUiElement)).toEqual(buttonWidget) + }) + + it('create input with valid properties', async () => { + const inputUiElement: UiElement = { + name: 'Username', + widget: 'textbox', + position: 16, + props: { + required: true, + maxlength: 20, + minlength: 10 + } + } + const { input: inputConfig } = completeAppConfigObject.widgets + const inputWidget = new Input(inputUiElement.props, inputUiElement.name, inputConfig) + + expect(widgetFactory.create(inputUiElement)).toEqual(inputWidget) + }) + + it('throw invalid widget error', async () => { + const inputUiElement: UiElement = { + widget: 'invalid', + name: '', + position: 16, + props: {} + } + + expect(() => { + widgetFactory.create(inputUiElement) + }).toThrow(Error) + }) + }) }) diff --git a/tslint.json b/tslint.json index cc0834d..d364f8f 100644 --- a/tslint.json +++ b/tslint.json @@ -1,3 +1,10 @@ { - "ter-indent": [true, 4] + "extends": "@oclif/tslint", + "rules": { + "indent": { + "options": ["tabs"] + }, + "ter-indent": [true, "tab"], + "object-curly-spacing": [true, "always"] + } }