├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app-extension ├── .npmignore ├── README.md ├── package.json └── src │ ├── boot │ └── register.js │ └── index.js ├── breaking changes.md ├── jsconfig.json ├── package-lock.json └── ui ├── .npmignore ├── README.md ├── build ├── config.js ├── entry │ ├── index.common.js │ ├── index.esm.js │ └── index.umd.js ├── index.js ├── script.app-ext.js ├── script.clean.js ├── script.css.js ├── script.javascript.js ├── script.open-umd.js └── utils.js ├── dev ├── .editorconfig ├── .firebase │ └── hosting.ZGlzdC9zcGE.cache ├── .firebaserc ├── .gitignore ├── .postcssrc.js ├── README.md ├── babel.config.js ├── firebase.json ├── package-lock.json ├── package.json ├── quasar.conf.js ├── quasar.extensions.json └── src │ ├── App.vue │ ├── assets │ └── .gitkeep │ ├── boot │ ├── register.js │ └── vueComponents.js │ ├── components │ ├── .gitkeep │ ├── InfoBoxWrapper.vue │ ├── PrimaryColorPicker.vue │ ├── Snarkdown.vue │ ├── Template.vue │ └── showMoreWrapper.vue │ ├── css │ ├── app.sass │ ├── colors.sass │ ├── quasar.variables.sass │ └── typography.sass │ ├── helpers │ ├── conversion.js │ ├── pageUtils.js │ ├── parseCodeForPrint.js │ └── schemaBuilders.js │ ├── index.template.html │ ├── layouts │ └── MyLayout.vue │ ├── new-examples │ └── Basics.vue │ ├── pages │ ├── EasyFieldDemo.vue │ ├── EasyFormDemo.vue │ ├── Index.vue │ └── NewDemo.vue │ ├── router │ ├── index.js │ └── routes.js │ ├── schemas │ ├── examples │ │ ├── EfBtn.js │ │ ├── EfDiv.js │ │ ├── EfMiniForm.js │ │ ├── QInput.js │ │ ├── QSelectModel.js │ │ ├── actionButtons.js │ │ ├── advanced.js │ │ ├── basics.js │ │ ├── computedFields.js │ │ ├── computedFields2.js │ │ ├── computedFields3.js │ │ ├── evaluatedProps1.js │ │ ├── evaluatedProps2.js │ │ ├── evaluatedProps3.js │ │ ├── evaluatedProps4.js │ │ ├── events1.js │ │ ├── events2.js │ │ ├── index.js │ │ ├── nestedData.js │ │ ├── responsiveStyle.js │ │ ├── validation.js │ │ └── validation2.js │ └── pages │ │ ├── QSelectModel.js │ │ ├── actionButtons.js │ │ ├── advanced.js │ │ ├── basics.js │ │ ├── computedFields.js │ │ ├── evaluatedProps.js │ │ ├── events.js │ │ ├── index.js │ │ ├── nestedData.js │ │ ├── responsiveStyle.js │ │ └── validation.js │ └── statics │ ├── app-logo-128x128.png │ └── icons │ ├── apple-icon-120x120.png │ ├── apple-icon-152x152.png │ ├── apple-icon-167x167.png │ ├── apple-icon-180x180.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── icon-128x128.png │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── ms-icon-144x144.png │ └── safari-pinned-tab.svg ├── package-lock.json ├── package.json ├── src ├── components │ ├── EasyField.vue │ ├── EasyForm.vue │ ├── fields │ │ ├── EfBtn.vue │ │ ├── EfDiv.vue │ │ ├── EfMiniForm.vue │ │ └── sharedProps.js │ └── sharedProps.js ├── helpers │ ├── dateHelpers.js │ ├── flattenPerSchema.js │ ├── focusIfInputEl.js │ ├── parseFieldValue.js │ ├── validation.js │ └── validation.ts ├── index.js ├── index.sass ├── margin-padding.sass ├── meta │ ├── dependencyMap.js │ ├── lang.js │ ├── lang.ts │ └── quasarPropsJson.js └── types │ └── index.ts ├── test ├── flattenPerSchema.js ├── index.js └── parseFieldValue.js └── umd-test.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mesqueeb 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | dist 5 | yarn.lock 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "semi": false, 7 | "bracketSpacing": true, 8 | "quoteProps": "consistent" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luca Ban - Mesqueeb 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quasar Easy Forms 📮 2 | 3 | New version: 4 | 5 | # Blitzar released! 🎉 6 | 7 | Quasar Easy Forms & Tables was upgraded to become much more powerful! 8 | 9 | The new version is called **Blitzar** 10 | 11 | - see blog post here: https://lucaban.medium.com/better-faster-vue-forms-with-blitzar-a0d71258a3bb 12 | - see documentation here: https://blitzar.cycraft.co 13 | - see the upgrade guide here: https://github.com/CyCraft/blitzar/releases/tag/v0.0.14 14 | 15 | # Support 16 | 17 | If this helped you in any way, you can contribute to the package's long term survival by supporting me: 18 | 19 | ### [💜 Support my open-source work on GitHub](https://github.com/sponsors/mesqueeb) 20 | 21 | Be sure to check out my sponsor page, I have a lot of open-source packages that might help you! 22 | 23 | (GitHub currently **doubles your support**! So if you support me with $10/mo, I will $20 instead! They're alchemists 🦾😉) 24 | 25 | Thank you so much!!! 26 | 27 | # License 28 | MIT (c) Luca Ban - Mesqueeb 29 | -------------------------------------------------------------------------------- /app-extension/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | yarn.lock 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | .editorconfig 16 | .eslintignore 17 | .eslintrc.js 18 | -------------------------------------------------------------------------------- /app-extension/README.md: -------------------------------------------------------------------------------- 1 | Quasar Easy Forms 📮 2 | 3 | New version: 4 | 5 | # Blitzar released! 🎉 6 | 7 | Quasar Easy Forms & Tables was upgraded to become much more powerful! 8 | 9 | The new version is called **Blitzar** 10 | 11 | - see blog post here: https://lucaban.medium.com/better-faster-vue-forms-with-blitzar-a0d71258a3bb 12 | - see documentation here: https://blitzar.cycraft.co 13 | - see the upgrade guide here: https://github.com/CyCraft/blitzar/releases/tag/v0.0.14 14 | 15 | # Support 16 | 17 | If this helped you in any way, you can contribute to the package's long term survival by supporting me: 18 | 19 | ### [💜 Support my open-source work on GitHub](https://github.com/sponsors/mesqueeb) 20 | 21 | Be sure to check out my sponsor page, I have a lot of open-source packages that might help you! 22 | 23 | (GitHub currently **doubles your support**! So if you support me with $10/mo, I will $20 instead! They're alchemists 🦾😉) 24 | 25 | Thank you so much!!! 26 | 27 | # License 28 | MIT (c) Luca Ban - Mesqueeb 29 | -------------------------------------------------------------------------------- /app-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quasar-app-extension-easy-forms", 3 | "version": "2.4.2", 4 | "description": "Easily generate forms by only defining a \"schema\" object.", 5 | "author": "Luca Ban - Mesqueeb", 6 | "license": "MIT", 7 | "main": "src/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mesqueeb/quasar-ui-easy-forms" 11 | }, 12 | "bugs": "https://github.com/mesqueeb/quasar-ui-easy-forms/issues", 13 | "homepage": "https://quasar-easy-forms.web.app", 14 | "dependencies": { 15 | "quasar-ui-easy-forms": "^2.4.1" 16 | }, 17 | "engines": { 18 | "node": ">= 8.9.0", 19 | "npm": ">= 5.6.0", 20 | "yarn": ">= 1.6.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app-extension/src/boot/register.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VuePlugin from 'quasar-ui-easy-forms' 3 | 4 | Vue.use(VuePlugin) 5 | -------------------------------------------------------------------------------- /app-extension/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Quasar App Extension index/runner script 3 | * (runs on each dev/build) 4 | * 5 | * Docs: https://quasar.dev/app-extensions/development-guide/index-api 6 | * API: https://github.com/quasarframework/quasar/blob/master/app/lib/app-extension/IndexAPI.js 7 | */ 8 | 9 | function extendConf (conf) { 10 | // register our boot file 11 | conf.boot.push('~quasar-app-extension-easy-forms/src/boot/register.js') 12 | 13 | // make sure app extension files & ui package gets transpiled 14 | conf.build.transpileDependencies.push(/quasar-app-extension-easy-forms[\\/]src/) 15 | 16 | // make sure the stylesheet goes through webpack to avoid SSR issues 17 | conf.css.push('~quasar-ui-easy-forms/src/index.sass') 18 | } 19 | 20 | module.exports = function (api) { 21 | // Quasar compatibility check; you may need 22 | // hard dependencies, as in a minimum version of the "quasar" 23 | // package or a minimum version of "@quasar/app" CLI 24 | api.compatibleWith('quasar', '^1.1.1') 25 | api.compatibleWith('@quasar/app', '^1.1.0') 26 | 27 | // Uncomment the line below if you provide a JSON API for your component 28 | // api.registerDescribeApi('EasyForm', '~quasar-ui-easy-forms/src/component/EasyForm.json') 29 | 30 | // We extend /quasar.conf.js 31 | api.extendQuasarConf(extendConf) 32 | } 33 | -------------------------------------------------------------------------------- /breaking changes.md: -------------------------------------------------------------------------------- 1 | 2 | In version 1.0 I was able to greatly simplify easy-forms. There are no wrapper components needed for every field. You can directly use Quasar components or ANY component you create yourself, without requiring any setup! 3 | 4 | Here is a list of all breaking changes. 5 | 6 | # Breaking changes 7 | 8 | ## actionButtons 9 | 10 | Action buttons now default to an empty array. 11 | 12 | **why:** 13 | It's better to be explicited about the components that should be rendered. It's easier to understand that there will be buttons shown at the top of an EasyForm if you see that they are actually passed. 14 | 15 | It's good that there are buttons pre-made for us, but it's better that these are explicitly added. 16 | 17 | **how to update code:** 18 | Anywhere you have not defined action buttons you can pass the previous default ones if you like: `['archive', 'cancel', 'edit', 'save']` 19 | 20 | ## fieldType 21 | 22 | EasyForms now uses `component` instead of `fieldType`. 23 | The "component" field can be passed any component name (string) which is globally registered, OR you can pass a component options object directly. 24 | 25 | It's very powerful because now you can have EasyForms use _any_ Quasar component by eg. `component: 'QBtn'`. You can also define all your own custom components and use those, as long as you register them globally via `Vue.component()` 26 | 27 | **why:** 28 | EasyForms used to provide a wrapper component for every single component that was usable. This becomes very unwieldly very fast. I refactored the code to be able to provide plain Quasar components. 29 | 30 | I still export the original wrapper components I did via the field names with `Ef` in front. Eg. `fieldType: 'input'` can now be used via `component: 'EfInput'`. 31 | 32 | **how to update code:** 33 | 34 | 1. replace `fieldType: 'input'` with `component: 'EfInput'` 35 | 2. repeat for all field types 36 | 37 | ## mode & formMode 38 | 39 | EasyForm has a `mode` which can be 'view', 'edit', 'add' and 'raw'. 40 | EasyField used to receive this prop as `formMode` but now receives it as just `mode`. 41 | 42 | An individual field could have a different 'mode' by passing `readonly: true` or `rawValue: true`. 43 | Now when you want to set the 'mode' of a single field, you should just set the mode the same as EasyForms: `mode: 'raw'` 44 | 45 | Another change is that EasyForms used to have mode="view" by default, but now it defaults to mode="edit" 46 | 47 | **why:** 48 | Unified syntax. A single prop called "mode" can be set globally at the EasyForm or locally at EasyField. 49 | I went for "edit" instead of "view" by default because then fields are editable by default and thus closest to their intended state. I think this will cause less confusion. 50 | 51 | **how to update code:** 52 | 53 | 1. replace `formMode` (in evaluated props) with just `mode` 54 | 2. replace `rawValue: true` with `mode: 'raw'` 55 | 3. replace `readonly: true` with `mode: 'view'` 56 | 4. add `mode: 'view'` or `mode="view"` to EasyForms without mode set 57 | 58 | ## disable 59 | 60 | `disable` used to be ignored in the case `readonly` is true. 61 | `disable` now is not modified as such any more. 62 | 63 | **why**: 64 | To avoid magic things happening without a strong reasoning. It will make working with EasyForms more predictable and thus easier to reason about. 65 | 66 | **how to update code:** 67 | If you have fields you set to `disable: true` but want it to be `false` when `readonly: true` you can do: 68 | 69 | ```js 70 | // change 71 | { 72 | disable: true, 73 | } 74 | // to 75 | { 76 | disable: (val, {mode}) => mode !== 'view', 77 | } 78 | ``` 79 | 80 | ## evaluatedProps 81 | 82 | An EasyField used to execute any prop which was a function to allow for these "Evaluated Props". Now you need to be explicit about which props you want to use as "Evaluated Props". 83 | 84 | **why:** 85 | Because now any component can a "field" (EasyForm doesn't use its own field wrappers). We cannot anticipate which props of these fields can accept functions. 86 | 87 | **how to update code:** 88 | 89 | The new prop called "evaluatedProps" defaults to an array with some prop names by default. 90 | If your EasyForms only use Evaluated Props for the following props, you don't need to do anything: 91 | 92 | ```js 93 | ;[ 94 | 'component', 95 | 'showCondition', 96 | 'label', 97 | 'subLabel', 98 | 'required', 99 | 'rules', 100 | 'fieldStyle', 101 | 'fieldClasses', 102 | 'componentStyle', 103 | 'componentClasses', 104 | 'disable', 105 | 'events', 106 | 'lang', 107 | ] 108 | ``` 109 | 110 | If you have other props in your schemas that were "Evaluated Props" you will have to manually pass an array with _all_ props that you use as evaluated props: `evaluatedProps: ['myProp', 'subLabel']` etc. 111 | 112 | This can be defined on the EasyForm level or individually on the EasyField level. 113 | 114 | ## format 115 | 116 | The prop called `format` in which you could pass a function is now renamed to `parseValue`. 117 | This was renamed for clarity because it's the opposite from `parseInput`. Now there is `parseValue` and `parseInput`. Much better!! 118 | 119 | Just search & replace ;) 120 | 121 | ## innerClasses, innerStyle 122 | 123 | Both `innerClasses` and `innerStyle` are renamed to `componentClasses` & `componentStyle` to make it clear that these are applied directly to the component. Just search & replace ;) 124 | 125 | ## CSS 126 | 127 | The class `.easy-field__field` was renamed to `.easy-field__component`. Just search & replace ;) 128 | 129 | ## externalLabels 130 | 131 | The prop called `externalLabels` was `true` by default and meant that labels were shown in the EasyField and not the component. 132 | 133 | Now when passing `internalLabels: true` it will show the labels as part of the component. 134 | 135 | So instead of `externalLabels: false` you need to pass `internalLabels: true` instead. 136 | 137 | Also check out the new prop: `internalErrors` 138 | 139 | ## Changes to fields (check all!!) 140 | 141 | I removed most fields, because they were _just_ wrapper components without much additive value. Also, the focus of the library is now only on the form generation, not providing a bunch of fields. 142 | 143 | You should update your code to use Quasar components directly instead. See below: 144 | 145 | ## Fields I deleted: 146 | 147 | ### title / EfTitle: 148 | 149 | ```js 150 | // Before: 151 | { fieldType: 'title', label: 'my title' } 152 | // v1.0: 153 | { 154 | // don't specify any component 155 | label: 'my title', 156 | span: true, 157 | fieldClasses: 'easy-field--title', 158 | } 159 | ``` 160 | 161 | You can make title fields prettier via CSS. Eg. 162 | 163 | ```sass 164 | .easy-field--title .easy-field__label 165 | font-weight: 600 166 | ``` 167 | 168 | ### Others: 169 | 170 | ```js 171 | // Before: 172 | { fieldType: 'btnToggle' } 173 | { fieldType: 'img' } 174 | { fieldType: 'input' } 175 | { fieldType: 'markdown' } 176 | { fieldType: 'range' } 177 | { fieldType: 'select' } 178 | { fieldType: 'slider' } 179 | { fieldType: 'toggle' } 180 | 181 | // v1.0: 182 | { component: 'QBtnToggle', spread: true } 183 | { component: 'QImg' } 184 | { component: 'QInput' } 185 | { component: 'QMarkdown', noContainer: true, /* this is a bug fix */, noLineNumbers: true } 186 | { component: 'QRange' } 187 | { component: 'QSelect' } 188 | { component: 'QSlider' } 189 | { component: 'QToggle', default: false } 190 | ``` 191 | 192 | Some fields might have small differences in behaviour from my previous wrapper components. However, you can check the [source code of my wrapper components here](https://github.com/mesqueeb/quasar-ui-easy-forms/tree/a6b5c9626e6cf172a42f588a361e3b665e9a840f/ui/src/components/fields) and copy the wrapper components to your project if you want. 193 | 194 | No equivalents: 195 | 196 | ```js 197 | // Before: 198 | { fieldType: 'inputDate' } 199 | { fieldType: 'link' } 200 | { fieldType: 'pdf' } 201 | { fieldType: 'uploaderFirebase' } 202 | { fieldType: 'video' } 203 | 204 | // v1.0 205 | // these don't exist anymore. 206 | ``` 207 | 208 | You can copy my [old source code](https://github.com/mesqueeb/quasar-ui-easy-forms/tree/a6b5c9626e6cf172a42f588a361e3b665e9a840f/ui/src/components/fields). 209 | 210 | It's better to have these components defined locally anyway, then it's much more flexible when you need new functionality. 211 | 212 | ## Fields I kept as is: 213 | 214 | ```jsack 215 | // Before: 216 | { fieldType: 'btn' } 217 | { fieldType: 'div' } 218 | { fieldType: 'form' } 219 | // v1.0: 220 | { component: 'EfBtn' } // because this is used in the action-buttons 221 | { component: 'EfDiv' } 222 | { component: 'EfMiniForm' } 223 | ``` 224 | 225 | I updated the name to EfMiniForm to make it easier to understand that this is a mini form, not a full fledged EasyForm. 226 | 227 | ## formDataNested 228 | 229 | `formDataNested` was renamed to just `formData`. Just search & replace ;) 230 | 231 | 232 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | // This must be specified if "paths" is set 5 | "baseUrl": ".", 6 | // Relative to "baseUrl" 7 | "paths": { 8 | "ui/*": ["src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /ui/.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dev 3 | umd-test.html 4 | 5 | .DS_Store 6 | .thumbs.db 7 | yarn.lock 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | .editorconfig 20 | .eslintignore 21 | .eslintrc.js 22 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | Quasar Easy Forms 📮 2 | 3 | New version: 4 | 5 | # Blitzar released! 🎉 6 | 7 | Quasar Easy Forms & Tables was upgraded to become much more powerful! 8 | 9 | The new version is called **Blitzar** 10 | 11 | - see blog post here: https://lucaban.medium.com/better-faster-vue-forms-with-blitzar-a0d71258a3bb 12 | - see documentation here: https://blitzar.cycraft.co 13 | - see the upgrade guide here: https://github.com/CyCraft/blitzar/releases/tag/v0.0.14 14 | 15 | # Support 16 | 17 | If this helped you in any way, you can contribute to the package's long term survival by supporting me: 18 | 19 | ### [💜 Support my open-source work on GitHub](https://github.com/sponsors/mesqueeb) 20 | 21 | Be sure to check out my sponsor page, I have a lot of open-source packages that might help you! 22 | 23 | (GitHub currently **doubles your support**! So if you support me with $10/mo, I will $20 instead! They're alchemists 🦾😉) 24 | 25 | Thank you so much!!! 26 | 27 | # License 28 | MIT (c) Luca Ban - Mesqueeb 29 | -------------------------------------------------------------------------------- /ui/build/config.js: -------------------------------------------------------------------------------- 1 | const { name, author, version } = require('../package.json') 2 | const year = (new Date()).getFullYear() 3 | 4 | module.exports = { 5 | name, 6 | version, 7 | banner: 8 | '/*!\n' + 9 | ' * ' + name + ' v' + version + '\n' + 10 | ' * (c) ' + year + ' ' + author + '\n' + 11 | ' * Released under the MIT License.\n' + 12 | ' */\n' 13 | } 14 | -------------------------------------------------------------------------------- /ui/build/entry/index.common.js: -------------------------------------------------------------------------------- 1 | import Plugin from '../../src/index' 2 | 3 | export default Plugin 4 | -------------------------------------------------------------------------------- /ui/build/entry/index.esm.js: -------------------------------------------------------------------------------- 1 | import Plugin from '../../src/index' 2 | 3 | export default Plugin 4 | export * from '../../src/index' 5 | -------------------------------------------------------------------------------- /ui/build/entry/index.umd.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Plugin from '../../src/index' 3 | 4 | Vue.use(Plugin) 5 | 6 | export * from '../../src/index' 7 | -------------------------------------------------------------------------------- /ui/build/index.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production' 2 | 3 | const parallel = require('os').cpus().length > 1 4 | const runJob = parallel ? require('child_process').fork : require 5 | const { join } = require('path') 6 | const { createFolder } = require('./utils') 7 | const { green, blue } = require('chalk') 8 | 9 | console.log() 10 | 11 | require('./script.app-ext.js').syncAppExt() 12 | require('./script.clean.js') 13 | 14 | console.log(` 📦 Building ${green('v' + require('../package.json').version)}...${parallel ? blue(' [multi-threaded]') : ''}\n`) 15 | 16 | createFolder('dist') 17 | 18 | runJob(join(__dirname, './script.javascript')) 19 | runJob(join(__dirname, './script.css')) 20 | -------------------------------------------------------------------------------- /ui/build/script.app-ext.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | path = require('path'), 4 | root = path.resolve(__dirname, '../..'), 5 | resolvePath = file => path.resolve(root, file), 6 | { blue } = require('chalk') 7 | 8 | const writeJson = function (file, json) { 9 | return fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf-8') 10 | } 11 | 12 | module.exports.syncAppExt = function (both = true) { 13 | // make sure this project has an app-extension project 14 | const appExtDir = resolvePath('app-extension') 15 | if (!fs.existsSync(appExtDir)) { 16 | return 17 | } 18 | 19 | // make sure this project has an ui project 20 | const uiDir = resolvePath('ui') 21 | if (!fs.existsSync(uiDir)) { 22 | return 23 | } 24 | 25 | // get version and name from ui package.json 26 | const { name, version } = require(resolvePath(resolvePath('ui/package.json'))) 27 | 28 | // read app-ext package.json 29 | const appExtFile = resolvePath('app-extension/package.json') 30 | let appExtJson = require(appExtFile), 31 | finished = false 32 | 33 | // sync version numbers 34 | if (both === true) { 35 | appExtJson.version = version 36 | } 37 | 38 | // check dependencies 39 | if (appExtJson.dependencies !== void 0) { 40 | if (appExtJson.dependencies[name] !== void 0) { 41 | appExtJson.dependencies[name] = '^' + version 42 | finished = true 43 | } 44 | } 45 | // check devDependencies, if not finished 46 | if (finished === false && appExtJson.devDependencies !== void 0) { 47 | if (appExtJson.devDependencies[name] !== void 0) { 48 | appExtJson.devDependencies[name] = '^' + version 49 | finished = true 50 | } 51 | } 52 | 53 | if (finished === true) { 54 | writeJson(appExtFile, appExtJson) 55 | console.log(` ⭐️ App Extension version ${blue(appExtJson.name)} synced.\n`) 56 | return 57 | } 58 | 59 | console.error(` App Extension version and dependency NOT synced.\n`) 60 | } -------------------------------------------------------------------------------- /ui/build/script.clean.js: -------------------------------------------------------------------------------- 1 | var 2 | rimraf = require('rimraf'), 3 | path = require('path') 4 | 5 | rimraf.sync(path.resolve(__dirname, '../dist/*')) 6 | console.log(` 💥 Cleaned build artifacts.\n`) 7 | -------------------------------------------------------------------------------- /ui/build/script.css.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const sass = require('node-sass') 3 | const postcss = require('postcss') 4 | const cssnano = require('cssnano') 5 | const rtl = require('postcss-rtl') 6 | const autoprefixer = require('autoprefixer') 7 | 8 | const buildConf = require('./config') 9 | const buildUtils = require('./utils') 10 | 11 | const postCssCompiler = postcss([ autoprefixer ]) 12 | const postCssRtlCompiler = postcss([ rtl({}) ]) 13 | 14 | const nano = postcss([ 15 | cssnano({ 16 | preset: ['default', { 17 | mergeLonghand: false, 18 | convertValues: false, 19 | cssDeclarationSorter: false, 20 | reduceTransforms: false 21 | }] 22 | }) 23 | ]) 24 | 25 | Promise 26 | .all([ 27 | generate('src/index.sass', `dist/index`) 28 | ]) 29 | .catch(e => { 30 | console.error(e) 31 | process.exit(1) 32 | }) 33 | 34 | /** 35 | * Helpers 36 | */ 37 | 38 | function resolve (_path) { 39 | return path.resolve(__dirname, '..', _path) 40 | } 41 | 42 | function generate (src, dest) { 43 | src = resolve(src) 44 | dest = resolve(dest) 45 | 46 | return new Promise((resolve, reject) => { 47 | sass.render({ file: src, includePaths: ['node_modules'] }, (err, result) => { 48 | if (err) { 49 | reject(err) 50 | return 51 | } 52 | 53 | resolve(result.css) 54 | }) 55 | }) 56 | .then(code => buildConf.banner + code) 57 | .then(code => postCssCompiler.process(code, { from: void 0 })) 58 | .then(code => { 59 | code.warnings().forEach(warn => { 60 | console.warn(warn.toString()) 61 | }) 62 | return code.css 63 | }) 64 | .then(code => Promise.all([ 65 | generateUMD(dest, code), 66 | postCssRtlCompiler.process(code, { from: void 0 }) 67 | .then(code => generateUMD(dest, code.css, '.rtl')) 68 | ])) 69 | } 70 | 71 | function generateUMD (dest, code, ext = '') { 72 | return buildUtils.writeFile(`${dest}${ext}.css`, code, true) 73 | .then(code => nano.process(code, { from: void 0 })) 74 | .then(code => buildUtils.writeFile(`${dest}${ext}.min.css`, code.css, true)) 75 | } 76 | -------------------------------------------------------------------------------- /ui/build/script.javascript.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rollup = require('rollup') 3 | const uglify = require('uglify-es') 4 | const buble = require('@rollup/plugin-buble') 5 | const json = require('@rollup/plugin-json') 6 | const nodeResolve = require('@rollup/plugin-node-resolve') 7 | const VuePlugin = require('rollup-plugin-vue') 8 | const commonjs = require('@rollup/plugin-commonjs') 9 | 10 | const buildConf = require('./config') 11 | const buildUtils = require('./utils') 12 | 13 | // include all dependencies as external by default 14 | const pkg = require('../package.json') 15 | const external = Object.keys(pkg.dependencies || []) 16 | 17 | const rollupPlugins = [ 18 | commonjs(), 19 | nodeResolve({ 20 | extensions: ['.js'], 21 | preferBuiltins: false, 22 | }), 23 | VuePlugin(), 24 | json(), 25 | buble({ 26 | objectAssign: 'Object.assign', 27 | }), 28 | ] 29 | 30 | const builds = [ 31 | { 32 | rollup: { 33 | input: { 34 | input: resolve(`entry/index.esm.js`), 35 | }, 36 | output: { 37 | file: resolve(`../dist/index.esm.js`), 38 | format: 'es', 39 | }, 40 | }, 41 | build: { 42 | unminified: true, 43 | minified: false, 44 | }, 45 | }, 46 | { 47 | rollup: { 48 | input: { 49 | input: resolve(`entry/index.common.js`), 50 | }, 51 | output: { 52 | file: resolve(`../dist/index.common.js`), 53 | format: 'cjs', 54 | }, 55 | }, 56 | build: { 57 | unminified: true, 58 | minified: false, 59 | }, 60 | }, 61 | { 62 | rollup: { 63 | input: { 64 | input: resolve(`entry/index.umd.js`), 65 | }, 66 | output: { 67 | name: 'easyForms', 68 | file: resolve(`../dist/index.umd.js`), 69 | format: 'umd', 70 | }, 71 | }, 72 | build: { 73 | unminified: true, 74 | minified: true, 75 | minExt: true, 76 | }, 77 | }, 78 | ] 79 | 80 | build(builds) 81 | 82 | /** 83 | * Helpers 84 | */ 85 | 86 | function resolve (_path) { 87 | return path.resolve(__dirname, _path) 88 | } 89 | 90 | function build (builds) { 91 | return Promise.all(builds.map(genConfig).map(buildEntry)).catch(buildUtils.logError) 92 | } 93 | 94 | function genConfig (opts) { 95 | Object.assign(opts.rollup.input, { 96 | plugins: rollupPlugins, 97 | external: external.concat(['vue', 'quasar']), 98 | }) 99 | 100 | Object.assign(opts.rollup.output, { 101 | banner: buildConf.banner, 102 | globals: { vue: 'Vue', quasar: 'Quasar' }, 103 | }) 104 | 105 | return opts 106 | } 107 | 108 | function addExtension (filename, ext = 'min') { 109 | const insertionPoint = filename.lastIndexOf('.') 110 | return `${filename.slice(0, insertionPoint)}.${ext}${filename.slice(insertionPoint)}` 111 | } 112 | 113 | function buildEntry (config) { 114 | return rollup 115 | .rollup(config.rollup.input) 116 | .then(bundle => bundle.generate(config.rollup.output)) 117 | .then(({ output }) => { 118 | const code = 119 | config.rollup.output.format === 'umd' 120 | ? injectVueRequirement(output[0].code) 121 | : output[0].code 122 | 123 | return config.build.unminified ? buildUtils.writeFile(config.rollup.output.file, code) : code 124 | }) 125 | .then(code => { 126 | if (!config.build.minified) { 127 | return code 128 | } 129 | 130 | const minified = uglify.minify(code, { 131 | compress: { 132 | pure_funcs: ['makeMap'], 133 | }, 134 | }) 135 | 136 | if (minified.error) { 137 | return Promise.reject(minified.error) 138 | } 139 | 140 | return buildUtils.writeFile( 141 | config.build.minExt === true 142 | ? addExtension(config.rollup.output.file) 143 | : config.rollup.output.file, 144 | buildConf.banner + minified.code, 145 | true 146 | ) 147 | }) 148 | .catch(err => { 149 | console.error(err) 150 | process.exit(1) 151 | }) 152 | } 153 | 154 | function injectVueRequirement (code) { 155 | const index = code.indexOf(`Vue = Vue && Vue.hasOwnProperty('default') ? Vue['default'] : Vue`) 156 | 157 | if (index === -1) { 158 | return code 159 | } 160 | 161 | const checkMe = ` if (Vue === void 0) { 162 | console.error('[ Quasar ] Vue is required to run. Please add a script tag for it before loading Quasar.') 163 | return 164 | } 165 | ` 166 | 167 | return code.substring(0, index - 1) + checkMe + code.substring(index) 168 | } 169 | -------------------------------------------------------------------------------- /ui/build/script.open-umd.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const open = require('open') 3 | 4 | open( 5 | resolve(__dirname, '../umd-test.html') 6 | ) 7 | -------------------------------------------------------------------------------- /ui/build/utils.js: -------------------------------------------------------------------------------- 1 | const 2 | fs = require('fs'), 3 | path = require('path'), 4 | zlib = require('zlib'), 5 | { green, blue, red, cyan } = require('chalk'), 6 | kebabRegex = /[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g 7 | 8 | function getSize (code) { 9 | return (code.length / 1024).toFixed(2) + 'kb' 10 | } 11 | 12 | module.exports.createFolder = function (folder) { 13 | const dir = path.join(__dirname, '..', folder) 14 | if (!fs.existsSync(dir)) { 15 | fs.mkdirSync(dir) 16 | } 17 | } 18 | 19 | module.exports.writeFile = function (dest, code, zip) { 20 | const banner = dest.indexOf('.json') > -1 21 | ? red('[json]') 22 | : dest.indexOf('.js') > -1 23 | ? green('[js] ') 24 | : dest.indexOf('.ts') > -1 25 | ? cyan('[ts] ') 26 | : blue('[css] ') 27 | 28 | return new Promise((resolve, reject) => { 29 | function report (extra) { 30 | console.log(`${banner} ${path.relative(process.cwd(), dest).padEnd(41)} ${getSize(code).padStart(8)}${extra || ''}`) 31 | resolve(code) 32 | } 33 | 34 | fs.writeFile(dest, code, err => { 35 | if (err) return reject(err) 36 | if (zip) { 37 | zlib.gzip(code, (err, zipped) => { 38 | if (err) return reject(err) 39 | report(` (gzipped: ${getSize(zipped).padStart(8)})`) 40 | }) 41 | } 42 | else { 43 | report() 44 | } 45 | }) 46 | }) 47 | } 48 | 49 | module.exports.readFile = function (file) { 50 | return fs.readFileSync(file, 'utf-8') 51 | } 52 | 53 | module.exports.logError = function (err) { 54 | console.error('\n' + red('[Error]'), err) 55 | console.log() 56 | } 57 | -------------------------------------------------------------------------------- /ui/dev/.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 | -------------------------------------------------------------------------------- /ui/dev/.firebase/hosting.ZGlzdC9zcGE.cache: -------------------------------------------------------------------------------- 1 | css/7.0e433876.css,1590723944563,f1b651238a58fe290baec6c5e32f3bdb1943dd2bd582f02569231f7a757c7837 2 | index.html,1590723944572,ffd050dd3badbbe455ad420f803ed5382effb2d121497bcbf0caafdef19bc2b8 3 | css/2.e669d2e7.css,1590723944561,77044317b9fdae84324079dcac7871562a8aa7c3a2f99fa99c1dacd49f6a87ef 4 | css/6.38879e49.css,1590723944564,257eb8d7c8d659d1d4673d81c77d148bca4e9b16ef601f733c54dee0abeedb38 5 | css/8.21c4a94b.css,1590723944564,a9c14f6ec4080423405590ea65fde098242b961bd4a84710d133689b60f7dbb4 6 | css/chunk-common.01efd12b.css,1590723944559,3571323bd7aac4d1cce6318bb4f0ff22e12e88ede3b3ac79f093f3c228cb3c9e 7 | js/10.9143b4f6.js,1590723944565,c235376741fc18a371f9a6d68a30fa2b03c73ba5088838b1672bf6a167c6f011 8 | js/12.49befc1b.js,1590723944565,ee2021e3df6a1dcd147e013266e60fa68e22654b414bdc22fb0825cc99584ae6 9 | js/11.ba239275.js,1590723944565,14abd0e1dec2e22bfadc5f3a799010dfb91f7915f7f829d2397d3397c9ec1bab 10 | js/13.6aa78827.js,1590723944565,e7bc013a6f8acb0a4de3fe9d7a3e4eb10d896d20f10f9659b035806742574dbf 11 | js/14.f1e6e009.js,1590723944565,7757d5991a59b32f0cb6b6a51b84a63b8215b01e559c886112a36288d17d6d57 12 | js/15.74c59a59.js,1590723944565,cf12887d3d2fd755769f13b024a9a27d3b0bdf48fa6ba0c2f97dd5a167d31a2d 13 | js/16.1df5fbd5.js,1590723944566,ec7ab42a126240ea7c3bbe34d3070682b28226aa6a2fa86f4c40865a7a925027 14 | js/17.f36d6ce8.js,1590723944566,69a38b085a87e8cb07c518ff7792eddd8df56f389effe3d639e143d19a7e9118 15 | js/18.8e6448a5.js,1590723944566,6127a20507b47dca4be7ca4ea8693b02fd672b80946398b93d791f5580ae3c54 16 | js/19.eccff282.js,1590723944567,a0cce2d545d28b78eee797967bbfc84916a738dc11412103b0a00ed734c8a571 17 | js/20.ddb21d19.js,1590723944567,24946d2a8f10ea722271ceb0c94b61bd1611fe0ce3384bb619d38052a6af7545 18 | js/21.922a1af6.js,1590723944567,8a40beca8fdfe9942a71f733951dd82689a9d280758aed3fea17fd69c9133791 19 | js/22.da4317a8.js,1590723944567,e9c2305b3c0d55779991dfb2c9f44c7649a27cdfb32dcdcbb532301cebb37f35 20 | js/23.f24abdac.js,1590723944567,80cccd825c80cd4d61e9b6c6615ebd00e2bc8cb52509224aeb627241b912c745 21 | js/24.25657f20.js,1590723944567,c1cd275bdc262e5ea312c68e3a2faf1c62a7da3ad01855cd4f4ffd6d8aaef3a3 22 | js/25.8487808e.js,1590723944567,9779cb6651f9ea6662dc909b123b7cae139c178123c67fbcbe3cc4442b20267a 23 | js/2.94549b58.js,1590723944561,0c4026b76420a55630d5491207be0a0ef369f41d78aa1e030f5b428e1bb4766c 24 | js/26.f349d5b2.js,1590723944567,acc385ad992045e95d3e4eb4396c199bcfd4c6cbfebb21dd2dbdc8410ad90f41 25 | js/28.299cb9ee.js,1590723944568,4096769daeee6ceceecd52de087ea21a4f437f92bc1c162b0078916cbf944276 26 | js/27.9da6f3bd.js,1590723944568,3a3f3e57400c8b91aeabb267613faaa498dc587d9b7b9ac41c9846f79259eb28 27 | js/29.20ec51bf.js,1590723944568,7c86bbaad920d4f4ed5585a0a663f6789f7a27cf5f7f89fe5db06ebeed9be228 28 | js/3.72dfc66c.js,1590723944561,c333d3153d2831058b2eb5b49c88f6264c094558119ed43cb0e16021e925d9e5 29 | js/30.89164d26.js,1590723944568,d36cc8ef1157c3c3c819b5c7c666385e0abe2b09f48e5d2f129c695e155da80c 30 | js/31.524fc0da.js,1590723944569,e270ea7e0aa257edf9c6ee321f44590947afb25c5c7c516ac5459c7ac63a04b3 31 | js/33.b9ba0943.js,1590723944570,dd27312495a68c2c9b84f5744fde250401ec1b1a33246691c0042c12f48ca30b 32 | js/6.30de475d.js,1590723944564,9fd7563cbfd64397d66bed747fbf9216c8b6fef8ae2c7d8ba72d9b4f5ab85004 33 | js/32.2ec5c654.js,1590723944569,69c2a5867e404bc644e712de13ff9fcb2b519647f872db3872ef1327479f4e76 34 | js/7.5a6efbb0.js,1590723944564,49d39fd5004eac431744c87f3bf1b399677bf54b23bc27b84c584392cea5f4d2 35 | js/9.ac3a7338.js,1590723944565,651e3fbd0b6f4e3ff3a64861b150543a8b9c6a360ac5e22606b3a74664e04dc1 36 | js/8.c3204439.js,1590723944564,4ccc4118b746df9abc8a994bc30ced7584a0d540307430ecf7e985ec770b2d28 37 | js/runtime.88fc62d4.js,1590723944564,2299ad1e1963d648e8bd69e412729cd9fb5f537e06b4b852367cab3aecefd837 38 | statics/app-logo-128x128.png,1590723944569,3ba36b6fe48820fa565c588c7057d5b20de3777c1ac01299e7a9f090a558f175 39 | fonts/KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff,1590723944562,42c7afdcff7aea098db26a417a9bb7929eed6df8823461b05cea5e6185216248 40 | fonts/KFOlCnqEu92Fr1MmSU5fBBc-.865f928c.woff,1590723944558,92689fd58bd8ba23da284d871afeb0e05fce981a9a28464b0acbad2051f6545a 41 | fonts/KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff,1590723944561,0cc82ea4e2383c3b0d0f46b32471cee5e920f87d2a151d302635cb2ca8cb6e43 42 | fonts/KFOlCnqEu92Fr1MmYUtfBBc-.bac8362e.woff,1590723944561,f225f153b48f4208f17a7fcc4595d2832b5b29916c357c8f7979ba64831b9ad0 43 | fonts/KFOmCnqEu92Fr1Mu4mxM.49ae34d4.woff,1590723944561,68fb4aa339f56ddb03b15f12cae4f82cd06640dca3dcfe230b802593f36312e4 44 | js/app.d9abd9e4.js,1590723944563,48dbade60e51fec2562dddcb9ff53dc06ccd1be9c758af528642b97cfb50fda6 45 | fonts/KFOlCnqEu92Fr1MmWUlfBBc-.2267169e.woff,1590723944561,de91d90d88d21edee1be62c920126d606e0beac61e6ee2243c417490b107279e 46 | statics/icons/apple-icon-120x120.png,1590723944572,9e147671e0051a9fe6042d4a0797037f077100fcb121d4206f4543838ddf87eb 47 | js/chunk-common.850d6742.js,1590723944561,bff60b1114b70951d86d8f13ff0fc8dcc4da0d63b00c41f3da3b1689c57e3c6d 48 | fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.12a47ed5.woff2,1590723944561,a99652277b6479b04b3231319123ca269fe04f85cddac90b8dcbc142d4625d4d 49 | statics/icons/apple-icon-152x152.png,1590723944572,6dc20f0fb7da357654cb1a9312a74702e1e97ceb3056d67b5ad0a066e96c0ed8 50 | statics/icons/apple-icon-167x167.png,1590723944570,89d93abc376213db7d1663adc8bd595d76daccd4e2890e29f19152e33d2ee4d8 51 | statics/icons/apple-icon-180x180.png,1590723944570,43f646dadecb2dbabbe53a3e888aa3c377594f843167dd54001fa3a34a4cabaa 52 | statics/icons/favicon-16x16.png,1590723944571,9a9519211345ba591ae9ecd63082d20cdec901bd22da4ff5fa1c0c0aa45fdbee 53 | statics/icons/favicon-32x32.png,1590723944571,6b423caf88b215fc7f52e76642141dd78522862cc989114233eb50c77deb16d8 54 | statics/icons/favicon-96x96.png,1590723944570,aea2f51632b5e5a4d9bbb3fd010b438940bb568838f6cb186a4b9e33eb85e562 55 | statics/icons/favicon.ico,1590723944571,f4066f8f9d3212c5156547c3e2e2f607d1b17f1887f96d3a0bf59221e6eb519e 56 | statics/icons/icon-128x128.png,1590723944571,3ba36b6fe48820fa565c588c7057d5b20de3777c1ac01299e7a9f090a558f175 57 | statics/icons/icon-192x192.png,1590723944571,efe6efc75397aab84684918e7c45e24aadc2f8612789d6c91a6e7432d47724c2 58 | statics/icons/ms-icon-144x144.png,1590723944573,a1bee2aec520eea50583e3ace8461eb229b8bd5679c573cabd6c54a37b2286da 59 | statics/icons/safari-pinned-tab.svg,1590723944573,3356180d03fd5493efc6f27816d1d4e5f908ac300f5efb9f8044d9ef8823c4d1 60 | statics/icons/icon-256x256.png,1590723944571,fef8007201e545ef561f5fa50a12fa654c0fa9f03e9d998b6199727de8238c25 61 | fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.f2a09334.woff,1590723944561,64fc5686c655301234b304dfb176d6ca1d6fd56f9434abf380b3549ce0058b69 62 | css/app.82899d9d.css,1590723944562,4d21eaef64e9082fc91f8654c1373fa98da4e66866c5a2d0c6f0c91225b7472e 63 | statics/icons/icon-384x384.png,1590723944573,4e90fdacc7e781857ab93ece4b8e816b65ed69319f3abbe18367df2f47111049 64 | statics/icons/icon-512x512.png,1590723944573,83cb2337e81081fd5a948805b4fb550c68daa81346add2e8ea86f43d13f6ce9d 65 | js/vendor.07ad97b1.js,1590723944559,18a3167319fa88282a8b4870eb00fca71a03a5e5c777b685769692d471ece097 66 | -------------------------------------------------------------------------------- /ui/dev/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "quasar-easy-forms" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ui/dev/.gitignore: -------------------------------------------------------------------------------- 1 | .quasar 2 | .DS_Store 3 | .thumbs.db 4 | node_modules 5 | /dist 6 | /src-cordova/node_modules 7 | /src-cordova/platforms 8 | /src-cordova/plugins 9 | /src-cordova/www 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | -------------------------------------------------------------------------------- /ui/dev/.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 | -------------------------------------------------------------------------------- /ui/dev/README.md: -------------------------------------------------------------------------------- 1 | # Dev app (playground) 2 | 3 | Adding .vue files to src/pages/ will auto-add them to the Index page list. 4 | -------------------------------------------------------------------------------- /ui/dev/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@quasar/babel-preset-app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /ui/dev/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/spa", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "version": "1.0.0", 4 | "description": "A Quasar Framework app", 5 | "productName": "Quasar App", 6 | "cordovaId": "org.cordova.quasar.app", 7 | "private": true, 8 | "scripts": { 9 | "build": "quasar build", 10 | "deploy": "quasar build && firebase deploy", 11 | "dev": "quasar dev", 12 | "dev:ssr": "quasar dev -m ssr", 13 | "dev:ios": "quasar dev -m ios", 14 | "dev:android": "quasar dev -m android", 15 | "dev:electron": "quasar dev -m electron" 16 | }, 17 | "dependencies": { 18 | "@planetar/api-card": "^0.1.8", 19 | "@planetar/example-card": "^0.1.2", 20 | "@quasar/extras": "^1.8.1", 21 | "case-anything": "^0.3.1", 22 | "copy-anything": "^1.6.0", 23 | "is-what": "^3.8.0", 24 | "merge-anything": "^2.4.4", 25 | "quasar-ui-component-info-card": "0.0.7", 26 | "raw-loader": "^4.0.1", 27 | "snarkdown": "github:mesqueeb/snarkdown#dist", 28 | "sort-anything": "0.0.1" 29 | }, 30 | "devDependencies": { 31 | "@quasar/app": "^1.8.10" 32 | }, 33 | "engines": { 34 | "node": ">= 8.9.0", 35 | "npm": ">= 5.6.0", 36 | "yarn": ">= 1.6.0" 37 | }, 38 | "browserslist": [ 39 | "last 1 version, not dead, ie >= 11" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /ui/dev/quasar.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration for your app 2 | // https://quasar.dev/quasar-cli/quasar-conf-js 3 | 4 | const path = require('path') 5 | 6 | module.exports = function (ctx) { 7 | return { 8 | // app boot file (/src/boot) 9 | // --> boot files are part of "main.js" 10 | boot: ['vueComponents.js', 'register.js'], 11 | 12 | css: ['app.sass'], 13 | 14 | extras: [ 15 | // 'ionicons-v4', 16 | // 'mdi-v4', 17 | // 'fontawesome-v5', 18 | // 'eva-icons', 19 | // 'themify', 20 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 21 | 22 | 'roboto-font', // optional, you are not bound to it 23 | 'material-icons', // optional, you are not bound to it 24 | ], 25 | 26 | framework: { 27 | // iconSet: 'ionicons-v4', // Quasar icon set 28 | // lang: 'de', // Quasar language pack 29 | 30 | // Possible values for "all": 31 | // * 'auto' - Auto-import needed Quasar components & directives 32 | // (slightly higher compile time; next to minimum bundle size; most convenient) 33 | // * false - Manually specify what to import 34 | // (fastest compile time; minimum bundle size; most tedious) 35 | // * true - Import everything from Quasar 36 | // (not treeshaking Quasar; biggest bundle size; convenient) 37 | all: 'auto', 38 | 39 | components: [ 40 | 'QBtn', 41 | 'QIcon', 42 | 'QPopupProxy', 43 | 'QColor', 44 | 'QInput', 45 | 'QBtnToggle', 46 | 'QSlider', 47 | 'QSelect', 48 | 'QToggle', 49 | 'QOptionGroup', 50 | ], 51 | directives: ['ClosePopup'], 52 | 53 | // Quasar plugins 54 | plugins: ['Dialog', 'Notify'], 55 | }, 56 | 57 | supportIE: false, 58 | 59 | // animations: 'all', // --- includes all animations 60 | animations: [], 61 | 62 | build: { 63 | vueRouterMode: 'history', 64 | 65 | chainWebpack (chain) { 66 | chain.resolve.alias.merge({ 67 | ui: path.resolve(__dirname, '../src/index.js'), 68 | }) 69 | chain.module 70 | .rule('md') 71 | .test(/\.md$/i) 72 | .use('raw-loader') 73 | .loader('raw-loader') 74 | }, 75 | }, 76 | 77 | devServer: { 78 | // port: 8080, 79 | open: 'firefox', // opens browser window automatically 80 | }, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/dev/quasar.extensions.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /ui/dev/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /ui/dev/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/dev/src/boot/register.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VuePlugin from 'ui' // "ui" is aliased in quasar.conf.js 3 | 4 | Vue.use(VuePlugin) 5 | -------------------------------------------------------------------------------- /ui/dev/src/boot/vueComponents.js: -------------------------------------------------------------------------------- 1 | import { InfoCard } from 'quasar-ui-component-info-card' 2 | import InfoBoxWrapper from '../components/InfoBoxWrapper' 3 | import PrimaryColorPicker from '../components/PrimaryColorPicker' 4 | import Snarkdown from '../components/Snarkdown' 5 | 6 | export default ({ Vue }) => { 7 | Vue.component('InfoCard', InfoCard) 8 | Vue.component(InfoBoxWrapper.name, InfoBoxWrapper) 9 | Vue.component(PrimaryColorPicker.name, PrimaryColorPicker) 10 | Vue.component(Snarkdown.name, Snarkdown) 11 | } 12 | -------------------------------------------------------------------------------- /ui/dev/src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/components/.gitkeep -------------------------------------------------------------------------------- /ui/dev/src/components/InfoBoxWrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 53 | -------------------------------------------------------------------------------- /ui/dev/src/components/PrimaryColorPicker.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 42 | -------------------------------------------------------------------------------- /ui/dev/src/components/Snarkdown.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /ui/dev/src/components/Template.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /ui/dev/src/components/showMoreWrapper.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | 69 | 99 | -------------------------------------------------------------------------------- /ui/dev/src/css/app.sass: -------------------------------------------------------------------------------- 1 | @import 'colors' 2 | @import 'typography' 3 | 4 | // app global css in Sass form 5 | .text-wrap-all 6 | white-space: pre-line 7 | word-break: break-word 8 | -------------------------------------------------------------------------------- /ui/dev/src/css/colors.sass: -------------------------------------------------------------------------------- 1 | 2 | // todo, delete this 3 | $c-mercury: #EAE9E9 4 | 5 | /** 6 | * There is no pure black in this interface, instead there is the awesomeness that is Blue-Zodiac. 7 | * Use blue-zodiac instead of black, and use it’s opacity variants for text and icon colors. 8 | * / 9 | $c-blue-zodiac: #0E2348 10 | $c-primary: rgba($c-blue-zodiac, 0.96) 11 | $c-secondary: rgba($c-blue-zodiac, 0.72) 12 | $c-hint: rgba($c-blue-zodiac, 0.56) 13 | $c-disabled: rgba($c-blue-zodiac, 0.28) 14 | 15 | /** 16 | * Dandelion is always used to signify what is of primary importance to the user. It can be applied to any action that is considered primary, and there can be up to two elements of primary importance per view. (Never more than two.) 17 | */ 18 | $c-dandelion: #FFD465 19 | $c-dandelion-light: #FFE9B1 20 | $c-dandelion-dark: #E4A500 21 | 22 | /** 23 | * Breezy and beautiful, Sail is used to signify what is of secondary importance to the user. 24 | */ 25 | $c-sail: #C1D9FE 26 | $c-sail-dark: #76ABFD 27 | 28 | /** 29 | * Using Blue-Ribbon is powerful. It signifies to the user that they can take action on an item, whether that item is an icon or a text link. 30 | * Listed along-side Blue-Ribbon are the colors Azure-Skies and Endeavor. These two colors work along-side Blue-Ribbon in the interface, but are named with unique names due to their presence in the logo mark. 31 | */ 32 | $c-blue-ribbon: #0563ED 33 | $c-azure-skies: #2989FF 34 | $c-endeavour: #0152A4 35 | 36 | /** 37 | * The subtlety of Stone allows it to serve as background color, and as a tool to separate elements on the page. Use Stone for backgrounds, less important elements, and divider lines. 38 | */ 39 | $c-stone: #EFF3F9 40 | $c-stone-light: #F6F9FC 41 | $c-stone-dark: #D9E2F1 42 | 43 | /** 44 | * Use the Error color to signify to the user that they need to take a corrective action. 45 | */ 46 | $c-error: #E52703 47 | 48 | /** 49 | * Success is used to let the user know that an input was successful. 50 | */ 51 | $c-success: #07634D 52 | 53 | /** 54 | * Dark mode colours 55 | */ 56 | $c-lucy-black: #1a1d26 57 | 58 | $colors: ("mercury": $c-mercury, "blue-zodiac": $c-blue-zodiac, "primary": $c-primary, "secondary": $c-secondary, "hint": $c-hint, "disabled": $c-disabled, "dandelion": $c-dandelion, "dandelion-light": $c-dandelion-light, "dandelion-dark": $c-dandelion-dark, "sail": $c-sail, "sail-dark": $c-sail-dark, "blue-ribbon": $c-blue-ribbon, "azure-skies": $c-azure-skies, "endeavour": $c-endeavour, "stone": $c-stone, "stone-light": $c-stone-light, "stone-dark": $c-stone-dark, "error": $c-error, "success": $c-success) 59 | 60 | @each $name, $color in $colors 61 | .bg-#{$name} 62 | background: $color 63 | .c-#{$name} 64 | color: $color 65 | .text-#{$name} 66 | color: $color 67 | -------------------------------------------------------------------------------- /ui/dev/src/css/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | // Quasar Sass (& SCSS) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Stylus variables found in Quasar's source Stylus 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 : #9c81dc 16 | $secondary : #26A69A 17 | $accent : #9C27B0 18 | 19 | $positive : #21BA45 20 | $negative : #C10015 21 | $info : #31CCEC 22 | $warning : #F2C037 23 | 24 | $_space-base: 16px 25 | 26 | $xxs: ($_space-base * .1) 27 | $xs: ($_space-base * .25) 28 | $sm: ($_space-base * .5) 29 | $md: $_space-base 30 | $lg: ($_space-base * 1.5) 31 | $xl: ($_space-base * 2.3) 32 | $xxl: ($_space-base * 3) 33 | $xxxl: ($_space-base * 5) 34 | -------------------------------------------------------------------------------- /ui/dev/src/css/typography.sass: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans&family=Poppins:wght@400;500;600;700&display=swap") 2 | @import 'colors' 3 | 4 | =font-globals() 5 | font-family: 'Poppins', sans-serif 6 | color: $c-primary 7 | 8 | =t-h1() 9 | +font-globals 10 | font-weight: bold 11 | font-size: 56px 12 | line-height: 64px 13 | letter-spacing: 0.01em 14 | 15 | =t-h2() 16 | +font-globals 17 | font-weight: 600 18 | font-size: 40px 19 | line-height: 56px 20 | 21 | =t-h3() 22 | +font-globals 23 | font-weight: bold 24 | font-size: 36px 25 | line-height: 40px 26 | letter-spacing: 0.01em 27 | 28 | =t-h4() 29 | +font-globals 30 | font-weight: 600 31 | font-size: 32px 32 | line-height: 40px 33 | 34 | =t-h5() 35 | +font-globals 36 | font-weight: 600 37 | font-size: 24px 38 | line-height: 30px 39 | letter-spacing: 0.01em 40 | 41 | =t-h6() 42 | +font-globals 43 | font-weight: 600 44 | font-size: 20px 45 | line-height: 24px 46 | 47 | =t-subtitle1() 48 | +font-globals 49 | font-weight: 600 50 | font-size: 16px 51 | line-height: 20px 52 | 53 | =t-subtitle2() 54 | +font-globals 55 | font-weight: 500 56 | font-size: 14px 57 | line-height: 20px 58 | letter-spacing: 0.005em 59 | 60 | =t-body1() 61 | +font-globals 62 | font-family: 'Open Sans', sans-serif 63 | font-weight: normal 64 | font-size: 16px 65 | line-height: 24px 66 | 67 | =t-body2() 68 | +font-globals 69 | font-family: 'Open Sans', sans-serif 70 | font-weight: normal 71 | font-size: 14px 72 | line-height: 20px 73 | letter-spacing: 0.01em 74 | 75 | =t-button() 76 | +font-globals 77 | font-weight: 600 78 | font-size: 14px 79 | line-height: 24px 80 | letter-spacing: 0.01em 81 | 82 | =t-caption() 83 | +font-globals 84 | font-weight: normal 85 | font-size: 12px 86 | line-height: 16px 87 | letter-spacing: 0.02em 88 | 89 | =t-caption-semibold() 90 | +font-globals 91 | font-weight: 600 92 | font-size: 12px 93 | line-height: 16px 94 | letter-spacing: 0.02em 95 | 96 | =t-overline() 97 | +font-globals 98 | font-weight: 600 99 | font-size: 12px 100 | line-height: 16px 101 | letter-spacing: 0.08em 102 | text-transform: uppercase 103 | 104 | =t-micro() 105 | +font-globals 106 | font-weight: normal 107 | font-size: 10px 108 | line-height: 14px 109 | letter-spacing: 0.02em 110 | 111 | .t-h1, h1 112 | +t-h1 113 | .t-h2, h2 114 | +t-h2 115 | .t-h3, h3 116 | +t-h3 117 | .t-h4, h4 118 | +t-h4 119 | .t-h5, h5 120 | +t-h5 121 | .t-h6, h6 122 | +t-h6 123 | .t-subtitle1 124 | +t-subtitle1 125 | .t-subtitle2 126 | +t-subtitle2 127 | .t-body1 128 | +t-body1 129 | .t-body2 130 | +t-body2 131 | .t-button 132 | +t-button 133 | .t-caption 134 | +t-caption 135 | .t-caption-semibold 136 | +t-caption-semibold 137 | .t-overline 138 | +t-overline 139 | .t-micro 140 | +t-micro 141 | 142 | h1, h2, h3, h4, h5, h6 143 | margin-top: 17px 144 | margin-bottom: 17px 145 | -------------------------------------------------------------------------------- /ui/dev/src/helpers/conversion.js: -------------------------------------------------------------------------------- 1 | import { isFullString } from 'is-what' 2 | 3 | export function stringToJs (string) { 4 | if (!isFullString(string)) return undefined 5 | const jsonString = string.replace(/([a-zA-Z0-9]+?):/g, '"$1":').replace(/'/g, '"') 6 | let parsed 7 | try { 8 | parsed = JSON.parse(jsonString) 9 | } catch (e) { 10 | return string 11 | } 12 | return parsed 13 | } 14 | -------------------------------------------------------------------------------- /ui/dev/src/helpers/pageUtils.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'case-anything' 2 | 3 | export function copyToClipboard (text) { 4 | const camelText = camelCase(text) 5 | var textArea = document.createElement('textarea') 6 | textArea.className = 'fixed-top' 7 | textArea.value = camelText 8 | document.body.appendChild(textArea) 9 | textArea.focus() 10 | textArea.select() 11 | 12 | document.execCommand('copy') 13 | document.body.removeChild(textArea) 14 | } 15 | 16 | export function copyHeading (id) { 17 | const text = window.location.origin + window.location.pathname + '#' + id 18 | 19 | copyToClipboard(text) 20 | 21 | this.$q.notify({ 22 | message: 'Anchor has been copied to clipboard.', 23 | color: 'white', 24 | textColor: 'primary', 25 | position: 'top', 26 | actions: [{ icon: 'close', color: 'primary' }], 27 | timeout: 2000, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /ui/dev/src/helpers/parseCodeForPrint.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'is-what' 2 | 3 | export default function parseCodeForPrint (code) { 4 | // return early on 0, undefined, null, etc. 5 | if (!code) return code 6 | const stringifiedFns = [] 7 | function replacer (key, value) { 8 | if (isFunction(value) && value.prototype.stringifiedFn) { 9 | const fnString = value.prototype.stringifiedFn 10 | stringifiedFns.push(fnString) 11 | return fnString 12 | } 13 | return value 14 | } 15 | const string = JSON.stringify(code, replacer, 2) 16 | const cleanedString = string.replace(/'/g, `\\'`).replace(/"/g, `'`) 17 | const parsedString = stringifiedFns.reduce((str, fnString) => { 18 | const fnStringRegex = `'${fnString.replace(/'/g, `\\'`).replace(/"/g, `'`)}'` 19 | return str.replace(fnStringRegex, fnString) 20 | }, cleanedString) 21 | return parsedString 22 | } 23 | -------------------------------------------------------------------------------- /ui/dev/src/helpers/schemaBuilders.js: -------------------------------------------------------------------------------- 1 | import { isArray, isUndefined, isFunction } from 'is-what' 2 | import copy from 'copy-anything' 3 | import merge from 'merge-anything' 4 | import EasyForms from 'ui' 5 | import { pascalCase } from 'case-anything' 6 | import { stringToJs } from './conversion' 7 | const { dependencyMap } = EasyForms 8 | const { getPassedProps } = dependencyMap 9 | 10 | export function getRawComponent (selectedField) { 11 | return EasyForms[selectedField] || {} 12 | } 13 | 14 | export function getAllComponentProps (selectedField) { 15 | const component = getRawComponent(selectedField) || {} 16 | const componentProps = component.props || {} 17 | const EasyField = EasyForms['EasyField'] || {} 18 | const EasyFieldProps = EasyField.props || {} 19 | const inheritedComponentProps = getPassedProps(selectedField) || {} 20 | const result = copy(merge(inheritedComponentProps, EasyFieldProps, componentProps)) 21 | return result 22 | } 23 | 24 | export function propToPropSchema (propKey, propInfo) { 25 | const { desc, type, inheritedProp, examples, default: df, values, category } = propInfo 26 | // make the raw prop info from the components into an EasyForm: 27 | // whatever the prop is, default to an 'input' EasyField 28 | const events = {} 29 | let component = 'QInput' 30 | let subLabel = desc 31 | let options, standout, disable, parseInput, parseValue, autogrow, debounce, span, emitValue, _type 32 | let outlined = true 33 | let fieldClasses = [] 34 | let _default = df === true || undefined 35 | // If it has a default, write it in the description 36 | if (!isUndefined(df)) subLabel += `\n\nDefault: \`${isFunction(df) ? JSON.stringify(df()) : df}\`` 37 | // if the prop is a Boolean, show this as a 'toggle' EasyField 38 | if (type === Boolean || (isArray(type) && type.includes(Boolean))) { 39 | component = 'QToggle' 40 | _default = df === true 41 | } 42 | // if it's a Number field 43 | if (type === Number) { 44 | _type = 'number' 45 | parseInput = Number 46 | } 47 | // if the prop has a fixed set of possible values, show this as an 'option' EasyField 48 | const propHasValues = isArray(values) && values.length 49 | if (propHasValues) { 50 | component = 'QSelect' 51 | emitValue = true 52 | options = values.map(v => ({ label: v, value: v })) 53 | } 54 | // Create a special input for defining arrays and/or objects 55 | if ( 56 | type === Array || 57 | type === Object || 58 | type === Function || 59 | (isArray(type) && [Array, Object].some(t => type.includes(t)) && type.length === 2) 60 | ) { 61 | // events.blur = (e, val) => console.log(stringToJs(val)) 62 | outlined = false 63 | standout = true 64 | debounce = 500 65 | parseInput = stringToJs 66 | parseValue = JSON.stringify 67 | autogrow = true 68 | if (isArray(examples)) subLabel += `\nExamples: \`${examples.join('` | `')}\`` 69 | } 70 | // Don't allow editing props that accept functions. 71 | if (type === Function) disable = true 72 | // If it's the prop called 'schema', span the entire form, add extra info and don't return any input field 73 | if (propKey === 'schema') { 74 | component = '' 75 | span = true 76 | subLabel += 77 | '\n\n> 👀 Check「Source tab」→「Schema」to see the following code in color and with indentation.' 78 | } 79 | // Create the EasyField schema for the prop 80 | return { 81 | id: propKey, 82 | component, 83 | type: _type, 84 | // schema, 85 | label: propKey, 86 | subLabel, 87 | placeholder: !isArray(examples) ? '' : examples.join(', '), 88 | inheritedProp, 89 | options, 90 | outlined, 91 | standout, 92 | disable, 93 | parseInput, 94 | parseValue, 95 | autogrow, 96 | category, 97 | fieldClasses, 98 | debounce, 99 | events, 100 | span, 101 | emitValue, 102 | // if the prop is `true` by default, set to true 103 | default: _default, 104 | // defaults 105 | hasMarkdown: true, 106 | } 107 | } 108 | 109 | export function getInfoCardPropsSchema (selectedField) { 110 | const allComponentProps = 111 | selectedField === 'EasyForm' 112 | ? copy(EasyForms['EasyForm'].props) 113 | : getAllComponentProps(selectedField) 114 | return Object.entries(allComponentProps).reduce((carry, [propKey, propInfo]) => { 115 | // fields to not include in the InfoCard settings: 116 | if (propKey === 'component' || propKey === 'value') { 117 | return carry 118 | } 119 | carry[propKey] = propToPropSchema(propKey, propInfo) 120 | return carry 121 | }, {}) 122 | } 123 | -------------------------------------------------------------------------------- /ui/dev/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.productName %> 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /ui/dev/src/layouts/MyLayout.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /ui/dev/src/new-examples/Basics.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 54 | -------------------------------------------------------------------------------- /ui/dev/src/pages/EasyFieldDemo.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 45 | 46 | 129 | -------------------------------------------------------------------------------- /ui/dev/src/pages/EasyFormDemo.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 46 | 47 | 126 | -------------------------------------------------------------------------------- /ui/dev/src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 123 | 124 | 130 | -------------------------------------------------------------------------------- /ui/dev/src/pages/NewDemo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 30 | -------------------------------------------------------------------------------- /ui/dev/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 | 13 | export default function (/* { store, ssrContext } */) { 14 | const Router = new VueRouter({ 15 | scrollBehavior: () => ({ x: 0, y: 0 }), 16 | routes, 17 | 18 | // Leave these as is and change from quasar.conf.js instead! 19 | // quasar.conf.js -> build -> vueRouterMode 20 | // quasar.conf.js -> build -> publicPath 21 | mode: process.env.VUE_ROUTER_MODE, 22 | base: process.env.VUE_ROUTER_BASE, 23 | }) 24 | 25 | // we get each page from server first! 26 | if (process.env.MODE === 'ssr' && process.env.CLIENT) { 27 | console.log('!!!!') 28 | console.log( 29 | 'On route change we deliberately load page from server -- in order to test hydration errors' 30 | ) 31 | console.log('!!!!') 32 | 33 | let reload = false 34 | Router.beforeEach((to, _, next) => { 35 | if (reload) { 36 | window.location.href = to.fullPath 37 | return 38 | } 39 | reload = true 40 | next() 41 | }) 42 | } 43 | 44 | return Router 45 | } 46 | -------------------------------------------------------------------------------- /ui/dev/src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | component: () => import('layouts/MyLayout.vue'), 5 | children: [ 6 | { path: '', component: () => import('pages/Index.vue') }, 7 | { 8 | path: '/EasyField/:component', 9 | component: () => import('pages/EasyFieldDemo.vue'), 10 | props: true, 11 | }, 12 | { path: '/new-demo', component: () => import('pages/NewDemo.vue') }, 13 | { path: '/:schemaId', component: () => import('pages/EasyFormDemo.vue'), props: true }, 14 | ], 15 | }, 16 | ] 17 | 18 | export default routes 19 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/EfBtn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'btn', 3 | component: 'EfBtn', 4 | btnLabel: 'Touch me', 5 | events: { 6 | click: (val, context) => console.log('@CLICK\nval → ', val, '\ncontext → ', context), 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/EfDiv.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'myRank', 3 | component: 'EfDiv', 4 | options: [ 5 | { value: '1', label: 'one' }, 6 | { value: '2', label: 'two' }, 7 | ], 8 | prefix: 'Rank ', 9 | value: '1', 10 | } 11 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/EfMiniForm.js: -------------------------------------------------------------------------------- 1 | export default { 2 | component: 'EfMiniForm', 3 | label: 'Expenses', 4 | subLabel: "fill in this month's expenses", 5 | schema: [ 6 | { 7 | component: 'QBtn', 8 | events: { 9 | click: (e, { $attrs, $parent }) => { 10 | const { rowIndex } = $attrs 11 | const { value = [] } = $parent 12 | const valueCopy = [...value] 13 | valueCopy.splice(rowIndex, 1) 14 | $parent.$emit('input', valueCopy) 15 | }, 16 | }, 17 | disable: (val, { $attrs, $parent }) => { 18 | const { rowIndex } = $attrs 19 | const { value = [] } = $parent 20 | return rowIndex === value.length 21 | }, 22 | span: '26px', 23 | // component props: 24 | icon: 'remove_circle', 25 | color: 'negative', 26 | size: 'sm', 27 | flat: true, 28 | dense: true, 29 | }, 30 | { 31 | id: 'type', 32 | label: 'Type', 33 | component: 'QSelect', 34 | // component props: 35 | emitValue: true, 36 | mapOptions: true, 37 | options: [ 38 | { label: 'Home', value: 'home' }, 39 | { label: 'Work', value: 'work' }, 40 | ], 41 | }, 42 | { 43 | id: 'amount', 44 | label: 'Amount', 45 | component: 'QInput', 46 | // component props: 47 | type: 'number', 48 | parseInput: Number, 49 | prefix: '€', 50 | }, 51 | { 52 | id: 'paid for', 53 | label: 'Paid for', 54 | component: 'QToggle', 55 | default: false, 56 | span: 'auto', 57 | }, 58 | ], 59 | default: () => [], 60 | } 61 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/QInput.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'myInput', 3 | label: 'My EasyField label', 4 | subLabel: 5 | 'This is a preview for all the props each field in a schema can have. Check the "props" section for the detailed documentation on each possible prop.', 6 | component: 'QInput', 7 | } 8 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/QSelectModel.js: -------------------------------------------------------------------------------- 1 | const formatForMarkdown = code => `\`\`\`js 2 | ${JSON.stringify(code, null, 2)} 3 | \`\`\`` 4 | 5 | const createExample = (config, i) => [ 6 | Object.assign( 7 | { 8 | component: 'QSelect', 9 | options: [ 10 | { label: 'Self taught', value: 'self' }, 11 | { label: 'By item', value: 'item' }, 12 | { label: 'Mutation', value: 'mutation' }, 13 | ], 14 | id: `example${i}`, 15 | }, 16 | config 17 | ), 18 | { 19 | id: `example${i}-m`, 20 | component: 'Snarkdown', 21 | noLineNumbers: true, 22 | src: (val, { formData }) => formatForMarkdown(formData[`example${i}`]), 23 | evaluatedProps: ['src'], 24 | }, 25 | { 26 | id: `example${i}-o`, 27 | component: 'Snarkdown', 28 | noLineNumbers: true, 29 | fieldClass: 'text-break-all', 30 | src: formatForMarkdown(config), 31 | }, 32 | ] 33 | 34 | const exampleConfigs = [ 35 | { span: true, label: 'Single select' }, 36 | { component: 'QSelect' }, 37 | { component: 'QSelect', emitValue: true }, 38 | { component: 'QSelect', emitValue: true, mapOptions: true }, 39 | { span: true, subLabel: 'Multiple select' }, 40 | { component: 'QSelect', multiple: true }, 41 | { component: 'QSelect', multiple: true, emitValue: true }, 42 | { component: 'QSelect', multiple: true, emitValue: true, mapOptions: true }, 43 | { 44 | span: true, 45 | label: 'Do not do this', 46 | subLabel: 'Select something and see why 😉', 47 | }, 48 | { component: 'QSelect', multiple: true, emitValue: true }, 49 | ] 50 | 51 | export default { 52 | actionButtons: [], 53 | mode: 'edit', 54 | columnCount: 3, 55 | schema: [ 56 | { 57 | subLabel: 'Select some values for each QSelect field down below, and see what happens.', 58 | }, 59 | { 60 | span: 1, 61 | label: 'Model:', 62 | }, 63 | { 64 | span: 1, 65 | label: 'Options:', 66 | }, 67 | ...exampleConfigs.reduce((carry, config, i) => { 68 | if (config.span === true) { 69 | carry.push(config) 70 | return carry 71 | } 72 | carry.push(...createExample(config, i)) 73 | return carry 74 | }, []), 75 | ], 76 | } 77 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/actionButtons.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'view', 3 | actionButtons: [ 4 | 'delete', 5 | 'archive', 6 | 'cancel', 7 | 'edit', 8 | 'save', 9 | { 10 | component: 'EfBtn', 11 | btnLabel: 'log the data (check console)', 12 | push: true, 13 | events: { 14 | click: (event, { formData }) => console.log(formData), 15 | }, 16 | }, 17 | ], 18 | actionButtonDefaults: { 19 | archive: { icon: 'archive', showCondition: (_, { mode }) => mode !== 'edit' }, 20 | delete: { icon: 'delete', showCondition: (_, { mode }) => mode !== 'edit' }, 21 | edit: { icon: 'edit', flat: false, outline: true }, 22 | save: { icon: 'save' }, 23 | }, 24 | schema: [ 25 | { 26 | id: 'name', 27 | component: 'QInput', 28 | label: 'Superhero name', 29 | subLabel: 'Think of something cool.', 30 | }, 31 | { 32 | id: 'powerOrigin', 33 | component: 'QBtnToggle', 34 | label: 'Power origin', 35 | subLabel: 'Where does your power come from?', 36 | // component props: 37 | options: [ 38 | { label: 'Mutation', value: 'mutation' }, 39 | { label: 'Self taught', value: 'self' }, 40 | { label: 'By item', value: 'item' }, 41 | ], 42 | spread: true, 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/advanced.js: -------------------------------------------------------------------------------- 1 | export default { 2 | columnCount: 4, 3 | actionButtons: ['delete', 'archive', 'cancel', 'edit', 'save'], 4 | schema: [ 5 | { 6 | span: 3, 7 | }, 8 | { 9 | span: 1, 10 | label: 'Example component:', 11 | }, 12 | { 13 | id: 'name', 14 | span: 3, 15 | component: 'QInput', 16 | label: 'Superhero name', 17 | subLabel: 'Think of something cool.', 18 | required: true, 19 | }, 20 | { 21 | id: 'md-input', 22 | component: 'Snarkdown', 23 | span: 1, 24 | // component props: 25 | noLineNumbers: true, 26 | src: "```js\n{component: 'QInput'}\n```", 27 | }, 28 | { 29 | id: 'powerOrigin', 30 | span: 3, 31 | component: 'QBtnToggle', 32 | label: 'Power origin', 33 | subLabel: 'Where does your power come from?', 34 | // component props: 35 | spread: true, 36 | options: [ 37 | { label: 'Mutation', value: 'mutation' }, 38 | { label: 'Self taught', value: 'self' }, 39 | { label: 'By item', value: 'item' }, 40 | ], 41 | }, 42 | { 43 | id: 'md-btn-toggle', 44 | component: 'Snarkdown', 45 | span: 1, 46 | // component props: 47 | noLineNumbers: true, 48 | src: "```js\n{component: 'QBtnToggle'}\n```", 49 | }, 50 | { 51 | id: 'stamina', 52 | span: 3, 53 | component: 'QSlider', 54 | label: 'Stamina', 55 | default: 50, 56 | // component props: 57 | min: 0, 58 | max: 100, 59 | labelAlways: true, 60 | }, 61 | { 62 | id: 'md-slider', 63 | component: 'Snarkdown', 64 | span: 1, 65 | // component props: 66 | noLineNumbers: true, 67 | src: "```js\n{component: 'QSlider'}\n```", 68 | }, 69 | { 70 | id: 'power', 71 | span: 3, 72 | component: 'QInput', 73 | label: 'Power', 74 | subLabel: 'Fill in a number.', 75 | parseInput: Number, 76 | // component props: 77 | type: 'number', 78 | suffix: 'PW', 79 | }, 80 | { 81 | id: 'md-input-nr', 82 | component: 'Snarkdown', 83 | span: 1, 84 | // component props: 85 | noLineNumbers: true, 86 | src: 87 | "```js\n{component: 'QInput'}\n```\n↳ But the input is saved as a number instead of a string.", 88 | }, 89 | { 90 | id: 'roleModel', 91 | span: 3, 92 | component: 'QSelect', 93 | label: 'Role model', 94 | subLabel: 'Who do you look up to?', 95 | // component props: 96 | options: [ 97 | { label: 'Steve Rogers/Captain America', value: 'captain-america' }, 98 | { label: 'Tony Stark/Iron Man', value: 'iron-man' }, 99 | { label: 'Thor Odinson', value: 'thor-odinson' }, 100 | { label: 'Bruce Banner/The Incredible Hulk', value: 'the-incredible-hulk' }, 101 | { label: 'Natasha Romanoff/Black Widow', value: 'black-widow' }, 102 | { label: 'Clint Barton/Hawkeye', value: 'hawkeye' }, 103 | { label: 'Pietro Maximoff/Quicksilver', value: 'quicksilver' }, 104 | { label: 'Wanda Maximoff/Scarlet Witch', value: 'scarlet-witch' }, 105 | { label: 'The Vision', value: 'the-vision' }, 106 | { label: 'James Rhodes/War Machine (Iron Patriot)', value: 'war-machine' }, 107 | { label: 'Sam Wilson/Falcon', value: 'falcon' }, 108 | { label: 'Bucky Barnes/The Winter Soldier (White Wolf)', value: 'the-winter-soldier' }, 109 | { label: "T'Challa/Black Panther", value: 'black-panther' }, 110 | { label: 'Stephen Strange/Doctor Strange', value: 'doctor-strange' }, 111 | { label: 'Peter Parker/Spider-Man', value: 'spider-man' }, 112 | { label: 'Scott Lang/Ant-Man (Giant-Man)', value: 'ant-man' }, 113 | { label: 'Hope van Dyne/Wasp', value: 'wasp' }, 114 | { label: 'Carol Danvers/Captain Marvel', value: 'captain-marvel' }, 115 | { label: 'Peter Quill/Star-Lord', value: 'star-lord' }, 116 | { label: 'Gamora', value: 'gamora' }, 117 | { label: 'Drax the Destroyer', value: 'drax-the-destroyer' }, 118 | { label: 'Rocket (Raccoon)', value: 'rocket-raccoon' }, 119 | { label: '(Baby, Teenage) Groot', value: 'groot' }, 120 | { label: 'Mantis', value: 'mantis' }, 121 | { label: 'Matthew Murdock/Daredevil', value: 'daredevil' }, 122 | { label: 'Jessica Jones (Jewel)', value: 'jessica-jones' }, 123 | { label: 'Carl Lucas/Luke Cage (Power Man)', value: 'luke-cage' }, 124 | { label: 'Danny Rand/Iron Fist', value: 'iron-fist' }, 125 | { label: 'Frank Castle/The Punisher', value: 'the-punisher' }, 126 | ], 127 | }, 128 | { 129 | id: 'md-select', 130 | component: 'Snarkdown', 131 | span: 1, 132 | // component props: 133 | noLineNumbers: true, 134 | src: "```js\n{component: 'QSelect'}\n```", 135 | }, 136 | { 137 | id: 'checkboxes', 138 | span: 3, 139 | component: 'QOptionGroup', 140 | label: 'Check some boxes', 141 | default: () => [], // 'QOptionGroup' might not work without a default array 142 | // component props: 143 | type: 'checkbox', 144 | options: [ 145 | { 146 | label: 'Option 1', 147 | value: 'op1', 148 | }, 149 | { 150 | label: 'Option 2', 151 | value: 'op2', 152 | }, 153 | { 154 | label: 'Option 3', 155 | value: 'op3', 156 | }, 157 | ], 158 | }, 159 | { 160 | id: 'md-option-group', 161 | component: 'Snarkdown', 162 | span: 1, 163 | // component props: 164 | noLineNumbers: true, 165 | src: "```js\n{component: 'QOptionGroup'}\n```", 166 | }, 167 | { 168 | id: 'consent', 169 | component: 'QToggle', 170 | span: 3, 171 | label: 'Do you agree with our terms?', 172 | rules: [val => val || 'You must accept our terms'], 173 | default: false, 174 | }, 175 | { 176 | id: 'md-toggle', 177 | component: 'Snarkdown', 178 | span: 1, 179 | // component props: 180 | noLineNumbers: true, 181 | src: "```js\n{component: 'QToggle'}\n```", 182 | }, 183 | { 184 | id: 'submissionDate', 185 | span: 3, 186 | component: 'QInput', 187 | label: 'Date of submission', 188 | parseInput: val => new Date(val), 189 | valueType: 'date', 190 | dateFormat: 'YYYY/MM/DD', // see `EfDiv` documentation for more info on `dateFormat` 191 | // component props: 192 | mask: '####/##/##', 193 | placeholder: 'YYYY/MM/DD', 194 | }, 195 | { 196 | id: 'md-input-date', 197 | component: 'Snarkdown', 198 | span: 1, 199 | // component props: 200 | noLineNumbers: true, 201 | src: 202 | "```js\n{component: 'QInput'}\n```\n↳ But the input is saved as a date instead of a string.", 203 | }, 204 | ], 205 | } 206 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/basics.js: -------------------------------------------------------------------------------- 1 | export default { 2 | schema: [ 3 | { 4 | id: 'name', 5 | component: 'QInput', 6 | label: 'Superhero name', 7 | subLabel: 'Think of something cool.', 8 | }, 9 | { 10 | id: 'powerOrigin', 11 | component: 'QBtnToggle', 12 | label: 'Power origin', 13 | subLabel: 'Where does your power come from?', 14 | // component props: 15 | options: [ 16 | { label: 'Mutation', value: 'mutation' }, 17 | { label: 'Self taught', value: 'self' }, 18 | { label: 'By item', value: 'item' }, 19 | ], 20 | spread: true, 21 | }, 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/computedFields.js: -------------------------------------------------------------------------------- 1 | export default { 2 | columnCount: 3, 3 | schema: [ 4 | { 5 | id: 'firstName', 6 | component: 'QInput', 7 | label: 'First name', 8 | }, 9 | { 10 | id: 'lastName', 11 | component: 'QInput', 12 | label: 'Last name', 13 | }, 14 | { 15 | id: 'fullName', 16 | component: 'QInput', 17 | label: 'Full name (computed)', 18 | disable: true, 19 | parseValue: (val, { formData }) => 20 | `${formData.firstName || ''} ${formData.lastName || ''}`.trim(), 21 | }, 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/computedFields2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | columnCount: 3, 3 | schema: [ 4 | { 5 | id: 'firstName', 6 | component: 'QInput', 7 | label: 'First name', 8 | events: { 9 | input: (val, { formData, fieldInput }) => { 10 | const { lastName = '' } = formData 11 | const value = `${val} ${lastName}`.trim() 12 | fieldInput({ id: 'fullName', value }) 13 | }, 14 | }, 15 | }, 16 | { 17 | component: 'QInput', 18 | label: 'Last name', 19 | id: 'lastName', 20 | events: { 21 | input: (val, { formData, fieldInput }) => { 22 | const { firstName = '' } = formData 23 | const value = `${firstName} ${val}`.trim() 24 | fieldInput({ id: 'fullName', value }) 25 | }, 26 | }, 27 | }, 28 | { 29 | id: 'fullName', 30 | component: 'QInput', 31 | label: 'Full name (computed)', 32 | disable: true, 33 | }, 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/computedFields3.js: -------------------------------------------------------------------------------- 1 | export default { 2 | columnCount: 3, 3 | schema: [ 4 | { 5 | id: 'firstName', 6 | component: 'QInput', 7 | label: 'First name', 8 | }, 9 | { 10 | id: 'lastName', 11 | component: 'QInput', 12 | label: 'Last name', 13 | }, 14 | { 15 | id: 'fullName', 16 | component: 'QInput', 17 | label: 'Full name (computed)', 18 | disable: true, 19 | parseValue: (val, { formData, fieldInput }) => { 20 | const value = `${formData.firstName || ''} ${formData.lastName || ''}`.trim() 21 | if (val !== value) fieldInput({ id: 'fullName', value }) 22 | return value 23 | }, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/evaluatedProps1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'edit', 3 | actionButtons: [], 4 | columnCount: 1, 5 | schema: [ 6 | { 7 | id: 'color', 8 | span: true, 9 | component: 'QBtnToggle', 10 | label: 'What is your favorite color?', 11 | evaluatedProps: ['subLabel'], 12 | subLabel: val => 13 | val === 'red' 14 | ? 'like the sun' 15 | : val === 'blue' 16 | ? 'like the sky' 17 | : val === 'green' 18 | ? 'green is not a creative color' 19 | : val === 'other' 20 | ? 'oh, come on, just pick one' 21 | : 'pick a color!', 22 | // component props: 23 | options: [ 24 | { label: 'red', value: 'red' }, 25 | { label: 'blue', value: 'blue' }, 26 | { label: 'green', value: 'green' }, 27 | { label: 'other', value: 'other' }, 28 | ], 29 | spread: true, 30 | }, 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/evaluatedProps2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'edit', 3 | actionButtons: [], 4 | columnCount: 2, 5 | schema: [ 6 | { 7 | id: 'over18', 8 | component: 'QToggle', 9 | default: false, 10 | label: 'Are you over 18?', 11 | }, 12 | { 13 | id: 'parentalConsent', 14 | component: 'QToggle', 15 | default: false, 16 | label: 'Do you have parental consent?', 17 | subLabel: 'This will be disabled when the first question is `true`.', 18 | evaluatedProps: ['disable'], 19 | // component props: 20 | disable: (val, { formData }) => formData.over18, 21 | }, 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/evaluatedProps3.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'edit', 3 | actionButtons: ['edit', 'save'], 4 | columnCount: 2, 5 | schema: [ 6 | { 7 | id: 'car', 8 | component: 'QToggle', 9 | default: false, 10 | label: 'Do you have a car?', 11 | }, 12 | { 13 | id: 'carType', 14 | component: 'QInput', 15 | label: 'What is the brand?', 16 | subLabel: 'This is only shown when the first question is `true`.', 17 | evaluatedProps: ['showCondition'], 18 | showCondition: (val, { formData }) => formData.car, 19 | }, 20 | { 21 | id: 'carNrPlate', 22 | component: 'QInput', 23 | label: 'Enter your license plate brand?', 24 | subLabel: "This is hidden when the form is set to 'view' mode. Try clicking 'save'.", 25 | evaluatedProps: ['showCondition'], 26 | showCondition: (val, { formData, mode }) => formData.car && mode === 'edit', 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/evaluatedProps4.js: -------------------------------------------------------------------------------- 1 | const carData = [ 2 | { year: 2015, make: 'Audi', model: 'A3', trim: '2.0' }, 3 | { year: 2015, make: 'Audi', model: 'A3', trim: '1.8' }, 4 | { year: 2015, make: 'Audi', model: 'A6', trim: '2.5' }, 5 | { year: 2015, make: 'Audi', model: 'A6', trim: '3.0' }, 6 | { year: 2015, make: 'BMW', model: 'M3', trim: 'b2.0' }, 7 | { year: 2015, make: 'BMW', model: 'M3', trim: 'b1.8' }, 8 | { year: 2015, make: 'BMW', model: 'M5', trim: 'b2.5' }, 9 | { year: 2015, make: 'BMW', model: 'M5', trim: 'b3.0' }, 10 | { year: 2016, make: 'Chevy', model: 'Impala', trim: 'c2.0' }, 11 | { year: 2016, make: 'Chevy', model: 'Impala', trim: 'c1.8' }, 12 | { year: 2016, make: 'Chevy', model: 'Malibu', trim: 'c2.5' }, 13 | { year: 2016, make: 'Chevy', model: 'Malibu', trim: 'c3.0' }, 14 | { year: 2016, make: 'Dodge', model: 'RAM', trim: 'd2.0' }, 15 | { year: 2016, make: 'Dodge', model: 'RAM', trim: 'd1.8' }, 16 | { year: 2016, make: 'Dodge', model: 'Challanger', trim: 'd2.5' }, 17 | { year: 2016, make: 'Dodge', model: 'Challanger', trim: 'd3.0' }, 18 | ] 19 | 20 | const uniqueValues = array => [...new Set(array)] 21 | const mapForQSelect = value => ({ value, label: value }) 22 | 23 | const clearFields = (fieldIds, fieldInput) => { 24 | fieldIds.forEach(id => fieldInput({ id, value: '' })) 25 | } 26 | 27 | export default { 28 | mode: 'edit', 29 | actionButtons: ['cancel', 'edit', 'save'], 30 | columnCount: 4, 31 | schema: [ 32 | { 33 | id: 'year', 34 | label: 'Year', 35 | component: 'QSelect', 36 | events: { 37 | // clear fields right from input to prevent invalid data 38 | input: (val, { fieldInput }) => clearFields(['make', 'model', 'trim'], fieldInput), 39 | }, 40 | // component props: 41 | options: uniqueValues(carData.map(d => d.year)).map(mapForQSelect), 42 | emitValue: true, 43 | }, 44 | { 45 | id: 'make', 46 | label: 'Make', 47 | component: 'QSelect', 48 | evaluatedProps: ['options'], 49 | events: { 50 | // clear fields right from input to prevent invalid data 51 | input: (val, { fieldInput }) => clearFields(['model', 'trim'], fieldInput), 52 | }, 53 | // component props: 54 | options: (val, { formData }) => { 55 | const { year } = formData || {} 56 | return uniqueValues(carData.filter(car => car.year === year).map(d => d.make)).map( 57 | mapForQSelect 58 | ) 59 | }, 60 | emitValue: true, 61 | }, 62 | { 63 | id: 'model', 64 | label: 'Model', 65 | component: 'QSelect', 66 | evaluatedProps: ['options'], 67 | events: { 68 | // clear fields right from input to prevent invalid data 69 | input: (val, { fieldInput }) => clearFields(['trim'], fieldInput), 70 | }, 71 | // component props: 72 | options: (val, { formData }) => { 73 | const { year, make } = formData || {} 74 | return uniqueValues( 75 | carData.filter(car => car.year === year && car.make === make).map(d => d.model) 76 | ).map(mapForQSelect) 77 | }, 78 | emitValue: true, 79 | }, 80 | { 81 | id: 'trim', 82 | label: 'Trim', 83 | component: 'QSelect', 84 | evaluatedProps: ['options'], 85 | // component props: 86 | options: (val, { formData }) => { 87 | const { year, make, model } = formData || {} 88 | return uniqueValues( 89 | carData 90 | .filter(car => car.year === year && car.make === make && car.model === model) 91 | .map(d => d.trim) 92 | ).map(mapForQSelect) 93 | }, 94 | emitValue: true, 95 | }, 96 | ], 97 | } 98 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/events1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'edit', 3 | actionButtons: [], 4 | columnCount: 1, 5 | schema: [ 6 | { 7 | id: 'testField', 8 | component: 'QInput', 9 | label: 'Type something', 10 | events: { 11 | input: (val, { $q }) => $q.notify(val), 12 | focus: (val, { id, label, $q }) => $q.notify(`focussed: 「${label}」 (id: ${id})`), 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/events2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mode: 'edit', 3 | actionButtons: [], 4 | columnCount: 2, 5 | schema: [ 6 | { 7 | id: 'tel', 8 | component: 'QInput', 9 | label: 'Phone nr (hyphenated)', 10 | subLabel: 'Type any number with `-` or `( )`', 11 | events: { 12 | input: (val, { fieldInput, formData }) => 13 | fieldInput({ id: 'telClean', value: !val ? '' : val.replace(/[^\d]/g, '').trim() }), 14 | }, 15 | }, 16 | { 17 | id: 'telClean', 18 | component: 'QInput', 19 | label: 'Phone nr (only numbers)', 20 | subLabel: 'This field is automatically updated when you type in a phone nr on the left.', 21 | // component props: 22 | disable: true, 23 | }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/index.js: -------------------------------------------------------------------------------- 1 | // easy forms 2 | export const basics = { 3 | code: [require('./basics').default], 4 | string: [require('!!raw-loader!./basics').default], 5 | } 6 | export const actionButtons = { 7 | code: [require('./actionButtons').default], 8 | string: [require('!!raw-loader!./actionButtons').default], 9 | } 10 | export const advanced = { 11 | code: [require('./advanced').default], 12 | string: [require('!!raw-loader!./advanced').default], 13 | } 14 | export const nestedData = { 15 | code: [require('./nestedData').default], 16 | string: [require('!!raw-loader!./nestedData').default], 17 | } 18 | export const validation = { 19 | code: [require('./validation').default, require('./validation2').default], 20 | string: [ 21 | require('!!raw-loader!./validation').default, 22 | require('!!raw-loader!./validation2').default, 23 | ], 24 | } 25 | export const responsiveStyle = { 26 | code: [require('./responsiveStyle').default], 27 | string: [require('!!raw-loader!./responsiveStyle').default], 28 | } 29 | export const QSelectModel = { 30 | code: [require('./QSelectModel').default], 31 | string: [require('!!raw-loader!./QSelectModel').default], 32 | } 33 | export const evaluatedProps = { 34 | code: [ 35 | require('./evaluatedProps1').default, 36 | require('./evaluatedProps2').default, 37 | require('./evaluatedProps3').default, 38 | require('./evaluatedProps4').default, 39 | ], 40 | string: [ 41 | require('!!raw-loader!./evaluatedProps1').default, 42 | require('!!raw-loader!./evaluatedProps2').default, 43 | require('!!raw-loader!./evaluatedProps3').default, 44 | require('!!raw-loader!./evaluatedProps4').default, 45 | ], 46 | } 47 | export const events = { 48 | code: [require('./events1').default, require('./events2').default], 49 | string: [require('!!raw-loader!./events1').default, require('!!raw-loader!./events2').default], 50 | } 51 | export const computedFields = { 52 | code: [ 53 | require('./computedFields').default, 54 | require('./computedFields2').default, 55 | require('./computedFields3').default, 56 | ], 57 | string: [ 58 | require('!!raw-loader!./computedFields').default, 59 | require('!!raw-loader!./computedFields2').default, 60 | require('!!raw-loader!./computedFields3').default, 61 | ], 62 | } 63 | 64 | // easy fields 65 | export const QInput = { 66 | code: [require('./QInput').default], 67 | string: [require('!!raw-loader!./QInput').default], 68 | } 69 | export const EfMiniForm = { 70 | code: [require('./EfMiniForm').default], 71 | string: [require('!!raw-loader!./EfMiniForm').default], 72 | } 73 | export const EfBtn = { 74 | code: [require('./EfBtn').default], 75 | string: [require('!!raw-loader!./EfBtn').default], 76 | } 77 | export const EfDiv = { 78 | code: [require('./EfDiv').default], 79 | string: [require('!!raw-loader!./EfDiv').default], 80 | } 81 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/nestedData.js: -------------------------------------------------------------------------------- 1 | export default { 2 | actionButtons: [], 3 | mode: 'edit', 4 | columnCount: 3, 5 | events: { 6 | 'field-input': val => { 7 | console.log('logging @field-input payload:', val) 8 | }, 9 | }, 10 | schema: [ 11 | { 12 | id: 'size.width', 13 | label: 'Width', 14 | component: 'QInput', 15 | parseInput: Number, 16 | // component props: 17 | type: 'number', 18 | suffix: 'cm', 19 | }, 20 | { 21 | id: 'size.depth', 22 | label: 'Depth', 23 | component: 'QInput', 24 | parseInput: Number, 25 | // component props: 26 | type: 'number', 27 | suffix: 'cm', 28 | }, 29 | { 30 | id: 'size.height', 31 | label: 'Height', 32 | component: 'QInput', 33 | parseInput: Number, 34 | // component props: 35 | type: 'number', 36 | suffix: 'cm', 37 | }, 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/responsiveStyle.js: -------------------------------------------------------------------------------- 1 | export default [] 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/validation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | columnCount: 3, 3 | actionButtons: ['delete', 'archive', 'cancel', 'edit', 'save'], 4 | schema: [ 5 | { 6 | id: 'name', 7 | label: 'Name', 8 | component: 'QInput', 9 | // component props: 10 | required: true, 11 | }, 12 | { 13 | id: 'age', 14 | label: 'Age', 15 | component: 'QInput', 16 | parseInput: Number, 17 | rules: [val => val >= 18 || 'You must be over 18'], 18 | // component props: 19 | type: 'number', 20 | }, 21 | { 22 | id: 'consent', 23 | label: 'Do you agree with our terms?', 24 | component: 'QToggle', 25 | rules: [val => val || 'You must accept our terms'], 26 | default: false, 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/examples/validation2.js: -------------------------------------------------------------------------------- 1 | import { validateFormPerSchema } from 'ui' 2 | // You must write: 3 | // import { validateFormPerSchema } from 'quasar-ui-easy-forms' 4 | 5 | const data = { 6 | name: undefined, 7 | age: undefined, 8 | consent: undefined, 9 | } 10 | 11 | const schema = [ 12 | { 13 | id: 'name', 14 | label: 'Name', 15 | component: 'QInput', 16 | required: true, 17 | }, 18 | { 19 | id: 'age', 20 | label: 'Age', 21 | component: 'QInput', 22 | type: 'number', 23 | parseInput: Number, 24 | rules: [val => val >= 18 || 'You must be over 18'], 25 | }, 26 | { 27 | id: 'consent', 28 | label: 'Do you agree with our terms?', 29 | component: 'QToggle', 30 | default: false, 31 | rules: [val => val || 'You must accept our terms'], 32 | }, 33 | ] 34 | 35 | export default { 36 | columnCount: 2, 37 | actionButtons: ['delete', 'archive', 'cancel', 'edit', 'save'], 38 | schema: [ 39 | { 40 | component: 'EfBtn', 41 | btnLabel: 'validate', 42 | subLabel: 'Click this and check the developer tools > console.', 43 | events: { 44 | click: e => { 45 | const result = validateFormPerSchema(data, schema) 46 | console.log('result → ', result) 47 | }, 48 | }, 49 | }, 50 | { 51 | component: 'EfBtn', 52 | btnLabel: 'set data', 53 | subLabel: 'Click this to set the correct data and then try "validate" again!', 54 | events: { 55 | click: e => { 56 | data.name = 'name' 57 | data.age = 18 58 | data.consent = true 59 | }, 60 | }, 61 | }, 62 | ], 63 | } 64 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/QSelectModel.js: -------------------------------------------------------------------------------- 1 | import merge from 'merge-anything' 2 | 3 | const description = `This is an example of an \`\` with a bunch of evaluated field props. 4 | 5 | In this example I display all the possibilities to manage your field "model" with a QSelect field. I always found this confusing in the Quasar docs, so here is a nice overview you can play around with. 6 | 7 | At the same time, if you check the source code, you'll see how easy it is to create such a complex form with such an easy schema. 🙃` 8 | 9 | export default { 10 | mode: 'edit', 11 | actionButtons: [], 12 | schema: [ 13 | { 14 | component: 'Snarkdown', 15 | noLineNumbers: true, 16 | src: description, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/actionButtons.js: -------------------------------------------------------------------------------- 1 | const description = ` 2 | Action buttons are buttons you would want to add to a form to do things like edit/save/delete etc... You can easily add action buttons like these on the top/bottom or sides of your EasyForm. 3 | 4 | Action buttons are set via the prop 'actionButtons'. You can use pre-made action buttons that emit events. You can also overwrite the look of these pre-made buttons. Finally you can also set custom buttons and fields. 5 | 6 | ### Pre-made action buttons 7 | 8 | Pre-made buttons can be added to your form by just passing the string of the button you want: 9 | 10 | \`:action-buttons="['edit', 'cancel', 'save', 'delete', 'archive']"\` 11 | 12 | When added you will see the buttons like the preview below. They each have a functionality: 13 | 14 | - \`'edit'\`: Adds a button that puts the form in "edit" mode 15 | - \`'cancel'\`: Adds a button that puts the form back into "view" mode & reverts the content to its original state 16 | - \`'save'\`: Adds a button that puts the form back into "view" mode & keeps the modified content 17 | - \`'delete'\` & \`'archive'\`: Adds a button that emits a delete or archive event (you must implement your own logic) 18 | 19 | The buttons above emits the events: \`@edit\`, \`@cancel\`, \`@save\`, \`@delete\`, \`@archive\` 20 | 21 | You can listen for these events on the \`\` to use do things like: 22 | - saving data to a DB when "save" is clicked 23 | - moving a popup when 'cancel' is clicked 24 | - clear the form data (\`value\`) when 'delete' is clicked 25 | 26 | The \`@save\` event receives a payload with the new and old form data. 27 | 28 | - \`@save="onSave"\` 29 | 30 | \`\`\`js 31 | { 32 | onSave ({newData, oldData}) { 33 | console.log(newData) // an object with only the updated fields 34 | console.log(oldData) // the original object with all the field values 35 | // if you need a combination of both: 36 | const newFormData = {...oldData, ...newData} 37 | } 38 | } 39 | \`\`\` 40 | 41 | ### Overwriting pre-made buttons 42 | 43 | You can overwrite how the pre-made buttons look by providing an object like so: 44 | 45 | \`\`\`js 46 | { 47 | 'edit': { icon: 'edit' }, 48 | 'save': { push: true }, 49 | 'delete': { color: 'secondary' } 50 | } 51 | \`\`\` 52 | 53 | ### Providing custom buttons & fields 54 | 55 | You can also pass custom buttons & fields with a schema. The schema you provide works just like the EasyForm schema. 56 | 57 | An example of a custom button could be: 58 | \`\`\`js 59 | actionButtons: [{ 60 | component: 'EfBtn', 61 | btnLabel: 'log the data', 62 | showCondition: (_, {formData}) => formData.enableLogging, 63 | events: { 64 | click: (event, {formData}) => console.log(formData), 65 | }, 66 | }] 67 | \`\`\` 68 | 69 | Being able to show/hide these button based on the \`formData\` can be very powerful. 70 | Be sure to check out the "Evaluated Props" and "Events" documentation. 71 | ` 72 | 73 | export default { 74 | mode: 'edit', 75 | actionButtons: [], 76 | schema: [ 77 | { 78 | component: 'Snarkdown', 79 | noLineNumbers: true, 80 | src: description, 81 | }, 82 | ], 83 | } 84 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/advanced.js: -------------------------------------------------------------------------------- 1 | const description = `This is a more advanced example so you can see the full power of EasyForms. All fields you see here are just regular Quasar components. 2 | 3 | You can use any Quasar component, but you need to be sure to register the components in \`quasar.conf.js\`. 4 | \`\`\`js 5 | { 6 | framework: { 7 | // you can use auto for components other than those you use in EasyForm 8 | all: 'auto', 9 | 10 | // you need to register components you use in EasyForm because Quasar can't auto detect them 11 | components: ['QInput'], 12 | } 13 | } 14 | \`\`\` 15 | 16 | You can check the source code of the example to see what kind of properties are used in the schema to generate this advanced form. Something you can notice is that the form's v-model is updated with the default values specified in the form's schema. 17 | ` 18 | 19 | export default { 20 | mode: 'edit', 21 | actionButtons: [], 22 | schema: [ 23 | { 24 | component: 'Snarkdown', 25 | noLineNumbers: true, 26 | src: description, 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/basics.js: -------------------------------------------------------------------------------- 1 | const description = `\`\` is a component that allows you to easily create forms by passing an object with a schema on how you want the form to look. By merely passing a schema array you can easily generate entire forms! No more writing clunky HTML forms! 🎉 2 | 3 | #### **Schema** 4 | 5 | The schema of an EasyForm is an array of objects that usually looks something like this: 6 | Eg.: 7 | \`\`\`js 8 | [ 9 | // you can use custom Vue components 10 | { id: 'myField', label: 'My Field', component: 'MyFieldVueComponent' }, 11 | 12 | // you can use quasar components 13 | { id: 'name', label: 'Name', component: 'QInput' }, 14 | ] 15 | \`\`\` 16 | 17 | Besides 'id', 'label' and 'component' there are many more props you can pass: subLabel; required; labelPosition; fieldClasses; componentClasses; parseValue; parseInput; events and many more. 18 | 19 | #### **Value/Model** 20 | 21 | There are several ways to work with the data of an EasyForm. 22 | 23 | 1. You can pass an empty object as \`v-model\` (or \`:value\` & listen to \`@input\`) 24 | In this case EasyForms will populate an object with the field's \`id\` as key and the user input as value. 25 | 26 | 2. You can listen to the \`@field-input\` event which triggers every time a field's value changes. It's payload is an object that looks like: \`{id, value}\`. 27 | 28 | 3. You can listen to the \`@save\` event which is triggered when the form's save button is pressed. It's payload is an object that looks like: \`{newData, oldData}\`. For more info see the [Action Buttons documentation](/actionButtons). 29 | 30 | #### **Mode** 31 | 32 | EasyForm has four modes: 33 | - \`'view'\`: Show fields based on the schema; but make each field \`readonly: true\`. 34 | - \`'edit'\`: Show editable fields based on the schema 35 | - \`'add'\`: The same as 'edit' 36 | - \`'raw'\`: Used to show raw data of your form. No fields are generated, just divs with the labels and values. This mode is powerful because it will automatically map values to the schema provided (eg. adding pre-/suffix; mapping to options of a select; etc.) 37 | 38 | When in 'view' mode, EasyForm can be used to just show data. These buttons can also be disabled and by providing a custom mode you can keep the form in a certain mode forever. 39 | 40 | -- 41 | 42 | Be sure to check out all the other pages for more functionality, but first... 43 | 44 | Try filling in the form below and play around with the props via the **interactive preview**. You can click "props" to update any props used in the and see how the how the preview and source code changes.` 45 | 46 | export default { 47 | mode: 'edit', 48 | actionButtons: [], 49 | schema: [ 50 | { 51 | component: 'Snarkdown', 52 | noLineNumbers: true, 53 | src: description, 54 | }, 55 | ], 56 | } 57 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/computedFields.js: -------------------------------------------------------------------------------- 1 | const description = `Computed fields are fields that can represent data which doesn't neccesarily exist in your data. They have a "caluculated value" based on the form data. 2 | 3 | Do not confuse this concept with "Evaluated Props". 4 | - Evaluated Props: a calculated prop of a field 5 | - Computed Fields: a field with a calculated value 6 | 7 | An example of a Computed Field could be a full name of a person which exists of \`\${formData.firstName} \${formData.lastName}\` 8 | 9 | There are three ways we could create such a field: 10 | ` 11 | 12 | export default { 13 | mode: 'edit', 14 | actionButtons: [], 15 | schema: [ 16 | { 17 | component: 'Snarkdown', 18 | 19 | noLineNumbers: true, 20 | src: description, 21 | }, 22 | { 23 | id: 'chosenExample', 24 | component: 'QBtnToggle', 25 | spread: true, 26 | noCaps: true, 27 | unelevated: true, 28 | options: [ 29 | { label: 'The parseValue prop', value: 0 }, 30 | { label: 'Update via fieldInput', value: 1 }, 31 | { label: 'Combine parseValue & fieldInput', value: 2 }, 32 | ], 33 | }, 34 | { 35 | component: 'Snarkdown', 36 | 37 | showCondition: (value, { formData }) => formData.chosenExample === 0, 38 | noLineNumbers: true, 39 | src: ` 40 | ### The \`parseValue\` prop 41 | 42 | \`\`\`js 43 | { 44 | id: 'fullName', 45 | component: 'QInput', // or any other component 46 | parseValue: (val, {formData}) => \`\${formData.firstName || ''} \${formData.lastName || ''}\` 47 | } 48 | \`\`\` 49 | 50 | So even though the field \`fullName\` has no \`value\` at all, it will always stay in sync with the current \`formData\`. 51 | 52 | When implementing a computed field this way however, \`fullName\` will never have that computed value emitted. This means that it won't be included in the EasyForm events: \`@input\`, \`@field-input\` and \`@save\`. So it's difficult to capture and save this calculated value alongside your other data. See the next tab for another method. 53 | `.trim(), 54 | }, 55 | { 56 | component: 'Snarkdown', 57 | 58 | showCondition: (value, { formData }) => formData.chosenExample === 1, 59 | noLineNumbers: true, 60 | src: ` 61 | ### Update via \`fieldInput\` 62 | 63 | It can be handy to also save the calculated value in your database so you can filter/search/sort on this field. (This is required when using eg. an [EasyTable](https://quasar-easy-tables.web.app) or QTable.) 64 | 65 | In this case we can use the method called \`fieldInput()\` which is accessible on the context and first explained on the [events documentation page](/events). 66 | 67 | \`\`\`js 68 | { 69 | id: 'firstName', 70 | events: { 71 | input: (val, {formData, fieldInput}) => { 72 | const { lastName = '' } = formData 73 | const value = \`\${val} \${lastName}\`.trim() 74 | fieldInput({id: 'fullName', value}) 75 | } 76 | }, 77 | }, 78 | { 79 | id: 'lastName', 80 | events: { 81 | input: (val, {formData, fieldInput}) => { 82 | const { firstName = '' } = formData 83 | const value = \`\${firstName} \${val}\`.trim() 84 | fieldInput({id: 'fullName', value}) 85 | } 86 | }, 87 | } 88 | \`\`\` 89 | 90 | This method has pro's and con's though: 91 | 92 | - PRO: you don't need to include the Computed Field (\`fullName\`) on the form at all 93 | - CON: this is quite verbose... 94 | - CON: it cannot be used if you need a computed field _not_ based on other fields (eg. a timestamp returning \`new Date()\`) 95 | - CON: you cannot use this method to add a new "caluculated field" at a later time, when your database already has some data 96 | 97 | There is also a third way we can create a computed field (see the last tab). 98 | `.trim(), 99 | }, 100 | { 101 | component: 'Snarkdown', 102 | 103 | showCondition: (value, { formData }) => formData.chosenExample === 2, 104 | noLineNumbers: true, 105 | src: ` 106 | ### Combine \`parseValue\` & \`fieldInput\` 107 | 108 | The third way to create a computed field is this: 109 | 110 | \`\`\`js 111 | { 112 | id: 'fullName', 113 | component: 'QInput', // or any other component 114 | parseValue: (val, {formData, fieldInput}) => { 115 | const value = \`\${formData.firstName || ''} \${formData.lastName || ''}\`.trim() 116 | if (val !== value) fieldInput({id: 'fullName', value}) 117 | return value 118 | }, 119 | // If you want to hide the computed field you can set: 120 | // showCondition: false 121 | } 122 | \`\`\` 123 | 124 | Basically you write your logic inside the \`parseValue\` prop of your computed field, and also trigger a \`fieldInput\` action from within here. 125 | 126 | However, as the more experienced developers will notice... 127 | ::: 128 | This is the same as introducing a side-effect to a computed property! By design this is discouraged, so isn't this bad? 129 | ::: 130 | 131 | I say "nay". The reason it is discouraged is because side-effects to computed properties that modify data are impossible to track. In a few months if you don't know why a certain value is being modified, you'll have a hard time finding eventually it was the side-effect from a computed property. 132 | 133 | If we understand this reason, then in our case, it is perfectly valid to do so, because we are only modifying the data of the field we are describing right there. We are simply doing something equivalent to triggering a \`emit('input', val)\` on a component manually, nothing wrong with that. 134 | 135 | However, keep in mind that also this method has its own pro's and con's: 136 | - PRO: it can be used as stand-alone, without relying on other fields & without the need to render other fields 137 | - PRO: because it just uses \`parseValue\` it's less verbose (opposed to listening to input events of other fields) 138 | - PRO: the logic for this field is contained in its own options object 139 | - PRO: even if your database already has data, a computed field like this can be added at a later date 140 | - CON: you have to include this "Computed Field" in all forms the user can edit the related fields (and probably with \`showCondition: false\`) 141 | `.trim(), 142 | }, 143 | ], 144 | } 145 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/evaluatedProps.js: -------------------------------------------------------------------------------- 1 | const description = `## Evaluated Props 2 | 3 | As you know, \`\` needs a 'schema' with information on each field you want to show. However, when using an EasyForm you can replace the value of any prop in any field with a function instead of the value directly. This function will be executed any time the data of any field changes. This allows you to have "dynamic" props, based on the data of the form. 4 | 5 | For example, when you pass \`disable: true\` to a certain field, it will appear as disabled inside the form. You can instead also pass \`disable: (val, context) => context.formData.myCheckBox\` to be able to only disable that field when \`myCheckBox\` is truthy. 6 | 7 | Evaluated props will receive 2 params: \`(val, context)\`. 8 | - \`val\` is the current value of the field 9 | - \`context\` is the Vue component reference of the \`\`, you can deconstruct this to access any other properties/values. 10 | 11 | The most important props you can access from \`context\`: 12 | - \`formData\` This is the *nested* data of all the fields inside an EasyForm. 13 | - \`formDataFlat\` This is the *flattened* data of all the fields inside an EasyForm. 14 | - \`mode\` The current mode of the EasyForm. Can be \`'view'\` | \`'edit'\` | \`'add'\` | \`raw\` 15 | - \`formId\` An 'id' of the EasyForm. This is only present when manually set. 16 | - Other common Vue props like: \`$store\`, \`$router\`, \`$q\` (for Quasar apps) etc. 17 | 18 | Try to refrain from accessing props other than the ones listed above, because these are mainly used internal and could have behaviour changes that could break your app. 19 | ` 20 | 21 | export default { 22 | mode: 'edit', 23 | actionButtons: [], 24 | schema: [ 25 | { 26 | component: 'Snarkdown', 27 | noLineNumbers: true, 28 | src: description, 29 | }, 30 | { 31 | id: 'chosenExample', 32 | component: 'QBtnToggle', 33 | spread: true, 34 | noCaps: true, 35 | unelevated: true, 36 | options: [ 37 | { label: 'Dynamic prop based on the value of the field', value: 0 }, 38 | { label: 'Dynamic prop based on the value of "another" field', value: 1 }, 39 | { label: 'Dynamic "conditional rendering" of a field', value: 2 }, 40 | { label: 'Dynamic "options" of a select-field', value: 3 }, 41 | ], 42 | }, 43 | { 44 | component: 'Snarkdown', 45 | noLineNumbers: true, 46 | showCondition: (value, { formData }) => formData.chosenExample === 0, 47 | src: ` 48 | ### Dynamic prop based on the value of the field 49 | Eg. \`subLabel: val => val === 'purple' ? 'nice!' : 'choose a color'\` 50 | `.trim(), 51 | }, 52 | { 53 | component: 'Snarkdown', 54 | noLineNumbers: true, 55 | showCondition: (value, { formData }) => formData.chosenExample === 1, 56 | src: ` 57 | ### Dynamic prop based on the value of "another" field 58 | Eg. \`disable: (val, {formData}) => formData.over18\` 59 | `.trim(), 60 | }, 61 | { 62 | component: 'Snarkdown', 63 | noLineNumbers: true, 64 | showCondition: (value, { formData }) => formData.chosenExample === 2, 65 | src: ` 66 | ### Dynamic "conditional rendering" of a field 67 | 68 | Eg. \`showCondition: (val, {formData}) => formData.car\` 69 | 70 | \`showCondition\` is a special prop that can only be used inside the schema of an EasyForm. 71 | `.trim(), 72 | }, 73 | { 74 | component: 'Snarkdown', 75 | noLineNumbers: true, 76 | showCondition: (value, { formData }) => formData.chosenExample === 3, 77 | src: ` 78 | ### Dynamic "options" of a select-field 79 | `.trim(), 80 | }, 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/events.js: -------------------------------------------------------------------------------- 1 | const description = `Each \`\` can be passed an prop called \`events\`. This prop can be passed like so: \`:events="events"\` and will set all the event listeners on the field like so: \`v-on="events"\`. 2 | 3 | An \`events\` prop would look like so: 4 | \`\`\`js 5 | events: { 6 | input: ($event, context) => { /* do something */ }, 7 | focus: ($event, context) => { /* do something */ }, 8 | // etc... 9 | } 10 | \`\`\` 11 | 12 | The benefits of passing your event listeners via the \`events\` prop are: 13 | - Besides the typical \`$event\` parameter they receive as first parameter, they will receive a second \`context\` parameter. 14 | - \`context\` is the Vue component reference of the \`\`, you can deconstruct this to access any other properties/values. 15 | - \`context\` has useful props like: \`$store\`, \`$router\`, \`formData\`, \`formDataFlat\`, \`mode\`, \`formId\`, ... All of these are explained in the *"Evaluated Props" documentation*, so be sure to check that. 16 | - \`context\` has a special function called \`fieldInput\` which can be used to modify other fields programatically. 17 | - And all this can be set from inside an \`\`'s \`schema\` so you don't need add anything manually inside your templates. 18 | 19 | Phew. That was a bit of a lot of information all at once. 😅 Let's look at some examples: 20 | ` 21 | 22 | export default { 23 | mode: 'edit', 24 | actionButtons: [], 25 | schema: [ 26 | { 27 | component: 'Snarkdown', 28 | noLineNumbers: true, 29 | src: description, 30 | }, 31 | { 32 | id: 'chosenExample', 33 | component: 'QBtnToggle', 34 | spread: true, 35 | noCaps: true, 36 | unelevated: true, 37 | options: [ 38 | { label: 'Notify on events', value: 0 }, 39 | { label: "Update other fields on 'input'", value: 1 }, 40 | ], 41 | }, 42 | { 43 | component: 'Snarkdown', 44 | showCondition: (value, { formData }) => formData.chosenExample === 0, 45 | noLineNumbers: true, 46 | src: ` 47 | ### Notify on events 48 | 49 | Here we see an example of two events being used. 50 | \`\`\`js 51 | events: { 52 | input: (val, {$q}) => $q.notify(val) 53 | focus: (val, {id, label, $q}) => $q.notify(\`focussed: 「\${label}」 (id: \${id})\`) 54 | } 55 | \`\`\``.trim(), 56 | }, 57 | { 58 | component: 'Snarkdown', 59 | showCondition: (value, { formData }) => formData.chosenExample === 1, 60 | noLineNumbers: true, 61 | src: ` 62 | ### Update other fields on 'input' 63 | Here we see an example of one field updating the contents of another on the input event. 64 | \`\`\`js 65 | events: { 66 | input: (val, {fieldInput}) => { 67 | // get only digits from input 68 | const value = !val ? '' : val.replace(/[^\d]/g, '').trim() 69 | 70 | // set field 'telClean' to this value 71 | fieldInput({id: 'telClean', value}) 72 | } 73 | } 74 | \`\`\` 75 | 76 | The \`fieldInput\` function can be used to update other fields inside your form. It receives a single parameter which should be an object that looks like \`{id, value}\` with the \`id\` of the field you want to update and a \`value\` you want to update it with. 77 | 78 | Be sure to also check the documentation on \`Computed Fields\`. 79 | `.trim(), 80 | }, 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as actionButtons } from './actionButtons' 2 | export { default as basics } from './basics' 3 | export { default as advanced } from './advanced' 4 | export { default as evaluatedProps } from './evaluatedProps' 5 | export { default as events } from './events' 6 | export { default as nestedData } from './nestedData' 7 | export { default as responsiveStyle } from './responsiveStyle' 8 | export { default as validation } from './validation' 9 | export { default as computedFields } from './computedFields' 10 | export { default as QSelectModel } from './QSelectModel' 11 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/nestedData.js: -------------------------------------------------------------------------------- 1 | const description = `An \`\` can use a nested data structure on a flat form schema. When you have a nested data structure you will need to appoint each field ID with dot notation. 2 | 3 | Eg. a field with ID \`size.width\` points to \`{size: {width}}\` in your data. 4 | 5 | Besides writing your field IDs with dot notation, nothing further needs to be done. 6 | 7 | The only thing you need to be careful with is the \`@field-input\` event: 8 | - Listening to the \`@input\` event will always return the full data nested 9 | - Listening to the \`@field-input\` event will always have the field ID with dot-notation in its payload. 10 | 11 | In the example below you can see a nested data structure when updating one of the fields, but if you check the developer tools > console, you will see that the \`id\`s are logged as eg. \`'size.width'\`` 12 | 13 | export default { 14 | mode: 'edit', 15 | actionButtons: [], 16 | schema: [ 17 | { 18 | component: 'Snarkdown', 19 | noLineNumbers: true, 20 | src: description, 21 | }, 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/responsiveStyle.js: -------------------------------------------------------------------------------- 1 | const description = `` 2 | 3 | export default { 4 | mode: 'edit', 5 | actionButtons: [], 6 | schema: [ 7 | { 8 | component: 'Snarkdown', 9 | noLineNumbers: true, 10 | src: description, 11 | }, 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /ui/dev/src/schemas/pages/validation.js: -------------------------------------------------------------------------------- 1 | const description = `EasyForms have validation enabled by default when clicking the save button or when executing \`validate\` on the EasyForm ref. 2 | 3 | There is also the possibility to do programatic validation. EasyForms provides a helper function which can be used without the need of rendering the form at all. It can be used like so: 4 | \`\`\`js 5 | import { validateFormPerSchema } from 'quasar-ui-easy-forms' 6 | 7 | validateFormPerSchema(formData, schema) 8 | \`\`\`` 9 | 10 | export default { 11 | mode: 'edit', 12 | actionButtons: [], 13 | schema: [ 14 | { 15 | component: 'Snarkdown', 16 | 17 | noLineNumbers: true, 18 | src: description, 19 | }, 20 | { 21 | id: 'chosenExample', 22 | component: 'QBtnToggle', 23 | spread: true, 24 | noCaps: true, 25 | unelevated: true, 26 | options: [ 27 | { label: 'Basic validation', value: 0 }, 28 | { label: 'Programatic validation', value: 1 }, 29 | ], 30 | }, 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /ui/dev/src/statics/app-logo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/app-logo-128x128.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/apple-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/apple-icon-167x167.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/favicon-16x16.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/favicon-32x32.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/favicon-96x96.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/favicon.ico -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/icon-128x128.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/icon-192x192.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/icon-256x256.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/icon-384x384.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/icon-512x512.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesqueeb/quasar-ui-easy-forms/853699b22091d184103e8682fe81d56a41c008e9/ui/dev/src/statics/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /ui/dev/src/statics/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quasar-ui-easy-forms", 3 | "version": "2.4.2", 4 | "author": "Luca Ban - Mesqueeb", 5 | "description": "A Vue plugin (that uses Quasar components) to easily generate forms by only defining a \"schema\" object.", 6 | "license": "MIT", 7 | "module": "dist/index.esm.js", 8 | "main": "dist/index.common.js", 9 | "scripts": { 10 | "deploy": "npm run build && cd dev && npm run deploy && cd .. && npm publish -otp $OTP && cd ../app-extension && npm publish -otp $OTP", 11 | "deploy-only-lib": "npm run build && npm publish", 12 | "dev": "cd dev && yarn dev && cd ..", 13 | "dev:umd": "yarn build && node build/script.open-umd.js", 14 | "dev:ssr": "cd dev && yarn 'dev:ssr' && cd ..", 15 | "dev:ios": "cd dev && yarn 'dev:ios' && cd ..", 16 | "dev:android": "cd dev && yarn 'dev:android' && cd ..", 17 | "dev:electron": "cd dev && yarn 'dev:electron' && cd ..", 18 | "build": "node build/index.js", 19 | "build:js": "node build/script.javascript.js", 20 | "build:css": "node build/script.css.js", 21 | "test": "ava" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mesqueeb/quasar-ui-easy-forms" 26 | }, 27 | "bugs": "https://github.com/mesqueeb/quasar-ui-easy-forms/issues", 28 | "homepage": "https://quasar-easy-forms.web.app", 29 | "devDependencies": { 30 | "@babel/register": "^7.9.0", 31 | "@rollup/plugin-buble": "^0.21.3", 32 | "@rollup/plugin-commonjs": "^11.1.0", 33 | "@rollup/plugin-json": "^4.0.3", 34 | "@rollup/plugin-node-resolve": "^6.1.0", 35 | "autoprefixer": "^9.8.0", 36 | "ava": "^2.4.0", 37 | "chalk": "^2.4.2", 38 | "cssnano": "^4.1.10", 39 | "esm": "^3.2.25", 40 | "node-sass": "^4.14.1", 41 | "open": "^6.4.0", 42 | "postcss": "^7.0.31", 43 | "postcss-rtl": "^1.7.3", 44 | "quasar": "^1.11.3", 45 | "rimraf": "^3.0.2", 46 | "rollup": "^1.32.1", 47 | "rollup-plugin-vue": "^5.1.9", 48 | "uglify-es": "^3.3.9", 49 | "vue": "^2.6.11", 50 | "vue-template-compiler": "^2.6.11", 51 | "zlib": "^1.0.5" 52 | }, 53 | "browserslist": [ 54 | "last 1 version, not dead, ie >= 11" 55 | ], 56 | "dependencies": { 57 | "@vue/composition-api": "^0.3.4", 58 | "case-anything": "0.0.1", 59 | "commafy-anything": "^1.1.3", 60 | "copy-anything": "^1.6.0", 61 | "flatten-anything": "^1.4.1", 62 | "is-what": "^3.8.0", 63 | "merge-anything": "^2.4.4", 64 | "nestify-anything": "0.0.0", 65 | "snarkdown": "github:mesqueeb/snarkdown#dist" 66 | }, 67 | "ava": { 68 | "require": [ 69 | "@babel/register", 70 | "esm" 71 | ], 72 | "helpers": [ 73 | "**/helpers/**/*" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ui/src/components/EasyForm.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 79 | 80 | 484 | -------------------------------------------------------------------------------- /ui/src/components/fields/EfBtn.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 54 | -------------------------------------------------------------------------------- /ui/src/components/fields/EfDiv.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 63 | -------------------------------------------------------------------------------- /ui/src/components/fields/EfMiniForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 50 | 51 | 183 | -------------------------------------------------------------------------------- /ui/src/components/fields/sharedProps.js: -------------------------------------------------------------------------------- 1 | export const getUsageDocs = fieldTag => `### Usage 2 | 3 | ${fieldTag} is a component that's registered for you; alongside EasyForm and EasyField. 4 | You can use it like: 5 | - \`<${fieldTag} />\` as standalone 6 | - \`\` inside a field (with label & sublabel) 7 | - in an EasyForm "schema" like so: \`component: '${fieldTag}'\`` 8 | -------------------------------------------------------------------------------- /ui/src/components/sharedProps.js: -------------------------------------------------------------------------------- 1 | // prop categories: 2 | // behavior 3 | // content 4 | // general 5 | // model 6 | // state 7 | // style 8 | 9 | const propOf = `This prop can be set on an EasyField or on an EasyForm to make it global.` 10 | 11 | // props both on EasyForm and EasyField 12 | export const mode = { 13 | category: 'state', 14 | type: String, 15 | default: 'edit', 16 | validator: prop => ['edit', 'add', 'view', 'raw'].includes(prop), 17 | values: ['edit', 'add', 'view', 'raw'], 18 | examples: [`'edit'`, `'add'`, `'view'`, `'raw'`], 19 | desc: `The mode represents how fields are rendered 20 | - "edit" or "add" means they can be interacted with 21 | - "view" means they can't 22 | - "raw" means the fields are not generated, just the raw value inside a div 23 | 24 | ${propOf}`, 25 | } 26 | export const labelPosition = { 27 | category: 'style', 28 | type: [String, Function], 29 | default: 'top', 30 | desc: `The position of the label in comparison to the field. ${propOf}`, 31 | values: ['top', 'left'], 32 | examples: ['top', 'left'], 33 | } 34 | export const evaluatedProps = { 35 | category: 'behavior', 36 | type: Array, 37 | default: () => [ 38 | 'component', 39 | 'showCondition', 40 | 'label', 41 | 'subLabel', 42 | 'required', 43 | 'rules', 44 | 'fieldStyle', 45 | 'fieldClasses', 46 | 'componentStyle', 47 | 'componentClasses', 48 | 'disable', 49 | 'events', 50 | 'lang', 51 | ], 52 | desc: `An array with prop names that should be treated as "Evaluated Props" when passed a function. ${propOf}`, 53 | } 54 | export const internalLabels = { 55 | category: 'style', 56 | type: [Boolean, undefined], 57 | required: false, 58 | default: undefined, 59 | desc: `Set to true if the component has its own labels and you do not want the EasyField to show a label. 60 | 61 | When \`true\` subLabels will be passed as a prop called 'hint'. 62 | 63 | ${propOf}`, 64 | } 65 | export const internalErrors = { 66 | category: 'behavior|content', 67 | type: [Boolean, undefined], 68 | required: false, 69 | default: undefined, 70 | desc: `Set to true if the component has its own error handling. This makes sure it passes on props like \`rules\` and does nothing with them in the EasyField. 71 | 72 | Will default to \`true\` for components with these names: QInput, QSelect, EfInput, EfSelect, EfInputDate. 73 | 74 | ${propOf}`, 75 | } 76 | -------------------------------------------------------------------------------- /ui/src/helpers/dateHelpers.js: -------------------------------------------------------------------------------- 1 | import { isString, isDate } from 'is-what' 2 | import { date } from 'quasar' 3 | const { formatDate, adjustDate } = date 4 | 5 | export function dateStamp (date, format) { 6 | if (isString(date)) date = new Date(date) 7 | if (!isDate(date)) return 'null' 8 | if (format === 'short') return formatDate(date, 'YYYY/MM/DD') 9 | if (isString(format)) return formatDate(date, format) 10 | return formatDate(date, 'YYYY年MM月DD日') 11 | } 12 | 13 | export function timeStamp (date) { 14 | if (isString(date)) date = new Date(date) 15 | if (!isDate(date)) return '00:00' 16 | return formatDate(date, 'HH:mm') 17 | } 18 | 19 | export function dateTimeStamp (date) { 20 | return dateStamp(date) + ' ' + timeStamp(date) 21 | } 22 | 23 | /** 24 | * Create a new date object from a dateStamp and timeStamp 25 | * 26 | * @export 27 | * @param {(string|date)} dateStamp eg. 2019-12-31 - a date string or date object 28 | * @param {string} timeStamp eg. 23:59 time in the 24h format 29 | * @returns {date} 30 | */ 31 | export function makeDateFromStamps (dateStamp, timeStamp) { 32 | let date = dateStamp 33 | if (isString(date)) date = new Date(date) 34 | if (!isDate(date)) throw new Error('invalid date parameter') 35 | if (!isString(timeStamp)) return date 36 | const hours = Number(timeStamp.split(':')[0]) 37 | const minutes = Number(timeStamp.split(':')[1]) 38 | return adjustDate(date, { hours, minutes }) 39 | } 40 | 41 | export function numberToHourTimestamp (number) { 42 | if (number < 0) { 43 | number = 24 + number 44 | } 45 | const numberAsString = number < 10 ? `0${String(number)}` : String(number) 46 | return `${numberAsString}:00` 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/helpers/flattenPerSchema.js: -------------------------------------------------------------------------------- 1 | import { flattenObjectProps } from 'flatten-anything' 2 | import { isArray } from 'is-what' 3 | 4 | /** 5 | * Flattens an object to be in line with a schema. 6 | * 7 | * @export 8 | * @param {Object} target the target object 9 | * @param {(Object|Object[])} schema 10 | * @returns {Object} 11 | */ 12 | export default function flattenPerSchema (target, schema) { 13 | const schemaArray = isArray(schema) ? schema : Object.values(schema) 14 | const schemaNestedIds = schemaArray 15 | .map(blueprint => blueprint.id) 16 | .filter(id => id && id.includes('.')) 17 | return flattenObjectProps(target, schemaNestedIds) 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/helpers/focusIfInputEl.js: -------------------------------------------------------------------------------- 1 | export default function focusIfInputEl (e) { 2 | if (!e || !e.srcElement) return 3 | if (e.srcElement.nodeName === 'INPUT') e.srcElement.focus() 4 | if (e.srcElement.nodeName === 'TEXTAREA') e.srcElement.focus() 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/helpers/parseFieldValue.js: -------------------------------------------------------------------------------- 1 | import { isDate, isNumber, isPlainObject, isArray } from 'is-what' 2 | import commafy from 'commafy-anything' 3 | import { dateStamp } from './dateHelpers' 4 | 5 | /** 6 | * takes a value and returns the parsed value based on an EasyField blueprint provided. 7 | * 8 | * @export 9 | * @param {*} value any value. In our example blueprint `1` should be returned as `'one'` 10 | * @param {Object} blueprint a blueprint like eg. 11 | * `{options: [{value: 1, label: 'one'}]}` out of which the "label" will be retrieved. 12 | * Besides `options` you can also have `prefix` and `suffix`. 13 | * When `valueType: 'date'` it will be printed as short date. 14 | * When `valueType: 'number'` it will receive thousand separators. 15 | * @returns {*} the parsed value 16 | */ 17 | export default function parseFieldValue (value, blueprint) { 18 | if (!blueprint) return value 19 | const { valueType, dateFormat, options, multiple, suffix, prefix } = blueprint 20 | let newValue = value 21 | if (isArray(options)) { 22 | if (valueType === 'object' && isPlainObject(value)) { 23 | newValue = multiple 24 | ? Object.values(value) 25 | .filter(v => v) 26 | .join(', ') 27 | : value.label 28 | } else { 29 | const valueArray = !isArray(value) ? [value] : value 30 | newValue = valueArray 31 | .map(selectedValue => { 32 | if (isPlainObject(selectedValue)) return selectedValue.label 33 | const option = options.find(o => o.value === selectedValue) || {} 34 | return option.label || selectedValue 35 | }) 36 | .join(', ') 37 | } 38 | } 39 | if (valueType === 'date' && isDate(value)) newValue = dateStamp(newValue, dateFormat) 40 | if (valueType === 'number' && isNumber(value)) newValue = commafy(newValue) 41 | if (suffix) newValue = `${newValue} ${suffix}` 42 | if (prefix) newValue = `${prefix} ${newValue}` 43 | return newValue 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/helpers/validation.js: -------------------------------------------------------------------------------- 1 | import flattenPerSchema from './flattenPerSchema' 2 | import { isArray, isFunction } from 'is-what' 3 | import defaultLang from '../meta/lang' 4 | 5 | export function createRequiredRule (requiredFieldErrorMsg) { 6 | return val => val === 0 || !!val || requiredFieldErrorMsg 7 | } 8 | 9 | /** 10 | * Validates a field data based on its blueprint 11 | * 12 | * @export 13 | * @param {*} payload 14 | * @param {Blueprint} { rules = [], required } 15 | * @param {Context} context 16 | * @returns {ValidationResultField} 17 | */ 18 | export function validateFieldPerSchema (payload, { rules = [], required }, context = {}) { 19 | const lang = context.lang || defaultLang 20 | const rulesEvaluated = !isFunction(rules) ? rules : rules(payload, context) 21 | const requiredEvaluated = !isFunction(required) ? required : required(payload, context) 22 | const requiredRule = createRequiredRule(lang.requiredField) 23 | const rulesToTest = !requiredEvaluated ? rulesEvaluated : [requiredRule, ...rulesEvaluated] 24 | const results = rulesToTest.reduce((carry, rule) => { 25 | carry.push(rule(payload)) 26 | return carry 27 | }, []) 28 | const hasAnError = Object.values(results).some(result => result !== true) 29 | return !hasAnError || results 30 | } 31 | 32 | /** 33 | * Validates a form data based on its schema 34 | * 35 | * @export 36 | * @param {PlainObject} formData the form data in an object that looks like: `{[fieldId: string]: any}` 37 | * @param {Schema} schema 38 | * @param {StringObject} lang the lang object with at least the key `requiredField` used as error message for required fields 39 | * @returns {ValidationResultForm} 40 | */ 41 | export function validateFormPerSchema (formData, schema, lang) { 42 | const schemaObject = !isArray(schema) 43 | ? schema 44 | : schema.reduce((carry, blueprint) => { 45 | carry[blueprint.id] = blueprint 46 | return carry 47 | }, {}) 48 | const formDataFlatEmpty = Object.keys(schemaObject) 49 | .reduce((carry, key) => ({ ...carry, [key]: null }), {}) // prettier-ignore 50 | const formDataFlatCurrent = flattenPerSchema(formData, schema) 51 | const formDataFlat = { ...formDataFlatEmpty, ...formDataFlatCurrent } 52 | const resultPerField = Object.entries(formDataFlat).reduce((carry, [fieldId, fieldValue]) => { 53 | const blueprint = schemaObject[fieldId] 54 | const context = { formData, formDataFlat, lang } 55 | carry[fieldId] = !blueprint || validateFieldPerSchema(fieldValue, blueprint, context) 56 | return carry 57 | }, {}) 58 | return resultPerField 59 | } 60 | -------------------------------------------------------------------------------- /ui/src/helpers/validation.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from 'is-what' 2 | import { PlainObject, Blueprint, Schema, StringObject } from '../types' 3 | import flattenPerSchema from './flattenPerSchema' 4 | 5 | export type ValidationResultField = boolean | (string | boolean)[] 6 | export type ValidationResultForm = { [fieldId: string]: ValidationResultField } 7 | 8 | /** 9 | * Validates a field data based on its blueprint 10 | * 11 | * @export 12 | * @param {Blueprint} { rules = [], required } 13 | * @param {*} payload 14 | * @param {StringObject} lang 15 | * @returns {ValidationResultField} 16 | */ 17 | export function validateFieldPerSchema ( 18 | { rules = [], required }: Blueprint, 19 | payload: any, 20 | lang: StringObject 21 | ): ValidationResultField { 22 | const requiredRule = (val: any) => val === 0 || !!val || lang.requiredField 23 | const testRules = !required ? rules : [requiredRule, ...rules] 24 | const results = testRules.reduce((carry, rule) => { 25 | carry.push(rule(payload)) 26 | return carry 27 | }, []) 28 | const hasAnError = Object.values(results).some(result => result !== true) 29 | return !hasAnError || results 30 | } 31 | 32 | /** 33 | * Validates a form data based on its schema 34 | * 35 | * @export 36 | * @param {PlainObject} formData the flattened form data in an object that looks like: `{[fieldId: string]: any}` 37 | * @param {Schema} schema 38 | * @param {StringObject} lang the lang object with at least the key `requiredField` used as error message for required fields 39 | * @returns {ValidationResultForm} 40 | */ 41 | export function validateFormPerSchema ( 42 | formData: PlainObject, 43 | schema: Schema, 44 | lang: StringObject 45 | ): ValidationResultForm { 46 | const schemaObject = !isArray(schema) 47 | ? schema 48 | : schema.reduce((carry, blueprint) => { 49 | carry[blueprint.id] = blueprint 50 | return carry 51 | }, {}) 52 | const formDataFlatEmpty = Object.keys(schemaObject) 53 | .reduce((carry, key) => ({ ...carry, [key]: null }), {}) // prettier-ignore 54 | const formDataFlatCurrent = flattenPerSchema(formData, schema) 55 | const formDataFlat = { ...formDataFlatEmpty, ...formDataFlatCurrent } 56 | const resultPerField = Object.entries(formDataFlat).reduce((carry, [fieldId, fieldValue]) => { 57 | const blueprint = schemaObject[fieldId] 58 | carry[fieldId] = validateFieldPerSchema(blueprint, fieldValue, lang) 59 | return carry 60 | }, {}) 61 | return resultPerField 62 | } 63 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json' 2 | 3 | import EasyForm from './components/EasyForm.vue' 4 | import EasyField from './components/EasyField.vue' 5 | 6 | import EfBtn from './components/fields/EfBtn.vue' 7 | import EfDiv from './components/fields/EfDiv.vue' 8 | import EfMiniForm from './components/fields/EfMiniForm.vue' 9 | 10 | import dependencyMap from './meta/dependencyMap' 11 | import { validateFormPerSchema } from './helpers/validation.js' 12 | 13 | export { 14 | version, 15 | EasyForm, 16 | EasyField, 17 | EfBtn, 18 | EfDiv, 19 | EfMiniForm, 20 | dependencyMap, 21 | validateFormPerSchema, 22 | } 23 | 24 | export default { 25 | version, 26 | 27 | EasyForm, 28 | EasyField, 29 | 30 | EfBtn, 31 | EfDiv, 32 | EfMiniForm, 33 | 34 | dependencyMap, 35 | validateFormPerSchema, 36 | 37 | install (Vue) { 38 | Vue.component(EasyField.name, EasyField) 39 | Vue.component(EasyForm.name, EasyForm) 40 | 41 | Vue.component(EfBtn.name, EfBtn) 42 | Vue.component(EfDiv.name, EfDiv) 43 | Vue.component(EfMiniForm.name, EfMiniForm) 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/index.sass: -------------------------------------------------------------------------------- 1 | // when installed as package needs to be: 2 | // @ import 'quasar/src/css/variables.sass' 3 | // when using npm run dev in `dev` folder it needs to be: 4 | // @ import '../node_modules/quasar/src/css/variables.sass' 5 | 6 | @import 'margin-padding.sass' 7 | -------------------------------------------------------------------------------- /ui/src/margin-padding.sass: -------------------------------------------------------------------------------- 1 | 2 | $_space-base: 16px 3 | 4 | $xxs: ($_space-base * .1) 5 | $xs: ($_space-base * .25) 6 | $sm: ($_space-base * .5) 7 | $md: $_space-base 8 | $lg: ($_space-base * 1.5) 9 | $xl: ($_space-base * 2.3) 10 | $xxl: ($_space-base * 3) 11 | $xxxl: ($_space-base * 5) 12 | -------------------------------------------------------------------------------- /ui/src/meta/dependencyMap.js: -------------------------------------------------------------------------------- 1 | import merge from 'merge-anything' 2 | // import { QBtn, QInput } from './quasarPropsJson' 3 | 4 | export const dependencyMap = { 5 | QInput: { 6 | componentName: 'QInput', 7 | component: 'QInput', 8 | passedProps: {}, 9 | }, 10 | EfBtn: { 11 | componentName: 'EfBtn', 12 | component: 'EfBtn', 13 | passedProps: {}, 14 | }, 15 | EfDiv: { 16 | componentName: 'EfDiv', 17 | component: 'EfDiv', 18 | passedProps: {}, 19 | }, 20 | EfMiniForm: { 21 | componentName: 'EfMiniForm', 22 | component: 'EfMiniForm', 23 | passedProps: {}, 24 | }, 25 | } 26 | 27 | export function getPassedProps (component) { 28 | const info = dependencyMap[component] || {} 29 | const { passedProps } = info 30 | return merge(...Object.values(passedProps)) 31 | } 32 | 33 | export default { ...dependencyMap, getPassedProps } 34 | -------------------------------------------------------------------------------- /ui/src/meta/lang.js: -------------------------------------------------------------------------------- 1 | const defaultLang = { 2 | archive: 'Archive', 3 | delete: 'Delete', 4 | cancel: 'Cancel', 5 | edit: 'Edit', 6 | save: 'Save', 7 | requiredField: 'Field is required', 8 | formValidationError: 'There are remaining errors.', 9 | } 10 | 11 | export default defaultLang 12 | -------------------------------------------------------------------------------- /ui/src/meta/lang.ts: -------------------------------------------------------------------------------- 1 | import { StringObject } from '../types' 2 | 3 | const defaultLang: StringObject = { 4 | archive: 'Archive', 5 | delete: 'Delete', 6 | cancel: 'Cancel', 7 | edit: 'Edit', 8 | save: 'Save', 9 | requiredField: 'Field is required', 10 | formValidationError: 'There are remaining errors.', 11 | } 12 | 13 | export default defaultLang 14 | -------------------------------------------------------------------------------- /ui/src/meta/quasarPropsJson.js: -------------------------------------------------------------------------------- 1 | import copy from 'copy-anything' 2 | import { camelCase } from 'case-anything' 3 | import { isArray } from 'is-what' 4 | import QBtnJson from 'quasar/dist/api/QBtn.json' 5 | import QBtnToggleJson from 'quasar/dist/api/QBtnToggle.json' 6 | import QImgJson from 'quasar/dist/api/QImg.json' 7 | import QInputJson from 'quasar/dist/api/QInput.json' 8 | import QRangeJson from 'quasar/dist/api/QRange.json' 9 | import QSelectJson from 'quasar/dist/api/QSelect.json' 10 | import QSliderJson from 'quasar/dist/api/QSlider.json' 11 | import QToggleJson from 'quasar/dist/api/QToggle.json' 12 | import QUploaderJson from 'quasar/dist/api/QUploader.json' 13 | import QVideoJson from 'quasar/dist/api/QVideo.json' 14 | 15 | const stringToTypeFnDictionary = { 16 | Object: Object, 17 | Function: Function, 18 | Array: Array, 19 | String: String, 20 | Number: Number, 21 | Boolean: Boolean, 22 | RegExp: RegExp, 23 | Date: Date, 24 | Symbol: Symbol, 25 | } 26 | const stringToTypeFn = string => stringToTypeFnDictionary[string] 27 | 28 | function jsonToPropFormat ({ props }) { 29 | if (!props) return {} 30 | return Object.entries(props).reduce((carry, [key, value]) => { 31 | value = copy(value) 32 | value.inheritedProp = true 33 | value.type = isArray(value.type) ? value.type.map(stringToTypeFn) : stringToTypeFn(value.type) 34 | carry[camelCase(key)] = value 35 | return carry 36 | }, {}) 37 | } 38 | 39 | export const QBtn = jsonToPropFormat(QBtnJson) 40 | export const QBtnToggle = jsonToPropFormat(QBtnToggleJson) 41 | export const QImg = jsonToPropFormat(QImgJson) 42 | export const QInput = jsonToPropFormat(QInputJson) 43 | export const QRange = jsonToPropFormat(QRangeJson) 44 | export const QSelect = jsonToPropFormat(QSelectJson) 45 | export const QSlider = jsonToPropFormat(QSliderJson) 46 | export const QToggle = jsonToPropFormat(QToggleJson) 47 | export const QUploader = jsonToPropFormat(QUploaderJson) 48 | export const QVideo = jsonToPropFormat(QVideoJson) 49 | 50 | export default { 51 | QBtn, 52 | QBtnToggle, 53 | QImg, 54 | QInput, 55 | QRange, 56 | QSelect, 57 | QSlider, 58 | QToggle, 59 | QUploader, 60 | QVideo, 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type PlainObject = { [key: string]: any } 2 | export type StringObject = { [key: string]: string } 3 | export type Schema = Blueprint[] | { [key: string]: Blueprint } 4 | 5 | export interface Blueprint { 6 | rules?: ((val: any) => boolean | string)[] 7 | required?: boolean 8 | [key: string]: any 9 | } 10 | -------------------------------------------------------------------------------- /ui/test/flattenPerSchema.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import flattenPerSchema from '../src/helpers/flattenPerSchema.js' 3 | 4 | test('flattenPerSchema', t => { 5 | let res, target, schema 6 | target = { 7 | payAsYouGoPrice: [], 8 | price: 0, 9 | timeFee: { d: 23, m: 0, n: 12.97 }, 10 | timeFeeBreakdown: { 11 | d: { from: 8, to: 22 }, 12 | e: { from: 21, to: 21 }, 13 | m: { from: 9, to: 9 }, 14 | n: { from: -2, to: 8 }, 15 | }, 16 | } 17 | schema = { 18 | 'payAsYouGoPrice': { 19 | id: 'payAsYouGoPrice', 20 | label: '使用料別電気単価', 21 | valueType: 'array', 22 | schema: [ 23 | { 24 | id: 'to', 25 | label: '電気量範囲', 26 | valueType: 'number', 27 | prefix: 'kWh以上~', 28 | suffix: 'kWh未満', 29 | }, 30 | { 31 | id: 'price', 32 | label: '電気単価', 33 | valueType: 'number', 34 | suffix: '円/kWh', 35 | }, 36 | ], 37 | showCondition: [['planType', '===', 'payAsYouGo']], 38 | }, 39 | 'timeFee.d': { 40 | id: 'timeFee.d', 41 | label: '昼間の電気単価', 42 | valueType: 'number', 43 | suffix: '円/kW', 44 | showCondition: [['planType', '===', 'allElecPlan']], 45 | }, 46 | 'timeFeeBreakdown.d': { 47 | id: 'timeFeeBreakdown.d', 48 | label: '昼間の適用時間帯', 49 | valueType: 'object', 50 | showCondition: [['planType', '===', 'allElecPlan']], 51 | min: 0, 52 | max: 24, 53 | }, 54 | 'timeFee.n': { 55 | id: 'timeFee.n', 56 | label: '夜間の電気単価', 57 | valueType: 'number', 58 | suffix: '円/kW', 59 | showCondition: [['planType', '===', 'allElecPlan']], 60 | }, 61 | 'timeFeeBreakdown.n': { 62 | id: 'timeFeeBreakdown.n', 63 | label: '夜間の適用時間帯', 64 | valueType: 'object', 65 | showCondition: [['planType', '===', 'allElecPlan']], 66 | min: -12, 67 | max: 12, 68 | }, 69 | 'hasMorningFee': { 70 | id: 'hasMorningFee', 71 | label: '朝夕間の電気料金の有無', 72 | valueType: 'boolean', 73 | showCondition: [['planType', '===', 'allElecPlan']], 74 | }, 75 | 'timeFeeBreakdown.m': { 76 | id: 'timeFeeBreakdown.m', 77 | label: '朝の適用時間帯', 78 | valueType: 'object', 79 | showCondition: [ 80 | ['planType', '===', 'allElecPlan'], 81 | ['hasMorningFee', '===', true], 82 | ], 83 | min: 0, 84 | max: 24, 85 | }, 86 | 'timeFee.m': { 87 | id: 'timeFee.m', 88 | label: '朝夕間の電気単価', 89 | valueType: 'number', 90 | suffix: '円/kW', 91 | showCondition: [ 92 | ['planType', '===', 'allElecPlan'], 93 | ['hasMorningFee', '===', true], 94 | ], 95 | }, 96 | 'timeFeeBreakdown.e': { 97 | id: 'timeFeeBreakdown.e', 98 | label: '夕方の適用時間帯', 99 | valueType: 'object', 100 | showCondition: [ 101 | ['planType', '===', 'allElecPlan'], 102 | ['hasMorningFee', '===', true], 103 | ], 104 | min: 0, 105 | max: 24, 106 | }, 107 | } 108 | res = flattenPerSchema(target, schema) 109 | t.deepEqual(res, { 110 | 'payAsYouGoPrice': [], 111 | 'price': 0, 112 | 'timeFee': {}, 113 | 'timeFee.d': 23, 114 | 'timeFee.m': 0, 115 | 'timeFee.n': 12.97, 116 | 'timeFeeBreakdown': {}, 117 | 'timeFeeBreakdown.d': { from: 8, to: 22 }, 118 | 'timeFeeBreakdown.e': { from: 21, to: 21 }, 119 | 'timeFeeBreakdown.m': { from: 9, to: 9 }, 120 | 'timeFeeBreakdown.n': { from: -2, to: 8 }, 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /ui/test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | test('t', t => { 4 | t.pass() 5 | }) 6 | -------------------------------------------------------------------------------- /ui/test/parseFieldValue.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable prefer-const */ 3 | import test from 'ava' 4 | import { flattenObject } from 'flatten-anything' 5 | import nestify from 'nestify-anything' 6 | import parseFieldValue from '../src/helpers/parseFieldValue.js' 7 | 8 | const parseObject = (target, schema) => { 9 | const flat = flattenObject(target) 10 | const parsed = Object.entries(flat).reduce((carry, [key, value]) => { 11 | const blueprint = schema.find(bp => bp.id === key) || {} 12 | carry[key] = parseFieldValue(value, blueprint) 13 | return carry 14 | }, {}) 15 | return nestify(parsed) 16 | } 17 | 18 | test('options', t => { 19 | let res, target, schema 20 | target = { chargeCycle: 2 } 21 | schema = [ 22 | { 23 | id: 'chargeCycle', 24 | label: '充電サイクル', 25 | valueType: 'number', 26 | options: [ 27 | { label: '1回', value: 1 }, 28 | { label: '2回', value: 2 }, 29 | ], 30 | }, 31 | ] 32 | res = parseObject(target, schema) 33 | t.deepEqual(res, { chargeCycle: '2回' }) 34 | }) 35 | 36 | test('suffix', t => { 37 | let res, target, schema 38 | target = { batteryCapacity: 1 } 39 | schema = [ 40 | { 41 | id: 'batteryCapacity', 42 | label: '定格容量', 43 | valueType: 'number', 44 | suffix: 'kWh', 45 | }, 46 | ] 47 | res = parseObject(target, schema) 48 | t.deepEqual(res, { batteryCapacity: '1kWh' }) 49 | }) 50 | 51 | test('prefix', t => { 52 | let res, target, schema 53 | target = { price: 1 } 54 | schema = [ 55 | { 56 | id: 'price', 57 | valueType: 'number', 58 | prefix: '¥', 59 | }, 60 | ] 61 | res = parseObject(target, schema) 62 | t.deepEqual(res, { price: '¥1' }) 63 | }) 64 | 65 | test('nested props', t => { 66 | let res, target, schema 67 | target = { sizes: { d: 1, h: 2 } } 68 | schema = [ 69 | { 70 | id: 'sizes.d', 71 | suffix: 'm', 72 | }, 73 | ] 74 | res = parseObject(target, schema) 75 | t.deepEqual(res, { sizes: { d: '1m', h: 2 } }) 76 | }) 77 | 78 | test('options, suffix & prefix', t => { 79 | let res, target, schema 80 | target = { chargeCycle: 2 } 81 | schema = [ 82 | { 83 | id: 'chargeCycle', 84 | options: [ 85 | { label: '1回', value: 1 }, 86 | { label: '2回', value: 2 }, 87 | ], 88 | prefix: 'a', 89 | suffix: 'z', 90 | }, 91 | ] 92 | res = parseObject(target, schema) 93 | t.deepEqual(res, { chargeCycle: 'a2回z' }) 94 | }) 95 | -------------------------------------------------------------------------------- /ui/umd-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | UMD test 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | quasar-ui-easy-forms v{{ version }} 22 | 23 | 24 |
Quasar v{{ $q.version }}
25 |
26 |
27 | 28 | 29 | 30 |
    31 |
  • In /ui, run: "yarn build"
  • 32 |
  • You need to build & refresh page on each change manually.
  • 33 |
  • Use self-closing tags only!
  • 34 |
  • Example: <my-component></my-component>
  • 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 57 | 58 | 59 | --------------------------------------------------------------------------------