<script src="../../dist/vue.global.js"></script> <!-- DemoGrid component template --> <script type="text/x-template" id="grid-template"> <table v-if="filteredData.length"> <thead> <tr> <th v-for="key in columns" @click="sortBy(key)" :class="{ active: sortKey == key }"> {{ capitalize(key) }} <span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'"> </span> </th> </tr> </thead> <tbody> <tr v-for="entry in filteredData"> <td v-for="key in columns"> {{entry[key]}} </td> </tr> </tbody> </table> <p v-else>No matches found.</p> </script> <!-- DemoGrid component script --> <script> const DemoGrid = { template: '#grid-template', props: { data: Array, columns: Array, filterKey: String }, data() { return { sortKey: '', sortOrders: this.columns.reduce((o, key) => (o[key] = 1, o), {}) } }, computed: { filteredData() { const sortKey = this.sortKey const filterKey = this.filterKey && this.filterKey.toLowerCase() const order = this.sortOrders[sortKey] || 1 let data = this.data if (filterKey) { data = data.filter(row => { return Object.keys(row).some(key => { return String(row[key]).toLowerCase().indexOf(filterKey) > -1 }) }) } if (sortKey) { data = data.slice().sort((a, b) => { a = a[sortKey] b = b[sortKey] return (a === b ? 0 : a > b ? 1 : -1) * order }) } return data } }, methods: { sortBy(key) { this.sortKey = key this.sortOrders[key] = this.sortOrders[key] * -1 }, capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1) } } } </script> <!-- App template (in DOM) --> <div id="demo"> <form id="search"> Search <input name="query" v-model="searchQuery"> </form> <demo-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery"> </demo-grid> </div> <!-- App script --> <script> Vue.createApp({ components: { DemoGrid }, data: () => ({ searchQuery: '', gridColumns: ['name', 'power'], gridData: [ { name: 'Chuck Norris', power: Infinity }, { name: 'Bruce Lee', power: 9000 }, { name: 'Jackie Chan', power: 7000 }, { name: 'Jet Li', power: 8000 } ] }) }).mount('#demo') </script> <style> body { font-family: Helvetica Neue, Arial, sans-serif; font-size: 14px; color: #444; } table { border: 2px solid #42b983; border-radius: 3px; background-color: #fff; } th { background-color: #42b983; color: rgba(255,255,255,0.66); cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } td { background-color: #f9f9f9; } th, td { min-width: 120px; padding: 10px 20px; } th.active { color: #fff; } th.active .arrow { opacity: 1; } .arrow { display: inline-block; vertical-align: middle; width: 0; height: 0; margin-left: 5px; opacity: 0.66; } .arrow.asc { border-left: 4px solid transparent; border-right: 4px solid transparent; border-bottom: 4px solid #fff; } .arrow.dsc { border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid #fff; } </style>