npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

logard

v1.1.1

Published

Vue Router data loading made easy

Downloads

471

Readme

Logard

Vue Router data loading made easy.

⚠️ v1.0 introduced a few small breaking changes

Logard helps streamline the process of data loading for single page web applications that choose to so before navigation. This means all required data is loaded before the navigation is performed and can be used immediately by the destination view.

It was initially conceived with Vue Router in mind, but only little code turned out to be specific to that. It was since adapted to make it more broadly usable but no support for other routers has been added yet. PR's welcome 😀. This documentation just assumes usage of Vue Router for now.

Logard introduces the concept of Loaders that load data based on route information such as query and path parameters. It tracks what information is actually used so that it knows when to refresh a loader. Loaders are associated with routes and the results are passed to the props of the view component.

This way, Logard helps avoid global state and its associated hazards by providing a familiar way of passing data to components. It also really shines when nested routes share (parts of) the same data, by automating the logic that decides when to load what.

It is not really a caching solution. It does avoid some needless data loading but discards results as soon as they're no longer needed. That being said, caching can be added by the user if needed.

Contents

  1. Install
  2. Usage
  3. Demo
  4. Guide
    1. Loader
    2. Scope
    3. Sanitizers
    4. Dependency tracking
    5. Flow
  5. Changelog

Install

$ npm install --saveDev logard

Usage

Consider this basic usage example:

import { Loader } from 'logard';
import { installRouteLoader, VueRouterRedirectError } from 'logard/dist/vue-router';
import { createRouter } from 'vue-router';

const userLoader = new Loader(async scope => {        // Create an instance of the Loader class, passing it a function that performs the actual loading
  const userId = scope.getPathParam('userId');        // Scopes expose an API for retrieving route information and simultaniously tracking dependencies
  const user = await loadUser(userId);                // hypothetical function to load some data
  if (!user) throw new VueRouterRedirectError('/');   // Logard will catch this exception and instruct Vue Router to perform a redirect to the given location 
  return user;
});

const router = createRouter({
  routes: [
    {
      path: '/user/:userId',  // thanks to it's dependency tracking, Logard knows the userLoader needs to be refreshed whenever `userId` changes.
      component: UserDetailsPage,
      props: {
        user: userLoader,     // the result of the userLoader will be passed to the `user` prop of the UserDetailsPage component.
      },
      // ...
    },
  ],
});

installRouteLoader(router);   // register the necessary guards and hooks to make Logard work

As you can see, loaders are linked to route props that are defined using "object mode". Logard scans your route config for instances of the Loader class and replaces these on the fly with the results of their execution. Multiple routes are allowed to use the same loader, which is typically done with nested routes. You can also combine "normal" props with loader props, as long as you stick with object mode.

Real-world applications will typically have many more routes and will often move routes and even loaders into separate files to keep things organised.

Demo

A basic demo application can be found here.

It can be seen in action here.

Guide

Loader

class Loader<Result> {
  constructor(
    onLoad: (scope: Scope, previousResult: Result | undefined) => Result | Promise<Result>,
    onFree?: (result: Result) => void,
  )

  getResult(scope: Scope): Promise<Result>;
}

The onLoad function accepts a scope and it's previous result (if invoked subsequently) and produces a new result. It can do so synchronously or - more usually - asynchronously by returning a Promise. The scope should be used to retrieve route parameters and attributes.

If provided, the onFree function will be called after navigation whenever a result is no longer used. It can be used to release any resources associated with a specific result.

With getResult a loader can use the result of another loader. The other loader doesn't even need to be linked to an active route, so this can be used purely to structure your code. getResult is meant to be called from within an onLoad function, with the scope that was passed to it. Don't use this in any other way.

Scope

class Scope {
  getQueryParam(name: string, fallback?: string): string | undefined;

  getQueryParam<T>(name: string, sanitizer: ParamSanitizer<T>, fallback?: string | T): T | undefined;

  getQueryParams(name: string, fallback?: Array<string>): Array<string>;

  getQueryParams<T>(name: string, sanitizer: ParamSanitizer<T>, fallback?: Array<string | T>): Array<T>;

  getPathParam(name: string): string | undefined;

  getPathParam<T>(name: string, sanitizer: ParamSanitizer<T>, fallback?: string | T): T | undefined;

