【问题标题】:Vuejs 2 v-if directive not rendered after catching event triggered by child componentVuejs 2 v-if 指令在捕获由子组件触发的事件后未呈现
【发布时间】:2017-07-17 06:39:57
【问题描述】:

我正在使用 vuejs 2.0 和 laravel 5.3。我正在从一个子组件(一个预先输入的组件)发出一个自定义事件,并且我正在使用 v-on 指令来捕获这个事件。

这是我的组件的代码:

<template>
    <input ref="input" class="typeahead-suggestions"
           :class="classes"
           :id = "id"
           v-bind:value="value"
           v-on:input="updateValue($event.target.value)"
           v-on:blur="formatValue"
           :placeholder="placeholder">
</template>
<script>
    var Bloodhound = require('typeahead.js')
    export default {
        data: function() {
            var id =  'typeahead-suggestion' + parseInt(Math.random() *100000);
            return {
                id,
                defaultSuggestions: [],
                query: ''
            };
        },
        props: {
            param: {
                type: String,
                default: 'item'
            },
            value: {
                type: String,
                default: ''
            },
            classes: {
                type: String,
                default: ''
            },
            displayKey: {
                type: String,
                default: ''
            },
            suggestionTemplate: {
                type: String,
                default: ''
            },
            name: {
                type: String,
                default: 'Vue Auto Complete'
            },
            prefetch: {
                type: String,
                default: ''
            },
            defaultSuggestion: {
                type: Boolean,
                default: false
            },
            remote: {
                type: String,
                default: ''
            },
            placeholder: {
                type: String,
                default: ''
            },
            local: {
                type: Array,
                default: function () {
                    return [];
                }
            },
            responseWrapper: {
                type: String,
                default: ''
            }
        },
        watch: {
            local: function(newVal) {
                if (this.defaultSuggestion) {
                    this.defaultSuggestions = [...newVal];
                }
                this.resetTypeahead();
            }
        },
        mounted: function() {
            this.initTypeahead();
            if (this.local.length) {
                this.defaultSuggestions = [...this.local];
            }
        },
        methods: {
            updateValue: function (value) {
                this.$emit('input', value);
            },
            formatValue: function () {
                this.$refs.input.value = this.value;
            },
            transformer: function (response) {
                if (this.responseWrapper) {
                    response = response[this.responseWrapper];
                }
                if (this.defaultSuggestion && this.local.length === 0) {
                    this.defaultSuggestions = response.splice(0, 5);
                }
                return response;
            },
            bloodhoundOption: function() {
                var bloodhoundConfig = {};
                if (this.prefetch) {
                    var prefetch = {
                        cache: false,
                        url: this.prefetch
                    };
                    if (this.defaultSuggestion) {
                        prefetch = {...prefetch, transform: this.transformer};
                    }
                    bloodhoundConfig = { prefetch};
                }
                if (this.local) {
                    bloodhoundConfig = {
                        local: this.local,
                        ...bloodhoundConfig
                    }
                }
                if (this.remote) {
                    bloodhoundConfig = {
                        remote: {
                            url: this.remote,
                            wildcard: '%QUERY',
                            transform: this.transformer
                        },
                        ...bloodhoundConfig
                    }
                }
                return bloodhoundConfig;
            },
            parseTemplate: function(data) {
                var res = Vue.compile(this.suggestionTemplate);
                var vm = new Vue({
                    data,
                    render: res.render,
                    staticRenderFns: res.staticRenderFns
                }).$mount();
                return vm.$el;
            },
            getSource: function() {
                var self = this;
                var bloodhoundConfig = this.bloodhoundOption();
                var datumTokenizer = this.displayKey ? Bloodhound.tokenizers.obj.whitespace(this.displayKey)
                                                    :  Bloodhound.tokenizers.whitespace;
                var engine = new Bloodhound({
                    datumTokenizer,
                    queryTokenizer: Bloodhound.tokenizers.whitespace,
                    ...bloodhoundConfig
                });
                var source = function(q, sync, async) {
                    if (q === '' && self.defaultSuggestions.length>0 && self.defaultSuggestion) {
                        sync(self.defaultSuggestions);
                    } else {
                        engine.search(q, sync, async);
                    }
                };
                return this.defaultSuggestion ? source : engine;
            },
            resetTypeahead: function() {
                $(document).find('#' + this.id).typeahead('destroy');
                this.initTypeahead();
            },
            initTypeahead: function() {
                var self =  this;
                var templates = {};
                if (this.suggestionTemplate) {
                    templates = {suggestion: self.parseTemplate}
                };
                var dataset = {
                    name: 'Typeahead-Suggestion',
                    display: this.displayKey,
                    source: this.getSource(),
                    templates
                };
                $(document).find('#' + self.id).typeahead({
                    minLength: 0,
                    highlight: true
                }, dataset)
                .on('typeahead:select', function(event, suggession) {
                    self.$emit('input', self.displayKey? suggession[self.displayKey]: suggession);
                    self.$emit('selected', suggession);

                    // Set value of hidden input to the id of selected item
                    $('input[name="' + self.param + '"]').attr("value", suggession['id']);

                    self.$emit(self.param + '_typeahead');
                });
            }
        }
    }
