Cookie Consent by Free Privacy Policy Generator 📌 Router, pages, layouts and async data in TiniJS apps


✅ Router, pages, layouts and async data in TiniJS apps


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Welcome again, friends! 🥳

In the previous topic, we explored about how to get started with the TiniJS Framework, if you have not read it yet, please see Getting started with TiniJS framework.

Today topic we will explore:

  • The Tini Router and alternatives
  • Working with Pages and layouts
  • Employ route guards
  • Scroll to anchors inside Shadow DOM
  • Title and meta tags management
  • Fetch and render async data

To get started, you can download the Blank starter template, or run:

npx @tinijs/cli@latest new my-app -t blank -l

Pages

Pages in TiniJS apps are special components which purpose are to represent views or endpoints of the app. Creating and working with pages is very similar to how we would work with components.

To quickly create a page, we can use the Tini CLI to generate it.

npx tini generate page xxx

Or, create a ./app/pages/xxx.ts file manually, a page looks like this:

import {html, css} from 'lit';
import {Page, TiniComponent} from '@tinijs/core';

@Page({
  name: 'app-page-xxx',
})
export class AppPageXXX extends TiniComponent {

  protected render() {
    return html`<p>This is a page!</p>`;
  }

  static styles = css``;
}

Beside the @Page() decorator, everything else would work the same as any component. But, please note the name: 'app-page-xxx' property, it plays a role later when we setup the Tini Router.

Layouts

Layouts in TiniJS apps are also special components which purpose are to share common elements between pages. You can think of layouts as containers of pages.

To quickly create a layout, we can use the Tini CLI to generate it.

npx tini generate layout xxx

Or, create a ./app/layouts/xxx.ts file manually, a layout looks like this:

import {html, css} from 'lit';
import {Layout, TiniComponent} from '@tinijs/core';

@Layout({
  name: 'app-layout-xxx',
})
export class AppLayoutXXX extends TiniComponent {

  protected render() {
    return html`
      <div class="page">
        <header>...</header>
        <slot></slot>
        <footer>...</footer>
      </div>
    `;
  }

  static styles = css``;
}

Beside the @Layout() decorator and the <slot></slot> in the template, everything else would work the same as any component. But, please note the name: 'app-layout-xxx' property, it plays a role later when we setup the Tini Router.

Tini Router

Tini Router is the default way to add routing capability to TiniJS apps. There are also other routers you may use with TiniJS, such as: Vaadin Router and Lit Router.

For today topic, we will only explore the usage of Tini Router, it has several useful features:

  • Bundle or lazy load pages
  • Routes with layouts
  • Many param patterns
  • Navigate using the a tag
  • Route guards
  • 404 pages
  • And more

Define routes

To define routes, we create the file ./app/routes.ts and add the route entries, for example:

import type {Route} from '@tinijs/router';

export const routes: Route[] = [
  {
    path: '',
    component: 'app-layout-default',
    children: [
      {
        path: '',
        component: 'app-page-home',
        action: () => import('./pages/home.js'),
      },
      {
        path: 'post/:slug',
        component: 'app-page-post',
        action: () => import('./pages/post.js'),
      },

      // more app routes
    ],
  },
  {
    path: 'admin',
    component: 'app-layout-admin',
    children: [
      {
        path: '',
        component: 'app-page-admin-home',
        action: () => import('./pages/admin-home.js'),
      },

      // more admin routes
    ],
  },
  {
     path: '**',
     component: 'app-page-404',
     action: () => import('./pages/404.js'),
  },
];

We can model our app routing system in several ways.

Without layout

Serve pages directly without a layout.

export const routes: Route[] = [
  {
    path: '',
    component: 'app-page-home',
  },
  {
    path: 'about',
    component: 'app-page-about',
  },
];

With layouts

Share similar elements between pages.

export const routes: Route[] = [
  {
    path: '',
    component: 'app-layout-default',
    children: [
      {
        path: '',
        component: 'app-page-home',
      },
      {
        path: 'about',
        component: 'app-page-about',
      },
    ],
  },
];

Bundle or lazy load

You can either bundle or lazy load pages and layouts. Please note that for bundled layouts and pages, they must be imported first either in routes.ts or app.ts.

import './layouts/default.js';
import './pages/home.js';

export const routes: Route[] = [

  // bundled layout
  {
    path: '',
    component: 'app-layout-default',
    children: [

      // bundled page
      {
        path: '',
        component: 'app-page-home',
      },

      // lazy-loaded page
      {
        path: 'about',
        component: 'app-page-about',
        action: () => import('./pages/about.js'),
      },
    ],
  },

  // lazy-loaded layout
  {
    path: 'admin',
    component: 'app-layout-admin',
    action: () => import('./layouts/admin.js'),
    children: [
      // ...
    ],
  },
];

Route parameters

Route parameters are defined using an express.js-like syntax. The implementation is based on the path-to-regexp library, which is commonly used in modern frontend libraries and frameworks.

The following features are supported:

Type Syntax
Named parameters profile/:user
Optional parameters :size/:color?
Zero-or-more segments kb/:path*
One-or-more segments kb/:path+
Custom parameter patterns image-:size(\d+)px
Unnamed parameters (user[s]?)/:id
// courtesy of: https://hilla.dev/docs/lit/guides/routing#parameters

export const routes: Route[] = [
  {path: '', component: 'app-page-home'},
  {path: 'profile/:user', component: 'app-page-profile'},
  {path: 'image/:size/:color?', component: 'app-page-image'},
  {path: 'kb/:path*', component: 'app-page-knowledge'},
  {path: 'image-:size(\\d+)px', component: 'app-page-image'},
  {path: '(user[s]?)/:id', component: 'app-page-profile'},
];

404 routes

You can define one or more 404 routes to catch not found routes, set path to **.

There are 2 level of 404: layout level and global level. The router will use the layout 404 first if no definition found at the layout level, it will then use the global 404.

export const routes: Route[] = [
  {
    path: '',
    component: 'app-layout-default',
    children: [
      // layout routes

      {
       path: '**',
       component: 'app-page-404-layout-default',
       action: () => import('./pages/404-layout-default.js'),
      },
    ],
  },

  // other routes

  // global 404
  {
     path: '**',
     component: 'app-page-404-global',
     action: () => import('./pages/404-global.js'),
  },
];

Init the Router

After defining routes for the app, next step would be init a router instance and register the routes.

From the app.ts we import the defined routes, create a router instance and add the router outlet to the template.

import {createRouter} from '@tinijs/router';

import {routes} from './routes.js';

@App({})
export class AppRoot extends TiniComponent {

  readonly router = createRouter(routes, {linkTrigger: true});

  protected render() {
    return html`<router-outlet .router=${this.router}></router-outlet>`;
  }

}

Navigate between pages

With the option linkTrigger: true enabled, you can navigate between pages using the a just like normal links.

<a href="/">Home</a>
<a href="/about">About</a>
<a href="/post/post-1">Post 1</a>

You can also use the <tini-link> component provided by the Tini UI when link trigger disabled (not set or linkTrigger: false), it has a similar signature compared to the a tag and other useful stuffs, such as marked as active link.

<tini-link href="/">Home</tini-link>
<tini-link href="/about" active="activated">About</tini-link>
<tini-link href="/post/post-1" active="activated">Post 1</tini-link>

Access router and params

You can also navigate between pages in the imperative manner by using the go() method from a router instance.

import {getRouter, UseRouter, type Router} from '@tinijs/router';

@Page({})
export class AppPageXXX extends TiniComponent {

  // via decorator
  @UseRouter() readonly router!: Router;

  // or, via util
  readonly router = getRouter();

  protected render() {
    return html`<button @click=${() => this.router.go('/')}>Go home</button>`;
  }

}

Access current route and params is similar to access router instance.

import {UseRoute, UseParams, type ActivatedRoute} from '@tinijs/router';

@Page({})
export class AppPageXXX extends TiniComponent {

  // current route
  @UseRoute() route!: ActivatedRoute;

  // route params
  @UseParams() readonly params!: {slug: string};

}

Route hooks and guards

Lifecycle hooks are used to perform actions when a route is activated or deactivated:

  • onBeforeEnter(): called when the route is about to be activated
  • onAfterEnter(): called when the route is activated
  • onBeforeLeave(): called when the route is about to be deactivated
  • onAfterLeave(): called when the route is deactivated

You can intercept the navigation process by returning a string or a function from the onBeforeEnter and onBeforeLeave hook:

  • nullish: continue the navigation process
  • string: cancel and redirect to the path
  • function: cancel and execute the function
@Page({})
export class AppPageAccount extends TiniComponent {

  onBeforeEnter() {
    if (user) return; // continue
    return '/login'; // redirect to login page
  }

}

You can also perform async actions inside hooks, then it will wait for the actions to be resolved before process further.

Scroll to anchors

Because we use the Shadow DOM to encapsulate our app, the browser seems to be unable to serve us the correct section when we present a link with an anchor fragment /post/post-1#section-a.

Tini Router provides some methods to direct visitors to the respected sections and retrieve section headings for outlined purpose (aka. table of content).

import {ref, createRef, type Ref} from 'lit/directives/ref.js';

@Page({})
export class AppPageXXX extends TiniComponent {

  @UseRouter() readonly router!: Router;

  private _articleRef: Ref<HTMLElement> = createRef();

  onRenders() {
    // will scroll to the #whatever section if presented
    this.router.renewFragments(
      this._articleRef.value!,
      { delay: 500 }
    );

    // will scroll to the #whatever section if presented
    // add extract all the available headings
    // IMPORTANT!!!:
    //   + never change a local state in onRenders() or updated() or it will cause a render loop
    //   + store 'fragments' in a global state or emit out to the parent component or employ render checkers
    const fragments = this.router
      .renewFragments(this._articleRef.value!, {delay: 500})
      .retrieveFragments();
  }

  protected render() {
    return html`
      <article ${ref(this._articleRef)}>
        ...
      </article>
    `;
  }

}

Async data

Pages are usually rendered based on some async data from server. You can use these techniques to work with such cases.

Task render

The @lit/task package provides a Task reactive controller to help manage this async data workflow.

import {Task} from '@lit/task';

@Page({})
export class AppPageXXX extends TiniComponent {

  @Reactive() productId?: string;

  private _productTask = new Task(this, {
    task: async ([productId], {signal}) => {
      const response = await fetch(`http://example.com/product/${productId}`, {signal});
      if (!response.ok) { throw new Error(response.status); }
      return response.json() as Product;
    },
    args: () => [this.productId]
  });

  protected render() {
    return this._productTask.render({
      pending: () => html`<p>Loading product...</p>`,
      complete: (product) => html`
          <h1>${product.name}</h1>
          <p>${product.price}</p>
        `,
      error: (e) => html`<p>Error: ${e}</p>`
    });
  }

}

For more detail, please see https://lit.dev/docs/data/task/

Section render

Similar to Task Render, Section Render renders a section of a page based on the values of local states. There are 4 render states:

  • loading: all dependencies are undefined
  • empty: all are null or [] or {} or zero-size Map
  • error: any instanceof Error
  • main: fulfilled scenario
import {
  render as sectionRender, // renamed to sectionRender in v0.18.0
  type RenderData as SectionRenderData // renamed to SectionRenderData in v0.18.0
} from '@tinijs/core';

@Page({})
export class AppPageXXX extends TiniComponent {

  @Reactive() product: SectionRenderData<Product>;

  async onInit() {
    this.product = await fetchProduct();
  }

  protected render() {
    return sectionRender([this.product], {
      loading: () => html`<p>Loading product ...</p>`,
      empty: () => html`<p>No product found!</p>`,
      error: () => html`<p>Errors!</p>`
      main: ([product]) => html`
        <h1>${product.name}</h1>
        <p>${product.price}</p>
      `,
    });
  }

}

Title and meta tags

To update page title and meta when navigating to different pages, init a meta instance at app.ts.

import {initMeta} from '@tinijs/meta';

@App({})
export class AppRoot extends TiniComponent {

  readonly meta = initMeta({
    metadata: undefined, // "undefined" means use the extracted values from index.html
    autoPageMetadata: true,
  });

}

Static pages

When autoPageMetadata: true for page which is static, meta can be provide via the metadata property.

import type {PageMetadata} from '@tinijs/meta';

@Page({})
export class AppPageXXX extends TiniComponent {

  readonly metadata: PageMetadata = {
    title: 'Some title',
    description: 'Some description ...',
    // ...
  };

}

Dynamic pages

For pages with data comes from the server, we can access the meta instance and set page metadata accordingly.

import {UseMeta, Meta} from '@tinijs/meta';

@Page({})
export class AppPageXXX extends TiniComponent {

  @UseMeta() readonly meta!: Meta;

  async onInit() {
    this.post = await fetchPost();
    this.meta.setPageMetadata(post);
  }

}

Next topic will be: Bringing functionalities to TiniJS apps.

Thank you for spending time with me. If there is anything not working for you, please leave a comment or contact me on Discord, I'm happy to assist.

Wish you all the best and happy coding! 💖

...

✅ Router, pages, layouts and async data in TiniJS apps


📈 89.98 Punkte

✅ Features, future-proof and interoperable in TiniJS apps


📈 37.98 Punkte

✅ Getting started with TiniJS framework


📈 32.63 Punkte

✅ Medium CVE-2020-28490: Async-git project Async-git


📈 30.2 Punkte

✅ Async… oh, wait (Introduction into Async/Await)


📈 30.2 Punkte

✅ Is async/await a good idea? 🤔 async/await vs promises


📈 30.2 Punkte

✅ Mastering Async/Await: Simplifying JavaScript's Async Operations


📈 30.2 Punkte

✅ Host NextJS for Free on GitHub Pages || Deploying a Next.js (App Router) Application to GitHub Pages


📈 27.77 Punkte

✅ Next.js: App Router vs Pages Router


📈 24.13 Punkte

✅ Entenda o Next.js 13! Pages Router vs App Router


📈 24.13 Punkte

✅ Use Stacks and Flex Layouts (4 of 18) | Building Apps with XAML and .NET MAUI


📈 23.44 Punkte

✅ Constructing Key Pages for Your E-Commerce Site: Shop, Cart, and Product Pages


📈 22.45 Punkte

✅ 2B Pages On Web Now Use Google's AMP, Pages Now Load Twice As Fast


📈 20.94 Punkte

✅ TLDR pages: Simplified, community-driven man pages


📈 20.94 Punkte

✅ CVE-2015-7034 | Apple iWork/Pages on iOS Pages Document memory corruption (SBV-53527 / ID 1033821)


📈 20.94 Punkte

✅ Toolbox for Pages 7.0.1 - Collection of Pages templates.


📈 20.94 Punkte

✅ How to deploy to GitHub Pages using gh-pages package


📈 20.94 Punkte

✅ How to Deploy Your React or Vite Project on GitHub Pages using gh-pages


📈 20.94 Punkte

✅ A tip for everyone that uses multiple keyboard layouts and always switches back and forth


📈 19.6 Punkte

✅ Creating responsive and fluid layouts with flexbox, rem units, and mobile-first approach


📈 19.6 Punkte

✅ Graphing ZFS - A tool to visualize and compare different ZFS RAID layouts and settings


📈 19.6 Punkte

✅ How to Set Up Message Queues for Async Tasks with RabbitMQ in Nest.js Apps


📈 18.94 Punkte

✅ Enhancing Asynchronous Data Fetching in Umbraco v14 with Lit Async Directives


📈 18.13 Punkte

✅ Fetch data with parameters from async thunk


📈 18.13 Punkte

✅ New in Chrome 63: Dynamic Module Imports, Async Iterators and Generators, and CSS Over-Scroll!


📈 18.12 Punkte

✅ Zorin OS 12 Business Edition Launches with macOS, Unity, and GNOME 2 Layouts


📈 18.09 Punkte

✅ Chrome 57: Grid based layouts, Media Session API and more


📈 18.09 Punkte

✅ Latte and a "Shared Layouts" dream...


📈 18.09 Punkte











matomo

Datei nicht gefunden!