Dialog: remove v-model, add append-to-body

This commit is contained in:
Leopoldthecoder 2017-07-26 16:39:32 +08:00 committed by 杨奕
parent 41c19249ab
commit 7f70ea6726
7 changed files with 175 additions and 69 deletions

View File

@ -22,6 +22,8 @@
dialogVisible: false, dialogVisible: false,
dialogTableVisible: false, dialogTableVisible: false,
dialogFormVisible: false, dialogFormVisible: false,
outerVisible: false,
innerVisible: false,
form: { form: {
name: '', name: '',
region: '', region: '',
@ -86,7 +88,7 @@ Dialog pops up a dialog box, and it's quite customizable.
<el-dialog <el-dialog
title="Tips" title="Tips"
:visible.sync="dialogVisible" :visible.sync="dialogVisible"
size="tiny" width="30%"
:before-close="handleClose"> :before-close="handleClose">
<span>This is a message</span> <span>This is a message</span>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
@ -116,6 +118,10 @@ Dialog pops up a dialog box, and it's quite customizable.
``` ```
::: :::
:::tip
`before-close` only works when user clicks the close icon or the backdrop. If you have buttons that close the Dialog in the `footer` named slot, you can add what you would do with `before-close` in the buttons' click event handler.
:::
### Customizations ### Customizations
The content of Dialog can be anything, even a table or a form. This example shows how to use Element Table and Form with Dialog。 The content of Dialog can be anything, even a table or a form. This example shows how to use Element Table and Form with Dialog。
@ -196,16 +202,56 @@ The content of Dialog can be anything, even a table or a form. This example show
``` ```
::: :::
### Nested Dialog
If a Dialog is nested in another Dialog, `append-to-body` is required.
:::demo Normally we do not recommend using nested Dialog. If you need multiple Dialogs rendered on the page, you can simply flat them so that they're siblings to each other. If you must nest a Dialog inside another Dialog, set `append-to-body` of the nested Dialog to true, and it will append to body instead of its parent node, so both Dialogs can be correctly rendered.
```html
<template>
<el-button type="text" @click="outerVisible = true">open the outer Dialog</el-button>
<el-dialog title="外层 Dialog" :visible.sync="outerVisible">
<el-dialog
width="30%"
title="内层 Dialog"
:visible.sync="innerVisible"
append-to-body>
</el-dialog>
<div slot="footer" class="dialog-footer">
<el-button @click="outerVisible = false">Cancel</el-button>
<el-button type="primary" @click="innerVisible = true">open the inner Dialog</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
outerVisible: false,
innerVisible: false
};
}
}
</script>
```
:::
:::tip
If the variable bound to `visible` is managed in Vuex store, the `.sync` can not work properly. In this case, please remove the `.sync` modifier, listen to `open` and `close` events of Dialog, and commit Vuex mutations to update the value of that variable in the event handlers.
:::
### Attributes ### Attributes
| Attribute | Description | Type | Accepted Values | Default | | Attribute | Description | Type | Accepted Values | Default |
|---------- |-------------- |---------- |-------------------------------- |-------- | |---------- |-------------- |---------- |-------------------------------- |-------- |
| visible | visibility of Dialog, supports the .sync modifier | boolean | — | false | | visible | visibility of Dialog, supports the .sync modifier | boolean | — | false |
| title | title of Dialog. Can also be passed with a named slot (see the following table) | string | — | — | | title | title of Dialog. Can also be passed with a named slot (see the following table) | string | — | — |
| size | size of Dialog | string | tiny/small/large/full | small | | width | width of Dialog | string | — | 50% |
| top | value for `top` of Dialog CSS, works when `size` is not `full` | string | — | 15% | | fullscreen | whether the Dialog takes up full screen | boolean | — | false |
| top | value for `margin-top` of Dialog CSS, works when `size` is not `full` | string | — | 15vh |
| modal | whether a mask is displayed | boolean | — | true | | modal | whether a mask is displayed | boolean | — | true |
| modal-append-to-body | whether to append modal to body element. If false, the modal will be appended to Dialog's parent element | boolean | — | true | | modal-append-to-body | whether to append modal to body element. If false, the modal will be appended to Dialog's parent element | boolean | — | true |
| append-to-body | whether to append Dialog itself to body. A nested Dialog should have this attribute set to `true` | boolean | — | false |
| lock-scroll | whether scroll of body is disabled while Dialog is displayed | boolean | — | true | | lock-scroll | whether scroll of body is disabled while Dialog is displayed | boolean | — | true |
| custom-class | custom class names for Dialog | string | — | — | | custom-class | custom class names for Dialog | string | — | — |
| close-on-click-modal | whether the Dialog can be closed by clicking the mask | boolean | — | true | | close-on-click-modal | whether the Dialog can be closed by clicking the mask | boolean | — | true |

View File

@ -19,9 +19,11 @@
name: '王小虎', name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄' address: '上海市普陀区金沙江路 1518 弄'
}], }],
dialogVisible: false, dialogVisible: true,
dialogTableVisible: false, dialogTableVisible: false,
dialogFormVisible: false, dialogFormVisible: false,
outerVisible: false,
innerVisible: false,
form: { form: {
name: '', name: '',
region: '', region: '',
@ -76,7 +78,7 @@
Dialog 弹出一个对话框,适合需要定制性更大的场景。 Dialog 弹出一个对话框,适合需要定制性更大的场景。
:::demo 需要设置`visible`属性,它接收`Boolean`,当为`true`时显示 Dialog。Dialog 分为两个部分:`body`和`footer``footer`需要具名为`footer`的`slot`。`title`属性用于定义标题,它是可选的,默认值为空。最后,本例还展示了`beforeClose`的用法。 :::demo 需要设置`visible`属性,它接收`Boolean`,当为`true`时显示 Dialog。Dialog 分为两个部分:`body`和`footer``footer`需要具名为`footer`的`slot`。`title`属性用于定义标题,它是可选的,默认值为空。最后,本例还展示了`before-close`的用法。
```html ```html
<el-button type="text" @click="dialogVisible = true">点击打开 Dialog</el-button> <el-button type="text" @click="dialogVisible = true">点击打开 Dialog</el-button>
@ -84,7 +86,7 @@ Dialog 弹出一个对话框,适合需要定制性更大的场景。
<el-dialog <el-dialog
title="提示" title="提示"
:visible.sync="dialogVisible" :visible.sync="dialogVisible"
size="tiny" width="30%"
:before-close="handleClose"> :before-close="handleClose">
<span>这是一段信息</span> <span>这是一段信息</span>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
@ -114,6 +116,10 @@ Dialog 弹出一个对话框,适合需要定制性更大的场景。
``` ```
::: :::
:::tip
`before-close` 仅当用户通过点击关闭图标或遮罩关闭 Dialog 时起效。如果你在 `footer` 具名 slot 里添加了用于关闭 Dialog 的按钮,那么可以在按钮的点击回调函数里加入 `before-close` 的相关逻辑。
:::
### 自定义内容 ### 自定义内容
Dialog 组件的内容可以是任意的,甚至可以是表格或表单,下面是应用了 Element Table 和 Form 组件的两个样例。 Dialog 组件的内容可以是任意的,甚至可以是表格或表单,下面是应用了 Element Table 和 Form 组件的两个样例。
@ -134,7 +140,7 @@ Dialog 组件的内容可以是任意的,甚至可以是表格或表单,下
<!-- Form --> <!-- Form -->
<el-button type="text" @click="dialogFormVisible = true">打开嵌套表单的 Dialog</el-button> <el-button type="text" @click="dialogFormVisible = true">打开嵌套表单的 Dialog</el-button>
<el-dialog title="收货地址" :visible.sync="dialogFormVisible"> <el-dialog title="收货地址" :visible.sync="dialogFormVisible" fullscreen>
<el-form :model="form"> <el-form :model="form">
<el-form-item label="活动名称" :label-width="formLabelWidth"> <el-form-item label="活动名称" :label-width="formLabelWidth">
<el-input v-model="form.name" auto-complete="off"></el-input> <el-input v-model="form.name" auto-complete="off"></el-input>
@ -193,15 +199,55 @@ Dialog 组件的内容可以是任意的,甚至可以是表格或表单,下
``` ```
::: :::
### 嵌套的 Dialog
如果需要在一个 Dialog 内部嵌套另一个 Dialog需要使用 `append-to-body` 属性。
:::demo 正常情况下,我们不建议使用嵌套的 Dialog如果需要在页面上同时显示多个 Dialog可以将它们平级放置。对于确实需要嵌套 Dialog 的场景,我们提供了`append-to-body`属性。将内层 Dialog 的该属性设置为 true它就会插入至 body 元素上,从而保证内外层 Dialog 和遮罩层级关系的正确。
```html
<template>
<el-button type="text" @click="outerVisible = true">点击打开外层 Dialog</el-button>
<el-dialog title="外层 Dialog" :visible.sync="outerVisible">
<el-dialog
width="30%"
title="内层 Dialog"
:visible.sync="innerVisible"
append-to-body>
</el-dialog>
<div slot="footer" class="dialog-footer">
<el-button @click="outerVisible = false">取 消</el-button>
<el-button type="primary" @click="innerVisible = true">打开内层 Dialog</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
outerVisible: false,
innerVisible: false
};
}
}
</script>
```
:::
:::tip
如果 `visible` 属性绑定的变量位于 Vuex 的 store 内,那么 `.sync` 不会正常工作。此时需要去除 `.sync` 修饰符,同时监听 Dialog 的 `open``close` 事件,在事件回调中执行 Vuex 中对应的 mutation 更新 `visible` 属性绑定的变量的值。
:::
### Attributes ### Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------------- |---------- |-------------------------------- |-------- | |---------- |-------------- |---------- |-------------------------------- |-------- |
| visible | 是否显示 Dialog支持 .sync 修饰符 | boolean | — | false | | visible | 是否显示 Dialog支持 .sync 修饰符 | boolean | — | false |
| title | Dialog 的标题,也可通过具名 slot (见下表)传入 | string | — | — | | title | Dialog 的标题,也可通过具名 slot (见下表)传入 | string | — | — |
| size | Dialog 的大小 | string | tiny/small/large/full | small | | width | Dialog 的宽度 | string | — | 50% |
| top | Dialog CSS 中的 top 值(仅在 size 不为 full 时有效) | string | — | 15% | | fullscreen | 是否为全屏 Dialog | boolean | — | false |
| top | Dialog CSS 中的 margin-top 值(仅在 size 不为 full 时有效) | string | — | 15vh |
| modal | 是否需要遮罩层 | boolean | — | true | | modal | 是否需要遮罩层 | boolean | — | true |
| modal-append-to-body | 遮罩层是否插入至 body 元素上,若为 false则遮罩层会插入至 Dialog 的父元素上 | boolean | — | true | | modal-append-to-body | 遮罩层是否插入至 body 元素上,若为 false则遮罩层会插入至 Dialog 的父元素上 | boolean | — | true |
| append-to-body | Dialog 自身是否插入至 body 元素上。嵌套的 Dialog 必须指定该属性并赋值为 true | boolean | — | false |
| lock-scroll | 是否在 Dialog 出现时将 body 滚动锁定 | boolean | — | true | | lock-scroll | 是否在 Dialog 出现时将 body 滚动锁定 | boolean | — | true |
| custom-class | Dialog 的自定义类名 | string | — | — | | custom-class | Dialog 的自定义类名 | string | — | — |
| close-on-click-modal | 是否可以通过点击 modal 关闭 Dialog | boolean | — | true | | close-on-click-modal | 是否可以通过点击 modal 关闭 Dialog | boolean | — | true |

View File

@ -3,15 +3,19 @@
<div class="el-dialog__wrapper" v-show="visible" @click.self="handleWrapperClick"> <div class="el-dialog__wrapper" v-show="visible" @click.self="handleWrapperClick">
<div <div
class="el-dialog" class="el-dialog"
:class="[sizeClass, customClass]" :class="[{ 'is-fullscreen': fullscreen }, customClass]"
ref="dialog" ref="dialog"
:style="style"> :style="style">
<div class="el-dialog__header"> <div class="el-dialog__header">
<slot name="title"> <slot name="title">
<span class="el-dialog__title">{{title}}</span> <span class="el-dialog__title">{{ title }}</span>
</slot> </slot>
<button type="button" class="el-dialog__headerbtn" aria-label="Close" <button
v-if="showClose" @click="handleClose"> type="button"
class="el-dialog__headerbtn"
aria-label="Close"
v-if="showClose"
@click="handleClose">
<i class="el-dialog__close el-icon el-icon-close"></i> <i class="el-dialog__close el-icon el-icon-close"></i>
</button> </button>
</div> </div>
@ -49,6 +53,11 @@
default: true default: true
}, },
appendToBody: {
type: Boolean,
default: false
},
lockScroll: { lockScroll: {
type: Boolean, type: Boolean,
default: true default: true
@ -69,10 +78,9 @@
default: true default: true
}, },
size: { width: String,
type: String,
default: 'small' fullscreen: Boolean,
},
customClass: { customClass: {
type: String, type: String,
@ -81,7 +89,7 @@
top: { top: {
type: String, type: String,
default: '15%' default: '15vh'
}, },
beforeClose: Function beforeClose: Function
}, },
@ -95,6 +103,9 @@
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.dialog.scrollTop = 0; this.$refs.dialog.scrollTop = 0;
}); });
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} else { } else {
this.$el.removeEventListener('scroll', this.updatePopper); this.$el.removeEventListener('scroll', this.updatePopper);
this.$emit('close'); this.$emit('close');
@ -103,11 +114,15 @@
}, },
computed: { computed: {
sizeClass() {
return `el-dialog--${ this.size }`;
},
style() { style() {
return this.size === 'full' ? {} : { 'top': this.top }; let style = {};
if (this.width) {
style.width = this.width;
}
if (!this.fullscreen) {
style.marginTop = this.top;
}
return style;
} }
}, },
@ -126,7 +141,6 @@
hide(cancel) { hide(cancel) {
if (cancel !== false) { if (cancel !== false) {
this.$emit('update:visible', false); this.$emit('update:visible', false);
this.$emit('visible-change', false);
} }
}, },
updatePopper() { updatePopper() {
@ -139,6 +153,9 @@
if (this.visible) { if (this.visible) {
this.rendered = true; this.rendered = true;
this.open(); this.open();
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} }
} }
}; };

