Akryum / vue-supply

Create resources that can automatically be activated and deactivated when used (like subscriptions)
159 stars 16 forks source link
realtime-data subscription vuejs2 vuex

vue-supply

Create resources that can automatically be activated and deactivated when used (like subscriptions)

💬 VueConf 2017 demo & slides

Table of contents

Why do I need this?

Efficiently managing reactive and living data from an external source can become difficult in a large app or when using vuex. With vue-supply, you can easily consume data and automatically activate or deactivate subscriptions.

The Vue+Meteor demo project demonstrate how to use Meteor reactive data easily with vue-supply inside components and a vuex store.

What kind of data?

vue-supply is suitable for any kind of reactive and realtime data. For example:

How does it work?

With vue-supply, you create Vue instances extending the Supply definition. You then define two methods: activate and deactivate. For example, you can subscribe to a realtime publication in the activate method and destroy this subscription in the deactivate method. When you will use this Supply in your components (called 'consumers'), it will automatically activate itself when it is first used (with the grasp method) or deactivate itself when no component use it anymore (with the release method). You can also easily store the realtime data inside the Supply and access it in the consumer components or in vuex getters. Anywhere in your code, you can wait for a Supply to be activated with the ensureActive method.

Supply also understands the notion of loading the data: when your subscription is being processed, just increment the loading property. When it's ready, decrement loading. If all the operations are done (which means that loading value is 0), the Supply will emit the is-ready event you can listen to. You can also use the ready property directly in your templates (or somewhere else). There is also a ensureReady method that waits for the Supply to be ready.

Installation

npm install --save vue-supply

Default import

import Vue from 'vue'
import VueSupply from 'vue-supply'

Vue.use(VueSupply)

Browser

<script src="https://github.com/Akryum/vue-supply/raw/master/vue.js"></script>
<script src="https://github.com/Akryum/vue-supply/raw/master/vue-supply/dist/vue-supply.browser.js"></script>

The plugin should be auto-installed. If not, you can install it manually with the instructions below.

Vue.use(VueSupply)

Usage

A supply is a Vue instance which is responsible for managing a piece of dynamic data (for example, a Meteor, GraphQL or Firebase subscription with data that may change and update from the server). It has an deactivated state (default), and an activated state when the data should be updated (for example, when a subscription is running).

To create a supply, write a Vue definition object extending the Supply definition:

import { Supply } from 'vue-supply'

export default {
  extends: Supply,
  // Vue options here
}

Then you can manually create a supply with the Vue constructor:

import Vue from 'vue'
import TestResourceDef from 'supply/test-resource'

const TestResource = new Vue(TestResource)

The two methods when using the supply are:

To activate or deactivate the supply, use the grasp and release methods where you need to access the supply:

console.log(TestResource.consumers) // 0
TestResource.grasp()
console.log(TestResource.consumers) // 1
console.log(TestResource.someData) // Access the data
TestResource.release()
console.log(TestResource.consumers) // 0

The supply will emit a consumers event with the count when it changes.

The supply is active if it has one or more consumers. When it becomes active, it calls the activate method, which you should override in the definition:

export default {
  extends: Supply,
  methods: {
    activate () {
      // Subscribe
    },
  },
}

Also, the active event is emitted on the supply, with a true boolean argument, and the is-active event.

TestResource.$on('active', (isActive) => {
  // Do something
})

And when there are no more consumer for the supply, the deactivate method is called:

export default {
  extends: Supply,
  methods: {
    activate () {
      // Subscribe
    },
    deactivate () {
      // Unsubscribe
    },
  },
}

Also, the active event is emitted on the supply, with a false boolean argument, and the is-not-active event.

There is a active computed boolean available that changes when the supply is activated or deactivated:

TestResource.$watch('active', isActive => {
  console.log(isActive)
})

You can also use the supply.ensureActive() method which return a promise that resolves as soon as the supply is activated (or immediatly if it is already):

TestResource.ensureActive().then(() => {
  // The supply is active
})

Registration

It is recommended to register the supply definition to enable injection in components and in the vuex store.

import { register } from 'vue-supply'
import TestResourceDef from 'supply/test-resource'
register('TestResource', TestResourceDef)

Usage in components

Inside a component, add a mixin with use(name, manageKeepAlive = true) to automatically grasp and release the supply when the component is created and destroyed, using the name used in the registration (see above):

import { use } from 'vue-supply'

export default {
  // This component now uses TestResource
  mixins: [use('TestResource')],

  // Use the values in computed properties
  computed: {
    answer () {
      return this.$supply.TestResource.someData
    },
  },

  // ...
}

Then you can use the supply data inside computed properties or inside methods with the this.$supply[name] object:

// Use the values in computed properties
computed: {
  answer () {
    return this.$supply.TestResource.someData
  },
},

Usage in Vuex store

Inside a vuex store, you can inject getters that use supplies:

export default {
  supply: {
    use: ['TestResource'],
    inject: ({ TestResource }) => ({
      getters: {
        'all-items': () => TestResource.items,
      },
    }),
  },

  getters: {
    'count': (state, getters) => getters['all-items'].length,
  },
}