</script>

在上面的代码中,我通过以下语句在预先选择完成('typeahead:select')后发出一个自定义事件:

self.$emit(self.param + '_typeahead');

这是我的根 vue 实例:

var typeahead = require('./components/typeahead.vue');

if (formElm = document.getElementById('form')) {

    new Vue({
        el: "#form",
        data: {
            form: new Form(formElm),
            initialData: (typeof initialData !== 'undefined') ? initialData : [],
            categories: (typeof categories !== 'undefined') ? categories : [],
            parents: (typeof parents !== 'undefined') ? parents : []
        },
        components: {
            'typeahead': typeahead
        },
        mounted() {
            if (typeof initialData !== 'undefined') {
                for (let field in initialData) {
                    this.form[field] = initialData[field];
                }
            }
        },
        methods: {
            onSubmit() {

                // get all the fields
                let formData = this.form.data();
                let form     = this.form;
                let formInfo = form.info(formElm);

                // setup the constraints for validate.js
                var constraints = {
                    name: {
                        presence: {
                            message: "is required."
                        }
                    },
                    description: {
                        presence: {
                            message: "is required."
                        }
                    },
                    category_id: {
                        presence: {
                            message: "is required."
                        }
                    }
                };

                // validate the fields
                validate.async(formData, constraints)
                        .then(function(success) {
                            if (formInfo['method'] == 'PUT') {
                                form.put(formInfo['url']);
                            } else {
                                form.post(formInfo['url']);
                            }
                        })
                        .catch(function(error) {
                            form.onFail(error);
                        });
            },

            onSuccess(response) {
                form.reset();
            }
        }
    });
}

以下是我使用此组件的刀片模板:

            <form method="post" id="form" action="{{ url('admin/eventType') }}" @submit.prevent="onSubmit"
                  @keydown="form.errors.clear($event.target.name)">
                {{ csrf_field() }}
                <div class="form-group">
                    <label for="name">Name</label>
                    <input type="text" class="form-control" id="name" name="name" v-model="form.name" placeholder="Name">
                    <span class="help text-danger" v-if="form.errors.has('name')" v-text="form.errors.get('name')"></span>
                </div>
                <div class="form-group">
                    <label for="description">Description</label>
                    <textarea class="form-control" id="description" name="description" v-model="form.description" placeholder="Description"></textarea>
                    <span class="help text-danger" v-if="form.errors.has('description')" v-text="form.errors.get('description')"></span>
                </div>
                <div class="form-group">
                    <label for="category_id">Category</label>
                    <input type="hidden" id="category_id" name="category_id" v-model="form.category_id" />
                    <typeahead v-on:category_id_typeahead="form.errors.clear('category_id')" param="category_id" :default-suggestion="true" :local="categories" display-key="value" classes="form-control">
                    </typeahead>
                    <span class="help text-danger" v-if="form.errors.has('category_id')" v-text="form.errors.get('category_id')"></span>
                </div>
                <div class="form-group">
                    <label for="parent_id">Parent</label>
                    <input type="hidden" id="parent_id" name="parent_id" v-model="form.parent_id" />
                    <typeahead param="parent_id" :default-suggestion="true" :local="parents" display-key="value" classes="form-control">
                    </typeahead>
                </div>
                <button type="submit" class="btn btn-default" :disabled="form.errors.any()">Create New Event Type</button>
            </form>

在上面的模板中,我通过 v-on 指令在组件标签中捕获事件:

<typeahead v-on:category_id_typeahead="form.errors.clear('category_id')"
           param="category_id" :default-suggestion="true"
           :local="categories" display-key="value"
           classes="form-control">

问题:我正在使用 v-if 指令,它决定根据方法的输出显示错误文本:

<span class="help text-danger" v-if="form.errors.has('category_id')" v-text="form.errors.get('category_id')"></span>

