如何编写一个 Vue JS 内嵌组件

file

Vue 「内嵌」组件是一种用于将第三方插件与 Vue 集成在一个自定义 Vue 组件中的技术。

内嵌意味着你可能会引入像 jQuery 和 jQuery 插件这样的库。在 Vue 应用程序中使用 jQuery 插件的这种组合似乎是最无处不在的。

我们快速介绍一下如何使用日期范围选择器构建内嵌组件的例子,这个组件允许你选择开始日期和结束日期。 在这个例子中,将使用 Date Range Picker,它是一个依赖于 jQuery 和 Moment.js 的 Bootstrap 组件。

你可以简单地在 jQuery 项目中使用这个组件:

$('input[name="daterange"]').daterangepicker();

在本文的例子中,如果想让它成为一个可以重用的 Vue 组件,就要这样:

<date-range-picker @apply="onDateChange"></date-range-picker>

正如上面 jQuery 项目中使用它的例子一样,这个插件得使用 jQuery 选择器,所以需要我们在组件中复制它。要初始化组件,我们可以在组件的生命周期事件 mounted() 中做如下的事情:

mounted: function () {
    this.$nextTick(function () {
        window.$(this.$el).daterangepicker();
    }
}

在这个组件中,我们使用 $nextTick 来确保 Vue 完成数据更改后更新 DOM。 我们还为此使用了组件中的根 DOM 元素 this.$el 属性。 然而,想象是美好的。就算这么写,Vue 组件还是无法对所做的更改作出反应。(我们经常以为行得通却总因此折腾很久)

理解你的组件不应该超出组件的上下文是很重要的。 最好的方法是在组件上使用根元素来使用 this.$el

如果想让我们的组件对日期范围选择器插件所做的更改作出反应,那么我们需要连接一个 jQuery 事件来响应更改:

Vue.component('date-range-picker', {
    template: '#date-range-template',
    props: {
        startDate: {
            default: function () {
                return moment().subtract(30, 'days');
            }
        },
        endDate: {
            default: function () {
                return moment();
            }
        }
    },
    data: function () {
        return {
            start: this.startDate,
            end: this.endDate
        };
    },
    computed: {
        dateRange: function () {
            var start = moment(this.start);
            var end = moment(this.end);

            return start.format('LL') + ' - ' + end.format('LL');
        }
    },
    mounted: function () {
        var vm = this;
        this.start = moment(this.start);
        this.end = moment(this.end);
        this.$nextTick(function () {
            var options = {
                startDate: this.start,
                endDate: this.end,
                alwaysShowCalendars: true
            };

            window.$(this.$el)
                .daterangepicker(options)
                .on('apply.daterangepicker', function (e, picker) {
                    vm.$emit('apply', picker.startDate, picker.endDate);
                    vm.start = picker.startDate;
                    vm.end = picker.endDate;
                });
        });
    }
});

可以看到,我们已经向组件添加了一堆代码,但是首先查看更新后的 mounted() 调用。 我们提供了一个 on('apply.daterangepicker') 事件处理程序,它会监听触发开始和结束的 apply 的日期事件。 接下来,回调函数会在组件的实例上设置新的开始日期和结束日期。

我们还提供了一些开始和结束日期的 props,默认值设置了过去 30 天的日期范围。

以下是组件的随同模板:


<script type="text/x-template" id="date-range-template">
    <div class="btn-group">
        <a class="btn btn-default btn-rounded calendar-picker"
           data-toggle="collapse"
           aria-expand="true"
        >
            <span class="date-range-label">{{ dateRange }}</span>
            <span class="caret"></span>
        </a>
    </div>
</script>

我已经使用了 Bootstrap 按钮组,你也可以为表单输入做额外的组件。 就我看来,我希望日期范围选择器是一个可点击的按钮,但用户会因此无法通过表单输入来编辑日期范围。

为此,我们有一个组件在日期范围更新时发出事件。 在父项中,我们可以通过定义一个事件属性来监听该事件:

<date-range-picker @apply="onDateChange"></date-range-picker>

简单的事件处理如下所示:


var app = new Vue({
    el: '#app',
    data: {
        start: null,
        end: null
    },
    methods: {
        onDateChange: function (start, end) {
            this.start = start;
            this.end = end;
        }
    }
});

我们可以通过给内嵌组件添加选项来为底层的日期范围选择器添加更多的功能。下面是完整的例子:

Vue.component('date-range-picker', {
    template: '#date-range-template',
    props: {
        showRanges: {
            type: Boolean,
            default: false
        },
        startDate: {
            default: function () {
                return moment().subtract(30, 'days');
            }
        },
        endDate: {
            default: function () {
                return moment();
            }
        },
        minDate: {
            default: false
        },
        opens: {
            default: 'right'
        },
        maxDate: {
            default: false
        },
        autoApply: {
            default: false
        },
    },
    data: function () {
        return {
            start: this.startDate,
            end: this.endDate
        };
    },
    computed: {
        dateRange: function () {
            var start = moment(this.start);
            var end = moment(this.end);
            var today = moment();
            if (
                start.format('LL') === end.format('LL') &&
                today.format('LL') === start.format('LL')
            ) {
                return 'Today';
            } else if (start.format('MM-DD-YYYY') === end.format('MM-DD-YYYY')) {
                return start.format('LL');
            }

            return start.format('LL') + ' - ' + end.format('LL');
        }
    },
    mounted: function () {
        var vm = this;
        this.start = moment(this.start);
        this.end = moment(this.end);
        this.$nextTick(function () {
            var options = {
                opens: this.opens,
                startDate: this.start,
                endDate: this.end,
                autoApply: this.autoApply,
                alwaysShowCalendars: true
            };

            if (this.minDate) {
                options.minDate = this.minDate;
            }
            if (this.maxDate) {
                options.maxDate = this.maxDate;
            }

            if (this.showRanges) {
                options.ranges = {
                    Today: [moment(), moment()],
                    Yesterday: [
                        moment().subtract(1, 'days'),
                        moment().subtract(1, 'days')
                    ],
                    'Last 7 Days': [
                        moment().subtract(6, 'days'),
                        moment()
                    ],
                    'Last 30 Days': [
                        moment().subtract(30, 'days'),
                        moment()
                    ],
                    'This Month': [
                        moment().startOf('month'),
                        moment().endOf('month')
                    ],
                    'Last Month': [
                        moment().subtract(1, 'month').startOf('month'),
                        moment().subtract(1, 'month').endOf('month')
                    ]
                };
            }

            window.$(this.$el)
                .daterangepicker(options)
                .on('apply.daterangepicker', function (e, picker) {
                    vm.$emit('apply', picker.startDate, picker.endDate);
                    vm.start = picker.startDate;
                    vm.end = picker.endDate;
                });
        });
    }
});

我已经尽可能地在上面添加了很多选项,尽管这些选项可能没有涵盖插件提供的所有可用选项。 因为我更倾向于只添加我在项目中所需要的选项。

基于组件传递的 props ,你可以自定义插件的可用功能和选项,这使得这个内嵌组件非常灵活,只显示你想要公开的选项,隔离或禁用一些不需要的选项。

computed 属性中用 dateRange 方法来更直观地显示日期范围。 如果开始日期和结束日期范围是今天,则标签将输出「今天」。如果日期范围仅包含一天,则只显示一个日期。 默认情况下,用户界面将显示开始日期和结束日期。

以下是如何使用组件的完整示例:

<date-range-picker
    @apply="onDateChange"
    :start-date="startDate"
    :end-date="endDate"
    :max-date="defaultEndDate"
    min-date="01-01-2017"
    opens="left"
>
</date-range-picker>

根据上面的内容,你可以定义最大日期、默认开始日期、结束日期以及范围选择的最短日期。 最后,你还可以配置日期范围选择器打开的方式。

小结

你可以使用 ES5 查看 完整的示例( JS Bin )并且可以快速将组件适配到 ES6。

在这个组件的例子中,你可以学习如何通过使用组件的根 DOM 元素 this.$el 来内嵌一个 jQuery 插件、如何在安装组件时初始化一个插件,以及如何连接插件来将数据发送到父组件。

Vue 的官方文档有一个 内嵌组件示例,它演示了如何使用流行的 Select2 jQuery 插件与 v-model 的内嵌 Vue 组件绑定一个自定义的内嵌组件。

本文翻译改编自 Learn How to Write a Vue JS Wrapper Component

Stay Hungry, Stay Foolish.