2
votes

I am attempting to make a custom component in Vue 2.0 that extends the existing functionality of the Bootstrap Vue library <b-table>. It mostly works how I would like it to except that the removeRow and resetData functions defined in the index.jsp don't work how I'd like them to.

removeRow does visually remove the row, and removes it from the data prop but the row reappears after the next interaction (sort, filter, etc.). So it's not actually updating the right thing. I'm trying to use a v-model as a shallow copy of items so that I can make deletions to it without affecting the original set of data but I'm just missing something.

resetData does set the data in the table back correctly, but doesn't re-render the table so you can't see the re-added rows, until you do a separate interaction (sort, filter, etc.), in which case they reappear.

So I know I'm somewhat close but would really appreciate any insight on how to get this working correctly and ways I could improve any part of this component.

OreillyTable.vue.js

const OreillyTable =  {
inheritAttrs: false,
data: function () {
    return {
        filter: null,
        sortDesc: false,
        hideEmpty: false,
        isBusy: false,
        currentPage: 1,
        data: null
    }
},
mounted: function () {
    let filtered = this.slots.filter(function(value, index, arr){
        return value.customRender;
    });
    this.slots = filtered;
},
methods: {
    oreillyTableSort (a, b, key) {
        if (a[key] === null || b[key] === null) {
            return a[key] === null && b[key] !== null ? -1 : (a[key] !== null && b[key] === null ? 1 : 0);
        } else if (typeof a[key] === 'number' && typeof b[key] === 'number') {
            // If both compared fields are native numbers
            return a[key] < b[key] ? -1 : (a[key] > b[key] ? 1 : 0)
        } else {
            // Stringify the field data and use String.localeCompare
            return this.toString(a[key]).localeCompare(this.toString(b[key]), undefined, {
                numeric: true
            });
        }
    },

    toString (val) {
        return typeof val !== "undefined" && val != null ? val.toString() : '';
    },

    oreillyFilter (filteredItems) {
        this.totalRows = filteredItems.length;
        this.currentPage = 1;
    }
},
props: {
    fields: {
        type: Array
    },
    items: {
        type: Array,
        required: true
    },
    hideEmpty: {
        type: Boolean
    },
    filterPlaceholder: {
        type: String,
        default: "Filter"
    },
    sortFunction: {
        type: Function,
        default: null
    },
    filterFunction: {
        type: Function,
        default: null
    },
    slots: {
        type: Array
    },
    sortBy: {
        type: String,
        default: null
    },
    perPage: {
        type: Number,
        default: 10
    },
    value: {

    }
},
template: `<div :class="{ 'no-events' : isBusy }">
            <b-row>
                <b-col md="12">
                    <b-form-group class="mb-2 col-md-3 float-right pr-0">
                        <b-input-group>
                            <b-form-input v-model="filter" :placeholder="filterPlaceholder" class="form-control" />
                        </b-input-group>
                    </b-form-group>
                </b-col>
            </b-row>
            <div class="position-relative">
                <div v-if="isBusy" class="loader"></div>
                <b-table stacked="md" outlined responsive striped hover
                    v-bind="$attrs"
                    v-model="data"
                    :show-empty="!hideEmpty"
                    :items="items"
                    :fields="fields"
                    :no-provider-sorting="true" 
                    :no-sort-reset="true"
                    :filter="filter"
                    :sort-by.sync="sortBy" 
                    :sort-desc.sync="sortDesc" 
                    :sort-compare="sortFunction === null ? this.oreillyTableSort : sortFunction" 
                    :busy.sync="isBusy"
                    :current-page="currentPage"
                    :per-page="perPage"
                    @filtered="filterFunction === null ? this.oreillyFilter : filterFunction">

                    <template :slot="slot.key" slot-scope="row" v-for="slot in slots">
                        <slot :name="slot.key" :data="row"></slot>
                    </template>
                </b-table>

                <b-row v-if="items.length > perPage">
                    <b-col sm="12">
                        <b-pagination size="md" :total-rows="items.length" v-model="currentPage" :per-page="perPage"></b-pagination>
                    </b-col>
                </b-row>
            </div>
        </div>`
};

index.jsp

<script>
Vue.use(window.vuelidate.default);

Vue.component('oreilly-table', OreillyTable);

