vue-ts-async
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

vue-ts-async

Library for extremely type-safe Typescript Vue asynchronous data and computed properties.

This library has two functions/decorators that allow you create extremely type-safe data and computed properties on Vue components using asynchronous functions.

Has convenient features for:

  • loading, pending, and error flags
  • ability to refresh data
  • debouncing, with cancel and now functions
  • defaults
  • error handling

Install and Setup

# peer dependency 
npm install --save vue vue-property-decorator
 
npm install --save vue-ts-async

Here's a simple example.

// src/vue-async.ts
import VueAsync from 'vue-ts-async'
 
// this object holds your default settings,
// and you'll use it in other components
// to create `data` and `computed` properties
const async = VueAsync({
  // your optional default error handler for async functions
  // (will be undefined by default, no function will be called)
  error: e => { console.error(e) },
  // default debounce length in milliseconds
  // (this is the default)
  debounce: 1000,
})
export default async

Then in the component itself:

// <script lang="ts">
import async from './vue-async'
import { Vue, Component, Prop } from 'vue-property-decorator'
 
@Component
export default class MyComponent extends Vue {
  @Prop() readonly oneNumber!: number
  @Prop() readonly twoNumber!: number
 
  // this will be called only once, when the component is created
  oneNumberAddFive = async.data(this, {
    async get() {
      // let's pretend this is doing something useful
      return await Promise.resolve(this.oneNumber + 5)
    },
    default: 0,
  })
 
  // async.computed properties unfortunately need this decorator
  @async.Computed
  addFivePlusTwoNumber = async.computed(this, {
    // a list of properties to watch
    // `watch` uses debouncing
    watch: ['twoNumber'],
    // a function that returns an object to watch
    // `watchClosely` doesn't use debouncing
    watchClosely() {
      return { oneNumberAddFivePromise: this.oneNumberAddFive.promise }
    },
    // `watch` and `watchClosely` must not intersect (have any keys in common)
    async get({ oneNumberAddFivePromise, twoNumber }) {
      // see how we can use the promise from another async property
      // and await it directly?
      return (await oneNumberAddFivePromise) + twoNumber
    },
  })
 
  // normal computed properties can just use the `values` from async properties
  get promiseNumbersNotZero() {
    const oneNotZero = this.oneNumberAddFive.value !== 0
    // since `addFivePlusTwoNumber` doesn't have a default,
    // its value could be null
    const twoNotZero = (this.addFivePlusTwoNumber.value || 0) !== 0
    return oneNotZero && twoNotZero
  }
 
  get somethingIsLoading() {
    // `loading` on the properties let's you know if any promises are unresolved
    return this.oneNumberAddFive.loading || this.addFivePlusTwoNumber.loading
  }
 
  get addFivePlusTwoNumberIsDebounced() {
    // `queued` on debounced `async.computed` properties
    // tells you if a call is currently debounced
    return this.addFivePlusTwoNumber.queued
  }
 
  get somethingIsErrored() {
    // `error` is the most recent error
    // or null if the most recent was successful
    return !!this.oneNumberAddFive.error || !!this.addFivePlusTwoNumber.error
  }
}

Very type-safe

This library uses advanced conditional types and function overloading to change the types of async.data and async.computed properties. If something could be null, you'll be informed.

API

type ErrorHandler = (e: Error) => void

A type alias for error handler functions.

function VueAsync({ debounce?: number = 1000, error?: ErrorHandler }) => VueTsAsync

The top level function that returns an instance used to create all other data and computed properties.

class VueTsAsync

The class that is returned by the VueAsync function. Has a data function to create data properties, computed function to create computed properties, and a Computed decorator that must be used on computed properties to make them reactive.

function data(vm: V extends Vue, options: AsyncDataOptions<T>) => AsyncData<T>

The full options object has this shape:

type AsyncDataOptions<T> = {
  // the asynchronous function
  // that is called just once on component creation
  get: (this: V) => Promise<T>,
 
  // a function that will be called
  // if the promise is rejected
  error?: ErrorHandler,
 
  // a default value, used before the promise fulfills
  // or if it is rejected
  default?: T,
 
  // this switch tells the `AsyncData`
  // to wait until `refresh` is manually called
  lazy?: true,
}

function data(vm: V extends Vue, (this: V) => Promise<T>) => AsyncData<T>

The data function has a simple overload that allows you to only pass a function. It is equivalent to passing an options object with only the get value.

class AsyncData<T>

This is (basically) what is returned from the data function, and is the object the rest of your component will interact with.

The types of the properties of AsyncData depend on what values you give for default and lazy.

