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

vue3-routable

v0.1.11

Published

Small utility to annotate classes of objects to be activated/deactivated upon vue-router routes change

Downloads

127

Readme

Vue3 Routable

Tests

TL;DR

You don't want to use VueX or Pinia? I hear ya! Use plain TypeScript decorated MVC controllers.

This is a simple set of decorators that work in conjuction with vue-router to initialise and detach objects that take care of your models, local or global, doesn't matter.

Requires TypeScript. Duh! For now....

Step 1

Install vue3-routable

yarn add vue3-routable

or

npm install vue3-routable

You might have to add this to your tsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Step 2

Annotate your controllers.

In this example we have a controller that takes care of the reactive model for a set of components in a Products list screen. They all share this component, regardless of the nested routes inside the screen.

We want to make sure that the model is in the correct state depending on the route.

It doesn't matter what the annotated method's names are.

// @/controllers/products-list-controller.ts

import {Routable, Query, RouteActivated, RouteDeactivated, RouteMatcher} from 'vue3-routable';
import {RouteLocation} from 'vue-router';
import {default as model, resetModel} from '@/models/products-list'
import {watch} from 'vue';

/**
 * this class will be registered to receive route
 * change updates for any route whose meta.pathName
 * (see below) starts with 'products-list'.
 * The parameter is optional.
 */
@Routable(/^products-list/)
export class ProductsListScreenController {
   #watchers = new Set<Function>();
    /**
     * Another way to register for routes events
     */
    @RouteMatcher
    decideWhetherToBeInvolved(route:RouteLocation):boolean {
        return !route.params.someReasonNotToGetInvolved;
    }

    @RouteActivated({priority : 10})
    async init(@Query('searchString') searchString:string) {
        if(/^products-list/.test(from.meta.pathName)) {
            //don't init the controller if navigating to a sub-route that is supposed to use the same controller
            return;
        }
        resetModel();//set the model's initial state
        if(searchString) {
            this.loadProducts(searchString)
        }
        this.watchers.add(
            watch() => model.selectedProduct,
            () => {
                //do something
            });
    }

    @RouteDeactivated()
    async cleanUp() {
        if(/^products-list/.test(to.meta.pathName)) {
            //don't cleanup if navigating to a sub-route that is supposed to use the same controller
            return;
        }
        //dispose of the watchers
        for(const unwatch of this.watchers.values()) {
            unwatch();
        }
    }

    @RouteUpdated()
    async updateProducts(@Query('searchString') searchString:string) {
        //parameters changed for this route so... update it
        this.loadProducts(searchString)
    }

    @GuardRouteLeave()
    async refuseLeaveIfUnsavedData() {
        if(model.hasUnsavedData) {
            alert('Please save or discard the changes before leaving');
            return;
        }
        return true;
    }

    @RouteWatcher(/product/)
    watchAllProductRelatedRouteChanges(@To('path') pagePath:string) {
        productPagesAudit.add(pagePath)
    }
}

export default new ProductsListScreenController();//good idea for it to be a singleton
// @/controllers/session-controller
import { Meta, Routable, GuardRouteEnter } from 'vue3-routable';

@Routable(/.*/)
export class SessionController {
  @GuardRouteEnter({ priority: 1000 })
  async checkRole(
    @Meta('noAuthRequired') noAuthRequired: boolean,
    @Meta('requiredRole') requiredRole?: string
  ) {
    if (noAuthRequired) {
      return;
    }

    if (!this.isUserAuthenticated()) {
      return { name: 'sign-in' };
    }

    if (requiredRole && !this.getUserRoles().includes(requiredRole)) {
      return { name: 'home' };
    }
  }

  //... session related methods
}

export default new SessionController();

Step 3 - Minimal boilerplate code

//@/router.ts (or @/router/index.ts)

import { registerRouter } from 'vue3-routable';

//...
const router = createRouter(routes);
registerRouter(router);
//somewhere as early as @/main.ts
import { registerRoutableClasses } from 'vue3-routable';
import ProductsListScreenController from '@/controllers/products-list-screen-controller';
import SessionController from '@/controllers/session-controller';

registerRoutableClasses(ProductsListScreenController, SessionController);

Done

If your Vue3 project is properly configured to work with TypeScript, the magic is done.

Your controllers will be activated, deactivated and updated based on the route matching rules

Rules matching

There are two ways for the registered classes to respond to route changes:

  • Via the @Routable parameter (string|RegExp|(route:RouteLocation) => boolean | Array<string|RegExp|(route:RouteLocation) => boolean>)
  • Via a @RouteMatcher annotated method ((route:RouteLocation) => boolean)

You can use both methods and the current route will be matched in an OR fashion, i.e. if any of the criteria is met.

meta.pathName

The module will add the meta.pathName property to your routes. Its value will be a concatenation of route.name with it's children's route.name.

