Git Product home page Git Product logo

tinro's Introduction

tinro

npm GitHub Workflow Status npm bundle size npm

tinro is a highly declarative, tiny, dependency free router for Svelte web applications.

Features

  • Just one component to declare routes in your app
  • Links are just common native <a> elements
  • History API, Hash-based, or in-memory navigation
  • Simple nested routes
  • Routes with parameters (/hello/:name)
  • Redirects
  • Fallbacks on any nested level
  • Parsing query parameters (?x=42&hello=world&fruits=apple,banana,orange)
  • Manage URL's hash and query parts
  • Svelte's REPL compatible

Documentation

Install

Install tinro as a dev dependency in your Svelte project:

$ npm i -D tinro

Getting started

tinro is very simple! It provides just one component<Route>. A common app structure looks like this:

<script>
    import {Route} from 'tinro'; 
    import Contacts from './Contacts.svelte'; // <h1>Contacts</h1>
</script>

<nav>
    <a href="/">Home</a>
    <a href="/portfolio">Portfolio</a>
    <a href="/contacts">Contacts</a>
</nav>

<Route path="/"><h1>This is the main page</h1></Route>
<Route path="/portfolio/*">
    <Route path="/">
        <h1>Portfolio introduction</h1>
        <nav>
            <a href="/portfolio/sites">Sites</a> 
            <a href="/portfolio/photos">Photos</a>
        </nav>
    </Route>
    <Route path="/sites"><h1>Portfolio: Sites</h1></Route>
    <Route path="/photos"><h1>Portfolio: Photos</h1></Route>
</Route>
<Route path="/contacts"><Contacts /></Route>

See the example in action in Svelte's REPL

Nesting

There are two types of routes you can declare in the <Route> component's path property:

Exact path

Shows its content only when path matches the URL of the page exactly. You can't place a nested <Route> inside these components.

<Route path="/">...</Route>
<Route path="/page">...</Route>
<Route path="/page/subpage">...</Route>

Non-exact path