  removeQueryParams(name: string): void;

  getAttribute<T>(name: string): T | undefined;
}

Parameter retrieval

As you saw above, Scope provides several useful functions to retrieve query and path parameters. Generally, they all work the same.

Since there can be multiple query parameters with the same name, use getQueryParam if you expect only one or getQueryParams if multiple are allowed. The latter returns an array. The former returns a single value, or undefined if no such parameter is found at all.

You can optionally provide a sanitizer. This is a simple function that checks that a value is valid and if needed transforms it into an internal format (e.g. parse a string into a number). In some cases, invalid values can be corrected ("sanitized") automatically. Otherwise, they're generally discarded, except for path parameters (they can't be removed). Sanitizers are discussed in more detail in the next section.

With most functions, you can also optionally provide a fallback value (or array of values in case of multiple query parameters). Be aware that it is simply coerced to string. If specific formatting is needed, perform your own string conversion in advance. Fallback values are used when no valid parameter is found with the given name, but there are a few notable exceptions:

  • Missing path parameters can't be added on the fly so this always results in undefined being returned. The fallback is only used when the parameter is invalid or empty ("").
  • In case of a query parameter where the first value is an empty string and no other valid values are found, undefined is returned as well. This is done to allow for a distinction between "no parameter" and "empty value".

If for any reason (sanitizer, fallback, redundant query parameters,...) the return value of any retrieval function would no longer correspond to the original value from the route, a redirect is performed to correct this. This is done by throwing a RedirectError, which means any further code in your loader is skipped. Do not worry though, as the process is just restarted with the corrected parameter. Therefore, this is mostly transparent to your code but it should be obvious that costly operations are best deferred until after all needed parameters are retrieved. This only applies within each loader, as loaders that have already been executed successfully will not be executed again.

Removing query parameters

As the name suggests removeQueryParams removes all query parameters with the given name. As with parameter retrieval, it does so by throwing a RedirectError. It also tracks the dependency so you should always call it to clean up parameters that are only used in some conditions.

Attributes (a.k.a. meta fields)

getAttribute can be used to obtain the value of a "meta field" with the given name.

Sanitizers

type ParamSanitizer<Output> = (input: string) => Output;

A function that sanitizes a value.

  • If the input is valid it should be returned as is or transformed ("parsed") into an internal representation (e.g. convert a string to a number).
  • If the input is invalid it should throw an InvalidParam error with a new "corrected" value or undefined (default) to indicate that the value should be discarded. Note that it should never return a corrected value but rather throw InvalidParam. This will trigger a redirect to a location with the corrected value so that this always reflects the application state.

Some basic sanitizers are included in sanitizers.ts, but you'll likely need to add your own.

Consider this arbitrary example to make it more clear:

function sanitizer(input: string): number {
  if (input === 'nothing') return 0;                      // valid: return
  if (input === 'all') return Number.POSITIVE_INFINITY;   // valid: return
  if (input === '') throw new InvalidParam('nothing');    // invalid but can be corrected: throw with argument
  throw new InvalidParam();                               // invalid but correction impossible: throw without argument
}

Dependency tracking

Logard keeps track of loader dependencies via the scope that is passed to OnLoad functions. This is the case for all Scope functions as well as Loader.getResult. This way it can determine what loaders need to be refreshed during subsequent navigations.

It should be obvious that the result of a loader should therefore only depend on values that are obtained this way. Avoid using any other means like global variables, LocalStorage, location.search, etc...

Flow

Below is a fairly detailed description of the steps performed before each navigation:

  1. Collect all loaders defined for the target route.
  2. For each of them, call onLoad if any dependencies have changed from the last invocation, or if this is the first invocation.
  3. Pass the results to the respective component props.
  4. If a RedirectError is thrown by any of the above steps, instruct the router to transition to the new route, which essentially restarts this flow from step 1. If the redirectLimit is reached however, the navigation is cancelled with a permanent error.

And after each navigation:

  1. Call onFree for all results that are no longer used by the current route.
  2. Reset the state of all loaders of the previous route that are no longer used by the current route. Note that this includes loaders of any intermediary routes visited due to redirects. Resetting the state essentially means that if they're ever invoked again, they will behave as if it's the first time.

Changelog

See CHANGES.md