View File

@ -405,9 +405,6 @@
-------------------------- */ -------------------------- */
--dialog-background-color: var(--color-secondary); --dialog-background-color: var(--color-secondary);
--dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); --dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--dialog-tiny-width: 30%;
--dialog-small-width: 50%;
--dialog-large-width: 90%;
--dialog-close-color: var(--color-extra-light-silver); --dialog-close-color: var(--color-extra-light-silver);
--dialog-close-hover-color: var(--color-primary); --dialog-close-hover-color: var(--color-primary);
--dialog-title-font-size: 16px; --dialog-title-font-size: 16px;

View File

@ -5,30 +5,17 @@
@component-namespace el { @component-namespace el {
@b dialog { @b dialog {
position: absolute; position: relative;
left: 50%; margin: 0 auto 50px;
transform: translateX(-50%);
background: var(--color-white); background: var(--color-white);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
box-shadow: var(--dialog-box-shadow); box-shadow: var(--dialog-box-shadow);
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 50px; width: 50%;
@modifier tiny { @when fullscreen {
width: var(--dialog-tiny-width);
}
@modifier small {
width: var(--dialog-small-width);
}
@modifier large {
width: var(--dialog-large-width);
}
@modifier full {
width: 100%; width: 100%;
top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
height: 100%; height: 100%;
overflow: auto; overflow: auto;

View File

@ -49,10 +49,6 @@ const getDOM = function(dom) {
}; };
export default { export default {
model: {
prop: 'visible',
event: 'visible-change'
},
props: { props: {
visible: { visible: {
type: Boolean, type: Boolean,
@ -145,7 +141,6 @@ export default {
open(options) { open(options) {
if (!this.rendered) { if (!this.rendered) {
this.rendered = true; this.rendered = true;
this.$emit('visible-change', true);
} }
const props = merge({}, this.$props || this, options); const props = merge({}, this.$props || this, options);
@ -174,8 +169,6 @@ export default {
this._opening = true; this._opening = true;
this.$emit('visible-change', true);
const dom = getDOM(this.$el); const dom = getDOM(this.$el);
const modal = props.modal; const modal = props.modal;
@ -245,7 +238,6 @@ export default {
}, },
doClose() { doClose() {
this.$emit('visible-change', false);
this._closing = true; this._closing = true;
this.onClose && this.onClose(); this.onClose && this.onClose();

View File

@ -10,7 +10,7 @@ describe('Dialog', () => {
vm = createVue({ vm = createVue({
template: ` template: `
<div> <div>
<el-dialog :title="title" v-model="visible"></el-dialog> <el-dialog :title="title" :visible="visible"></el-dialog>
</div> </div>
`, `,
@ -34,7 +34,7 @@ describe('Dialog', () => {
vm = createVue({ vm = createVue({
template: ` template: `
<div> <div>
<el-dialog :title="title" v-model="visible"> <el-dialog :title="title" :visible="visible">
<span>这是一段信息</span> <span>这是一段信息</span>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click.native="dialogVisible = false">取消</el-button> <el-button @click.native="dialogVisible = false">取消</el-button>
@ -61,11 +61,33 @@ describe('Dialog', () => {
}, 100); }, 100);
}); });
it('append to body', done => {
vm = createVue({
template: `
<div>
<el-dialog :title="title" append-to-body :visible="visible"></el-dialog>
</div>
`,
data() {
return {
title: 'dialog test',
visible: true
};
}
}, true);
const dialog = vm.$children[0];
setTimeout(() => {
expect(dialog.$el.parentNode).to.equal(document.body);
done();
}, 10);
});
it('open and close', done => { it('open and close', done => {
vm = createVue({ vm = createVue({
template: ` template: `
<div> <div>
<el-dialog :title="title" v-model="visible"> <el-dialog :title="title" :visible.sync="visible">
<span>这是一段信息</span> <span>这是一段信息</span>
</el-dialog> </el-dialog>
</div> </div>
@ -83,11 +105,11 @@ describe('Dialog', () => {
vm.visible = true; vm.visible = true;
setTimeout(() => { setTimeout(() => {
expect(dialog.$el.style.display).to.not.equal('none'); expect(dialog.$el.style.display).to.not.equal('none');
document.querySelector('.v-modal').click(); vm.visible = false;
setTimeout(() => { setTimeout(() => {
expect(vm.visible).to.equal(false); expect(dialog.$el.style.display).to.equal('none');
done(); done();
}, 50); }, 400);
}, 50); }, 50);
}); });
@ -96,7 +118,7 @@ describe('Dialog', () => {
return createVue(Object.assign({ return createVue(Object.assign({
template: ` template: `
<div> <div>
<el-dialog ${ props } :title="title" v-model="visible"> <el-dialog ${ props } :title="title" :visible="visible">
<span>这是一段信息</span> <span>这是一段信息</span>
</el-dialog> </el-dialog>
</div> </div>
@ -111,14 +133,14 @@ describe('Dialog', () => {
}, options), true); }, options), true);
}; };
it('size', () => { it('fullscreen', () => {
vm = getDialogVm('size="full"'); vm = getDialogVm('fullscreen');
expect(vm.$el.querySelector('.el-dialog').classList.contains('el-dialog--full')).to.true; expect(vm.$el.querySelector('.el-dialog').classList.contains('is-fullscreen')).to.true;
}); });
it('top', () => { it('top', () => {
vm = getDialogVm('top="100px"'); vm = getDialogVm('top="100px"');
expect(vm.$el.querySelector('.el-dialog').style.top).to.equal('100px'); expect(vm.$el.querySelector('.el-dialog').style.marginTop).to.equal('100px');
}); });
it('custom-class', () => { it('custom-class', () => {
@ -127,7 +149,7 @@ describe('Dialog', () => {
}); });
}); });
it('callbacks', done => { it('events', done => {
vm = createVue({ vm = createVue({
template: ` template: `
<div> <div>
@ -135,7 +157,7 @@ describe('Dialog', () => {
@open="handleOpen" @open="handleOpen"
@close="handleClose" @close="handleClose"
:title="title" :title="title"
v-model="visible"> :visible.sync="visible">
<span>这是一段信息</span> <span>这是一段信息</span>
</el-dialog> </el-dialog>
</div> </div>
@ -159,11 +181,10 @@ describe('Dialog', () => {
}; };
} }
}, true); }, true);
const dialog = vm.$children[0]; vm.visible = true;
dialog.open();
setTimeout(() => { setTimeout(() => {
expect(vm.state).to.equal('open'); expect(vm.state).to.equal('open');
dialog.close(); vm.visible = false;
setTimeout(() => { setTimeout(() => {
expect(vm.state).to.equal('closed'); expect(vm.state).to.equal('closed');
done(); done();