由于触发 'category_id_typeahead' 会清除该字段的错误,因此 form.errors.has('category_id') 方法的输出应为 false,因此应删除错误,但事实并非如此。根据我通过 console.log 打印的内容,错误对象在触发事件后不包含此字段的错误,但在对表单进行另一次更改之前不会删除文本。这一步似乎没有渲染 v-if。

这是我的表单类的代码:

const formToJSON = elements => [].reduce.call(elements, (data, element) => {
    if (!['submit'].includes(element.type) && !['_token'].includes(element.name)) {
        data[element.name] = element.value;
    }
    return data;
}, {});

const formInfo = elements => [].reduce.call(elements, (data, element) => {
    if (['_method'].includes(element.name)) {
        data['method'] = element.value;
    }
    if (typeof initialData !== 'undefined' && data['method'] == "PUT") {
        data['id'] = initialData['id'];
    }
    return data;
}, {});

// get entity
const currentEntity = function() {
    url           = window.location.href;
    adminPosition = url.indexOf('admin/') + 6;
    entity        = url.substring(adminPosition, url.indexOf('/', adminPosition));

    return entity;
}

class Errors {
    constructor() {
        this.errors = { };
    }

    has(field) {
        console.log('has error field : ', field, ' => ', this.errors.hasOwnProperty(field));

        return this.errors.hasOwnProperty(field);
    }

    any() {
        return Object.keys(this.errors).length > 0;
    }

    get(field) {
        if (this.errors[field]) {
            return this.errors[field][0];
        }
    }

    record(errors) {
        this.errors = errors;
    }

    clear(field) {
        if (field) {
            console.log('Before : clear error of ', field, ' error:', this.errors[field]);
            delete this.errors[field];

            return;
        }

        this.errors = {};
    }
}

class Form {
    constructor(formElm) {
        // get form data
        data = formToJSON(formElm.elements);

        this.originalData = data;

        for (let field in data) {
            this[field] = data[field];
        }

        this.errors = new Errors();
    }

    info(formElm) {
        data   = formInfo(formElm.elements);
        entity = currentEntity();
        if (data['method'] == 'PUT') {
                data['url'] = '/admin' + '/' + entity + '/' + data['id'];
        } else {
                data['url'] = '/admin' + '/' + entity;
        }

        return data;
    }

    data() {
        let data = {};
        for (let property in this.originalData) {
            var propertyValue  = document.getElementById(property);
            if (typeof propertyValue !== 'undefined' && propertyValue.value !== null &&
                propertyValue.value !== "") {
                data[property] = propertyValue.value; 
            } else if (typeof initialData !== 'undefined' &&
                       typeof initialData[property] !== 'undefined') {
                data[property] = initialData[property];
            }
        }

        return data;
    }

    reset() {
        for (let field in this.originalData) {
            this[field] = '';
        }

        typeaheads = document.querySelectorAll('[id^="typeahead"]');
        for (let item in typeaheads) {
            typeaheads[item].value = '';
        }

        this.errors.clear();
    }

    post(url) {
        return this.submit('post', url);
    }

    put(url) {
        return this.submit('put', url);
    }

    submit(requestType, url) {
        return new Promise((resolve, reject) => {
            axios[requestType](url, this.data())
                 .then(response => {
                     this.onSuccess(response.data);
                     resolve(response.data);
                 })
                 .catch(error => {
                     this.onFail(error.response.data);
                     reject(error.response.data);
                 });
        });
    }

    onSuccess(data) {
        this.reset();
    }

    onFail(errors) {
        this.errors.record(errors);
    }
}

module.exports = Form;

我应该如何解决这个问题?我错过了什么吗?

【问题讨论】:

    标签: javascript laravel-5.3 vuejs2 vue.js


    【解决方案1】:

    您尝试对复杂对象(表单)属性的更改做出反应。 Vue 将 getter 和 setter 添加到触发(UI)更新的属性。 https://vuejs.org/v2/guide/reactivity.html

    在您的情况下,表单更新中某处的更新不会通知视图执行渲染更新。

    你应该让v-on:category_id_typeahead="form.errors.clear('category_id')" 调用一个单独的方法来执行form.errors.clear('category_id'); 和触发ui 更新的东西。这可能是 https://vuejs.org/v2/api/#vm-forceUpdate 或发生更改的新 data:{showError:false... 属性。

    【讨论】:

      猜你喜欢
      • 2017-03-28
      • 1970-01-01
      • 2021-07-25
      • 2016-04-29
      • 2019-01-03
      • 2018-05-11
      • 2013-08-25
      • 2016-08-09
      • 1970-01-01
      相关资源
      最近更新 更多