Skip to content

[feature request] automatic rebind #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wujekbogdan opened this issue Sep 3, 2018 · 7 comments
Closed

[feature request] automatic rebind #208

wujekbogdan opened this issue Sep 3, 2018 · 7 comments

Comments

@wujekbogdan
Copy link
Contributor

wujekbogdan commented Sep 3, 2018

Currently, if we want to implement automatic rebind we have to use a watcher, unbind and then rebind firebase refs using $bindAsArray or $bindAsObject methods.

If such a feature was implemented then re-binding refs after instance properites change would be as easy as:

const vm = new Vue({
  el: '#my-el',
  firebase(vm) {
    const key = vm.key;
    return {
      anArray: {
        source: db.ref(`url/to/my/collection/${key}`),
        autoRebind: true,
      },
    }
  },
  data: () => ({
    key: 'some-value',
  }),
})

Is it possible to implement such a feature?

@posva
Copy link
Member

posva commented Sep 3, 2018

using a watcher is the way the go 🙂 Adding such feature would be reinventing the wheel
For Firestore you can also use a computed property and return the promise, which is more useful

@posva posva closed this as completed Sep 3, 2018
@wujekbogdan
Copy link
Contributor Author

using a watcher is the way the go 🙂 Adding such feature would be reinventing the wheel (...)

IMO automatic re-binding would be more Vue-way. Vue is all about declarative programming. Even the Vue guide recommends using computed properties over watchers.

For Firestore you can also use a computed property and return the promise, which is more useful

Can you explain, please?

@posva
Copy link
Member

posva commented Sep 16, 2018

using a watcher is the vue way because we are doing a side effect 😉
with firestore you can do this:

computed: {
	documentPromise() {
		return this.$bind('document', db.collection('items').doc(this.documentId)
	}
}

And then you can consume use the promise state to display a loading, fulfilled or error state using something like vue-promised

with the RTDB, you still have to do it a bit manually by creating the promise yourself and passing resolve, and reject as readyCallback and rejectCallback

@wujekbogdan
Copy link
Contributor Author

And then you can consume use the promise state to display a loading, fulfilled or error state using something like vue-promised

with the RTDB, you still have to do it a bit manually by creating the promise yourself and passing resolve, and reject as readyCallback and rejectCallback

Thanks for the clarification.

@wujekbogdan
Copy link
Contributor Author

@posva

Here's the solution I came up with. I wrote a little Vue plugin that automatically rebinds Firestore queries. It's not very elegant because it creates intermediate computed properties prefixed with _, but I couldn't find a better solution. Other than that I can't see (yet ;)) any downside of this approach. Comments are welcome!

VueFirestoreAutoRebind plugin:

export default {
  install(Vue) {
    Vue.mixin({
      beforeCreate() {
        if (!this.$options.db) {
          return;
        }

        const keys = Object.keys(this.$options.db);
        const computed = {};

        // For $options.$db property register a computed property prefixed with _
        keys.forEach((key) => {
          computed[`_${key}`] = this.$options.db[key];
        });

        // Merge dynamically created computed properties with component's computed properties
        this.$options.computed = {
          ...this.$options.computed,
          ...computed,
        };
      },
      created() {
        if (!this.$options.db) {
          return;
        }

        const keys = Object.keys(this.$options.db);

        keys.forEach((key) => {
          // Bind the query when the component is created in order to run the query at least once
          this.$bind(key, this[`_${key}`]);

          // Re-bind properties when computed properties re-evaluate
          this.$watch(`_${key}`, (query) => {
            this.$bind(key, query);
          });
        });
      },
    });
  },
};

Initialization:

import Vue from 'vue';
import VueFire from 'vuefire';
import App from './App.vue';
import VueFirestoreAutoRebind from './plugins/VueFirestoreAutoRebind';

Vue.use(VueFire);
Vue.use(VueFirestoreAutoRebind);

new Vue({
  render: h => h(App),
}).$mount('#js--app');

Usage:

<template>
  <div class="auto-rebind-test">
    <input type="number" v-model.number="limit" min="1" max="100">

    <ul>
      <li v-for="{ key, name } in companies" :key="key">
        {{ name }}
      </li>
    </ul>

  </div>