So, for instance, for a product editor's (name : 'product-editor-screen') nested route for editing the product image (name : 'product-image-editor'), you'll have meta.pathName : 'product-editor-screen.product-image-editor.

The meta.pathName property is used to match against the @Routable arguments.

Route Handlers

Important: methods annotated with the @RouteActivated, @RouteDeactivated. @RouteUpdated, GuardRouteEnter and GuardRouteLeave must by either declared async or return a Promise or the app will fail at class-registration time.

Route Watchers

@RouteWatcher(config:RouteWatcherConfig) can be used to observe route changes,

The class still needs to be annotated with @Routable(matcher) but no further handlers/guards need to be declared.

Watchers are called if the routable class is active.

type RouteHandlerEventType = 'enter' | 'leave' | 'update';

type RouteWatcherConfig = {
  priority?: number;
  match?: RouteMatchExpression;
  on? : Array<RouteHandlerEventType> | RouteHandlerEventType;
}

All watcher configuration parameters are optional. If none are set the watcher will be called every time a route changes and the @Routable() class matcher pattern matches.

Parameters injectors can be used as usual.


@Routable(/.*/)
class Auditor {
    @RouteWatcher({match : 'product-page', priority : 0})
    productPageAuditor(@Param('productId') productId:string ) {
        if(productId) //watchers are called both on enter/exit route
            audit.productVisited(productId);
    }
    
    @RouteWatcher({ match : 'help-page' })
    helpPageSpy(@Query('topic-search') topicSearch:string) {
        if(topicSearch) 
            audit.addRequestedSearchTopic(searchTopic);
    }
}

Parameter Injectors

vue3-routable provides param decorators to inject route handlers with route information. This makes the code more readable and adds some nice abstraction over the router's inner workings.

@Param(name?:string)

Injects the to route's named param.

// Route path /products/:productId
// https://mystore.com/products/78439784395
@RouteActivated()
activate(@Param('productId') productId:string) {
    this.loadProduct(productId); //<-- '78439784395'
}

@Query(name?:string)

Injects the to route's named search query param.

// Route path /products
// https://mystore.com/products?search-for=bananas
@RouteActivated()
activate(@Query('search-for') searchFor:string) {
    this.findProducts(searchFor); //<-- 'bananas'
}

@Meta(path?:string)

Injects the to route's meta property (deep).

@GuardRouteEnter({priority:1000})
beforeEnter(@Meta('requirements.user_privileges') privileges:Array<string>) {
    if(!sessionController.userHasPrivileges(privileges)) {
        return {name : 'unauthorised'};
    }
}

To inject query, param and meta from the from route, use @From('property.path')

@To(path?:string) and @From(path?:string)

Injects the to and from routes or their (deep) property

@RouteActivated({)
activate(
    @To() to:RouteLocation,
    @To('name') toName:string) {
    console.log(`Landing to route path ${to.path} named ${toName}`);
}
@RouteActivated()
activate(
    @From('name') fromRouteName:string,
    @From('meta.foo') fromFoo:string
    ) {
    console.log(`Coming from route name ${fromRouteName} that has a meta foo=${fromFoo}`);
}

Motivation benind this module

Disclamer: opinion based on my experience. Love Pinia? More power to you. Wrote VueX? Respect. Think I'm talking nonsense? Possible. Peace.

I wrote this module because I'm a big fan of Vue's simplicity and of simplicity in general. And I love the IoC (Inversion of Control) approach, reminiscent of my Java/ActionScript Spring days.

I think that boilerplate code and artificial constructs that depart too much from the nature of the programming language and the toolset at hand, for the sake of representing some generally laudable design pattern, are more likely to hinder programmers' productivity rather than making their life easier by virtue of solving the issues that the design pattern is meant to solve.

When it's "too much" is a matter of opinion, of course. You'll draw that line.

Reading through the documentation and motivation behind VueX and Pinia, it might seem like the only two available options when writing Vue apps were:

  • having all the business logic in the vue components and having to deal with difficult passing down props
  • using VueX/Pinia stores

I don't think this is a correct assumption. Models can live and be referenced from outside view components since Vue v2.6 and the logic can be separated simply by applying general MVC principles.

Besides, from what I've seen in apps using VueX/Pinia out there, there is still too much code in view components and store files are a mix of data and behaviour.

Sure, there are quirks associated with using reactive objects directly, and, without the necessary understanding of how they work, one can end up with unexpected and hard to debug side effects. But hey, get to know your tools instead of dumping the issue on yet another tool, right?

Test coverage

The current tests are based on a mock implementation of vue-router that mimicks the effects of route navigation on routable objects. That's to avoid having to rely on heavy virtual DOM libraries.

Feedback

Any counter-rant, suggestions, insults, feel free to contact me on Discord ✌🏻