title: Only display items in bootstrap-vue b-table when filter is applied
body: <p>Is there a way to only display items on bootstrap-vue b-tables when the user has a filter applied (entered a value into the input)? E.g. if "filteredItems" does not exist, display nothing? This is mainly to prevent my table from rendering all rows (>2k rows) and impeding performance.</p>
  
  <p>jsfiddle with b-table: <a href="https://jsfiddle.net/asc82spc/" rel="nofollow noreferrer">https://jsfiddle.net/asc82spc/</a></p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>const template = `
      &lt;table :id="id || null"
             role="grid"
             :aria-busy="isBusy ? 'true' : 'false'"
             :class="tableClass"
      &gt;
          &lt;thead :class="headVariant ? ('thead-' + headVariant) : ''"&gt;
          &lt;tr role="row"&gt;
              &lt;th v-for="field,key in fields"
                  @click="headClick($event,field,key)"
                  @keydown.enter="headClick($event,field,key)"
                  @keydown.space.prevent="headClick($event,field,key)"
                  :class="fieldClass(field,key)"
                  :aria-label="field.sortable ? (sortDesc ? labelSortAsc : labelSortDesc) : null"
                  :aria-sort="(field.sortable &amp;&amp; sortBy === key) ? (sortDesc ? 'descending' : 'ascending') : null"
                  :tabindex="field.sortable?'0':null"
                  v-html="field.label"
              &gt;&lt;/th&gt;
          &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tfoot v-if="footClone" :class="footVariant ? ('thead-' + footVariant) : ''"&gt;
          &lt;tr role="row"&gt;
              &lt;th v-for="field,key in fields"
                  @click="headClick($event,field,key)"
                  @keydown.enter="headClick($event,field,key)"
                  @keydown.space.prevent="headClick($event,field,key)"
                  :key="key"
                  :class="fieldClass(field,key)"
                  :aria-label="field.sortable ? ((sortDesc) ? labelSortAsc : labelSortDesc) : null"
                  :aria-sort="(field.sortable &amp;&amp; sortBy === key) ? (sortDesc ? 'descending' : 'ascending') : null"
                  :tabindex="field.sortable?'0':null"
                  v-html="field.label"
              &gt;&lt;/th&gt;
          &lt;/tr&gt;
          &lt;/tfoot&gt;
          &lt;tbody&gt;
          &lt;tr v-for="(item,index) in _items"
              role="row"
              :key="items_key"
              :class="rowClass(item)"
              @click="rowClicked($event,item,index)"
          &gt;
              &lt;td v-for="(field,key) in fields" :class="cellClass(field)"&gt;
                  &lt;slot :name="key" :value="item[key]" :item="item" :index="index"&gt;{{item[key]}}&lt;/slot&gt;
              &lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr v-if="showEmpty &amp;&amp; _items.length === 0" role="row"&gt;
              &lt;td :colspan="Object.keys(fields).length"&gt;
                  &lt;div v-if="filter" role="alert" aria-live="polite"&gt;
                      &lt;slot name="emptyfiltered"&gt;
                          &lt;div class="text-center" v-html="emptyFilteredText"&gt;&lt;/div&gt;
                      &lt;/slot&gt;
                  &lt;/div&gt;
                  &lt;div v-else role="alert" aria-live="polite"&gt;
                      &lt;slot name="empty"&gt;
                          &lt;div class="text-center" v-html="emptyText"&gt;&lt;/div&gt;
                      &lt;/slot&gt;
                  &lt;/div&gt;
              &lt;/td&gt;
          &lt;/tr&gt;
          &lt;/tbody&gt;
      &lt;/table&gt;
  `;
  const toString = v =&gt; {
    if (!v) {
      return '';
    }
    if (v instanceof Object) {
      return Object.keys(v).map(k =&gt; toString(v[k])).join(' ');
    }
    return String(v);
  };
  const recToString = v =&gt; {
    if (!(v instanceof Object)) {
      return '';
    }
    // Exclude these fields from record stringification
    const exclude = {
      state: true,
      _rowVariant: true
    };
    return toString(Object.keys(v).filter(k =&gt; !exclude[k]).reduce((o, k) =&gt; {
      o[k] = v[k];
      return o;
    }, {}));
  };
  const defaultSortCompare = (a, b, sortBy) =&gt; {
    return toString(a[sortBy]).localeCompare(toString(b[sortBy]), undefined, {
      numeric: true
    });
  };
  
  const bTable = {
   template: template,
    data() {
        return {
          sortBy: null,
          sortDesc: true,
          isBusy: false,
          localItems: null
        };
      },
      props: {
        id: {
          type: String,
          default: ''
        },
        items: {
          type: Array,
          default: () =&gt; []
        },
        fields: {
          type: Object,
          default: () =&gt; {
            return {};
          }
        },
        striped: {
          type: Boolean,
          default: false
        },
        bordered: {
          type: Boolean,
          default: false
        },
        inverse: {
          type: Boolean,
          default: false
        },
        hover: {
          type: Boolean,
          default: false
        },
        small: {
          type: Boolean,
          default: false
        },
        responsive: {
          type: Boolean,
          default: false
        },
        headVariant: {
          type: String,
          default: ''
        },
        footVariant: {
          type: String,
          default: ''
        },
        perPage: {
          type: Number,
          default: null
        },
        items_key: {
          type: String,
          default: null
        },
        currentPage: {
          type: Number,
          default: 1
        },
        filter: {
          type: [String, RegExp, Function],
          default: null
        },
        sortCompare: {
          type: Function,
          default: null
        },
        itemsProvider: {
          type: Function,
          default: null
        },
        noProviderPaging: {
          type: Boolean,
          default: false
        },
        noProviderSorting: {
          type: Boolean,
          default: false
        },
        noProviderFiltering: {
          type: Boolean,
          default: false
        },
        value: {
          type: Array,
          default: () =&gt; []
        },
        footClone: {
          type: Boolean,
          default: false
        },
        labelSortAsc: {
          type: String,
          default: 'Click to sort Ascending'
        },
        labelSortDesc: {
          type: String,
          default: 'Click to sort Descending'
        },
        showEmpty: {
          type: Boolean,
          default: false
        },
        emptyText: {
          type: String,
          default: 'There are no records to show'
        },
        emptyFilteredText: {
          type: String,
          default: 'There are no records matching your request'
        }
      },
      watch: {
       items(newVal, oldVal) {
         console.log('items.watch');
         if (oldVal === newVal) {
           return;
          }
         this.localItems = this.items;
        },
        sortDesc(newVal, oldVal) {
        console.log('watch sortDesc:', newVal, oldVal);
         if (!this.noProviderSorting) {
           this.updater(newVal, oldVal);
          }
        },
        sortBy(newVal, oldVal) {
        console.log('watch sortBy:', newVal, oldVal);
         if (!this.noProviderSorting) {
           this.updater(newVal, oldVal);
          }
        },
        perPage(newVal, oldVal) {
        console.log('watch perPage:', newVal, oldVal);
         if (!this.noProviderPaging) {
           this.updater(newVal, oldVal);
          }
        },
        currentPage(newVal, oldVal) {
        console.log('watch currentPage:', newVal, oldVal);
         if (!this.noProviderPaging) {
           this.updater(newVal, oldVal);
          }
        },
        filter(newVal, oldVal) {
        console.log('watch filter:', newVal, oldVal);
         if (!this.noProviderFiltering) {
           this.updater(newVal, oldVal);
          }
        },
        localItems(newVal, oldVal) {
         console.log('localItems updated');
        }
      },
      computed: {
        tableClass() {
            return [
              'table',
              this.striped ? 'table-striped' : '',
              this.hover ? 'table-hover' : '',
              this.inverse ? 'table-inverse' : '',
              this.bordered ? 'table-bordered' : '',
              this.responsive ? 'table-responsive' : '',
              this.small ? 'table-sm' : ''
            ];
          },
          _items() {
            if (!this.localItems) {
             if (this.itemsProvider) {
              this.updater(1,2);
               return this.items || [];
              } else {
               this.localItems = this.items || [];
              }
            }
            let items = this.localItems.slice();
            // Apply local filter
            if (this.filter &amp;&amp; !(this.itemsProvider &amp;&amp; !this.noProviderFiltering)) {
              if (this.filter instanceof Function) {
                items = items.filter(this.filter);
              } else {
                let regex;
                if (this.filter instanceof RegExp) {
                  regex = this.filter;
                } else {
                  regex = new RegExp('.*' + this.filter + '.*', 'ig');
                }
                items = items.filter(item =&gt; {
                   const test = regex.test(recToString(item));
                  regex.lastIndex = 0;
                  return test;
                });
              }
            }
            // Apply local Sort
            const sortCompare = this.sortCompare || defaultSortCompare;
            if (this.sortBy &amp;&amp; !(this.itemsProvider &amp;&amp; !this.noProviderSorting)) {
             console.log('b-table sorting...');
              items = items.sort((a, b) =&gt; {
                const r = sortCompare(a, b, this.sortBy);
                return this.sortDesc ? r : r * -1;
              });
            }
            // Apply local pagination
            if (this.perPage &amp;&amp; !(this.itemsProvider &amp;&amp; !this.noProviderPaging)) {
              items = items.slice((this.currentPage - 1) * this.perPage, this.currentPage * this.perPage);
            }
            // Clear busy state
            this.$nextTick(() =&gt; {
             this.isBusy = false;
            });
            // Update the value model with the filtered/sorted/paginated data set
            this.$emit('input', items);
            return items;
          }
      },
      methods: {
        fieldClass(field, key) {
            return [
              field.sortable ? 'sorting' : '',
              (field.sortable &amp;&amp; this.sortBy === key) ? 'sorting_' + (this.sortDesc ? 'desc' : 'asc') : '',
              field.variant ? ('table-' + field.variant) : '',
              field.class ? field.class : ''
            ];
          },
          cellClass(field) {
            field.variant ? ('table-' + field.variant) : '',
              field.class ? field.class : ''
          },
          rowClass(item) {
            // Prefer item._rowVariant over deprecated item.state
            const variant = item._rowVariant || item.state || null;
            return [
              variant ? ('table-' + variant) : ''
            ];
          },
          rowClicked(e, item, index) {
           if (this.isBusy) {
             e.preventDefault();
              e.stopPropagation();
             return;
            }
            this.$emit('row-clicked', item, index);
          },
          headClick(e, field, key) {
           if (this.isBusy) {
             e.preventDefault();
              e.stopPropagation();
             return;
            }
            if (!field.sortable) {
              this.sortBy = null;
            } else {
              if (key === this.sortBy) {
                this.sortDesc = !this.sortDesc;
              } else {
               this.sortDesc = true;
              }
              this.sortBy = key;
            }
            this.$emit('head-clicked', key, this.sortDesc);
          },
          updater(a,b) {
           // @TODO: add providerDebounce
            if (a === b || !this.itemsProvider || this.isBusy) {
              return;
            }
            // Set busy state
           this.isBusy = true;
            this.$nextTick(() =&gt; {
            // If async, we just keep localItems as is, and awaite provider callback
              const items = this.itemsProvider(this);
              if (items) {
                this.localItems = items.slice();
              }
            });
          },
          providerCallback(data) {
            this.localItems = data ? data.slice() : [];
          }
      }
  };
  
  new Vue({
    el: '#app',
    components: {bTable},
    data: {
      fields: {
        name: {
          label: 'Person Full name',
          sortable: true
        },
        age: {
          label: 'Person age',
          sortable: true
        },
        isActive: {
          label: 'is Active'
        },
        actions: {
          label: 'Actions'
        }
      },
      currentPage: 1,
      perPage: 5,
      filter: null,
      async: true
    },
    methods: {
      details(item) {
          alert(JSON.stringify(item));
        },
        provider(ctx) {
          console.log('provider called', ctx);
          let items = [{
            isActive: true,
            age: 40,
            name: {
              first: 'Dickerson',
              last: 'Macdonald'
            }
          }, {
            isActive: false,
            age: 21,
            name: {
              first: 'Larsen',
              last: 'Shaw'
            }
          }, {
            isActive: false,
            age: 9,
            state: 'success',
            name: {
              first: 'Minni',
              last: 'Navarro'
            }
          }, {
            isActive: false,
            age: 102,
            name: {
              first: 'Woodrow',
              last: 'Wilson'
            }
          }, {
            isActive: true,
            age: 38,
            name: {
              first: 'Jami',
              last: 'Carney'
            }
          }, {
            isActive: false,
            age: 42,
            name: {
              first: 'Justin',
              last: 'Truedeau'
            }
          }, {
            isActive: true,
            age: 72,
            name: {
              first: 'Dickerson',
              last: 'Macdonald Sr.'
            }
          }, {
            isActive: false,
            age: 12,
            name: {
              first: 'Larsen',
              last: 'Shaw Jr.'
            }
          }, {
            isActive: false,
            age: 26,
            name: {
              first: 'Mitzi',
              last: 'Navarro'
            }
          }, {
            isActive: false,
            age: 22,
            name: {
              first: 'Geneva',
              last: 'Wilson'
            }
          }, {
            isActive: true,
            age: 38,
            name: {
              first: 'Janice',
              last: 'Carney'
            }
          }, {
            isActive: false,
            age: 27,
            name: {
              first: 'Essie',
              last: 'Dunlap'
            }
          }];
          if (this.filter) {
           const regex = new RegExp('.*' + this.filter + '.*', 'ig');
            items = items.filter(item =&gt; {
             const test = regex.test(recToString(item));
              //regex.lastIndex = 0;
              return test;
            });
          }
          if(ctx) {
            if (ctx.sortBy) {
              items = items.sort((a, b) =&gt; {
               const r = defaultSortCompare(a, b, ctx.sortBy);
               return ctx.sortDesc ? r : r * -1;
             });
            }
            items = items.slice((this.currentPage - 1) * this.perPage, this.currentPage * this.perPage);
            if (this.async) {
             // Emulate async request
             const p = new Promise(resolve =&gt; setTimeout(resolve, 1000));
             p.then(() =&gt; {
             ctx.providerCallback(items);
             });
            } else {
             // Non Async
             return items;
            }
          } else {
           // Our own app is requesting total # rows
       return items;
      }
        }
    }
  });</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>#app {
    padding: 20px;
    height: 500px;
  }
  html,body { font-size: 14px;}
  table[aria-busy="false"] {
    opacity: 1;
  }
  table[aria-busy="true"] {
    opacity: .5;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"&gt;&lt;/script&gt;
  &lt;div id="app"&gt;
    &lt;div class="justify-content-center my-1 row"&gt;
  
      &lt;b-form-fieldset horizontal label-text-align="right" label="Provider:" class="col-4" :label-size="4"&gt;
        &lt;b-form-select class="form-control" :options="[{text:'Async',value:true},{text:'Sync',value:false}]" v-model="async"&gt;
        &lt;/b-form-select&gt;
      &lt;/b-form-fieldset&gt;
  
      &lt;b-form-fieldset horizontal label-text-align="right" label="Page Size:" class="col-4" :label-size="6"&gt;
        &lt;b-form-select class="form-control" :options="[{text:5,value:5},{text:10,value:10},{text:15,value:15}]" v-model="perPage"&gt;
        &lt;/b-form-select&gt;
      &lt;/b-form-fieldset&gt;
  
      &lt;b-form-fieldset label-text-align="right" horizontal label="Filter:" class="col-4" :label-size="2"&gt;
        &lt;b-form-input v-model="filter" placeholder="Type to Search"&gt;&lt;/b-form-input&gt;
      &lt;/b-form-fieldset&gt;
    &lt;/div&gt;
  
    &lt;!-- Main table element --&gt;
    &lt;!-- :current-page="currentPage" :per-page="perPage" :filter="filter" no-provider-filtering --&gt;
    &lt;b-table striped hover head-variant="inverse" :items-provider="provider" :fields="fields" :filter="filter" :current-page="currentPage" :per-page="perPage" show-empty&gt;
      &lt;template slot="name" scope="item"&gt;
        {{item.value.first}} {{item.value.last}}
      &lt;/template&gt;
      &lt;template slot="isActive" scope="item"&gt;
        {{item.value?'Yes :)':'No :('}}
      &lt;/template&gt;
      &lt;template slot="actions" scope="item"&gt;
        &lt;b-btn size="sm" @click="details(item.item)"&gt;Details&lt;/b-btn&gt;
      &lt;/template&gt;
    &lt;/b-table&gt;
  
    &lt;div class="justify-content-center row my-1"&gt;
      &lt;b-pagination size="md" :total-rows="provider(null).length" :per-page="perPage" v-model="currentPage" /&gt;
    &lt;/div&gt;
  &lt;/div&gt;</code></pre>
  </div>
  </div>
  </p>