├── src ├── boot │ └── .gitkeep ├── css │ ├── app.scss │ └── quasar.variables.scss ├── assets │ └── icon.png ├── App.vue ├── pages │ ├── Index.vue │ ├── FormTest.vue │ └── Error404.vue ├── components │ ├── Tab01.vue │ ├── Tabs01.vue │ ├── _compInfo.js │ ├── TabPanel01.vue │ ├── Icon01.vue │ ├── OptionGroup01.vue │ ├── Form01.vue │ ├── Card01.vue │ ├── Input01.vue │ ├── Expansion01.vue │ ├── ButtonToggle01.vue │ ├── Radio01.vue │ ├── Toggle01.vue │ ├── CheckBox01.vue │ ├── Slider01.vue │ ├── _componentMap01.js │ ├── TimePicker01.vue │ ├── DatePicker01.vue │ ├── Editor01.vue │ ├── Rating01.vue │ ├── Select01.vue │ ├── Range01.vue │ ├── TimeInput01.vue │ ├── DateInput01.vue │ ├── Uploader01.vue │ ├── DateTimeInput01.vue │ └── FormGenerator.vue ├── router │ ├── routes.js │ └── index.js ├── index.template.html ├── layouts │ └── MainLayout.vue └── data │ └── schema.js ├── public ├── favicon.ico └── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon-128x128.png ├── assets ├── screenshot.png └── DataDrivenUI-2.png ├── .eslintignore ├── babel.config.js ├── .editorconfig ├── .postcssrc.js ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── jsconfig.json ├── LICENSE ├── package.json ├── .eslintrc.js ├── quasar.conf.js ├── quasar.config.js └── README.md /src/boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // app global css in SCSS form 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/src/assets/icon.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/DataDrivenUI-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/assets/DataDrivenUI-2.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | /src-capacitor 4 | /src-cordova 5 | /.quasar 6 | /node_modules 7 | .eslintrc.js 8 | -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maceto2016/VueDataDrivenUI/HEAD/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | presets: [ 5 | '@quasar/babel-preset-app' 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/components/Tab01.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/components/Tabs01.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig", 6 | "johnsoncodehk.volar", 7 | "wayou.vscode-todo-highlight" 8 | ], 9 | "unwantedRecommendations": [ 10 | "octref.vetur", 11 | "hookyqr.beautify", 12 | "dbaeumer.jshint", 13 | "ms-vscode.vscode-typescript-tslint-plugin" 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.guides.bracketPairs": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": [ 7 | "source.fixAll.eslint" 8 | ], 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "vue" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/components/_compInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple mixin object that returns $props and $attrs in data field $$attrs 3 | */ 4 | export default { 5 | data() { 6 | return { 7 | $$attrs: {}, 8 | }; 9 | }, 10 | methods: { 11 | _attrs() { 12 | return { 13 | ...this.$props, 14 | ...this.$attrs, 15 | }; 16 | }, 17 | }, 18 | created() { 19 | this.$$attrs = this._attrs(); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/pages/FormTest.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/components/TabPanel01.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: "/", 4 | component: () => import("layouts/MainLayout.vue"), 5 | children: [ 6 | { path: "", component: () => import("pages/FormTest.vue") }, 7 | { path: "/form", component: () => import("pages/FormTest.vue") }, 8 | ], 9 | }, 10 | 11 | // Always leave this as last one, 12 | // but you can also remove it 13 | { 14 | path: "*", 15 | component: () => import("pages/Error404.vue"), 16 | }, 17 | ]; 18 | 19 | export default routes; 20 | -------------------------------------------------------------------------------- /src/components/Icon01.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | 35 | install.txt -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": [ 6 | "src/*" 7 | ], 8 | "app/*": [ 9 | "*" 10 | ], 11 | "components/*": [ 12 | "src/components/*" 13 | ], 14 | "layouts/*": [ 15 | "src/layouts/*" 16 | ], 17 | "pages/*": [ 18 | "src/pages/*" 19 | ], 20 | "assets/*": [ 21 | "src/assets/*" 22 | ], 23 | "boot/*": [ 24 | "src/boot/*" 25 | ], 26 | "vue$": [ 27 | "node_modules/vue/dist/vue.esm.js" 28 | ] 29 | } 30 | }, 31 | "exclude": [ 32 | "dist", 33 | ".quasar", 34 | "node_modules" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/components/OptionGroup01.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2; 16 | $secondary : #26A69A; 17 | $accent : #9C27B0; 18 | 19 | $dark : #1D1D1D; 20 | 21 | $positive : #21BA45; 22 | $negative : #C10015; 23 | $info : #31CCEC; 24 | $warning : #F2C037; 25 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import routes from './routes' 5 | 6 | Vue.use(VueRouter) 7 | 8 | /* 9 | * If not building with SSR mode, you can 10 | * directly export the Router instantiation; 11 | * 12 | * The function below can be async too; either use 13 | * async/await or return a Promise which resolves 14 | * with the Router instance. 15 | */ 16 | 17 | export default function (/* { store, ssrContext } */) { 18 | const Router = new VueRouter({ 19 | scrollBehavior: () => ({ x: 0, y: 0 }), 20 | routes, 21 | 22 | // Leave these as they are and change in quasar.conf.js instead! 23 | // quasar.conf.js -> build -> vueRouterMode 24 | // quasar.conf.js -> build -> publicPath 25 | mode: process.env.VUE_ROUTER_MODE, 26 | base: process.env.VUE_ROUTER_BASE 27 | }) 28 | 29 | return Router 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Form01.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /src/components/Card01.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 42 | -------------------------------------------------------------------------------- /src/components/Input01.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/Expansion01.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Marcelo Aceto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datadrivenui", 3 | "version": "0.0.1", 4 | "description": "Data driven UI APP", 5 | "productName": "Data driven UI APP", 6 | "author": "Marcelo Aceto ", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --ext .js,.vue ./", 10 | "test": "echo \"No test specified\" && exit 0" 11 | }, 12 | "dependencies": { 13 | "@quasar/extras": "^1.0.0", 14 | "clone-deep": "^4.0.1", 15 | "core-js": "^3.6.5", 16 | "quasar": "^1.0.0" 17 | }, 18 | "devDependencies": { 19 | "@quasar/app": "^2.0.0", 20 | "babel-eslint": "^10.0.1", 21 | "eslint": "^7.21.0", 22 | "eslint-config-prettier": "^8.1.0", 23 | "eslint-plugin-vue": "^7.7.0", 24 | "eslint-webpack-plugin": "^2.4.0" 25 | }, 26 | "browserslist": [ 27 | "last 10 Chrome versions", 28 | "last 10 Firefox versions", 29 | "last 4 Edge versions", 30 | "last 7 Safari versions", 31 | "last 8 Android versions", 32 | "last 8 ChromeAndroid versions", 33 | "last 8 FirefoxAndroid versions", 34 | "last 10 iOS versions", 35 | "last 5 Opera versions" 36 | ], 37 | "engines": { 38 | "node": ">= 10.18.1", 39 | "npm": ">= 6.13.4", 40 | "yarn": ">= 1.21.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /src/components/ButtonToggle01.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | -------------------------------------------------------------------------------- /src/components/Radio01.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 64 | -------------------------------------------------------------------------------- /src/components/Toggle01.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 64 | -------------------------------------------------------------------------------- /src/components/CheckBox01.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 64 | -------------------------------------------------------------------------------- /src/components/Slider01.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 68 | -------------------------------------------------------------------------------- /src/components/_componentMap01.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A mixin object that mantain a dictionary de components 3 | */ 4 | 5 | export default { 6 | data() { 7 | return { 8 | componentMap: {}, 9 | }; 10 | }, 11 | methods: { 12 | initComponentsMap() { 13 | this.componentMap = { 14 | // Group components 15 | card: () => import("./Card01"), 16 | tabs: () => import("./Tabs01"), 17 | tab: () => import("./Tab01"), 18 | tabpanel: () => import("./TabPanel01"), 19 | expansion: () => import("./Expansion01"), 20 | 21 | // Form component 22 | form: () => import("./Form01"), 23 | 24 | // From field components 25 | inputtext: () => import("./Input01"), 26 | inputdate: () => import("./DateInput01"), 27 | inputtime: () => import("./TimeInput01"), 28 | inputdatetime: () => import("./DateTimeInput01"), 29 | select: () => import("./Select01"), 30 | checkbox: () => import("./CheckBox01"), 31 | radio: () => import("./Radio01"), 32 | toggle: () => import("./Toggle01"), 33 | btntoggle: () => import("./ButtonToggle01"), 34 | optgroup: () => import("./OptionGroup01"), 35 | range: () => import("./Range01"), 36 | slider: () => import("./Slider01"), 37 | datepicker: () => import("./DatePicker01"), 38 | timepicker: () => import("./TimePicker01"), 39 | rating: () => import("./Rating01"), 40 | uploader: () => import("./Uploader01"), 41 | editor: () => import("./Editor01"), 42 | 43 | // Other 44 | icon: () => import("./Icon01"), 45 | }; 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/TimePicker01.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 77 | -------------------------------------------------------------------------------- /src/components/DatePicker01.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 76 | -------------------------------------------------------------------------------- /src/components/Editor01.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 85 | -------------------------------------------------------------------------------- /src/components/Rating01.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 85 | -------------------------------------------------------------------------------- /src/components/Select01.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 81 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 4 | // This option interrupts the configuration hierarchy at this file 5 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 6 | root: true, 7 | 8 | parserOptions: { 9 | parser: 'babel-eslint', 10 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 11 | sourceType: 'module' // Allows for the use of imports 12 | }, 13 | 14 | env: { 15 | browser: true 16 | }, 17 | 18 | // Rules order is important, please avoid shuffling them 19 | extends: [ 20 | // Base ESLint recommended rules 21 | // 'eslint:recommended', 22 | 23 | // Uncomment any of the lines below to choose desired strictness, 24 | // but leave only one uncommented! 25 | // See https://eslint.vuejs.org/rules/#available-rules (look for Vuejs 2 ones) 26 | 'plugin:vue/essential', // Priority A: Essential (Error Prevention) 27 | // 'plugin:vue/strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 28 | // 'plugin:vue/recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 29 | 30 | // https://github.com/prettier/eslint-config-prettier#installation 31 | // usage with Prettier, provided by 'eslint-config-prettier'. 32 | 'prettier' 33 | ], 34 | 35 | plugins: [ 36 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file 37 | // required to lint *.vue files 38 | 'vue', 39 | 40 | // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 41 | // Prettier has not been included as plugin to avoid performance impact 42 | // add it as an extension for your IDE 43 | 44 | ], 45 | 46 | globals: { 47 | ga: 'readonly', // Google Analytics 48 | cordova: 'readonly', 49 | __statics: 'readonly', 50 | process: 'readonly', 51 | Capacitor: 'readonly', 52 | chrome: 'readonly' 53 | }, 54 | 55 | // add your custom rules here 56 | rules: { 57 | 58 | 'prefer-promise-reject-errors': 'off', 59 | 'func-names': 'off', 60 | 61 | // allow debugger during development only 62 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Range01.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 92 | -------------------------------------------------------------------------------- /src/components/TimeInput01.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 89 | -------------------------------------------------------------------------------- /src/components/DateInput01.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 89 | -------------------------------------------------------------------------------- /src/components/Uploader01.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 119 | -------------------------------------------------------------------------------- /src/components/DateTimeInput01.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 118 | -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* 4 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 5 | * the ES6 features that are supported by your Node version. https://node.green/ 6 | */ 7 | 8 | // Configuration for your app 9 | // https://v1.quasar.dev/quasar-cli/quasar-conf-js 10 | 11 | const ESLintPlugin = require('eslint-webpack-plugin') 12 | 13 | module.exports = function (/* ctx */) { 14 | return { 15 | // https://v1.quasar.dev/quasar-cli/supporting-ts 16 | supportTS: false, 17 | 18 | // https://v1.quasar.dev/quasar-cli/prefetch-feature 19 | // preFetch: true, 20 | 21 | // app boot file (/src/boot) 22 | // --> boot files are part of "main.js" 23 | // https://v1.quasar.dev/quasar-cli/boot-files 24 | boot: [ 25 | 26 | 27 | ], 28 | 29 | // https://v1.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 30 | css: [ 31 | 'app.scss' 32 | ], 33 | 34 | // https://github.com/quasarframework/quasar/tree/dev/extras 35 | extras: [ 36 | // 'ionicons-v4', 37 | // 'mdi-v5', 38 | // 'fontawesome-v6', 39 | // 'eva-icons', 40 | // 'themify', 41 | // 'line-awesome', 42 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 43 | 44 | 'roboto-font', // optional, you are not bound to it 45 | 'material-icons', // optional, you are not bound to it 46 | ], 47 | 48 | // Full list of options: https://v1.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 49 | build: { 50 | vueRouterMode: 'hash', // available values: 'hash', 'history' 51 | 52 | // transpile: false, 53 | 54 | // Add dependencies for transpiling with Babel (Array of string/regex) 55 | // (from node_modules, which are by default not transpiled). 56 | // Applies only if "transpile" is set to true. 57 | // transpileDependencies: [], 58 | 59 | // rtl: false, // https://v1.quasar.dev/options/rtl-support 60 | // preloadChunks: true, 61 | // showProgress: false, 62 | // gzip: true, 63 | // analyze: true, 64 | 65 | // Options below are automatically set depending on the env, set them if you want to override 66 | // extractCSS: false, 67 | 68 | // https://v1.quasar.dev/quasar-cli/handling-webpack 69 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 70 | chainWebpack (chain) { 71 | chain.plugin('eslint-webpack-plugin') 72 | .use(ESLintPlugin, [{ extensions: [ 'js', 'vue' ] }]) 73 | } 74 | }, 75 | 76 | // Full list of options: https://v1.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 77 | devServer: { 78 | https: false, 79 | port: 8080, 80 | open: true // opens browser window automatically 81 | }, 82 | 83 | // https://v1.quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 84 | framework: { 85 | iconSet: 'material-icons', // Quasar icon set 86 | lang: 'en-us', // Quasar language pack 87 | config: {}, 88 | 89 | // Possible values for "importStrategy": 90 | // * 'auto' - (DEFAULT) Auto-import needed Quasar components & directives 91 | // * 'all' - Manually specify what to import 92 | importStrategy: 'auto', 93 | 94 | // For special cases outside of where "auto" importStrategy can have an impact 95 | // (like functional components as one of the examples), 96 | // you can manually specify Quasar components/directives to be available everywhere: 97 | // 98 | // components: [], 99 | // directives: [], 100 | 101 | // Quasar plugins 102 | plugins: [] 103 | }, 104 | 105 | // animations: 'all', // --- includes all animations 106 | // https://v1.quasar.dev/options/animations 107 | animations: [], 108 | 109 | // https://v1.quasar.dev/quasar-cli/developing-ssr/configuring-ssr 110 | ssr: { 111 | pwa: false 112 | }, 113 | 114 | // https://v1.quasar.dev/quasar-cli/developing-pwa/configuring-pwa 115 | pwa: { 116 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 117 | workboxOptions: {}, // only for GenerateSW 118 | manifest: { 119 | name: `Data driven UI APP`, 120 | short_name: `Data driven UI APP`, 121 | description: `Data driven UI APP`, 122 | display: 'standalone', 123 | orientation: 'portrait', 124 | background_color: '#ffffff', 125 | theme_color: '#027be3', 126 | icons: [ 127 | { 128 | src: 'icons/icon-128x128.png', 129 | sizes: '128x128', 130 | type: 'image/png' 131 | }, 132 | { 133 | src: 'icons/icon-192x192.png', 134 | sizes: '192x192', 135 | type: 'image/png' 136 | }, 137 | { 138 | src: 'icons/icon-256x256.png', 139 | sizes: '256x256', 140 | type: 'image/png' 141 | }, 142 | { 143 | src: 'icons/icon-384x384.png', 144 | sizes: '384x384', 145 | type: 'image/png' 146 | }, 147 | { 148 | src: 'icons/icon-512x512.png', 149 | sizes: '512x512', 150 | type: 'image/png' 151 | } 152 | ] 153 | } 154 | }, 155 | 156 | // Full list of options: https://v1.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 157 | cordova: { 158 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 159 | }, 160 | 161 | // Full list of options: https://v1.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 162 | capacitor: { 163 | hideSplashscreen: true 164 | }, 165 | 166 | // Full list of options: https://v1.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 167 | electron: { 168 | bundler: 'packager', // 'packager' or 'builder' 169 | 170 | packager: { 171 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 172 | 173 | // OS X / Mac App Store 174 | // appBundleId: '', 175 | // appCategoryType: '', 176 | // osxSign: '', 177 | // protocol: 'myapp://path', 178 | 179 | // Windows only 180 | // win32metadata: { ... } 181 | }, 182 | 183 | builder: { 184 | // https://www.electron.build/configuration/configuration 185 | 186 | appId: 'datadrivenui' 187 | }, 188 | 189 | // More info: https://v1.quasar.dev/quasar-cli/developing-electron-apps/node-integration 190 | nodeIntegration: true, 191 | 192 | extendWebpack (/* cfg */) { 193 | // do something with Electron main process Webpack cfg 194 | // chainWebpack also available besides this extendWebpack 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /quasar.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* 4 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 5 | * the ES6 features that are supported by your Node version. https://node.green/ 6 | */ 7 | 8 | // Configuration for your app 9 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js 10 | 11 | 12 | const ESLintPlugin = require('eslint-webpack-plugin') 13 | 14 | 15 | const { configure } = require('quasar/wrappers'); 16 | 17 | module.exports = configure(function (ctx) { 18 | return { 19 | // https://v2.quasar.dev/quasar-cli-webpack/supporting-ts 20 | supportTS: false, 21 | 22 | // https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature 23 | // preFetch: true, 24 | 25 | // app boot file (/src/boot) 26 | // --> boot files are part of "main.js" 27 | // https://v2.quasar.dev/quasar-cli-webpack/boot-files 28 | boot: [ 29 | 30 | 31 | ], 32 | 33 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css 34 | css: [ 35 | 'app.scss' 36 | ], 37 | 38 | // https://github.com/quasarframework/quasar/tree/dev/extras 39 | extras: [ 40 | // 'ionicons-v4', 41 | // 'mdi-v5', 42 | // 'fontawesome-v6', 43 | // 'eva-icons', 44 | // 'themify', 45 | // 'line-awesome', 46 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 47 | 48 | 'roboto-font', // optional, you are not bound to it 49 | 'material-icons', // optional, you are not bound to it 50 | ], 51 | 52 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build 53 | build: { 54 | vueRouterMode: 'hash', // available values: 'hash', 'history' 55 | 56 | // transpile: false, 57 | // publicPath: '/', 58 | 59 | // Add dependencies for transpiling with Babel (Array of string/regex) 60 | // (from node_modules, which are by default not transpiled). 61 | // Applies only if "transpile" is set to true. 62 | // transpileDependencies: [], 63 | 64 | // rtl: true, // https://quasar.dev/options/rtl-support 65 | // preloadChunks: true, 66 | // showProgress: false, 67 | // gzip: true, 68 | // analyze: true, 69 | 70 | // Options below are automatically set depending on the env, set them if you want to override 71 | // extractCSS: false, 72 | 73 | // https://v2.quasar.dev/quasar-cli-webpack/handling-webpack 74 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 75 | 76 | chainWebpack (chain) { 77 | chain.plugin('eslint-webpack-plugin') 78 | .use(ESLintPlugin, [{ extensions: [ 'js', 'vue' ] }]) 79 | } 80 | 81 | }, 82 | 83 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer 84 | devServer: { 85 | server: { 86 | type: 'http' 87 | }, 88 | port: 8080, 89 | open: true // opens browser window automatically 90 | }, 91 | 92 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework 93 | framework: { 94 | config: {}, 95 | 96 | // iconSet: 'material-icons', // Quasar icon set 97 | // lang: 'en-US', // Quasar language pack 98 | 99 | // For special cases outside of where the auto-import strategy can have an impact 100 | // (like functional components as one of the examples), 101 | // you can manually specify Quasar components/directives to be available everywhere: 102 | // 103 | // components: [], 104 | // directives: [], 105 | 106 | // Quasar plugins 107 | plugins: [] 108 | }, 109 | 110 | // animations: 'all', // --- includes all animations 111 | // https://quasar.dev/options/animations 112 | animations: [], 113 | 114 | // https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr 115 | ssr: { 116 | pwa: false, 117 | 118 | // manualStoreHydration: true, 119 | // manualPostHydrationTrigger: true, 120 | 121 | prodPort: 3000, // The default port that the production server should use 122 | // (gets superseded if process.env.PORT is specified at runtime) 123 | 124 | maxAge: 1000 * 60 * 60 * 24 * 30, 125 | // Tell browser when a file from the server should expire from cache (in ms) 126 | 127 | 128 | chainWebpackWebserver (chain) { 129 | chain.plugin('eslint-webpack-plugin') 130 | .use(ESLintPlugin, [{ extensions: [ 'js' ] }]) 131 | }, 132 | 133 | 134 | middlewares: [ 135 | ctx.prod ? 'compression' : '', 136 | 'render' // keep this as last one 137 | ] 138 | }, 139 | 140 | // https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa 141 | pwa: { 142 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 143 | workboxOptions: {}, // only for GenerateSW 144 | 145 | // for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts]) 146 | // if using workbox in InjectManifest mode 147 | 148 | chainWebpackCustomSW (chain) { 149 | chain.plugin('eslint-webpack-plugin') 150 | .use(ESLintPlugin, [{ extensions: [ 'js' ] }]) 151 | }, 152 | 153 | 154 | manifest: { 155 | name: `Vue Data Driven UI`, 156 | short_name: `Vue Data Driven UI`, 157 | description: `Vue Data Driven UI`, 158 | display: 'standalone', 159 | orientation: 'portrait', 160 | background_color: '#ffffff', 161 | theme_color: '#027be3', 162 | icons: [ 163 | { 164 | src: 'icons/icon-128x128.png', 165 | sizes: '128x128', 166 | type: 'image/png' 167 | }, 168 | { 169 | src: 'icons/icon-192x192.png', 170 | sizes: '192x192', 171 | type: 'image/png' 172 | }, 173 | { 174 | src: 'icons/icon-256x256.png', 175 | sizes: '256x256', 176 | type: 'image/png' 177 | }, 178 | { 179 | src: 'icons/icon-384x384.png', 180 | sizes: '384x384', 181 | type: 'image/png' 182 | }, 183 | { 184 | src: 'icons/icon-512x512.png', 185 | sizes: '512x512', 186 | type: 'image/png' 187 | } 188 | ] 189 | } 190 | }, 191 | 192 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova 193 | cordova: { 194 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 195 | }, 196 | 197 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor 198 | capacitor: { 199 | hideSplashscreen: true 200 | }, 201 | 202 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron 203 | electron: { 204 | bundler: 'packager', // 'packager' or 'builder' 205 | 206 | packager: { 207 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 208 | 209 | // OS X / Mac App Store 210 | // appBundleId: '', 211 | // appCategoryType: '', 212 | // osxSign: '', 213 | // protocol: 'myapp://path', 214 | 215 | // Windows only 216 | // win32metadata: { ... } 217 | }, 218 | 219 | builder: { 220 | // https://www.electron.build/configuration/configuration 221 | 222 | appId: 'article-vue-data-driven-ui' 223 | }, 224 | 225 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 226 | 227 | chainWebpackMain (chain) { 228 | chain.plugin('eslint-webpack-plugin') 229 | .use(ESLintPlugin, [{ extensions: [ 'js' ] }]) 230 | }, 231 | 232 | 233 | 234 | chainWebpackPreload (chain) { 235 | chain.plugin('eslint-webpack-plugin') 236 | .use(ESLintPlugin, [{ extensions: [ 'js' ] }]) 237 | }, 238 | 239 | } 240 | } 241 | }); 242 | -------------------------------------------------------------------------------- /src/data/schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /* 3 | * Group type: Only 'tab' is currently supported 4 | */ 5 | groupModel: "tab", 6 | /* 7 | * List od group itens 8 | */ 9 | groups: [ 10 | { 11 | /* 12 | * Main properties (name, label, icon) 13 | */ 14 | name: "Group 1", 15 | label: "Group 1", 16 | icon: "mail", 17 | 18 | /* 19 | * Control type specific properties 20 | */ 21 | flat: true, 22 | "expand-separator": true, 23 | 24 | /* 25 | * Field list: name, id and fieldType 26 | are the mais properties, the others are 27 | UI control specific properties. 28 | */ 29 | fields: [ 30 | { 31 | /* 32 | * Main field properties 33 | */ 34 | name: "id", 35 | id: "g1_id", 36 | fieldType: "inputtext", 37 | /* 38 | * Control type specific properties 39 | */ 40 | label: "id", 41 | dense: false, 42 | readonly: true, 43 | hidden: true, 44 | }, 45 | /* 46 | * Other fields definitions... 47 | */ 48 | { 49 | name: "name", 50 | id: "g1_name", 51 | fieldType: "inputtext", 52 | label: "Name", 53 | placeholder: "Name...", 54 | hint: "Inform the name...", 55 | dense: true, 56 | clearable: true, 57 | "clear-icon": "close", 58 | /* 59 | * Validation rules can be defined as in the example below 60 | */ 61 | rules: [ 62 | { 63 | params: ["val"], 64 | exp: '!!val || "Name is required!"', 65 | }, 66 | ], 67 | }, 68 | { 69 | name: "on", 70 | id: "g1_on", 71 | fieldType: "btntoggle", 72 | label: "On?", 73 | hint: "Report if ON or OFF...", 74 | dense: false, 75 | clearable: true, 76 | "stack-label": true, 77 | filled: false, 78 | options: [ 79 | { label: "On", value: "on" }, 80 | { label: "Off", value: "off" }, 81 | ], 82 | }, 83 | { 84 | name: "onoff", 85 | id: "g1_onoff", 86 | fieldType: "checkbox", 87 | "outer-label": "On or Off?", 88 | label: "On/Off", 89 | hint: "Report if ON or OFF...", 90 | "indeterminate-value": null, 91 | "true-value": "on", 92 | "false-value": "off", 93 | dense: false, 94 | clearable: true, 95 | "stack-label": true, 96 | filled: false, 97 | }, 98 | { 99 | name: "alive", 100 | id: "g1_alive", 101 | fieldType: "radio", 102 | "outer-label": "Is alive?", 103 | label: "Alive", 104 | hint: "let me know if you're alive...", 105 | val: "alive", 106 | dense: false, 107 | clearable: true, 108 | "stack-label": true, 109 | filled: false, 110 | }, 111 | { 112 | name: "birthday", 113 | id: "g1_birthday", 114 | fieldType: "datepicker", 115 | label: "Birthday", 116 | hint: "enter your birthday...", 117 | mask: "YYYY-MM-DD", 118 | titleFormat: "ddd., DD [de] MMM.", 119 | dense: false, 120 | clearable: true, 121 | "stack-label": true, 122 | filled: false, 123 | }, 124 | { 125 | name: "time", 126 | id: "g1_time", 127 | fieldType: "timepicker", 128 | label: "Time", 129 | hint: "Inform the time...", 130 | format24h: true, 131 | dense: false, 132 | clearable: true, 133 | "stack-label": true, 134 | filled: false, 135 | }, 136 | { 137 | name: "date", 138 | id: "g1_date", 139 | fieldType: "inputdate", 140 | label: "Date", 141 | placeholder: "Date...", 142 | dateMask: "DD/MM/YYYY", 143 | mask: "##/##/####", 144 | hint: "Inform the date...", 145 | titleFormat: "ddd., DD [de] MMM.", 146 | dense: true, 147 | clearable: true, 148 | }, 149 | { 150 | name: "time2", 151 | id: "g1_time2", 152 | fieldType: "inputtime", 153 | label: "Time", 154 | placeholder: "Time...", 155 | timeMask: "HH:mm:ss", 156 | mask: "##:##:##", 157 | hint: "Inform the time...", 158 | format24h: true, 159 | withSeconds: true, 160 | dense: true, 161 | clearable: true, 162 | }, 163 | { 164 | name: "date_time", 165 | id: "g1_date_time", 166 | fieldType: "inputdatetime", 167 | label: "Date/Time", 168 | placeholder: "Date/Time...", 169 | dateMask: "DD/MM/YYYY HH:mm:ss", 170 | mask: "##/##/#### ##:##:##", 171 | hint: "Inform the date and time...", 172 | dateTitleFormat: "ddd., DD [de] MMM.", 173 | format24h: true, 174 | withSeconds: true, 175 | dense: true, 176 | clearable: true, 177 | }, 178 | { 179 | name: "options", 180 | id: "g1_options", 181 | fieldType: "select", 182 | label: "Options", 183 | hint: "Inform the option...", 184 | dense: true, 185 | clearable: true, 186 | transitionShow: "flip-up", 187 | transitionHide: "flip-down", 188 | options: ["Google", "Facebook", "Twitter", "Apple", "Oracle"], 189 | }, 190 | { 191 | name: "word", 192 | id: "g1_word", 193 | fieldType: "editor", 194 | label: "Editor", 195 | hint: "Spills the beans...", 196 | clearable: true, 197 | "stack-label": true, 198 | "min-height": "5rem", 199 | }, 200 | { 201 | name: "range", 202 | id: "g1_range", 203 | fieldType: "range", 204 | outerLabel: "Range", 205 | hint: "Inform the range...", 206 | clearable: true, 207 | "stack-label": true, 208 | min: 0, 209 | max: 50, 210 | label: true, 211 | }, 212 | { 213 | name: "track", 214 | id: "g1_track", 215 | fieldType: "slider", 216 | outerLabel: "Track", 217 | hint: "Drag...", 218 | clearable: true, 219 | "stack-label": true, 220 | min: 0, 221 | max: 50, 222 | step: 5, 223 | label: true, 224 | }, 225 | { 226 | name: "evaluate", 227 | id: "g1_evaluate", 228 | fieldType: "rating", 229 | label: "Rating", 230 | hint: "Do the evaluation...", 231 | clearable: true, 232 | "stack-label": true, 233 | max: 5, 234 | size: "2em", 235 | color: "primary", 236 | }, 237 | { 238 | name: "open_close", 239 | id: "g1_open_close", 240 | fieldType: "toggle", 241 | "outer-label": "Open?", 242 | label: "Open", 243 | hint: "Open or closed report...", 244 | dense: false, 245 | clearable: true, 246 | "stack-label": true, 247 | filled: false, 248 | color: "primary", 249 | "true-value": "on", 250 | "false-value": "off", 251 | }, 252 | { 253 | name: "files", 254 | id: "g1_files", 255 | fieldType: "uploader", 256 | "outer-label": "Send files", 257 | label: "Select the files", 258 | hint: "Select the files...", 259 | dense: false, 260 | clearable: true, 261 | multiple: true, 262 | "stack-label": true, 263 | }, 264 | ], 265 | }, 266 | { 267 | name: "Group 2", 268 | label: "Group 2", 269 | icon: "alarm", 270 | 271 | flat: true, 272 | "expand-separator": true, 273 | }, 274 | { 275 | name: "Group 3", 276 | label: "Group 3", 277 | icon: "movie", 278 | 279 | flat: true, 280 | "expand-separator": true, 281 | }, 282 | ], 283 | }; 284 | -------------------------------------------------------------------------------- /src/components/FormGenerator.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 480 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Driven Dynamic UI Generation with Vue.js and Quasar 2 | 3 | ![Data Driven UI](./assets/DataDrivenUI-2.png) 4 | 5 | ## Description 6 | 7 | In mid-March/2020 we started a first attempt at dynamic UI generation, based on a schema definitions in JSON (**Data Driven UI**) using the frameworks **Vue.js + Quasar**. 8 | 9 | The **Data Driven UI** concept allows interesting solutions such as: 10 | 11 | - Define UI model definition schema related to database tables and views that generates UI dynamically; 12 | - Create the UI model definition schema agnostic to technologies and frameworks (one can develop a generator for **Vue+Quasar**, another in **React+Material UI**, and so on). 13 | 14 | The idea was to link to the database schema API, an API that provides UI definitions for forms related to tables and views (entities) in the database. These UI definitions would be structured in JSON format and a client-side interpreter would generate the UI based on JSON information (at that time in **Vue.js 2.0 + Quasar framework 1.0**). 15 | 16 | The dynamically generated form would present a field definition schema for each corresponding entity field in the database with the type of edit control component (and other relevant properties) for the field. These controls would be rendered one below the other or within groups (tabs, cards, expansions, and so on). The scheme also provided lookup fields related to their dependencies on each other (_eg countries, states, cities_). The edit controls are based on the **_Quasar Framework's form controls_** with some tweaks such as the use of **_event bus_** for event communication and **_scoped slots_** for property communication between the form, edit controls and the wrapper component. Some complex component compositions using slots in the JSON schema were also implemented. A **_renderless wrapper component_** was also provided for interaction with the RESTful/GraphQL API to interact with the data of the corresponding entity / lookups in the database. 17 | 18 | For reasons of simplicity, most features were excluded from the original code to focus only on dynamic rendering of the main components, i.e. form, groups and edit controls (_which is the focus of this article_). We only kept the implementation of forms with the fields grouped in tabs. 19 | 20 | ## Pre-requisites 21 | 22 | We assume you have a good knowledge of **git cli**, **javascript**, **Vue.js** and **Quasar Framework**. You must have **Vue cli** and **quasar cli** installed on your system. This tutorial was run in a **_linux environment_**, but you would easily tweak this for your preferred operating system. 23 | 24 | ## The JSON schema structure 25 | 26 | The JSON structure is fairly simple. Define the groups and list of fields in each group item. 27 | 28 | However, defining field properties can be as complex as supported Quasar UI controls allow (_to find out which properties are supported, see the documentation for the corresponding **Quasar** control_). 29 | 30 | The field properties in the schema allow you to define validation rules on the value entered for the field, editing mask, many visual aspects and much more. 31 | 32 | The JSON structure is as follows: 33 | 34 | - **_groupModel: string_** => (Only 'tab' is currently supported); 35 | - **_groups: array_** => array of group itens: 36 | - Main group properties (**_name, label, icon_**); 37 | - Other optional group control type specific properties 38 | - **_fields: array_** => UI controls definition list for fields: 39 | - Main field properties (**_name, id, fieldType_**); 40 | - Other optional field control type specific properties. 41 | 42 | Below is an example of a JSON schema used in this article: 43 | 44 | ```javascript 45 | export default { 46 | /* 47 | * Group type: Only 'tab' is currently supported 48 | */ 49 | groupModel: "tab", 50 | /* 51 | * List of group itens 52 | */ 53 | groups: [ 54 | { 55 | /* 56 | * Main properties (name, label, icon) 57 | */ 58 | name: "Group 1", 59 | label: "Group 1", 60 | icon: "mail", 61 | 62 | /* 63 | * Control type specific properties 64 | */ 65 | flat: true, 66 | "expand-separator": true, 67 | 68 | /* 69 | * Field list: name, id and fieldType 70 | are the main properties, the others are 71 | UI control specific properties. 72 | */ 73 | fields: [ 74 | { 75 | /* 76 | * Main field properties 77 | */ 78 | name: "id", 79 | id: "g1_id", 80 | fieldType: "inputtext", 81 | /* 82 | * Control type specific properties 83 | */ 84 | label: "id", 85 | dense: false, 86 | readonly: true, 87 | hidden: true, 88 | }, 89 | /* 90 | * Other fields definitions... 91 | */ 92 | { 93 | name: "name", 94 | id: "g1_name", 95 | fieldType: "inputtext", 96 | label: "Name", 97 | placeholder: "Name...", 98 | hint: "Inform the name...", 99 | dense: true, 100 | clearable: true, 101 | "clear-icon": "close", 102 | /* 103 | * Validation rules can be defined as in the example below 104 | */ 105 | rules: [ 106 | { 107 | params: ["val"], 108 | exp: '!!val || "Name is required!"', 109 | }, 110 | ], 111 | }, 112 | { 113 | name: "on", 114 | id: "g1_on", 115 | fieldType: "btntoggle", 116 | label: "On?", 117 | hint: "Report if ON or OFF...", 118 | dense: false, 119 | clearable: true, 120 | "stack-label": true, 121 | filled: false, 122 | options: [ 123 | { label: "On", value: "on" }, 124 | { label: "Off", value: "off" }, 125 | ], 126 | }, 127 | { 128 | name: "onoff", 129 | id: "g1_onoff", 130 | fieldType: "checkbox", 131 | "outer-label": "On or Off?", 132 | label: "On/Off", 133 | hint: "Report if ON or OFF...", 134 | "indeterminate-value": null, 135 | "true-value": "on", 136 | "false-value": "off", 137 | dense: false, 138 | clearable: true, 139 | "stack-label": true, 140 | filled: false, 141 | }, 142 | { 143 | name: "alive", 144 | id: "g1_alive", 145 | fieldType: "radio", 146 | "outer-label": "Is alive?", 147 | label: "Alive", 148 | hint: "let me know if you're alive...", 149 | val: "alive", 150 | dense: false, 151 | clearable: true, 152 | "stack-label": true, 153 | filled: false, 154 | }, 155 | { 156 | name: "birthday", 157 | id: "g1_birthday", 158 | fieldType: "datepicker", 159 | label: "Birthday", 160 | hint: "enter your birthday...", 161 | mask: "YYYY-MM-DD", 162 | titleFormat: "ddd., DD [de] MMM.", 163 | dense: false, 164 | clearable: true, 165 | "stack-label": true, 166 | filled: false, 167 | }, 168 | { 169 | name: "time", 170 | id: "g1_time", 171 | fieldType: "timepicker", 172 | label: "Time", 173 | hint: "Inform the time...", 174 | format24h: true, 175 | dense: false, 176 | clearable: true, 177 | "stack-label": true, 178 | filled: false, 179 | }, 180 | { 181 | name: "date", 182 | id: "g1_date", 183 | fieldType: "inputdate", 184 | label: "Date", 185 | placeholder: "Date...", 186 | dateMask: "DD/MM/YYYY", 187 | mask: "##/##/####", 188 | hint: "Inform the date...", 189 | titleFormat: "ddd., DD [de] MMM.", 190 | dense: true, 191 | clearable: true, 192 | }, 193 | { 194 | name: "time2", 195 | id: "g1_time2", 196 | fieldType: "inputtime", 197 | label: "Time", 198 | placeholder: "Time...", 199 | timeMask: "HH:mm:ss", 200 | mask: "##:##:##", 201 | hint: "Inform the time...", 202 | format24h: true, 203 | withSeconds: true, 204 | dense: true, 205 | clearable: true, 206 | }, 207 | { 208 | name: "date_time", 209 | id: "g1_date_time", 210 | fieldType: "inputdatetime", 211 | label: "Date/Time", 212 | placeholder: "Date/Time...", 213 | dateMask: "DD/MM/YYYY HH:mm:ss", 214 | mask: "##/##/#### ##:##:##", 215 | hint: "Inform the date and time...", 216 | dateTitleFormat: "ddd., DD [de] MMM.", 217 | format24h: true, 218 | withSeconds: true, 219 | dense: true, 220 | clearable: true, 221 | }, 222 | { 223 | name: "options", 224 | id: "g1_options", 225 | fieldType: "select", 226 | label: "Options", 227 | hint: "Inform the option...", 228 | dense: true, 229 | clearable: true, 230 | transitionShow: "flip-up", 231 | transitionHide: "flip-down", 232 | options: ["Google", "Facebook", "Twitter", "Apple", "Oracle"], 233 | }, 234 | { 235 | name: "word", 236 | id: "g1_word", 237 | fieldType: "editor", 238 | label: "Editor", 239 | hint: "Spills the beans...", 240 | clearable: true, 241 | "stack-label": true, 242 | "min-height": "5rem", 243 | }, 244 | { 245 | name: "range", 246 | id: "g1_range", 247 | fieldType: "range", 248 | outerLabel: "Range", 249 | hint: "Inform the range...", 250 | clearable: true, 251 | "stack-label": true, 252 | min: 0, 253 | max: 50, 254 | label: true, 255 | }, 256 | { 257 | name: "track", 258 | id: "g1_track", 259 | fieldType: "slider", 260 | outerLabel: "Track", 261 | hint: "Drag...", 262 | clearable: true, 263 | "stack-label": true, 264 | min: 0, 265 | max: 50, 266 | step: 5, 267 | label: true, 268 | }, 269 | { 270 | name: "evaluate", 271 | id: "g1_evaluate", 272 | fieldType: "rating", 273 | label: "Rating", 274 | hint: "Do the evaluation...", 275 | clearable: true, 276 | "stack-label": true, 277 | max: 5, 278 | size: "2em", 279 | color: "primary", 280 | }, 281 | { 282 | name: "open_close", 283 | id: "g1_open_close", 284 | fieldType: "toggle", 285 | "outer-label": "Open?", 286 | label: "Open", 287 | hint: "Open or closed report...", 288 | dense: false, 289 | clearable: true, 290 | "stack-label": true, 291 | filled: false, 292 | color: "primary", 293 | "true-value": "on", 294 | "false-value": "off", 295 | }, 296 | { 297 | name: "files", 298 | id: "g1_files", 299 | fieldType: "uploader", 300 | "outer-label": "Send files", 301 | label: "Select the files", 302 | hint: "Select the files...", 303 | dense: false, 304 | clearable: true, 305 | multiple: true, 306 | "stack-label": true, 307 | }, 308 | ], 309 | }, 310 | { 311 | name: "Group 2", 312 | label: "Group 2", 313 | icon: "alarm", 314 | 315 | flat: true, 316 | "expand-separator": true, 317 | }, 318 | { 319 | name: "Group 3", 320 | label: "Group 3", 321 | icon: "movie", 322 | 323 | flat: true, 324 | "expand-separator": true, 325 | }, 326 | ], 327 | }; 328 | ``` 329 | 330 | ## How the magic happens 331 | 332 | ### The resources needed in the framework 333 | 334 | For the thing to work the framework would have to support the possibility to create components dynamically, conditionally and also support iteration over an array of definitions. Fortunately **Vue.js** is very good at these things! 335 | 336 | **Vue.js** suports [**Conditional Rendering - (v-if/v-else/v-else-if)**](https://vuejs.org/guide/essentials/conditional.html), and [**List Rendering - (v-for)**](https://vuejs.org/guide/essentials/list.html). These features allow you to iterate over the JSON schema and conditionally render the UI components. 337 | 338 | Conditional rerendering is ok for a few types of controls, but not the best option when you have a lot of them (_in this article we've defined about **20 different** types of form controls as bonus for you!_) 339 | 340 | For this type of challenge **Vue.js** supports [**dynamic component creation - (:is)**](https://v2.vuejs.org/v2/guide/components-dynamic-async.html). This feature allows you to reference dynamically imported component instance. 341 | 342 | Also remember the section above where we mentioned that each control type has its different set of properties. For the thing to work, **Vue.js** would need to allow linking all the properties of an object in batch. And once again Vue.js has the solution for this: [**Passing all properties of an Object - (v-bind)**](https://v2.vuejs.org/v2/guide/components-props.html#Passing-the-Properties-of-an-Object). 343 | 344 | In the section below we will see how all the features above will be used inside the `template` section of **FormGenerator.vue** 345 | to create a clean and concise solution to the problem. 346 | 347 | ### The component infrastructure 348 | 349 | The **_src/components_** folder has a series of source codes. Let's analyze them to understand how the whole thing was implemented: 350 | 351 | #### **\_compoenentMap01.js** 352 | 353 | This [**mixin object**](https://v2.vuejs.org/v2/guide/mixins.html?redirect=true) is injected into the **FormGenerator.vue**. Its function is to provide a data dictionary (**componentMap[]**) in which each component name resolves to a factory that dynamically imports and returns the component instance for that name: 354 | 355 | ```javascript 356 | /** 357 | * A mixin object that mantain a dictionary de components 358 | */ 359 | 360 | export default { 361 | data() { 362 | return { 363 | componentMap: {}, 364 | }; 365 | }, 366 | methods: { 367 | initComponentsMap() { 368 | this.componentMap = { 369 | // Group components 370 | card: () => import("./Card01"), 371 | tabs: () => import("./Tabs01"), 372 | tab: () => import("./Tab01"), 373 | tabpanel: () => import("./TabPanel01"), 374 | expansion: () => import("./Expansion01"), 375 | 376 | // Form component 377 | form: () => import("./Form01"), 378 | 379 | // From field components 380 | inputtext: () => import("./Input01"), 381 | inputdate: () => import("./DateInput01"), 382 | inputtime: () => import("./TimeInput01"), 383 | inputdatetime: () => import("./DateTimeInput01"), 384 | select: () => import("./Select01"), 385 | checkbox: () => import("./CheckBox01"), 386 | radio: () => import("./Radio01"), 387 | toggle: () => import("./Toggle01"), 388 | btntoggle: () => import("./ButtonToggle01"), 389 | optgroup: () => import("./OptionGroup01"), 390 | range: () => import("./Range01"), 391 | slider: () => import("./Slider01"), 392 | datepicker: () => import("./DatePicker01"), 393 | timepicker: () => import("./TimePicker01"), 394 | rating: () => import("./Rating01"), 395 | uploader: () => import("./Uploader01"), 396 | editor: () => import("./Editor01"), 397 | 398 | // Other 399 | icon: () => import("./Icon01"), 400 | }; 401 | }, 402 | }, 403 | }; 404 | ``` 405 | 406 | Afterwards the dictionary is used to create dynamic components in the `template` by their name as: 407 | 408 | ```html 409 | 410 | 411 | ``` 412 | 413 | #### **FormGenerator.vue** 414 | 415 | This one does the bulk of the work to dynamically assemble the UI based on the JSON schema. 416 | 417 | It has a series of functions for internal services, so let's focus on the part that really matters. 418 | 419 | - First it imports the componetMap so that it can be injected as a mixin and accessible in the template; 420 | - Create and provide an event bus to communicate with the component ecosystem; 421 | - Defines the property that will receive the JSON schema; 422 | - Defines the formData data to maintain the input field contents. 423 | 424 | ```javascript 425 | 426 | ... 427 | 428 | import componentMap from "./_componentMap01"; 429 | 430 | ... 431 | 432 | export default { 433 | name: "FormGenerator", 434 | 435 | mixins: [componentMap], 436 | 437 | provide() { 438 | return { 439 | // The event bus to comunicate with components 440 | eventBus: this.eventBus, 441 | }; 442 | }, 443 | props: { 444 | // The schema placeholder property 445 | schema: { 446 | type: Object, 447 | }, 448 | }, 449 | data() { 450 | return { 451 | // The event bus instance 452 | eventBus: new Vue(), 453 | ... 454 | // Form data with input field contents 455 | formData: {}, 456 | ... 457 | } 458 | } 459 | 460 | ... 461 | 462 | } 463 | ``` 464 | 465 | And finally the `template` that creates the dynamic components - the comments in the template clearly explain how the **Vue.js** features work together to make the thing work: 466 | 467 | ```html 468 | 539 | ``` 540 | 541 | #### **The other ".vue" files in /src/components** 542 | 543 | The other components basically encapsulate one or more of the original **Quasar Components** to deliver the desired functionality. They pass the events back to **FormGenerator.vue** via its `event bus` and receive event handlers and data from parent by means `v-on="$listners"` and `v-bind="$attrs"`. 544 | 545 | As an example we have the following source code from **input.vue**: 546 | 547 | ```javascript 548 | 568 | 569 | 592 | ``` 593 | 594 | ### How to use the FormGenerator 595 | 596 | Now comes the easy part, in `src/pages/FormTest.vue` we have the page that loads a JSON Schema and passes it to **FormGenerator** component - and that's all! 597 | 598 | ```javascript 599 | 602 | 603 | 619 | ``` 620 | 621 | By running the example with the command below: 622 | 623 | ```bash 624 | # Run the Quasar/Vue application 625 | $ yarn quasar dev 626 | ``` 627 | 628 | and then enter the following URL in your preferred browser: 629 | 630 | **_http://localhost:8080_** 631 | 632 | You get this impressive result: 633 | 634 | ![SREENSHOT](./assets/screenshot.png) 635 | 636 | ## Running the example from this tutorial 637 | 638 | ### Installation 639 | 640 | ```bash 641 | # Clone tutorial repository 642 | $ git clone https://github.com/maceto2016/VueDataDrivenUI 643 | 644 | # access the project folder through the terminal 645 | $ cd VueDataDrivenUI 646 | 647 | # Install dependencies 648 | $ npm install 649 | ``` 650 | 651 | ### Running the application (from NestJSDynLoad folder) 652 | 653 | ```bash 654 | # Run the Quasar/Vue application 655 | $ yarn quasar dev 656 | ``` 657 | 658 | ### Testing the application 659 | 660 | Enter the following URL in your preferred browser 661 | 662 | **_http://localhost:8080_** 663 | 664 | ## Conclusion 665 | 666 | In this article we present the concept of **Data Driven UI**, which is nothing more than the dynamic creation of a UI based on the information present in a definition data. The article demonstrated how easy it is to define a **JSON Schema** and create an infrastructure using the **Vue.js + Quasar frameworks** to dynamically create components. As a **bonus** we provide about **20 UI components** based on the **Quasar framework UI** components. 667 | 668 | Feel free to use the source code and ideas presented here. There is huge room for improvement including migration to **Vue.js 3, Quasar 2 and Typescript**. Now it's up to you! 669 | 670 | I thank you for reading. I would be happy to hear your feedback! 671 | --------------------------------------------------------------------------------