<Route> components with a path property that ends with /* show their content when a part of the page's URL matches with the path before the /*. A nested <Route> can be placed inside routes with a non-exact path only.

<Route path="/books/*">
    Books list:
    <Route path="/fiction">...</Route>
    <Route path="/drama">...</Route>
</Route>

The path property of a nested <Route> is relative to its parent. To see the Fiction category in the above example, you would point your browser to http://mysite.com/books/fiction.

Nested routes also work inside child components. So, we can rewrite the example this way:

<!-- Bookslist.svelte-->
...
Books list:
<Route path="/fiction">...</Route>
<Route path="/drama">...</Route>

<!-- App.svelte-->
...
<Route path="/books/*">
    <Bookslist/>
</Route>

Show first matched nested route only

Sometimes, you need to show only the first nested route from all those matched with a given URL. Use the firstmatch property on the parent Route:

<Route path="/user/*" firstmatch>

    <!-- Will be open when URL is /user/add -->
    <Route path="/add">Add new user</Route> 

    <!-- Will be open when URL is /user/alex or /user/bob, but not /user/add -->
    <Route path="/:username" let:meta>Show user {meta.params.username}'s profile</Route> 

</Route>

Links

There is no special component for links. Just use native <a> elements. When the href attribute starts with a single / (like /mypage or just /) or is a relative path(like foo, foo/bar), it will be treated as an internal link which will be matched with defined routes. Other cases do not affect the links' behavior.

All internal links will be passed into the tinro router. However, it is possible to prevent this by adding the tinro-ignore or data-tinro-ignore attributes:

<a href="/api/auth" tinro-ignore>Go to API page</a>

If you need to add the active class to links where the path corresponds to the current URL, use the active action from the tinro package:

<script>
    import {active} from 'tinro';
</script>   

<!-- Common usage:
     class `active` will be added when URL is '/page' or any relative path like '/page/sub/sub' -->
<a href="/page" use:active>Link</a>

<!-- Exact match:
     class `active` will be added only when URL exactly equals '/page'  (but NOT '/page/sub') -->
<a href="/page" use:active exact>Link</a>

<!-- Custom class:
    class `myactive` will be added if link is active -->
<a href="/page" use:active active-class="myactive">Link</a>

<!-- Valid HTML usage:
    if you prefer to have valid HTML use `data-` prefix -->
<a href="/page" use:active data-exact data-active-class="myactive">Link</a>

Redirects

You can redirect the browser to any path by using the redirect property:

<!-- Exact redirect -->
<Route path="/noenter" redirect="/newurl"/>

<!-- Non-exact redirect will also work for any nested path -->
<Route path="/noenter/*" redirect="/newurl"/>

You can also redirect to a relative path — just write the new URL without / in front of it:

<!-- This will redirect to /subpage/newurl -->
<Route path="/subpage/*">
    <Route path="/" redirect="newurl"/>
</Route>

Fallbacks

Routes with the fallback property show their content when no matched address was found. Fallbacks may be placed inside a non-exact <Route> or belong to root routes. Fallbacks bubble, so if there is no fallback on the current level, the router will try to find one on any parent levels. See the example:

<Route>  <!-- same as <Route path="/*"> -->
    <Route path="/">Root page</Route>
    <Route path="/page">Page</Route>
    <Route path="/sub1/*">
        <Route path="/subpage">Subpage1</Route>
    </Route>
    <Route path="/sub2/*">
        <Route path="/subpage">Subpage2</Route>
        <Route fallback>No subpage found</Route>
    </Route>
    <Route fallback>No page found</Route>
</Route>

<a href="/">...</a>               <!-- shows Root page -->
<a href="/page">...</a>           <!-- shows Page -->
<a href="/blah">...</a>           <!-- shows No page found -->
<a href="/sub1/subpage">...</a>   <!-- shows Subpage1 -->
<a href="/sub1/blah">...</a>      <!-- shows No page found -->
<a href="/sub1/blah/blah">...</a> <!-- shows No page found -->
<a href="/sub2/subpage">...</a>   <!-- shows Subpage2 -->
<a href="/sub2/blah">...</a>      <!-- shows No subpage found -->
<a href="/sub2/blah/blah">...</a> <!-- shows No subpage found -->

Route meta

You can get useful meta data for each route by importing and calling meta from the tinro package. Notice, that meta() must be called only inside any <Route>'s child component.

<script>
    import {meta} from 'tinro';
    const route = meta();  
</script>

<h1>My URL is {route.url}!</h1>

<!-- If you need reactive updates, use it as a store -->
<h1>My URL is {$route.url}!</h1>

You can also get meta data with the let:meta directive:

<Route path="/hello" let:meta>
    <h1>My URL is {meta.url}!</h1>
</Route>

meta.url

Current browser URL (includes query).

Example: /books/stanislaw_lem/page2?order=descend

meta.pattern

The pattern of the route path, including parameter placeholders. It is a combination of the path properties of all parent routes.

Example: /books/:author

meta.match

Part of the browser URL that is matched with the route pattern.

Example: /books/stanislaw_lem

meta.from

If present, the value of the browser URL before navigation to the current page. Useful to make a back button, for example.

Example: /books/stanislaw_lem/page1?order=descend

meta.query

Object containing keys/values from the browser URL query string (if present).

Example: {order: "descend"}

meta.params

If the route pattern has parameters, their values will be in the meta.params object.

<!-- Example for URL "/books/stanislaw_lem/solaris"> -->
<Route path="/books/:author/*" let:meta>

    <!-- meta.params here {author:stanislaw_lem} -->
    Author: {meta.params.author}

    <Route path="/:title" let:meta>

        <!-- meta.params here {author:stanislaw_lem, title:solaris} -->
        Book: {meta.params.title}

    </Route>
</Route>

meta.breadcrumbs

All parent routes that have a breadcrumb property will add a breadcrumb to the meta.breadcrumbs array. Each breadcrumb is an object with name and path fields.

<Route path="/*" breadcrumb="Home">
    <Route path="/portfolio" breadcrumb="My Portfolio" let:meta>
        <ul class="breadcrumbs">
        {#each meta.breadcrumbs as bc}
            <li><a href={bc.path}>{bc.name}</a></li>
        {/each}
        </ul>

        This is my portfolio
    </Route>
</Route>

Parameters

! route.params and let:params are DEPRECATED since v.0.5.0. and will be deleted in future versions!

See meta.params section

Navigation method

By default, navigation uses the History API which allows you to have clean page URLs, although it needs some setup on the server side. Instead, you may choose to use hash or memory navigation methods. There is no need to change links or paths in your app, everything else will still work the same.

<!-- Root file of your project, ex. App.svelte -->
<script>
    import {Route,router} from 'tinro';

    router.mode.hash(); // enables hash navigation method

    // - OR -

    router.mode.memory(); // enables in-memory navigation method
</script>

<!-- Link will point browser to '/#/page/subpage' -->
<a href="/page/subpage">Subpage</a>

<!-- Route shows content when URL is '/#/page/subpage' -->
<Route path="/page/subpage">Subpage content</Route>

Note: default navigation method in non-browser environment or inside iframes is memory

Server side setup for History API method

When you use the History API and point the browser to the root path / (usually /index.html) all links and Routes will work properly. But when you start your app on any subpage, like /page/subpage, you will see the 404 Not found error. Because of this, you need to setup your server to point all requests to /index.html.

This is easy if you use the official Svelte template. Just open package.json and find this NPM script:

"start": "sirv public"

Replace it with this line:

"start": "sirv public --single"

Now, start your app with npm run dev and open a URL like http://localhost:5000/page/subpage. You should see the app page, instead of the "Not found" error.

For other servers you can read the following links: Nginx, Apache, Caddy

Base path

When you deploy your app in subdirectory on the host and use history navigation mode you must use full links and routes for correct navigation. Other way is to set base path, and all links and routes will be treated relatively. For example, if you deploy on https://myserver.com/subdir, then set base path to /subdir in root component of your app:

<script>
    import {router, Route} from 'tinro';
    router.base('/subdir');
</script>

<nav>
  <a href="/foo">Foo</a>
  <a href="/bar">Bar</a>
</nav>

<Route path="/foo">This is Foo</Route>
<Route path="/bar">This is Bar</Route>

Notice: Base path must start but not end with /

Manage hash and query

You can change URL's parts (such as query and hash) using router.location methods:

import {router} from 'tinro';

router.goto('/foo'); //URL: /foo
router.location.query.set('name','alex'); //URL: /foo?name=alex
router.location.hash.set('bar'); //URL: /foo?name=alex#bar
router.location.query.set('page',1); //URL: /foo?name=alex&page=1#bar
router.location.query.replace({hello: 'world'}); //URL: /foo?hello=world#bar
router.location.query.clear(); //URL: /foo#bar
router.location.hash.clear(); //URL: /foo

API

You can import the router object from the tinro package:

router.goto(href)

Programmatically change the URL of the current page.

router.mode

Methods to change curent router mode:

  • history() - set HistoryAPI navigation method
  • hash() - set hash navigation method
  • memory() - set memory navigation method

router.base(path)

Sets base path for router

router.location.hash

Methods, which allows to get or set current value of the URL's hash part:

  • get() - get current hash value
  • set(value) - set new hash value
  • clear() - remove hash from the current URL

router.location.query

Methods, which allows to get or modify current value of the URL's query part:

  • get(name?) - get current query object, or its property value when name specified
  • set(name,value) - update or add query property by name
  • delete(name) - remove property with specified name from the query object
  • replace(object) - replace current query object with new one
  • clear() - remove query from the current URL

router.subscribe(func)

The router object is a valid Svelte store, so you can subscribe to get the changing navigation data. func gets an object with page data:

  • url - current browser URL (with query string)
  • from - previous URL before navigation to current page (if present)
  • path - current browser URL
  • hash - the hash part of the URL, after # sign
  • query - object, containing parsed query string

Note: you can use Svelte's auto-subscription to retrieve data from the router store:

<script>
    import {router} from 'tinro';
</script>

Current page URL is: {$router.path}

router.mode.[history()|hash()|memory()]

Run this in the app's root file to set the navigation method you need.

router.params()

Deprecated. See router.meta instead.

Recipes

tinro is not the most powerful router among all those available for Svelte applications. We prefer a smaller footprint in your bundles over having all possible features out of the box. But you can easily code some features yourself using the recipies below:

Lazy loading components

If you want to have code-splitting and load components only when that page is requested, make this little component:

<!-- Lazy.svelte-->
<script>
    export let component;
</script>

{#await component.then ? component : component()}
    Loading component...
{:then Cmp}
   <svelte:component this={Cmp.default} />
{/await}

And use it when you need a lazy loaded component in your routes:

<Route path="/lazypage">
    <Lazy component={()=>import('./mypage.svelte')}/>
        <!-- OR -->
    <Lazy component={import('./mypage.svelte')}/>      
</Route>

Transitions

If you want a transiton when the path changes, create a component like this:

<!-- Transition.svelte -->
<script>
    import {router} from 'tinro';
    import {fade} from 'svelte/transition';
</script>

{#key $router.path}
    <div in:fade="{{ duration: 700 }}">
        <slot></slot>
    </div>
{/key}

Then, put your routes inside the Transition component:

<Transition> 
    <Route path="/">...</Route>
    <Route path="/page1">...</Route>
    <Route path="/page2">...</Route>
</Transition>

Guarded routes

You can protect routes from being loaded using only Svelte's logic blocks, like the {#if} statement:

{#if user.authed}
    <Route path="/profile">This is a private page...</Route>
{:else}
    <Route path="/profile"><a href="/login">Please sign in first</a></Route>
    <Route path="/login">This is the sign in form...</Route>
{/if}

You can also create a special guard component as shown in this example.

Scroll to top

tinro doesn't control scrolling in your app, but sometimes you need to scroll to the top of the page after navigation. To do this, just add the router store subscription to your root component (ex. App.svelte). This way you can run any actions (not just scrolling) every time the URL changes.

import {router} from `tinro`;
router.subscribe(_ => window.scrollTo(0, 0));

Navigation announcer

The problem of any SPA router is that it does not use default browser navigation when user click the link. This cause accessibility issue for people who use screenreaders, because it won't announce that new page was loaded. You can fix this creating Announce component:

<!-- Announcer.svelte-->
<script>
  import { router } from 'tinro';
  $: current = $router.path === '/' ? 'Home' : $router.path.slice(1);
</script>

<div aria-live="assertive" aria-atomic="true">
  {#key current}
    Navigated to {current}
  {/key}
</div>

<style>
  div {
    position: absolute;
    left: 0;
    top: 0;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
    width: 1px;
    height: 1px;
  }
</style>

Then place this component somewhere in your App.svelte root file:

...
<Announcer />
...

Troubleshooting

If you use Vite to bandle your app (including SvelteKit), you should exclude tinro from the optimizedDeps in Vite's config:

  ...
  optimizeDeps: {
    exclude: ['tinro']
  },
  ...

tinro's People

Contributors

adam-the avatar alexxnb avatar blissini avatar ckiee avatar dependabot[bot] avatar frederikhors avatar jacobmischka avatar jlecordier avatar jnordberg avatar kindoflew avatar mateomorris avatar wtachau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tinro's Issues

Lazy loading and params

Hello, thank you once again for this library!

I have a weird problem with getting url params inside lazy loaded components.

Let's imagine we have a lazy component inside a route <Route path="/:name"><Lazy component={...}></Route>.

When I call router.getParams() method inside the component, I get an error:
TypeError: (intermediate value).getParams is not a function

Here is the example I put up in REPL (for code overview) and a ready environment with the example: svelte-app.zip

<a> link issues

Hey Alex, great lib.

<a target="_blank" ... should open in new window

Clicking on any <a href=.... while holding the cmd (or ctrl in widows) should open in new window.

Edit: Actually, if any modifier key is pressed while clicking on the link, then the <a...> should not be handled by the lib.

Fallback route showing in wildcard routes

In a structure like this:

<Route>  <!-- same as <Route path="/*"> -->
    <Route path="/"><h1>It is main page</h1></Route>
    <Route path="/books/*">
        <Route path="/"><Index /></Route>
        <Route path="/fiction"><Page /></Route>
        <Route path="/drama"><Page /></Route>
    </Route>
    <Route fallback><NotFound /></Route>
</Route>

Loading route "/books" also triggers the NotFound component (see the console log in this REPL).

In the site I'm developing, the NotFound is also visible briefly as a flash of content...

Is this intentional? I hoped the fallback route to only be loaded if there was no match from any previous route.

Recipes: Component to announce route changes for screen readers

After reading a few articles (like this one and this one) I've learned that screen readers don't announce navigation events with SPA routers, while this is the default browser behavior with traditional navigation. In one of my websites I've implemented a component to announce route changes by subscribing to the router store. It looks something like this:

<script>
  import { router } from 'tinro';

  $: current = $router.path === '/' ? 'Home' : $router.path.slice(1);
</script>

<div aria-live="polite" aria-atomic="true">
  {#key current}
    <p>Navigated to {current} page</p>
  {/key}
</div>

<style>
/* screen reader only styles from Bootstrap */
  div, p {
    position: absolute;
    top: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
</style>

Would it be cool if I submitted a PR to add this to the Recipes section of the README? I figured it might not be easy/possible to add this to tinro itself without making a new component, but having a recipe could help spread awareness. Thanks!

Nested redirects

When in a nested route it would be handy to be able to redirect relative to the current nested route. Perhaps something like this?

<Route>
    <Route path="/">
        <p>Root</p>
    </Route>
    <Route path="/sub/*">
        <!-- this would redirect to /sub/one instead of /one  -->
        <Route path="/" relative-redirect="/one" />
        <Route path="/one">One</Route>
        <Route path="/two">Two</Route>
    </Route>
</Route>

Alternatively a redirect="one" (without the leading slash) could be treated as relative

router.goto not working in testing-library

Description
When using version 0.4.6 I was able to use router.goto to navigate to the home page to setup each of my tests due to router state persisting between test. With the update to 0.5.3 router.goto has stopped working.

Example

Test file:

import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"
import { router } from "tinro"
import App from "@/App.svelte"

beforeEach(() => { router.goto("/") })

it("should navigate to page b", async () => {
  render(App)
  expect(screen.getByTestId("home")).toBeInTheDocument()
  await fireEvent.click(screen.getByText("Page B"))
  await waitFor(() => expect(screen.getByTestId("page-b")).toBeInTheDocument())
})

it("should navigate to page c", async () => {
  render(App)
  // the following fails as route state persists from previous test.
  expect(screen.getByTestId("home")).toBeInTheDocument()
  await fireEvent.click(screen.getByText("Page C"))
  await waitFor(() => expect(screen.getByTestId("page-c")).toBeInTheDocument())
})

Routes component:

<script lang="ts">
  import { Route } from "tinro"
  import Home from "./home.svelte"
  import PageC from "./page-c.svelte"
  import PageB from "./page-b.svelte"
</script>

<Route>
  <Route path="/page-c"><PageC /></Route>
  <Route path="/page-b"><PageB /></Route>
  <Route path="/"><Home /></Route>
</Route>

tinro version: 0.5.3
svelte version: 3.31.2

Navigating to other path params from path parameter route does not update page

Description
Using path parameters for routes, when I navigate to other pages that also use parameters the page does not update. See below.

version: 0.4.6

Routes component:

<script lang="ts">
  import { Route } from "tinro"
</script>

<Route path="/root/:id">
  <Page />
</Route>

Page component:

<script lang="ts">
  import { router } from "tinro"
  let params: Record<string, string>
  $: params = router.params()
</script>

<div data-test-id="api-response-page">
  {#if params.id === 'assets'}
    You are on the assets page
  {:else if params.id === 'goals'}
    You are on the goals page
  {/if}
  <p>
     // clicking on the following anchors does not update the page.
    <a href="/root/assets">assets</a> 
    <a href="/root/goals">goals</a>
  </p>
</div>

TypeError: Cannot read property 'getAttribute' of null

Hi!

After update tinro to 0.2.7, if click on any element, excluding routed links, I see error in console:

Uncaught TypeError: Cannot read property 'getAttribute' of null
    at tinro_lib.js:1
    at Array.reduce (<anonymous>)
    at i$1 (tinro_lib.js:1)
    at e (tinro_lib.js:1)
(anonymous) @ tinro_lib.js:1
i$1 @ tinro_lib.js:1
e @ tinro_lib.js:1

can't navigate to routes manually

Thanks for making this beautifully simple router -- I'm excited to start using it.

I'm probably misunderstanding something, but I expected to be able to navigate directly to a URL in the address bar if there exists a <Route /> for it. However, with the following code there are no reachable pages at all:

{#if user == null}
  <Route path="/" redirect="/login" />
  <Route path="/customers" redirect="/login" />
  <Route path="/admin" redirect="/login" />
  <Route path="/accounts" redirect="/login" />
  <Route path="/login"><Login /></Route>
{:else}
  <NavBar navTitle="RevTek">
    <span slot="leftNav">
        {#if user.admin }
          <a use:active href="/customers">Customers</a>
          <a use:active href="/admin">User Admin</a>
        {:else}
          <a use:active href="/accounts">Bank Accounts</a>
        {/if}
    </span>
    <span slot="rightNav">
      <button href="#" use:active on:click={logout} class="cursor-pointer {classNotActive}">Log Out</button>
    </span>
  </NavBar>
  {#if user.admin }
    <Route path="/" redirect="/customers" />
    <Route path="/customers"><Customers /></Route>
    <Route path="/admin"><Admin /></Route>
    <Route path="/accounts" redirect="/customers" />
    <Route path="/login" redirect="/customers" />
  {:else}
    <Route path="/" redirect="/customers" />
    <Route path="/customers" redirect="/accounts" />
    <Route path="/admin" redirect="/accounts" />
    <Route path="/accounts"><Accounts /></Route>
    <Route path="/login" redirect="/accounts" />
  {/if}
{/if}

I've gathered one piece of the puzzle, which is that if I add a <nav> with an anchor link to, say, /login, I can click that and the component which is associated with that route renders as expected. Is there any way to make routing work even when not clicking on a link?

Typescript problem

When using in typescript enabled components

<script lang="ts">
  import { Route } from 'tinro';
  ...
</script>

<Route path="/">
  <h1>It is main page</h1>
</Route>
...

getting typescript complaints

JSX element class does not support attributes because it does not have a '$$prop_def' property.ts(2607)
'Route' cannot be used as a JSX component.
  Its instance type 'Route' is not a valid JSX element.
    Property '$$prop_def' is missing in type 'Route' but required in type 'ElementClass'.ts(2786)

Other than that this router is wonderful little gem, thank you.

Tiro breaks on links with custom URI schemes

I have an app that uses a link with a custom url scheme and when it is clicked a error is thrown in tinro's click handler:

SecurityError: Blocked attempt to use history.pushState() to change session history URL from http://localhost:8080 to esr:[redacted]. Protocols, domains, ports, usernames, and passwords must match.

get current route path with parameters

Is there a way to get the current route path (not url path) from the writable store?

Right now, the value stored is of format {path, query, hash}, where the path value contains the current url path, e.g. /accounts/12345

I am looking for the route path value, e.g. /accounts/:account

use:active on a <button in svelte 3 vanilla rollup SPA throws "tinro_lib.js:1 Uncaught TypeError: e.startsWith is not a function"

I'm not sure if I'm using it wrong, thought I'd post here to see if anyone else is experiencing this issue.

My navigation buttons are actual <button> elements iterated over an array, as such:

    {#each main_navigation as navitem}
      <button
        use:active
        data-exact
        data-active-class="border-opacity-100"
        on:click={() => router.goto(navitem.path)}
        class="cursor-pointer bg-primary text-secondary border-accent-green border-2 border-opacity-0 hover:border-opacity-100  py-2 px-4 mx-1 text-lg elect-none"
      >
        {navitem.name}
      </button>
    {/each}```

everything works fine without that `use:active` directive. 

if I leave only `data-exact` and `data-active-class` the active class is not applied, no error is thrown but it just doesn't work. 

I'll keep investigating and post here if I encounter any other issue.

Rendered together routes with parameters and chars

Using this:

<Route path="/player/new">
  <New />
</Route>
<Route path="/player/:playerID" let:params>
  <Show playerID={params.playerID} />
</Route>

or this:

<Route path="/player/*">
  <Route path="/new">
    <New />
  </Route>
  <Route path="/:playerID" let:params>
    <Show playerID={params.playerID} />
  </Route>
</Route>

it renders /player/new and /player/123 together.

Expected

It should render <New /> if I'm on /player/new and <Show /> if I'm on /player/* (everything is not /player/new).

Am I wrong?

Issue with route matching

I currently have a project where I have the following routes

  • /
  • /about
  • /map
  • /:username

The issue I am currently having is that when a user visits /map its also matching with /:username so I get both routes loading when I think it should have stopped from the first route was matched. I know this has been discussed before and you suggested to change the root level routing to something like /u/:username, but in a lot of standard web applications you don't need to do this, if you look at Github for example, you can load other routes at the root level while still having access to /:username. I noticed in react router they solve this with the exact property on their Route component. I also think react router have a central place (Router component) where route matching happens while it looks like this happens on a per Route component basis with tinro.

Support for guarded routes

Hello, just saying this project is awesome!

However, I do have a question. Is there a best practice to guard routes, e.g. handling auth case? For example, it seems that React Router encourages imperative approach to guards leveraging a Switch component: (stackoverflow link).

At the moment, I see only one approach to implement guards in tinro:

{#if $isAuthenticated}
  <Route path="/enter">Authenticated!</Router>
{:else}
  <Route path="/noenter" redirect="/newurl"/>
{/if}

Also, I would like to help expanding the documentation with such a case, thanks!

Further instructions on how to use 'meta' needed

Hi

according to the readme you can access meta data using the following code

<script>
    import {meta} from 'tinro';
    const route = meta();  
</script>

I however cannot seem to get this code to work... I have added it to my App.svelte and I get an error

Uncaught TypeError: Cannot read property 'meta' of undefined
    at g (tinro_lib.js:1)
    at instance (App.svelte:12)
    at init (index.mjs:1474)
    at new App (App.svelte:48)
    at main.js:3
    at main.js:8

I have added the code into the repl example and get Cannot read property 'meta' of undefined

can you please add more guidance on how to init this property so we do not get this error. I have tried putting it in onMount() of App.svelte too and get the same error.

Thank you

Feature request - Route independent breadcrumbs

The current implementation of the breadcrumbs feature is not very useful since it is available only in the matched route.

For example, the following - trivial - layout does not make any sense since the <Breadcrumb> is outside the <Route> components. Is there something I am missing?

Can I get the computed breadcrumb - for example for the url /user/details right inside the <Breadcrumb /> Component?

<App>
  <SideBar />
  <Breadcrumb/>
  <Route path="*" breadcrumb="Home">
    <Route path="/dashboard" breadcrumb="Dashboard">
      Dashboard here...
    </Route>
    <Route path="/user/*" breadcrumb="user">
      <Route path="/" breadcrumb="details">
        User Details here...
      </Route>
      <Route path="/edit" breadcrumb="edit">
        Edit the user here...
      </Route>
    </Route>
  </Route>
</App>

It would be useful if the lib exports a breadcrumb array...

Active links

Very cool tiny router! Especially love the fallback handling that bubbles up to parent routes.

One thing that seems to be missing though is support for detecting / styling active links. Have you thought about adding that to tinro?

Route Fails on Navigation

Uncaught (in promise) TypeError: Cannot read property 'c' of undefined
    at transition_out (index.mjs:751)

Seems like it's having an issue with this. Not sure if this transition_out is Svelte itself or it is tinro, but it seems to only effect the route when changed client side by clicking a link or using a goto. If you are refreshing the page, the route works just fine.

function transition_out(block, local, detach, callback) {
    if (block && block.o) {
        if (outroing.has(block))
            return;
        outroing.add(block);
        outros.c.push(() => {
            outroing.delete(block);
            if (callback) {
                if (detach)
                    block.d(1);
                callback();
            }
        });
        block.o(local);
    }
}

I realize this might be an odd bug and looks like a possible Svelte issue itself, sveltejs/svelte#3165

Just wanted to let you know this is happening feel free to close.

    "svelte": "^3.22.2",
   "tinro": "^0.2.7"

'hasContext' is not exported by node_modules\svelte\index.mjs

Hi! I've wanted to try this package for a personal project using the svelte + electron template but I'm getting this error:

Error: 'hasContext' is not exported by node_modules\svelte\index.mjs, imported by node_modules\tinro\dist\tinro_lib.js
    at error (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:5206:30)
    at Module.error (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:9687:16)
    at handleMissingExport (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:9609:28)
    at Module.traceVariable (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:10082:24)
    at ModuleScope.findVariable (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:8631:39)
    at FunctionScope.findVariable (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:2762:38)
    at ChildScope.findVariable (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:2762:38)
    at Identifier$1.bind (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:4119:40)
    at Identifier$1.getReturnExpressionWhenCalledAtPath (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:4167:18)  
    at CallExpression$1.getReturnExpression (C:\Users\jolortegui\dev\soguer\node_modules\rollup\dist\shared\rollup.js:6757:57)

I think this is a problem with my rollup config but i cannot solve it so here's my rollup.config.js file in case anyone can help my with this.

import svelte from 'rollup-plugin-svelte'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import livereload from 'rollup-plugin-livereload'
import { terser } from 'rollup-plugin-terser'
import sveltePreprocess from 'svelte-preprocess'
import typescript from '@rollup/plugin-typescript'
import copy from 'rollup-plugin-copy'

const production = !process.env.ROLLUP_WATCH

function serve() {
  let server

  function toExit() {
    if (server) server.kill(0)
  }

  return {
    writeBundle() {
      if (server) return
      server = require('child_process').spawn(
        'npm',
        ['run', 'start', '--', '--dev'],
        {
          stdio: ['ignore', 'inherit', 'inherit'],
          shell: true,
        }
      )

      process.on('SIGTERM', toExit)
      process.on('exit', toExit)
    },
  }
}

export default {
  input: 'src/main.ts',
  output: {
    sourcemap: true,
    format: 'iife',
    name: 'app',
    file: 'public/build/bundle.js',
  },
  plugins: [
    svelte({
      // enable run-time checks when not in production
      dev: !production,
      // we'll extract any component CSS out into
      // a separate file - better for performance
      css: (css) => {
        css.write('public/build/bundle.css')
      },
      preprocess: sveltePreprocess({
        sourceMap: !production,
        postcss: true,
      }),
    }),

    // If you have external dependencies installed from
    // npm, you'll most likely need these plugins. In
    // some cases you'll need additional configuration -
    // consult the documentation for details:
    // https://github.com/rollup/plugins/tree/master/packages/commonjs
    resolve({
      browser: true,
      dedupe: ['svelte'],
    }),
    commonjs(),
    typescript({ sourceMap: !production }),
    copy({
      targets: [
        {
          src: [
            'public/fonts/**/*.eot',
            'public/fonts/**/*.svg',
            'public/fonts/**/*.ttf',
            'public/fonts/**/*.woff',
            'public/fonts/**/*.woff2',
          ],
          dest: 'public/build',
        },
      ],
    }),

    // In dev mode, call `npm run start` once
    // the bundle has been generated
    !production && serve(),

    // Watch the `public` directory and refresh the
    // browser on changes when not in production
    !production && livereload('public'),

    // If we're building for production (npm run build
    // instead of npm run dev), minify
    production && terser(),
  ],
  watch: {
    clearScreen: false,
  },
}

Greetings from Paraguay!

Action on each navigation

In many routers is possible to have a function called after each navigation, useful for actions like:

window.scrollTo(0, 0)

Is this possible with tinro?

Browser back button doesn't work with `router.mode.hash()`

I reported this in #42, but since the main issue is fixed, I'm re-reporting this as its own issue:

Without router.mode.hash(), I can:

  • open a page
  • click on a link
  • The page updates and shows the <Route> content for that link.
  • click the browser back button
  • The previous page's contents are shown.

But, if I add router.mode.hash(), clicking the back button doesn't work. It does change the URL in the browser bar, but the contents rendered in the page do not revert to the original contents. (Fully reloading the page fixes this.)

Should this work the same as the default router mode?

Cannot read property 'c' of undefined in unit test

I'm using Testing Library which uses Jest to simulate the browser experience. Below is example of how I am testing routes.

it("should navigate to hierarchy page", async () => {
  render(App)
  await fireEvent.click(nav.getByText(/hierarchy/i))
  await waitFor(() => screen.getByTestId("hierarchy-page"))
  expect(screen.getByTestId("hierarchy-page")).toBeInTheDocument()

  await fireEvent.click(nav.getByText(/business/i))
  await waitFor(() => screen.getByTestId("business-page"))
  expect(screen.getByTestId("business-page")).toBeInTheDocument()
})

The following error happens when i navigate to another page:
TypeError: Cannot read property 'c' of undefined.

Node provides some additional information: "UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()."

The call stack:

at C (node_modules/tinro/dist/tinro.js:1:1782)
      at Object.o (node_modules/tinro/dist/tinro.js:1:7283)
      at transition_out (node_modules/svelte/internal/index.js:820:15)
      at Object.o (src/routes/routes.mock.svelte:519:4)
      at transition_out (node_modules/svelte/internal/index.js:820:15)
      at Object.o (src/App.svelte:42:4)
      at transition_out (node_modules/svelte/internal/index.js:820:15)
      at Object.o (src/template/base.svelte:62:4)
      at transition_out (node_modules/svelte/internal/index.js:820:15)
      at Object.p (src/template/base.svelte:113:5)
      at update (node_modules/svelte/internal/index.js:768:36)
      at flush (node_modules/svelte/internal/index.js:736:13)
      at update (node_modules/svelte/internal/index.js:1085:13)
      at node_modules/svelte/internal/index.js:1092:13

Any idea what is going on?

Typescript error when using `Route` props with `let:` directive: "Property 'params' does not exist on type '{}'"

I set up the project using https://github.com/sveltejs/template with Typescript enabled.

Using tinro's Parameters example from README.md:

	<Route path="/books/:author/*" let:params>
		Books by {params.author}
		<Route path="/:genre" let:params>
			Books by {params.author} in category {params.genre}
		</Route>
	</Route>

let:params in Typescript yield error:

Property 'params' does not exist on type '{}'. ts(2339)

tinro

The same error yields if using any other props (path, fallback, etc.)

`router.mode.hash()` broken?

Hello! New user here, so I may very well be getting something wrong.

I tried setting the router mode like this (in my index.html before I load the Svelte app):

<script type="module">
    import {router} from "tinro"
    router.mode.hash()
</script>

And alternatively like this (inside my .svelte app file):

<script context="module">
    import {router} from "tinro"
    router.mode.hash()
</script>

But while I can confirm that the code runs in either location... an <a href="/foo/"> still uses the History API to change the location to point to a non-existent page (/foo/). (I expected something like /myapp/#/foo/.)

The Route does work and display properly in my window, but if I reload the page the URL doesn't work because I don't have (and don't want) server-side support for wrong URLs.

[Update]: There are also no errors in the console. I'm running FireFox 84.0.1.

Promise and lazy routes...

Amazing router! Congratulations!

Looking at yrv router (a good router!) we can see two ways of importing routes:

<Router>
  <Route exact path="/promise" component={import('path/to/other-component.svelte')}/>
  <Route exact path="/lazy" component={() => import('path/to/another-component.svelte')}/>
</Router>

The promise way I think is amazing because I can have a lot of routes and maybe I just need to load immediately just the one I need and after that in background start downloading others.

Can we do the same with tinro?

Refreshing a route gives 404

As I mentioned in the title, the app doesn't navigate to desired page/route other than / if page is refreshed or navigated from address bar. I followed installation and usage documentation which is written in README.md here.

Path parameters with lazy loading

Hi guys,

Really great work with this library! Is it possible to use path parameters with lazy loading? I'm wondering how you would go about passing parameters as props to something lazy loaded.

matches multiple routes

Hi! This is a nice router. I am using it for my project. I have defined some routes:

<Route path='/dashboard'>
  <Dashboard />
</Route>
<Route path='/:account' let:params>
  <Account {params} />
</Route>

When I visit http://mysite.com/dashboard, I expect /dashboard to match and stop searching for routes. However tinro continues and loads both routes on top of each other, with dashboard as the value of the account parameter in the second route.

Do you know of a clean way to make tinro stop after the first matching route? Thank you. I know that I can use something like /accounts/:account and /dashboard but I am wondering if there is another way.

Relative paths

According to the README and my testing, tinro only treats links that begin with a / as internal. This means that relative links, like navigating from /items/ to /items/12 using an href="12" triggers a full page reload.

It would be nice if paths that don't begin with a protocol (http:// or just //) were treated as internal, or if there were some way to opt into this behavior (an internal attribute on the anchor tag, for example).

Thanks for the great library!

Error 'Function called outside component initialization' when calling route.meta()

First of all, congrats for this amazing component.

I'm trying to get route information through route.meta() function inside an Svelte's onMount(), but I'm getting the follow error message: Function called outside component initialization

I just tried to use example at https://github.com/AlexxNB/tinro/#route-meta in a fresh app, but without success.

I've added an example in REPL to show the problem (it's the first page of Svelte quickstart template): https://svelte.dev/repl/9f8ca34b8aef4b6e88f1eb64c1eff27f?version=3.32.1

PS.: If I call route.meta() as is in the example, the message changes to Cannot read property 'meta' of undefined.

Anything I'm doing wrong? Or is it a bug?

use:active styling and strange behavior

I'm hoping to style the active link. I'm using it like this inside a component:

<a href='/foo/{id}' use:active>
 ... more html content
</a>

I see the active class is applied but the styling with .active in the component has no effect:

<style>
.active {
 color: red;
}
</style

I also noticed that for some reason the active is being applied to two different <a> tags even though they have a different id. e.g. these both get an active class:

/foo/123
/foo/456

I tried using <a href='/foo/{id}' use:active exact> but that didn't seem to work either.

Any ideas?

Advanced matching

Is there anyway to match something like /:id-:slug? Or does it have to be /:id/:slug?

What would be the easiest way to achieve /:id-:slug?

Support for <Route ignore>

I have a link that goes to /api/auth and this request would usually go to the API but tinro catches that.
I'd like to be able to do this and I tried to make a PR but I don't understand the code very well yet:

<Route path="/api/*" ignore />

Alternative name

  • passthrough

Unused CSS class when using use:active action

I am trying to use the active action and i'm getting the following warning from svelte linter:
Unused CSS selector "a.active"

Here's a basic example of what is happening.

<script lang="ts">
  import { active } from "tinro"
  interface Page {
    url: string
    label: string
  }
  export let pages: Page[]
</script>

<ul>
  {#each pages as { url, label }}
    <li><a href={url} use:active data-exact>{label}</a></li>
  {/each}
</ul>

<style>
  a {
    padding: 1rem 0;
    display: block;
    text-decoration: none;
  }
  a.active { /* svelte linter complains about this line */
    font-weight: 600;
  }
</style>

Thoughts on how I can resolve this?

TypeScript support

Using a tinro Route with let:params in TypeScript results in the following error

Error: Property '$$slot_def' does not exist on type 'unknown'. (ts)
 let:params>

Is there a way to make router.params() reactive?

When navigating between routes that have the same shape, e.g. /things/:id, it doesn't appear that router.params() is updated. Is there a way to make it reactive?

If I go back to /things and navigate to another /things/:id, it works as expected.
If I pass in the param using the let:params, the params.id is updated as expected.

Package Changelog

Thank you for a small wonderful router with great docs. 👏🏼

I've been using tinro for a while, and the only friction I've noticed has been keeping it up to date. That's largely because changes are not noted down anywhere. Every time I've been updating tinro I had to scroll over the list of source changes to understand whether adaptions in my projects were required.

Compare this:

image

To:

image

While this is no big deal, the absence of a changelog is the only place where the package is not best-in-class.

Possible solutions could be to add a CHANGELOG.md, or to use github releases. Either description could be generated manually or automatically - the latter being easier if a machine readable commit structure like conventional commits were being used.

I'd be happy to contribute to making it happen, if you deem this worth tackling.

Nesting with params

Starting new to this lib and struggling to get the routes structured with optional params.

<!-- App.svelte --> 
...
<Route path="/*">
   <Main />
</Route>

<!-- Main.svelte --> 
...
<Route path="/targets/*" breadcrumb="Targets" firstmatch>
  <TargetsPage />
</Route>

<!-- TargetsPage.svelte --> 
...
  <Route path="/list/:T1/:T2" let:meta><List /></Route>
  <Route path="/summary/:T1/:T2" let:meta><Summary /></Route>
  <Route path="/:T1/:T2" let:meta><List /></Route>
  <Route fallback>
    <div>No Page</div>
  </Route>

What I want to see:
/targets --> show List
/targets/:T1 --> show List with meta T1 defined
/targets/:T1/:T2 --> show List with meta T1 & T2 defined
/targets/list --> show List
/targets/list/:T1 --> show List with meta T1 defined
/targets/list/:T1/:T2 --> show List with meta T1 & T2 defined
/targets/summary --> show Summary
/targets/summary/:T1 --> show Summary with T1 defined
...etc

What I'm seeing:
/targets --> incorrectly shows No Page
/targets/aa --> incorrectly shows No Page
/targets/aa/bb --> correctly shows List with T1=aa & T2=bb
/targets/list --> incorrectly shows No Page
/targets/list/aa --> incorrectly shows List with T1=list and T2=aa
/targets/list/aa/bb --> correctly shows List with T1=aa and T2=bb

In theory, I shouldn't need the fallback as the (sub)roots should land. But maybe could redirect if needed. The missing and mismatching of params is what's throwing me.

Anything obvious I'm missing?

Thank you for this library!

Redirect to another route on fallback

Can there be a functionality to redirect to another route on fallback condition (like "/" page or /404 something?).

I am asking because on fallback, the url stays the same and the content get rendered. But can we do something like redirect to /404.

<Route path="/"> <Home /> </Route>
<Route fallback redirect="/">

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.