</template>

<script>
import { db } from '../modules/firebase-client';

export default {
  name: 'auto-rebind-test',
  // The VueFirestoreAutoRebind plugin requires the `db` property
  db: {
    companies() {
      return db.collection('companies')
        .where('isActive', '==', true)
        .limit(this.limit);
    },
  },
  data: () => ({
    companies: [], // Initial value is required
    limit: 10,
  }),
};
</script>

@posva
Copy link
Member

posva commented Sep 18, 2018 via email

@wujekbogdan
Copy link
Contributor Author

wujekbogdan commented Sep 21, 2018

Update:

I've added: before(), resolve(), reject() callback and a missing wait fearture

The plugin

import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import noop from 'lodash/noop';
import has from 'lodash/has';

export default {
  install(Vue) {
    Vue.mixin({
      beforeCreate() {
        if (!this.$options.db) {
          return;
        }

        const keys = Object.keys(this.$options.db);
        const computed = {};

        // For $options.$db property register a computed property prefixed with _
        keys.forEach((key) => {
          const settings = this.$options.db[key];
          const computedProp = `${key}Query`;

          if (isFunction(settings)) {
            computed[computedProp] = settings;
          }

          if (isObject(settings)) {
            if (has(settings, 'ref') && isFunction(settings.ref)) {
              computed[computedProp] = settings.ref;
            }
          }
        });

        // Merge dynamically created computed properties with component's computed properties
        this.$options.computed = {
          ...this.$options.computed,
          ...computed,
        };
      },
      created() {
        if (!this.$options.db) {
          return;
        }

        // Apply vuefire bindings to all entries
        Object.keys(this.$options.db)
          .forEach((key) => {
            const settings = this.$options.db[key];
            const callbacks = {
              before: isFunction(settings.before) ? settings.before : noop,
              resolve: isFunction(settings.resolve) ? settings.resolve : noop,
              reject: isFunction(settings.reject) ? settings.reject : noop,
            };

            const bind = (k, q) => {
              callbacks.before.call(this);

              // tempKey key used when settings.wait is `true`
              // It's a workaround for: https://github.com/vuejs/vuefire/issues/83
              const tempKey = `_temp_${k}`;

              if (settings.wait) {
                this[tempKey] = null;
              }

              const fieldToBindTo = settings.wait ? tempKey : k;

              this.$bind(fieldToBindTo, q)
                .then(() => {
                  if (settings.wait) {
                    this[k] = this[tempKey];
                    delete this[tempKey];
                  }
                  callbacks.resolve.call(this);
                })
                .catch(() => {
                  delete this[tempKey];
                  callbacks.reject.call(this);
                });
            };

            // Bind the collection when the component is created
            const query = this[`${key}Query`];
            bind(key, query);

            // Re-bind properties when computed properties re-evaluate
            this.$watch(`${key}Query`, (q) => {
              bind(key, q);
            });
          });
      },
    });
  },
};

Initialization

import Vue from 'vue';
import VueFire from 'vuefire';
import App from './App.vue';
import VueFirestoreAutoRebind from './plugins/VueFirestoreAutoRebind';

Vue.use(VueFire);
Vue.use(VueFirestoreAutoRebind);

new Vue({
  render: h => h(App),
}).$mount('#js--app');

Usage

<template>
  <div>
    <input type="text" v-model="name">
    <input type="text" v-model="city">
  </div>
</template>

<script>
import { db } from '../modules/firebase-client';

export default {
  name: 'vuefire-autobind-example',
  db: {
    // Basic usage
    companiesByCity() {
      return db.collection('companies')
        .where('city', '==', this.city);
    },
    // Full example
    companiesByName: {
      ref() {
        return db.collection('companies')
          .where('name', '==', this.name);
      },
      before() {
        this.loading = true;
      },
      resolve() {
        this.loading = false;
      },
      reject() {
        this.loading = false;
        this.error = true;
      },
      wait: true,
    },
  },
  data: () => ({
    companiesByCity: [],
    companiesByName: [],
    name: 'My Company Name',
    city: 'Boston',
    error: false,
    loading: false,
  }),
};
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants