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

@aster-js/app

v2.1.7

Published

Aster core library part of Aster js library

Downloads

123

Readme

@aster-js/app

npm install --save @aster-js/app

Main concept

This library has the goal of helping organizing your application services and lifecycles by creating a hierarchy of ApplicationPart drived by states declared in routes.

Application Part Schema

Gets started

To create a basic application, you need to create a new SinglePageApplication.

  • A SinglePageApplication is a built to handle navigations in a dependency injection context.

  • A SinglePageApplication is also an IoCContainer configurable and extensible. See @aster-js/ioc

The shortest way

The static start method will create an app, configure it through the provided callback or IAppConfigureHandler, build it and start it.

import { SinglePageApplication } from "@aster-js/app";

const app = await SinglePageApplication.start("Library", (builder) => {
    builder.configure(services => services.addSingleton(MyService))
});

The more detailled way

This way will allow to create the application synchronously allowing synchronous references to the app then start it.

import { SinglePageApplication } from "@aster-js/app";

const builder = SinglePageApplication.create("Library");

builder.configure(services => services.addSingleton(MyService));

export const app =  builder.build();
app.start();

For example, some component based scenarios may require this way to build and start an app. The application may be required then to be exported built from its module and referenced in all components. Then each component can use the ready promise to await its loading and render a custom loading UI.

Child ApplicationPart & IAppConfigureHandler

Even if using a callback to configure a dependency injection container can be a good solution, using an IAppConfigureHandler can relocate the code that configure a part of your application more contextual.

// File: /src/modules/client/configure-client-module.ts
import { IAppConfigureHandler, IApplicationPartBuilder, IApplicationPart } from "@aster-js/app";

export class ConfigureClientModule implements IAppConfigureHandler {
    [configure](builder: IApplicationPartBuilder, host?: IApplicationPart): void {
        // Configure services
    }
}

// File: /src/main.ts
import { SinglePageApplication } from "@aster-js/app";
import { ConfigureClientModule } from "./modules/client";

// Use the "part" route value name to identify where the part is retreived
await SinglePageApplication.start("StoreApp", x => x.addPart("/:part", ConfigureClientModule);

Using ApplicationPartLifecycleHooks

Lifecycle hooks allow you to register automatically methods to execute on important application part lifecycle through the following symbols:

  • ApplicationPartLifecycleHooks.setup: The first time the module is instanciated.
  • ApplicationPartLifecycleHooks.activated: When a url match with a part route.
  • ApplicationPartLifecycleHooks.deactivated: When a route stop matching a part route.

The following example declare a service in charge of loading the settings from a custom service and render them using an other one.

import { IRouteData, ApplicationPartLifecycleHooks } from "@aster-js/app";
import { IRenderingService, IDataService } from "./services";
import { Setting } from "./models";

export class DefaultSettingService {
    private settings?: Setting[];

    constructor(
        // Container route data gives with url load current part
        @IPartRouteData private readonly routeData: IRouteData,
        // Custom services you have to declare and create
        @IRenderingService private readonly renderer: IRenderingService,
        @IDataService private readonly dataService: IDataService
    ){}

    async [ApplicationPartLifecycleHooks.setup](app: IApplicationPart): Promise<void> {
        const moduleName = this.routeData["module"];
        this.settings = this.dataService.load(moduleName);
    }
    [ApplicationPartLifecycleHooks.activated](app: IApplicationPart): Promise<void> {
        return this.renderer.renderView("settings", { settings: this.settings });
    }
    [ApplicationPartLifecycleHooks.deactivated](app: IApplicationPart): Promise<void> {
        return this.renderer.destroyView("settings");
    }
}

IPartRouteData and IContainerRouteData are two way to get route data values in services. Part for the values that allow the part to load and Container for the values that match a route declared during the part loading.

Using ApplicationPartLifecycleHooks decorators

From the example above, this is how to declare the same hooks using decorators:

import { IRouteData, IApplicationPart, ApplicationPartSetup, ApplicationPartActivated, ApplicationPartDeactivated } from "@aster-js/app";
import { IRenderingService, IDataService } from "./services";
import { Setting } from "./models";

export class DefaultSettingService {
    private settings?: Setting[];

    constructor(
        // Custom services you have to declare and create
        @IRenderingService private readonly renderer: IRenderingService
    ){}

    @ApplicationPartSetup
    async load(app: IApplicationPart): Promise<void> {
        const routeData = app.services.get(IPartRouteData, true);
        const moduleName = routeData["module"];

        const dataService = app.services.get(IDataService, true);
        this.settings = dataService.load(moduleName);
    }

    @ApplicationPartActivated
    render(app: IApplicationPart): Promise<void> {
        return this.renderer.renderView("settings", { settings: this.settings });
    }

    @ApplicationPartDeactivated
    destroy(app: IApplicationPart): Promise<void> {
        return this.renderer.destroyView("settings");
    }
}

This example shows that you can use the IApplicationPart provided as parameter to retreive services.

Nesting parts

The routing allow you to let the remaining part of the url to a child module, this way each module can decide of its own url strategy:

// File: /src/modules/settings/configure-client-module.ts
import { IAppConfigureHandler, configure, IApplicationPartBuilder, IApplicationPart } from "@aster-js/app";
import { DefaultSettingService } from "./services";

export class ConfigureSettingsModule implements IAppConfigureHandler {
    // `configure` is a symbol and that helps to distinguish between a callback or an `IAppConfigureHandler` implementation
    [configure](builder: IApplicationPartBuilder, host?: IApplicationPart): void {
        builder.configure(x => x.addSingleton(DefaultSettingService));
    }
}

// File: /src/modules/client/configure-client-module.ts
import { IAppConfigureHandler, configure, IApplicationPartBuilder, IApplicationPart } from "@aster-js/app";
import { ConfigureSettingsModule } from "./modules/settings/";

export class ConfigureClientModule implements IAppConfigureHandler {
    [configure](builder: IApplicationPartBuilder, host?: IApplicationPart): void {
        x.addPart("~/:action<settings>", ConfigureSettingsModule) // Put "~/" at the start to match relative urls.
    }
}

// File: /src/main.ts
import { SinglePageApplication } from "@aster-js/app";
import { ConfigureClientModule } from "./modules/client/";

await SinglePageApplication.start("StoreApp", builder => {
    builder.addPart("/:part/*", ConfigureClientModule); // Put "/*" at the end to match url that contains more unhandled parts.
});

Other way to handle navigation

Route handlers are simplified and their is many way to register them. One way is to register an action that will register an ActionRoutingHandler:

import { SinglePageApplication } from "@aster-js/app";

const builder = SinglePageApplication.create("Library");

builder.addAction("/:action", ctx => console.warn(`Action ${ctx.data.values["action"]} called`));

const app =  builder.build();
await app.start();

You can also call a service method registering a ServiceRoutingHandler:

import { SinglePageApplication } from "@aster-js/app";
import { IRenderService } from "./services";

const builder = SinglePageApplication.create("Library");

builder.addAction("/:view?index", IRenderService, (svc, data) => svc.render(data.values["view"]));

const app =  builder.build();
await.start();

Routing options

  • Static segments: Segments that never change. A perfect match is expected, ex: "/static-segment" // "static-segment" is the static segment
  • Route value segments: Start with : and the name of the route value, ex: "/:nameOfTheRouteValue" // "nameOfTheRouteValue" is the route value name
    • Use regex to validate content like "/name<^\w$>". The regex must be surrounded by ^ at start and $ at the end forcing the validation of the entire string
    • Use ? to make this segment optional, ex: "/:nameOfTheRouteValue?"
    • Add a default value after the ? optional segments, ex: "/:nameOfTheRouteValue?12" // "12" is the default value
    • Prefix the variable name with + to parse the value as a number, ex: "/:+nameOfTheRouteValue?12" // +12 is the default value and segment has to be valid number to match
    • Prefix the variable name with ! to parse the value as a boolean, ex: "/:!option?true" // true is the default value and segment has to be valid number to match
    • Allow string enums this syntax ":value<value1|value2|value3>" where "value1", "value1" and "value1" are the only allowed values for the route value named value
    • Customise boolean values by providing <true|false> as argument like ":!option<ok|no>" where "ok" is true and "no" is false
    • Restrict the range of number using range arguments like this ":!percent<0..100>"

Then, its easy to create optional static segments using a dynamic enum like this:

builder.addAction("/:page<index>", _ => console.warn("Action called"));

To debug the routing, open the chrome console and watch the logger output of the routing:

[14:03:02.179] [root/CustomerApp] Routing match url "/" with route "/:page?/*"

Declaring your first controller

Controller are a other way to handle routing. Controller use routing result to avoid including rendering code in it.

// File: ./controllers/customer-view-controller.ts
import { RoutePath, FromSearch, FromRoute } from "@aster-js/app";

export class CustomerViewController {

    @RoutePath("/customers")
    viewAll(@FromSearch("page") page?: string): void {
        console.info(`Show page #${page} of customers`);
    }

    @RoutePath("/customers/detail/:+id")
    viewCustomer(@FromRoute("id") id: number | null): void {
        console.info(`Show detail for customer #${id}`);
    }
}

// File: ./src$main
import { SinglePageApplication } from "@aster-js/app";
import { CustomerViewController } from "../controllers/";

await SinglePageApplication.start("Library", x => x.addController(CustomerViewController));

IRoutingResult

In this case, we are going to use Svelte to render our views so we want to return a IRoutingResult to make this boiler plate code somewhere else:

// File: ./src/shared/svelte-view-result.ts
import { Constructor } from "@aster-js/core";
import { SinglePageApplication } from "@aster-js/app";

export class SvelteViewResult implements IRoutingResult {
    constructor(
        private readonly _component: Constructor,
        private readonly _args: any
    )
    exec(app: IApplicationPart): Promise<void> {
        const root = document.getElementById("#root");
        new this._component(root, this._args);
        return Promise.resolve();
    }
}

// File: ./controllers/customer-view-controller.ts
import { RoutePath, FromSearch, FromRoute } from "@aster-js/app";
import { SvelteViewResult } from "../shared/svelte-view-result";
import CustomerList from "../views/customer-list.svelte";
import CustomerDetail from "../views/customer-detail.svelte";

export class CustomerViewController {
    @RoutePath("/customers")
    viewAll(@FromSearch("page") page?: string): IRoutingResult {
        return new SvelteViewResult(CustomerList, { page: page ? +page : 1 })
    }

    @RoutePath("/customers/detail/:+id")
    viewCustomer(@FromRoute("id") id: number | null) {
        if(id === null) return
        return new SvelteViewResult(CustomerDetail, { id });
    }
}

// File: ./src/main.ts
import { SinglePageApplication } from "@aster-js/app";
import { CustomerViewController } from "../controllers/";

await SinglePageApplication.start("Library", x => x.addController(CustomerViewController));

Controller decorators

  • @RoutePath: Bind a route to a controller method
  • @FromRoute: Inject parameter values from the route
  • @FromSearch: Inject parameter values from comming after the ?
  • @FromUrl: Inject any parameter from either the route, either the search

Controller built-in results

Even if most of real world scenarios will require to implements your custom IRoutingResult, these are the provided ones:

  • htmlResult(html: string | HTMLElement, target: HTMLElement, mode?: HtmlInsertionMode) Will replace of append raw html content into a div. Warning: This technic can lead to security risks, use it carefully and never use it with user custom inputs.
  • openResult(url: string, target: string = "_blank", features: OpenWindowOptions = {}) will open a new window, can be usefull for many scenario the must open an url in a separated window.
  • partResult(name: string, configure: Constructor<IAppConfigureHandler> | AppConfigureDelegate) will load a child application part and activate it.
  • aggregateResults(...results: IRoutingResult[]) will execute sequentially multiple results.