Before creating the Vuex store, transform the options with the injectSupply(options, cache) method:

import { injectSupply } from 'vue-supply'

const supplyCache = {}
const suppliedStoreOptions = injectSupply(storeOptions, supplyCache)

const store = new Vuex.Store(suppliedStoreOptions)

Provide the supply cache to the root Vue instance so that the supplies created for the store are reused in the components:

new Vue({
  // ...
  supplyCache,
}),

Then to activate/deactivate the supply, you can either call the grasp and release methods inside actions:

supply: {
  use: ['TestResource'],
  inject: ({ TestResource }) => ({
    getters: {
      'all-items': () => TestResource.items,
    },

    actions: {
      'subscribe-action' () {
        // Request usage in the store
        // Ex: subscribing to a Meteor publication
        TestResource.grasp()
      },

      'unsubscribe-action' () {
        // No longer used in the store
        // Ex: unsubscribing from a Meteor publication
        TestResource.release()
      },
    }
  }),
},

Or with the mixins and the use function inside components using the getter:

import { use } from 'vue-supply'
import { mapGetters } from 'vuex'

export default {
  // This component now uses TestResource supply
  mixins: [use('TestResource')],

  // Use getter that utilize the supply
  computed: {
    ...mapGetters({
      items: 'all-items',
    })
  },
}

Asynchronous data

A loading system is included in the supply supplies. Change the loading integer property:

You should change the loading property inside the activate and deactive methods:

import { Supply } from 'vue-supply'

export default new Vue({
  extends: Supply,
  methods: {
    activate () {
      console.log('subscribing...')
      // Use the integer `loading` property
      // 0 mean ready
      this.loading ++
      // Faking a server request here :p
      setTimeout(() => {
        console.log('data is loaded')
        this.loading --
      }, 1000)
    },
  },
})

You can get the loading state with the ready computed property, a boolean which is true when there are no loading in progress. It can directly used inside computed properties:

import TestResource from 'supply/test-resource'

export default {
  // Use the values in computed properties
  computed: {
    isDataReady () {
      return TestResource.ready
    },
  },
}

There are the ready (with a boolean argument), is-ready and is-not-ready events.

You can also use the supply.ensureReady() method which return a promise that resolves as soon as the supply is ready (or immediatly if it is already):

TestResource.ensureReady().then(() => {
 // The supply is ready
})

There is a useful function, consume, which comes in handy when you only need to use the supply periodically. It both graspes and wait for ready and return a release function:

import { consume } from 'vue-supply'
import TestResource from 'supply/test-resource'
// This will grasp and wait for the supply to be 'ready'
const release = await consume(TestResource)
// Count of active supply consumers
console.log('consumers', TestResource.consumers)
// When you are done with the supply, release it
release()

Base supply definition

It's often useful to create a base definition for each supply.

Example for Meteor:

// base.js
import { Supply } from 'vue-supply'

export default {
  extends: Supply,

  methods: {
    activate () {
      this.$startMeteor()
    },

    deactivate () {
      this.$stopMeteor()
    },
  },

  meteor: {
    $lazy: true,
  },
}

Example supply:

// Items.js
import base from './base'
import { Items } from '../api/collections'

export default {
  extends: base,

  data () {
    return {
      items: [],
    }
  },

  meteor: {
    $subscribe: {
      'items': [],
    },

    items () {
      return Items.find({})
    },
  },
}

Examples

Basics

Create a supply:


export default {
  extends: Supply,
  data () {
    return {
      someData: null,
    }
  },
  methods: {
    activate () {
      console.log('subscribing...')
      // Use the integer `loading` property
      // 0 mean ready
      this.loading ++
      // Faking a server request here :p
      setTimeout(() => {
        this.someData = 42
        this.loading --
      }, 1000)
    },
    deactivate () {
      console.log('unsubscribing...')
    },
  },
}

Register the supply:

import { register } from 'vue-supply'
import TestResource from './supply/test-resource'
register('TestResource', TestResource)

Use the supply in components:

import { use } from 'vue-supply'

export default {
  // This component now uses TestResource
  mixins: [use('TestResource')],

  // Use the values in computed properties
  computed: {
    answer () {
      return this.$supply.TestResource.someData
    }
  },

  // ...
}

Or in the vuex store:

export default {
  supply: {
    use: ['TestResource'],
    inject: ({ TestResource }) => ({
      getters: {
        // Use the supply data in getters
        'my-getter': () => TestResource.someData,
      },
      actions: {
        'subscribe-action' () {
          // Request usage in the store
          // Ex: subscribing to a Meteor publication
          TestResource.grasp()
        },

        'unsubscribe-action' () {
          // No longer used in the store
          // Ex: unsubscribing from a Meteor publication
          TestResource.release()
        },

        async 'consume-action' ({ commit }) {
          // This will wait for the supply to be 'ready'
          const release = await consume(TestResource)
          // Count of active supply consumers
          console.log('consumers', TestResource.consumers)
          commit('my-commit', TestResource.someData)
          // When you are done with the supply, release it
          release()
        },
      },
    }),
  },
}

License

MIT