type AsyncData<T> = {
  // the raw promise returned by your asynchronous function
  // can be useful if you'd prefer to have other computed properties
  // use `await` to depend on this
  // if you set `lazy` to `true`, then this could be null
  // and if you don't give a default, the internal value could be null
  promise: Promise<T | null> | null,
 
  // the promise's resolved value
  // if you don't provide a `default`, this could be null
  value: T | null,
 
  // whether the promise is currently unresolved
  loading: boolean,
 
  // the most recently thrown error,
  // or null if the most recent call was successful
  error: Error | null,
 
  // calls the `get` function again,
  // and updates all the values
  // this is useful since `AsyncData` aren't reactive
  refresh: () => void,
}

function computed(vm: V extends Vue, options: AsyncComputedOptions<T>) => AsyncComputed<T>

The computed function reactively updates when its dependencies change, and it must be decorated with Computed. It can be debounced so that many quick reactive triggers don't cause a flood of asynchronous function calls, which can be very expensive.

You have to provide a get function that returns a promise, and either a watch or watchClosely parameter which are either arrays of strings referring to properties on the vue instance, or a function that returns an object you want tracked.

You might be asking "Why are watch and watchClosely necessary? Why not just pass a function that's reactively watched?" Well, in order for Vue to reactively track a function, it has to invoke that function when you create the watcher. Since we have a function that performs an expensive async operation, which we also want to debounce, we can't really do that.

type AsyncComputedOptions<T> = {
  // the asynchronous function,
  // that will be called without debouncing when `watchClosely` is triggered
  // and with debouncing when `watch` is triggered
  // this function receives the merged outputs of `watch` and `watchClosely`
  get: (watchedAttributes: OutputOfWatchAndWatchClosely) => Promise<T>,
 
  // a function that will be called
  // if the promise is rejected
  error?: ErrorHandler,
 
  // whether the watchers should be deep or not
  // passed to [Vue.$watch](https://vuejs.org/v2/api/#vm-watch)
  deep?: boolean,
 
  // `watch` and `watchClosely`
  // can either be a list of the component's property names
  // or a function that returns something
  // these are both used to make `AsyncComputed` reactive
  // `watch` is reactive but with a debounce
  watch?: (keyof V)[] | (this: V) => any,
  // `watchClosely` is reactive but has *no* debounce
  watchClosely?: (keyof V)[] | (this: V) => any,
 
  // a switch that determines if this computed will be run immediately
  // or wait for the first trigger of its watchers
  eager?: true,
 
  // a default value, used before the promise fulfills
  // or if it is rejected
  default?: T,
 
  // an override debounce length,
  // which defaults to the value given to `VueAsync`
  // if you don't want to debounce, only use `watchClosely`
  debounce?: number,
}

class AsyncComputed<T>

This is (basically) what is returned from the computed function, and is the object the rest of your component will interact with.

The types of the properties of AsyncComputed depend on what values you give for default, eager, watch, and watchClosely.

type AsyncComputed<T> = {
  // same as `AsyncData`
  // if you *don't* set `eager`, then this could be null
  // and if you don't give a default, the internal value could be null
  promise: Promise<T | null> | null,
 
  // same as `AsyncData`
  value: T | null,
 
  // same as `AsyncData`
  loading: boolean,
 
  // same as `AsyncData`
  error: Error | null,
 
  // whether a call is currently debounced
  // if you only provide `watchClosely`,
  // then no debounce will ever be used on this `AsyncComputed`,
  // and it won't have this flag
  queued?: boolean,
 
  // cancels any currently debounced call,
  // preventing it from actually doing anything asynchronous
  cancel: () => void,
 
  // makes any currently debounced call happen immediately
  now: () => void,
}

Computed: VueDecorator

This decorator must be used on async.computed properties to make them reactive.

Contributing

This package has testing set up with mocha and chai expect. Since many of the tests are on the functionality of Vue components, the vue testing docs are a good place to look for guidance.

If you'd like to contribute, perhaps because you uncovered a bug or would like to add features:

  • fork the project
  • clone it locally
  • write tests to either to reveal the bug you've discovered or cover the features you're adding (write them in the test directory, and take a look at existing tests as well as the mocha, chai expect, and vue testing docs to understand how)
  • run those tests with npm test (use npm test -- -g "text matching test description" to only run particular tests)
  • once you're done with development and all tests are passing (including the old ones), submit a pull request!

Readme

Keywords

none

Package Sidebar

Install

npm i vue-ts-async

Weekly Downloads

34

Version

0.2.0

License

MIT

Unpacked Size

62.9 kB

Total Files

18

Last publish

Collaborators

  • blainehansen