├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ └── Dialog.vue ├── dialog.js └── main.js └── tests └── unit └── dialog.spec.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | }, 14 | overrides: [ 15 | { 16 | files: [ 17 | "**/__tests__/*.{j,t}s?(x)", 18 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 19 | ], 20 | env: { 21 | jest: true 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Renderless Dialogs (Alert, Confirm, Prompt) in Vue 2 | 3 | See the full article at [https://danielkelly.io/blog/renderless-vue-dialog](https://danielkelly.io/blog/renderless-vue-dialog) 4 | 5 | ### Demo 6 | 1. Clone the repo 7 | 2. `npm install` 8 | 3. `npm run serve` 9 | 10 | ### Use 11 | Dialog object exists in `src/dialog.js`. If enough people find this useful I can turn it into a npm package. Won't know to do that though unless you reach out and tell me it's useful. 12 | ```javascript 13 | /* 14 | * Alert 15 | */ 16 | await dialog.alert(message) 17 | // carry on only after alert is dismissed 18 | 19 | /* 20 | * Confirm 21 | */ 22 | const confirmed = await dialog.confirm(message, optionalTitle, options) 23 | if(confirmed){ 24 | // do the thing needing confirming 25 | }else{ 26 | // don't 27 | } 28 | 29 | /* 30 | * Prompt 31 | */ 32 | const value = await dialog.prompt(message) 33 | console.log(value) // the result of the prompt 34 | ``` 35 | 36 | ### Options 37 | All options are set via chainable methods 38 | ```javascript 39 | await dialog 40 | .title('Prompt Title') 41 | .inputType('number') 42 | .cancelText('Nevermind') 43 | .okText('Absolutely') 44 | .html() 45 | .prompt('Hello message with html') 46 | ``` 47 | 48 | ### Follow Me 49 | Twitter: [@danielkelly_io](https://twitter.com/danielkelly_io) 50 | 51 | Github: [@danielkellyio](https://github.com/danielkellyio) 52 | 53 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest" 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renderless-dialog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.6.5", 13 | "vue": "^2.6.11" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.5.0", 17 | "@vue/cli-plugin-eslint": "^4.5.0", 18 | "@vue/cli-plugin-unit-jest": "^4.5.0", 19 | "@vue/cli-service": "^4.5.0", 20 | "@vue/eslint-config-prettier": "^6.0.0", 21 | "@vue/test-utils": "^1.0.3", 22 | "babel-eslint": "^10.1.0", 23 | "eslint": "^6.7.2", 24 | "eslint-plugin-prettier": "^3.1.3", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "prettier": "^1.19.1", 27 | "vue-template-compiler": "^2.6.11" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielkellyio/renderless-dialog/e5768c3cf8b1542fb27b6cc1835f82324d16859d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | 53 | 61 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielkellyio/renderless-dialog/e5768c3cf8b1542fb27b6cc1835f82324d16859d/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | 33 | 53 | -------------------------------------------------------------------------------- /src/dialog.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | const state = Vue.observable({ 3 | type: 'alert', 4 | active: false, 5 | message: '', 6 | title: '', 7 | okText: 'Ok', 8 | cancelText: 'Cancel', 9 | inputType: 'text', 10 | html: false, 11 | }) 12 | 13 | // ----------------------------------- 14 | // Private Methods 15 | // ----------------------------------- 16 | let close // will hold our promise resolve function 17 | const dialogPromise = () => new Promise((resolve) => (close = resolve)) 18 | const open = (message) => { 19 | state.message = message 20 | state.active = true 21 | return dialogPromise() 22 | } 23 | const reset = () => { 24 | state.active = false 25 | state.message = '' 26 | state.okText = 'Ok' 27 | state.cancelText = 'Cancel' 28 | state.inputType = 'text' 29 | state.html = false 30 | state.title = '' 31 | state.type = 'alert' 32 | } 33 | 34 | // ----------------------------------- 35 | // Public interface 36 | // ----------------------------------- 37 | 38 | const dialog = { 39 | get state() { 40 | return state 41 | }, 42 | title(title) { 43 | state.title = title 44 | return this 45 | }, 46 | okText(text) { 47 | state.okText = text 48 | return this 49 | }, 50 | 51 | cancelText(text) { 52 | state.cancelText = text 53 | return this 54 | }, 55 | inputType(type) { 56 | state.inputType = type 57 | return this 58 | }, 59 | html(enabled = true) { 60 | state.html = enabled 61 | return this 62 | }, 63 | alert(message) { 64 | state.type = 'alert' 65 | return open(message) 66 | }, 67 | confirm(message) { 68 | state.type = 'confirm' 69 | return open(message) 70 | }, 71 | prompt(message) { 72 | state.type = 'prompt' 73 | return open(message) 74 | }, 75 | cancel() { 76 | close(false) 77 | reset() 78 | }, 79 | ok(input = true) { 80 | input = state.type === 'prompt' ? input : true 81 | close(input) 82 | reset() 83 | }, 84 | } 85 | 86 | export default dialog 87 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount("#app"); 9 | -------------------------------------------------------------------------------- /tests/unit/dialog.spec.js: -------------------------------------------------------------------------------- 1 | import dialog from '@/dialog' 2 | 3 | describe('dialog object', ()=>{ 4 | test('dialog.alert() assigns message to the dialog', ()=>{ 5 | dialog.alert('hello world') 6 | expect(dialog.state.message).toBe('hello world') 7 | }) 8 | 9 | test('dialog.alert() waits until user closes it to be inactive', (done)=>{ 10 | dialog.alert('hello world').then(async ()=>{ 11 | expect(dialog.state.active).toBe(false) 12 | done() 13 | }) 14 | expect(dialog.state.active).toBe(true); 15 | setTimeout(dialog.ok, 100) 16 | }) 17 | 18 | test('can do multiple alerts one after another',()=>{ 19 | dialog.alert('hello world') 20 | expect(dialog.state.active).toBe(true); 21 | expect(dialog.state.message).toBe('hello world'); 22 | dialog.ok() 23 | 24 | dialog.alert('hello world 2') 25 | expect(dialog.state.active).toBe(true); 26 | expect(dialog.state.message).toBe('hello world 2'); 27 | dialog.ok() 28 | }) 29 | 30 | test('dialog.confirm() can be canceled', (done)=>{ 31 | dialog.confirm('are you sure').then((confirmed)=>{ 32 | expect(confirmed).toBe(false) 33 | done() 34 | }) 35 | dialog.cancel() 36 | }) 37 | 38 | test('dialog.confirm() can be confirmed', (done)=>{ 39 | dialog.confirm('are you sure').then((confirmed)=>{ 40 | expect(confirmed).toBe(true) 41 | done() 42 | }) 43 | dialog.ok() 44 | }) 45 | 46 | test('dialog.prompt() can be return user input', (done)=>{ 47 | dialog.prompt('do you like dogs?').then((userInput)=>{ 48 | expect(userInput).toBe('of course!') 49 | done() 50 | }) 51 | dialog.ok('of course!') 52 | }) 53 | 54 | test('dialog options can be chained', (done)=>{ 55 | dialog 56 | .title('Dialog title') 57 | .html() 58 | .okText('Delete') 59 | .cancelText('Dont delete') 60 | .inputType('autofill') 61 | .prompt('Why are you deleting this thing>').then((userInput)=>{ 62 | expect(userInput).toBe('It stinks') 63 | done() 64 | }) 65 | expect(dialog.state.title).toBe('Dialog title') 66 | expect(dialog.state.html).toBe(true) 67 | expect(dialog.state.okText).toBe('Delete') 68 | expect(dialog.state.cancelText).toBe('Dont delete') 69 | expect(dialog.state.inputType).toBe('autofill') 70 | dialog.ok('It stinks') 71 | }) 72 | 73 | test('dialog is reset on close', (done)=>{ 74 | dialog 75 | .title('Dialog title') 76 | .html() 77 | .okText('Delete') 78 | .cancelText('Dont delete') 79 | .inputType('autofill') 80 | .prompt('Why are you deleting this thing>').then((userInput)=>{ 81 | expect(dialog.state.title).toBe('') 82 | expect(dialog.state.html).toBe(false) 83 | expect(dialog.state.okText).toBe('Ok') 84 | expect(dialog.state.cancelText).toBe('Cancel') 85 | expect(dialog.state.inputType).toBe('text') 86 | done() 87 | }) 88 | dialog.ok('It stinks') 89 | }) 90 | }) 91 | --------------------------------------------------------------------------------