const dashboardItems = [
    { id: 12, firstName: "John", lastName: "Adams", tmNumber: "588999", team: "Corporate", flapjackCount: 4, enrollDate: "2018-11-05" },
    { id: 13, firstName: "George", lastName: "Washington", tmNumber: "422111", team: "America", flapjackCount: 28, enrollDate: "2018-10-01" },
    { id: 14, firstName: "Abraham", lastName: "Lincoln", tmNumber: "358789", team: "America", flapjackCount: 16, enrollDate: "2017-09-02" },
    { id: 15, firstName: "Jimmy", lastName: "Carter", tmNumber: "225763", team: "Core", flapjackCount: 9, enrollDate: "2018-03-02" },
    { id: 16, firstName: "Thomas", lastName: "Jefferson", tmNumber: "169796", team: "Core", flapjackCount: 14, enrollDate: "2018-05-01" }
];

const Dashboard = {
    template: `<jsp:include page="dashboard.jsp"/>`,
    data: function(){
        return {            
            notification: {
                text: "The Great Flapjack Contest will be held on December 25, 2018.",
                variant: "primary",
                timer: true
            },
            fields: [
                { key: "name", label: "Name", sortable: true, customRender: true },
                { key: "team", label: "Team", sortable: true },
                { key: "enrollDate", label: "Enrollment Date", sortable: true, formatter: (value) => {return new Date(value).toLocaleDateString();} },
                { key: "flapjackCount", sortable: true },
                { key: "id", label: "", 'class': 'text-center', customRender: true }
            ]
        }
    },
    methods: {
        removeRow: function(id) {
            this.$refs.table.isBusy = true;
            setTimeout(() => { console.log("Ajax Request Here"); this.$refs.table.isBusy = false; }, 1000);
            const index = this.$refs.table.data.findIndex(item => item.id === id) 
            if (~index) 
                this.$refs.table.data.splice(index, 1)  
        },
        resetData: function() {
            this.$refs.table.data = dashboardItems;
        }
    }
};

const router = new VueRouter({
    mode: 'history',
    base: "/ProjectTemplate/flapjack",
    routes: [
        { path: '/enroll', component: Enroll },
        { path: '/', component: Dashboard },
        { path: '/404', component: NotFound },  
        { path: '*', redirect: '/404' }
    ]
});

new Vue({router}).$mount('#app');

dashboard.jsp

<compress:html>
<div>
    <oreilly-table ref="table" :items="dashboardItems" :slots="fields" :fields="fields">
        <template slot="name" slot-scope="row">
            {{ row.data.item.firstName }} {{ row.data.item.lastName }} ({{ row.data.item.tmNumber }})
        </template>
        <template slot="id" slot-scope="row">
            <a href="#" @click.prevent="removeRow(row.data.item.id)">Remove</a>
        </template>
    </oreilly-table>
    <footer class="footer position-sticky fixed-bottom bg-light">
        <div class="container text-center">
            <router-link tag="button" class="btn btn-outline-secondary" id="button" to="/enroll">Enroll</router-link>
            &emsp;
            <b-button @click.prevent="resetData" size="md" variant="outline-danger">Reset</b-button>
        </div>
    </footer>
</div>

1

1 Answers

4
votes

I tried to reproduce your problem with a simple example (see: https://codesandbox.io/s/m30wqm0xk8?module=%2Fsrc%2Fcomponents%2FGridTest.vue) and I came across the same problem you have. Just like the others already said, I agree that the easiest way to reset original data is to make a copy. I wrote two methods to remove and reset data.

methods: {
    removeRow(id) {
      const index = this.records.findIndex(item => item.index === id);

      this.records.splice(index, 1);
    },
    resetData() {
      this.records = this.copyOfOrigin.slice(0);
    }
  }

On mount I execute a function that makes a copy of the data. This is done with slice because otherwise it makes only a reference to the original array (note that normally JS is pass-by-value, but as stated in the vue documentation with objects, and thus internally in vue it is pass by reference (see: Vue docs scroll to the red marked text)).

mounted: function() {
    this.copyOfOrigin = this.records.slice(0);
  } 

Now you can simple remove a record but also reset all the data.

SEE FULL DEMO

I hope this fixes your issue and if you have any questions, feel free to ask.