├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Answer: {{answer}}
10 |
11 |
12 |
13 |
14 | Confirmed: {{confirmed ? 'Yes' : 'No' }}
15 |
16 |
17 |
18 |
19 |
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 |
2 |
3 |
4 |
{{dialog.state.title}}
5 |
6 |
7 |
{{ dialog.state.message }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------