Full Code of VeliovGroup/flow-router for AI

master 1c8eedef079e cached
116 files
362.3 KB
97.8k tokens
222 symbols
1 requests
Download .txt
Showing preview only (391K chars total). Download the full file or copy to clipboard to get everything.
Repository: VeliovGroup/flow-router
Branch: master
Commit: 1c8eedef079e
Files: 116
Total size: 362.3 KB

Directory structure:
gitextract_0qjsbjv_/

├── .agents/
│   └── skills/
│       └── meteor-flow-router/
│           ├── SKILL.md
│           └── reference.md
├── .cursorignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   ├── PULL_REQUEST_TEMPLATE
│   └── workflows/
│       └── test_suite.yml
├── .gitignore
├── .meteorignore
├── .versions
├── AGENTS.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── HISTORY.md
├── LICENSE
├── ORIGINAL FlowRouter LICENSE
├── README.md
├── client/
│   ├── _init.js
│   ├── active.route.js
│   ├── group.js
│   ├── modules.js
│   ├── renderer.js
│   ├── route.js
│   ├── router.js
│   └── triggers.js
├── docs/
│   ├── README.md
│   ├── api/
│   │   ├── README.md
│   │   ├── current.md
│   │   ├── decodeQueryParamsOnce.md
│   │   ├── getParam.md
│   │   ├── getQueryParam.md
│   │   ├── getRouteName.md
│   │   ├── go.md
│   │   ├── group.md
│   │   ├── initialize.md
│   │   ├── onRouteRegister.md
│   │   ├── path.md
│   │   ├── pathRegExp.md
│   │   ├── refresh.md
│   │   ├── reload.md
│   │   ├── render.md
│   │   ├── route.md
│   │   ├── setParams.md
│   │   ├── setQueryParams.md
│   │   ├── triggers.md
│   │   ├── url.md
│   │   ├── wait.md
│   │   ├── watchPathChange.md
│   │   └── withReplaceState.md
│   ├── auto-scroll.md
│   ├── fast-render-integration.md
│   ├── full.md
│   ├── helpers/
│   │   ├── README.md
│   │   ├── RouterHelpers.md
│   │   ├── currentRouteName.md
│   │   ├── currentRouteOption.md
│   │   ├── isActivePath.md
│   │   ├── isActiveRoute.md
│   │   ├── isNotActivePath.md
│   │   ├── isNotActiveRoute.md
│   │   ├── param.md
│   │   ├── pathFor.md
│   │   ├── queryParam.md
│   │   └── urlFor.md
│   ├── hooks/
│   │   ├── README.md
│   │   ├── action.md
│   │   ├── data.md
│   │   ├── endWaiting.md
│   │   ├── onNoData.md
│   │   ├── triggersEnter.md
│   │   ├── triggersExit.md
│   │   ├── waitOn.md
│   │   ├── waitOnResources.md
│   │   └── whileWaiting.md
│   ├── original-readme.md
│   ├── quick-start.md
│   ├── react.md
│   ├── templating-with-data.md
│   ├── templating-with-regions.md
│   └── templating.md
├── index.d.ts
├── index.test-d.ts
├── lib/
│   ├── _helpers.js
│   ├── constants.js
│   ├── group-base.js
│   ├── micro-router.js
│   ├── qs.js
│   ├── route-base.js
│   └── router-base.js
├── package-types.json
├── package.js
├── package.json
├── server/
│   ├── _init.js
│   ├── group.js
│   ├── plugins/
│   │   └── fast-render.js
│   ├── route.js
│   └── router.js
├── test/
│   ├── client/
│   │   ├── _helpers.js
│   │   ├── group.spec.js
│   │   ├── loader.spec.js
│   │   ├── route.reactivity.spec.js
│   │   ├── router.core.spec.js
│   │   ├── router.reactivity.spec.js
│   │   ├── router.subs_ready.spec.js
│   │   ├── trigger.spec.js
│   │   └── triggers.js
│   ├── common/
│   │   ├── fast_render_route.js
│   │   ├── group.spec.js
│   │   ├── route.spec.js
│   │   ├── router.addons.spec.js
│   │   ├── router.path.spec.js
│   │   └── router.url.spec.js
│   └── server/
│       ├── _helpers.js
│       └── plugins/
│           └── fast_render.js
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .agents/skills/meteor-flow-router/SKILL.md
================================================
---
name: meteor-flow-router
description: Guides Meteor routing with ostrio:flow-router-extra plus head/title companions ostrio:flow-router-meta and ostrio:flow-router-title — registration, hooks, globals, triggers, RouterHelpers, document.title, and meta/link/script merge rules. Use when editing Flow Router ecosystem packages, wiring Meteor client routes, SEO head tags, JSON-LD, 404 routes, TypeScript imports, or debugging navigation and head sync.
---

# Meteor Flow Router (ostrio stack)

## When this applies

- Implementing or debugging **client** routing in Meteor with **`ostrio:flow-router-extra`** (not legacy `kadira:flow-router`).
- **`ostrio:flow-router-meta`**: `<head>` **`meta` / `link` / `script`** from route/group/globals options.
- **`ostrio:flow-router-title`**: **`document.title`** from **`title` / `titlePrefix`** on routes, groups, globals, and not-found flows.
- Isomorphic **path/url** registration (client + server); navigation and DOM only on **client**.

Longer file maps and maintainer detail: [reference.md](reference.md) (links to canonical **`AGENTS.md`** per repo).

---

## Stack overview

| Package | Atmosphere | Arch | Role |
|---------|--------------|------|------|
| Core router | `ostrio:flow-router-extra` | client + server | Routes, groups, hooks, `matchPath`, `RouterHelpers` (client) |
| Head tags | `ostrio:flow-router-meta` | **client only** | `meta`, `link`, `script`; implies **`ostrio:flow-router-title`** and re-exports **`FlowRouterTitle`** |
| Title | `ostrio:flow-router-title` | **client only** | `title`, `titlePrefix` → `document.title` |

**Peer versions (align on release):** router **`package.js`** / README “Compatibility”; meta README pins **`ostrio:flow-router-extra@3.13.0+`** and implies **`ostrio:flow-router-title@3.5.0`**.

---

## `ostrio:flow-router-extra` — identity

- **Singleton:** **`FlowRouter`** is a **`Router`** instance with **`FlowRouter.Router`** / **`FlowRouter.Route`** attached.
- **Types:** **`index.d.ts`** + **`package-types.json`** (`typesEntry`). Apps: **`meteor add zodern:types`**, [Meteor TS guide](https://docs.meteor.com/guide/typescript.html); gate **`RouterHelpers`** and other client-only APIs with **`Meteor.isClient`** or split modules — server bundle does not export **`RouterHelpers`**.
- **Canonical API narrative:** repo **`docs/`** (often excluded from app bundle via **`.meteorignore`**).

### Public exports

**Client** (`meteor/ostrio:flow-router-extra`):

`FlowRouter`, `Router`, `Route`, `Group`, `Triggers`, `BlazeRenderer`, `RouterHelpers`.

**Server:** same minus **`RouterHelpers`**. **`Triggers`** and **`BlazeRenderer`** are **empty stubs** on server — do not use there.

### Routes

- **`FlowRouter.route(pathDef, options?)`** → **`Route`**. Paths start with **`/`**, except catch-all **`'*'`** (register **last** internally so it does not shadow concrete routes).
- **Named routes:** **`options.name`**; use in **`FlowRouter.path` / `FlowRouter.url`**.
- **Isomorphic:** register same table on **client** and **server** (SSR / `matchPath` / meta packages). Server has no **`go`** / DOM.

### Groups

- **`FlowRouter.group({ name, prefix, ... })`**. **`prefix`** starts with **`/`**; nested prefixes **concatenate**.
- **Merge:** child routes get **`triggersEnter` / `triggersExit`** merged (group first, then route). Group **`waitOn`** becomes **`waitFor`** chain on route.
- **Not merged** from group→route for addons (stay on route/group): among others **`meta`**, **`link`**, **`script`**, **`title`**, **`titlePrefix`** — see **`lib/group-base.js`** `omit` list.

### Wildcard / 404

- **Preferred:** **`FlowRouter.route('*', { name: '…', action() { … } })`** (any name; e.g. **`__notFound`**).
- **Deprecated:** **`FlowRouter.notFound = { … }`** — logs deprecation, rewrites to **`route('*', …)`**. Companion title/meta packages still support legacy not-found shape via **`_notfoundRoute`** wrapping.

### Globals and router instance

| Surface | Role |
|---------|------|
| **`FlowRouter.globals`** | **`Array`**: **`push({ waitOn, waitOnResources, … })`** merged into every route’s wait pipeline |
| **`FlowRouter.subscriptions`** | Global subscription hook on internal **`_globalRoute`** |
| **`FlowRouter.decodeQueryParamsOnce`** | Set **`true`** in new apps (fixes double-decode; default **`false`** legacy) |
| **`FlowRouter.triggers.enter` / `.exit`** | Global triggers; optional **`{ only: ['routeName'] }`** or **`{ except: […] }`** |
| **`FlowRouter.env`** | **`replaceState`**, **`reload`**, **`trailingSlash`** helpers |
| **`FlowRouter.wait()`** / **`FlowRouter.initialize(options)`** | Defer / start **`MicroRouter`** (**`click`**, **`popstate`** defaults). **`initialize`** **once** — throws if twice. Optional **`options.maxWaitFor`** (ms) sets **`FlowRouter.maxWaitFor`** (default **`120000`**, same as **`MAX_WAIT_FOR_MS`** export). |
| **`FlowRouter.maxWaitFor`** | Default max time (ms) for each route’s **`waitOn`** promise phase and subscription **`ready()`** wait; override per route with **`route({ maxWaitFor, … })`**. When time is exceeded, **`action`** still runs; **navigating away** aborts **`waitOn`** and skips **`action`** for the route being left. |
| **`FlowRouter.onRouteRegister(cb)`** | Fires on route registration (payload strips heavy hooks) |

### Hook order (core)

1. **`whileWaiting`** → **`waitOn`** → **`waitOnResources`** → **`endWaiting`**
2. **`data`** → **`onNoData`**
3. **`triggersEnter`** (after global **`FlowRouter.triggers.enter`**)
4. **`action`**
5. **`triggersExit`**

**Add-on keys** **`title`**, **`meta`**, **`link`**, **`script`** are consumed by **title/meta** packages, not core router execution.

**Tracker:** avoid reactive globals inside **`.subscriptions`** in ways that break **`safeToRun`** (see router **`_buildTracker`**).

### Triggers

- **`FlowRouter.triggers.enter(triggers, filter?)`** / **`exit`** — **`filter`**: **`only`** OR **`except`**, not both.
- **Signature (conceptual):** **`(context, redirect, stop, data)`** — **`redirect`** must be **synchronous**; **`stop()`** aborts chain.

### RouterHelpers (client)

From **`meteor/ostrio:flow-router-extra`**: **`name`**, **`path`**, **`pathFor`**, **`configure`**, etc. With Blaze: **`pathFor`**, **`urlFor`**, **`param`**, **`queryParam`**, **`currentRouteName`**, **`subsReady`**, active-route helpers. **Server:** subset (e.g. **`pathFor`** / **`urlFor`**), not full client helper set.

### Implementation map (core)

| Area | Paths (typical) |
|------|------------------|
| Client router | **`client/router.js`**, **`client/route.js`**, **`client/group.js`**, **`client/triggers.js`** |
| Shared | **`lib/router-base.js`**, **`lib/micro-router.js`**, **`lib/group-base.js`** |
| Server | **`server/router.js`** (`matchPath`), **`server/plugins/fast-render.js`** |

### Tips

- **`ROOT_URL_PATH_PREFIX`**: base path strip/add in client router.
- **Idempotent `go`:** no-op if path unchanged unless **`reload`** env forces redo.
- **Named subs:** **`FlowRouter.subsReady('name')`** for **`this.register('name', sub)`** in **`subscriptions`**.
- **External redirect:** triggers cannot redirect off-origin HTTP(S); use **`window.location`**.
- **Debugging:** logs prefixed **`[ostrio:flow-router-extra]`**; common errors: double **`initialize`**, async **`redirect`** misuse, **`wait()`** after init.

### Testing (router package)

**`meteor test-packages ./`** from package root; **`meteor npm run test:tsd`** or **`meteor npm exec tsd`** against **`index.test-d.ts`** — update **`index.test-d.ts`** when **`index.d.ts`** / package exports change.

---

## `ostrio:flow-router-meta` — identity

- **Client-only** — **`api.mainModule(..., 'client')`**; no SSR head injection from this package.
- **Implies** **`ostrio:flow-router-title`** — import **`FlowRouterMeta`** and **`FlowRouterTitle`** from **`meteor/ostrio:flow-router-meta`** if you want one import line.

### Wiring

```js
import { FlowRouterMeta, FlowRouterTitle } from 'meteor/ostrio:flow-router-meta';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// After all FlowRouter.route / group / globals (including `*` 404 if used):
new FlowRouterMeta(FlowRouter);
new FlowRouterTitle(FlowRouter);
```

- **`new FlowRouterMeta(router)`** — registers **`router.triggers.enter`** with **`metaHandler`**; wraps **`router._notfoundRoute`** so 404 still syncs head.
- Does **not** monkey-patch **`route` / `group`** — reads **`context.route.options`**, **`context.route.group`**, **`router.globals`** at enter time.
- **`metaHandler`** receives **`data`** from route **`data()`** (fourth arg to enter triggers). **Debounce:** **5ms** timer coalesces rapid navigations.

### Allowed option keys

On **`FlowRouter.route`**, **`FlowRouter.group`**, and objects in **`FlowRouter.globals.push`**:

- **`meta`**, **`link`**, **`script`** — plain object, **`(params, queryParams, data) => object`**, or nested functions resolved by **`_getValue`**.
- **`null`** / empty resolved value **removes** that logical key’s DOM node. Keys **absent** from merged result are **not** auto-removed on navigation — use **`null`** to unset stale logical names.

### Merge order (`_setTags`)

1. **`FlowRouter.globals`** — iterated **last index → 0**; **`Object.assign`** so **earlier `push` wins** over later for same logical key.
2. **Groups** — walk **`group.parent`**: **innermost group that defines that tag type** (`meta` / `link` / `script`) supplies the group branch (not a deep merge of every ancestor’s separate objects).
3. **Route** — merged **last** (**route wins** over globals + group for same logical keys).

**Logical names** = object keys under `meta` / `link` / `script`; DOM uses **`data-name="<key>"`**.

### Attribute shorthand

- **`meta`:** string → **`name="<key>"`**, **`content="<string>"`**; object spreads attrs.
- **`link`:** string → **`rel="<key>"`**, **`href="…"`**.
- **`script`:** string → **`src="…"`**; object as-is for attrs.
- **`innerHTML`:** special-cased (e.g. **`application/ld+json`**). Non-string attr values skipped.

**Loaded CSS/JS** stay in memory when tags removed — cannot fully “unload” global side effects.

---

## `ostrio:flow-router-title` — identity

- **Peer:** app must add **`ostrio:flow-router-extra@3.13.0+`**.
- **Do not import from server bundles.**

### Wiring

1. Define routes / groups / globals (including **`FlowRouter.route('*', …)`** if used).
2. **`new FlowRouterTitle(FlowRouter)`** after routes (typically end of client router module).
3. **`FlowRouter.initialize()`** when app ready.

### API

- Registers **`triggers.enter`** / **`triggers.exit`**; wraps **`_notfoundRoute`** for legacy **`FlowRouter.notFound`** / not-found options.
- **`instance.set(string): boolean`** — sets **`document.title`** (reactive internal **`ReactiveVar`** + **`setTimeout(0)`**).

### Options

| Option | Notes |
|--------|--------|
| **`title`** | string or **`(params, queryParams, data) => string`** — route wins over group; functions can run in **`Tracker.autorun`** |
| **`titlePrefix`** | On groups; nested: **parent prefixes first**, concatenated |
| **`FlowRouter.globals`** | First object with **`title`** seeds **default** when route has no **`title`** |

**Priority (high → low):** explicit route **`title`** (with prefix rules) → group **`title`** / prefixes → **`globals`** default → initial HTML **`document.title`**.

**404:** supports catch-all **`*`** and legacy **`FlowRouter.notFound`** (see package README).

---

## Conventions (do not regress)

- Prefer **`FlowRouter.route('*', …)`** over deprecated **`FlowRouter.notFound`** setter.
- Set **`FlowRouter.decodeQueryParamsOnce = true`** for new apps.
- **`underscore`:** not a runtime dependency of core router.
- **Ecosystem releases:** often ship **router + meta + title + demos** together.

---

## Companion init (typical client entry)

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { FlowRouterMeta, FlowRouterTitle } from 'meteor/ostrio:flow-router-meta';

// … define routes, groups, globals …

new FlowRouterMeta(FlowRouter);
new FlowRouterTitle(FlowRouter);

Meteor.startup(() => {
  FlowRouter.initialize();
});
```

If you only need title (no meta/link/script), import **`FlowRouterTitle`** from **`meteor/ostrio:flow-router-title`** instead.


================================================
FILE: .agents/skills/meteor-flow-router/reference.md
================================================
# Canonical `AGENTS.md` sources

Read these for full upstream wording, file-level maps, and maintainer notes.

| Package | Repo `AGENTS.md` (raw) |
|--------|-------------------------|
| `ostrio:flow-router-extra` | [AGENTS.md](https://raw.githubusercontent.com/veliovgroup/flow-router/master/AGENTS.md) |
| `ostrio:flow-router-meta` | [AGENTS.md](https://raw.githubusercontent.com/veliovgroup/Meteor-flow-router-meta/master/AGENTS.md) |
| `ostrio:flow-router-title` | [AGENTS.md](https://raw.githubusercontent.com/veliovgroup/Meteor-flow-router-title/master/AGENTS.md) |

In a checkout of this monorepo, prefer the local copies next to each `package.js`.


================================================
FILE: .cursorignore
================================================
.build*
.DS_Store
.eslintcache
.npm
.github
node_modules
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
HISTORY.md
LICENCE
LICENSE
ORIGINAL FlowRouter LICENSE

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: dr-dimitru
custom: https://paypal.me/veliovgroup


================================================
FILE: .github/ISSUE_TEMPLATE
================================================
### I'm having an issue:
  - Give an expressive description of what is went wrong
  - Version of `flow-router-extra` you're experiencing this issue
  - Version of `Meteor` you're experiencing this issue
  - Browser name and its version (Chrome, Firefox, Safari, etc.)?
  - Platform name and its version (Win, Mac, Linux)?
  - If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot

### I have a suggestion:
  - Describe your feature / request
  - How you're going to use it? Give a usage example(s)

### Documentation is missing something or incorrect (have typos, etc.):
  - Give an expressive description what you have changed/added and why
  - Make sure you're using correct markdown markup
  - Make sure all code blocks starts with triple ``` (*backtick*) and have a syntax tag, for more read [this docs](https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting)
  - Post addition/changes in issue, we will manage it

## Thank you, and do not forget to get rid of this default message


================================================
FILE: .github/PULL_REQUEST_TEMPLATE
================================================
Thank you for contribution. Before you go:
  1. Make sure you're using `spaces` for indentation
  2. Make sure all new code is documented in-code-docs
  3. Make sure new features, or changes in behavior is documented in README.md and/or other docs materials
  4. Make sure this PR was previously discussed, if not create new issue ticket for your PR
  5. Give an expressive description what you have changed/added and why
  
Thank you for making this package better :)

## Do not forget to get rid of this default message

================================================
FILE: .github/workflows/test_suite.yml
================================================
name: Test suite

# run ci on direct pushes to master
# or on any pull request update
on:
  push:
    branches:
      - master
  pull_request:

jobs:
  tests:
    name: Meteor package tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: setup node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-22-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-22-

      # we use mtest to run tinytest headless
      - run: npm install -g mtest

      - name: Setup meteor
        uses: meteorengineer/setup-meteor@v3
        with:
          meteor-release: '3.4'

      - run: |
          meteor npm install
          meteor npm run test:once


================================================
FILE: .gitignore
================================================
*.browserify.js.cached
*.browserify.js.map
.build*
.DS_Store
.eslintcache
.npm
/client/tmp
node_modules
/.cursor/hooks


================================================
FILE: .meteorignore
================================================
.github
.agents/
.cursorignore
.gitignore
.npm
node_modules/
.DS_Store
.eslintcache
.eslintrc
AGENTS.md
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
HISTORY.md
LICENCE
LICENSE
ORIGINAL FlowRouter LICENSE
index.test-d.ts
package-lock.json
package.json
tsconfig.json
/test/
/docs/

================================================
FILE: .versions
================================================
allow-deny@2.1.0
babel-compiler@7.14.0
babel-runtime@1.5.2
base64@1.0.13
binary-heap@1.0.12
boilerplate-generator@2.1.0
callback-hook@1.7.0
check@1.5.0
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.2.0
ddp-common@1.4.4
ddp-server@3.2.0
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.18.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.13.0
ecmascript-runtime-server@0.11.1
ejson@1.1.5
facts-base@1.0.2
fetch@0.1.6
geojson-utils@1.0.12
http@1.0.1
id-map@1.2.0
inter-process-messaging@0.1.2
local-test:ostrio:flow-router-extra@3.15.0
logging@1.3.6
meteor@2.3.0
minimongo@2.1.0
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.3.0
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@6.16.1
ordered-dict@1.2.0
ostrio:flow-router-extra@3.15.0
promise@1.0.0
random@1.2.2
react-fast-refresh@0.3.0
reactive-dict@1.3.2
reactive-var@1.0.13
reload@1.3.2
retry@1.1.1
routepolicy@1.1.2
socket-stream-client@0.6.1
tinytest@1.4.0
tracker@1.3.4
typescript@5.10.0
underscore@1.6.4
webapp@2.1.2
webapp-hashing@1.1.2
zodern:types@1.0.13


================================================
FILE: AGENTS.md
================================================
# Agent notes: `ostrio:flow-router-extra`

Use when **editing this repo**, **shipping Atmosphere releases**, or **implementing / debugging routing** in modern Meteor (including TS-first apps importing `meteor/ostrio:flow-router-extra`). Canonical long-form API: repo **`docs/`** (not bundled — see **`.meteorignore`**). This file is the **single agent-oriented** surface: patterns, gotchas, and where logic lives in source.

---

## Package identity

- **Atmosphere name:** `ostrio:flow-router-extra` (not legacy `kadira:flow-router`; tests sometimes alias `Package['kadira:flow-router']` for compatibility).
- **Version:** **`package.js`** → keep **README “Compatibility”** and siblings (`ostrio:flow-router-meta`, `ostrio:flow-router-title`) aligned on release.

---

## `package.js` surface

| Item | Detail |
|------|--------|
| **Meteor** | `api.versionsFrom(['1.4', '2.8.0', '3.0.1', '3.4'])` |
| **Core deps** | `modules`, `ecmascript`, `promise`, `tracker`, `reactive-dict`, `reactive-var`, `ejson`, `check` both archs |
| **Weak TS** | `zodern:types@1.0.13`, `typescript` (weak) |
| **Weak Blaze** | `templating`, `blaze@2.0.0 \|\| 3.0.0` **client only** |
| **Entry** | `api.mainModule('client/_init.js', 'client')`, `api.mainModule('server/_init.js', 'server')` |
| **Types** | `api.addAssets('index.d.ts', ['client', 'server'])` + **`package-types.json`** (`typesEntry`) |

---

## Public exports

**Client** (`client/_init.js`):

```js
import {
  FlowRouter,
  Router,
  Route,
  Group,
  Triggers,
  BlazeRenderer,
  RouterHelpers,
} from 'meteor/ostrio:flow-router-extra';
```

**Server** (`server/_init.js`): same minus **`RouterHelpers`**. **`Triggers`** and **`BlazeRenderer`** are **empty stubs** — do not use them on the server.

**Singleton:** `FlowRouter` is a **`Router`** instance with **`FlowRouter.Router`** / **`FlowRouter.Route`** attached (companion packages, e.g. `ostrio:flow-router-meta`).

---

## TypeScript

- Types: **`index.d.ts`** + **`package-types.json`**. Apps: **`meteor add zodern:types`**, [Meteor TS guide](https://docs.meteor.com/guide/typescript.html), generate types so `meteor/ostrio:flow-router-extra` resolves.
- **Isomorphic imports:** gate **`RouterHelpers`** (and client-only APIs) with **`Meteor.isClient`** or split modules — server bundle does not export `RouterHelpers`.
- **`index.test-d.ts`:** keep in sync whenever **`index.d.ts`**, **`package.js`**, or public exports (`client/_init.js`, `server/_init.js`) change — extend assertions so **`tsd`** stays green.
- **Run type tests** from package root: **`meteor npm run test:tsd`** / **`meteor npm exec tsd`** (same **`index.test-d.ts`** vs **`index.d.ts`**).

---

## Routes registration

- **API:** `FlowRouter.route(pathDef, options?)` → **`Route`** instance. Paths must start with **`/`**, except the catch-all **`'*'`** (see below).
- **Named routes:** set **`options.name`**. Use name or path fragment in **`FlowRouter.path(nameOrPathDef, params, queryParams)`** / **`FlowRouter.url(...)`**.
- **Isomorphic:** register the same table on **client** (navigation) and **server** (SSR / `matchPath`, meta packages). **`import { FlowRouter } from 'meteor/ostrio:flow-router-extra'`** in both; server has no `go` / DOM.

**Minimal:**

```js
FlowRouter.route('/', {
  name: 'home',
  action() {
    // Blaze: this.render(...); React/other: mount here
  },
});
```

**With param:**

```js
FlowRouter.route('/post/:id', {
  name: 'post',
  action(params) {
    // params.id
  },
});
```

**Implementation refs:** `client/router.js` (`route`, `_updateCallbacks`), `client/route.js` (hooks, Blaze `this.render`).

---

## Route groups registration

- **API:** `FlowRouter.group({ name, prefix, ...options })` → **`Group`**. Nested groups: **`group.group({ ... })`** (`lib/group-base.js`).
- **`prefix`:** must start with **`/`**; nested prefixes **concatenate**.
- **Merge rules:** child routes get **`triggersEnter` / `triggersExit`** merged (group first, then route). **`waitOn`** from group becomes **`waitFor`** chain on the route.
- **Omitted from group→route merge** (stay on route / group for addons): among others **`meta`**, **`link`**, **`script`**, **`title`**, **`titlePrefix`** — see **`lib/group-base.js`** `omit` list.

```js
const app = FlowRouter.group({
  name: 'app',
  prefix: '/app',
  triggersEnter: [/* shared enter */],
});

app.route('/dashboard', {
  name: 'dashboard',
  action() { /* matches /app/dashboard */ },
});

const admin = app.group({ name: 'admin', prefix: '/admin' });
admin.route('/users', { name: 'adminUsers' }); // /app/admin/users
```

**Tests / examples:** `test/client/group.spec.js`, `test/common/group.spec.js`.

---

## Wildcard (404 / not-found) route

- **Preferred:** `FlowRouter.route('*', { name: '__notFound', action() { ... } })` (or any name). Registered **last** internally so it does not shadow concrete routes (`client/router.js` `_updateCallbacks`).
- **Deprecated:** **`FlowRouter.notFound = { ... }`** — logs deprecation, rewrites to `route('*', ...)` with default name **`__notFound`** (`client/router.js`).

```js
FlowRouter.route('*', {
  name: 'notFound',
  action() {
    // 404 UI
  },
});
```

Companion packages (`ostrio:flow-router-title`, `ostrio:flow-router-meta`) document both styles.

---

## Global options (`FlowRouter` instance)

| Surface | Role |
|---------|------|
| **`FlowRouter.globals`** | **`Array`**: **`push({ waitOn, waitOnResources, ... })`** — merged into every route’s wait pipeline (see `client/route.js` / `docs/hooks/waitOnResources.md`). |
| **`FlowRouter.subscriptions`** | **Function** run as global subscription hook on the internal **`_globalRoute`** (`client/router.js` `_buildTracker`). |
| **`FlowRouter.decodeQueryParamsOnce`** | **`boolean`** — set **`true`** for new apps (fixes double-decode; default **`false`** for legacy). See **`docs/api/decodeQueryParamsOnce.md`**. |
| **`FlowRouter.triggers.enter` / `.exit`** | Register **global** triggers with optional **`{ only: ['routeName'] }`** or **`{ except: [...] }`** (`client/router.js` `_initTriggersAPI`, `client/triggers.js`). |
| **`FlowRouter.env`** | **`replaceState`**, **`reload`**, **`trailingSlash`** `Meteor.EnvironmentVariable`s — **`withReplaceState`**, **`reload`**, **`withTrailingSlash`** helpers on client. |
| **`FlowRouter.wait()`** | Defers default **`Meteor.startup`** **`initialize()`** until you call **`FlowRouter.initialize(options)`** (custom boot order). |
| **`FlowRouter.initialize(options)`** | **Once.** Calls **`MicroRouter.start`**: **`click`** (default `true`), **`popstate`** (default `true`). **Note:** some markdown in **`docs/api/initialize.md`** mentions `page.click`; **implementation** uses **top-level** `options.click` / `options.popstate` (`client/router.js`). |
| **`FlowRouter.onRouteRegister(cb)`** | Fires when a route is registered; payload strips heavy hooks (**`onRouteRegister`** / **`_triggerRouteRegister`** in `client/router.js` / `lib/router-base.js`). |

```js
FlowRouter.decodeQueryParamsOnce = true;

FlowRouter.globals.push({
  waitOnResources() {
    return { images: ['/logo.png'] };
  },
});

FlowRouter.subscriptions = function() {
  // this.register(name, handle) on global route
};

FlowRouter.triggers.enter([(context, redirect) => {
  if (!Meteor.userId()) redirect('/login');
}]);
```

---

## Hooks (execution order)

Order matches **`docs/hooks/README.md`**:

1. **`whileWaiting`**
2. **`waitOn`**
3. **`waitOnResources`**
4. **`endWaiting`**
5. **`data`**
6. **`onNoData`**
7. **`triggersEnter`** (after global **`FlowRouter.triggers.enter`** concatenation)
8. **`action`**
9. **`triggersExit`**

**Per-file docs:** `docs/hooks/*.md`. **Implementation:** `client/route.js` (`waitOn`, `callAction`, etc.).

**Add-on keys** on route/group options (**`title`**, **`meta`**, **`link`**, **`script`**, …) are for **`ostrio:flow-router-title`** / **`ostrio:flow-router-meta`**, not core router logic.

**Tracker rule:** do not use reactive globals (**`Session`**, etc.) inside **`.subscriptions`** in a way that trips **`safeToRun`** — error from **`_buildTracker`** in `client/router.js`.

---

## Global triggers API (`Triggers` + `FlowRouter.triggers`)

- **`FlowRouter.triggers.enter(triggers, filter?)`** / **`exit(...)`** — **`filter`**: **`{ only: ['routeName', ...] }`** OR **`{ except: [...] }`**, not both (`client/triggers.js` **`applyFilters`**).
- **Route-level:** **`triggersEnter`**, **`triggersExit`** on **`FlowRouter.route`** / group **`route`**.
- **Signature (conceptually):** `(context, redirect, stop, data)` — **`redirect(url, params?, query?)`** must be synchronous; **`stop()`** aborts chain (see `client/triggers.js` **`runTriggers`**).
- **`Triggers` export:** helpers like **`applyFilters`**, **`createRouteBoundTriggers`**, **`runTriggers`** — used internally; server **`Triggers`** is `{}`.

---

## RouterHelpers (client)

**Source:** `client/active.route.js` (initialized in `client/_init.js` with **`RouterHelpers = helpersInit(FlowRouter)`**).

**Programmatic (no Blaze):** use **`RouterHelpers`** methods directly:

| Method | Purpose |
|--------|---------|
| **`RouterHelpers.name(pattern)`** | Current route **name** matches **string** / **RegExp** (optional params for building path to compare). |
| **`RouterHelpers.path(pattern)`** | Current **path** matches **string** / **RegExp**. |
| **`RouterHelpers.pathFor(pathDef, params)`** | Build path string (like Blaze **`pathFor`**). |
| **`RouterHelpers.configure({ activeClass, caseSensitive, disabledClass, regex })`** | Active-route styling defaults. |

**With Blaze** (`templating` present): global helpers registered — **`pathFor`**, **`urlFor`**, **`param`**, **`queryParam`**, **`currentRouteName`**, **`subsReady`**, **`isSubReady`**, **`currentRouteOption`**, plus **active-route** style: **`isActiveRoute`**, **`isActivePath`**, **`isNotActiveRoute`**, **`isNotActivePath`**.

**Server:** only **`pathFor`**-ish subset per **`active.route.js`** (`pathFor`, `urlFor` on server object) — not full client helper set.

**Conflicts:** built-in replaces **`zimme:active-route`** and **`arillo:flow-router-helpers`** (`client/_init.js` warns if those packages exist).

---

## Repo layout (implementation map)

| Path | Role |
|------|------|
| **`client/_init.js`** | Singletons, exports, deprecated-package warnings |
| **`client/router.js`** | Client **`Router`**: **`MicroRouter`**, **`go`**, triggers, **`initialize`/`wait`**, **`_updateCallbacks`** (`'*'` last) |
| **`client/route.js`** | **`waitOn`**, **`data`**, **`action`**, Blaze, **`subscriptions`** |
| **`client/group.js`** | Group extends **`lib/group-base.js`** |
| **`client/triggers.js`** | **`Triggers.runTriggers`**, filters |
| **`client/active.route.js`** | **`RouterHelpers`** |
| **`lib/router-base.js`** | **`RouterBase`**: **`path`/`url`**, **`globals`**, **`group`**, **`onRouteRegister`** |
| **`lib/micro-router.js`** | History, **`pathToRegExp`** / **`matchPath`** (shared with server) |
| **`lib/group-base.js`** | Nested groups, prefix merge, **`route()`** option merge |
| **`server/router.js`** | **`matchPath`**, no navigation |
| **`server/plugins/fast-render.js`** | Fast render — see **`docs/fast-render-integration.md`** |

---

## Architecture (short)

1. **`RouterBase`** — shared route table, **`path`/`url`**, **`globals`**, **`group()`**.
2. **Client** — **`MicroRouter`** → **`_actionHandle`** → **`waitOn`** → **`Triggers.runTriggers`** (global + route) → Tracker → **`subscriptions`** + **`action`**.
3. **Server** — same registration for **matching**; **`matchPath`** uses **`lib/micro-router.js`**.

---

## Tips and tricks

- **Base path:** app served under **`ROOT_URL_PATH_PREFIX`** — router strips/adds base when talking to **`MicroRouter`** (`client/router.js` **`_stripBase`**).
- **Idempotent navigation:** **`go`** no-ops if path unchanged unless **`reload`** env forces redo (`client/router.js` **`go`**).
- **Named subs:** **`FlowRouter.subsReady('name')`** resolves handles registered with **`this.register('name', sub)`** inside **`subscriptions`** (route + global).
- **External redirect:** triggers cannot redirect off-origin HTTP(S); use **`window.location`** (`client/router.js` **`_redirectFn`**).
- **Avoid duplicate community packages** listed in **`client/_init.js`** (deprecated **`meteorhacks:*`**, etc.).

---

## Debugging

- **Console:** many paths log with prefix **`[ostrio:flow-router-extra]`** (`Meteor._debug`) — e.g. **`lib/_helpers.js`**, **`client/route.js`** (promise/wait errors).
- **Initialization:** **`FlowRouter.initialize()`** throws if called twice; **`wait()`** throws if called after init (`client/router.js`).
- **Triggers:** **`already redirected`** / **`redirect needs to be done in sync`** from **`client/triggers.js`** — async redirect misuse.
- **Tests:** **`meteor test-packages ./`** from repo root; helpers set **`decodeQueryParamsOnce = true`** in **`test/client/_helpers.js`** / **`test/server/_helpers.js`**.
- **Bundle:** **`docs/`**, **`test/`**, **`AGENTS.md`** excluded from app bundle via **`.meteorignore`** — edits do not affect Meteor client weight.

---

## Conventions (do not regress)

- **404:** prefer **`route('*', ...)`**; **`notFound` setter** deprecated.
- **Query strings:** **`FlowRouter.decodeQueryParamsOnce = true`** for new apps.
- **`underscore`:** not a runtime dependency (tests only if needed).

---

## Testing

- **`meteor test-packages ./`**, **`package.js` `onTest`** lists **`test/client/*.spec.js`**, **`test/common/*.spec.js`**.
- Meteor 3: **`package.js`** notes on fast-render test compatibility — verify before re-enabling.

---

## Ecosystem

Often released together: **`ostrio:flow-router-extra`**, **`ostrio:flow-router-title`**, **`ostrio:flow-router-meta`**, **`Flow-Router-Demos`**. After route table exists: **`new FlowRouterMeta(FlowRouter)`**, **`new FlowRouterTitle(FlowRouter)`** (client).

---

## Dev workflow

- Clean env: after **`meteor reset`** / removing **`node_modules`**, **`meteor npm install`** before **`meteor run`**.

---

## Learned User Preferences

- Prefer **`import`/`export`** over globals.
- Prefer **`async`/`await`** in **`Meteor.startup`** when wiring initialization.
- Changelog / release notes: preserve commit emojis, highlight new features, split into **`⚠️ major changes`**, **`Changes`**, **`✨ New`**, **`📦 Dependencies`** (prod vs dev).
- Prefer Meteor-wrapped npm commands in this workspace (e.g., **`meteor npm run ...`**, **`meteor npm exec ...`**) to keep Meteor-managed Node/tooling environment consistency.

## Learned Workspace Facts

- Blaze **`client/renderer.js`** / **`client/modules.js`** use **`requestAnimationFrame`** to chunk queued route renders and defer attaching in-memory layout to the live DOM; still appropriate (not deprecated); trimming legacy `webkit`/`moz` rAF prefixes is optional cleanup.
- **`ostrio:flow-router-meta`** and **`ostrio:flow-router-title`** hook private **`router._notfoundRoute`** / **`router._current`** and **`notFound`** / **`notfound`** option shape; changes to 404 or not-found internals in **`client/router.js`** must stay compatible with those integrations.
- Companion packages using **`tsd`** with **`meteor/ostrio:flow-router-extra`**: add a local **`tsd-stubs`** **`Router`** shim, wire **`paths`** under **`package.json` → `tsd.compilerOptions`**, and prefer **`import('meteor/ostrio:flow-router-extra').Router`** inside **`declare module`** (avoid `import type` inside the block); **`index.test-d.ts`** may need **`/// <reference path="./index.d.ts" />`** so tsd loads ambient package typings.
- **`maxWaitFor`**: when the time limit is hit during **`waitOn`**, the route still proceeds to **`action`**; **navigating away** aborts **`waitOn`** and skips **`action`** for the route being left.
- `ostrio:flow-router-extra` now uses internal query module **`lib/qs.js`** (no npm `qs` runtime dependency); query APIs/docs/types standardize on **`queryParams`** naming, and nested query merge goes through `qs.merge(...)` in router path/url building.


================================================
FILE: CHANGELOG.md
================================================
For full package history, please see [releases on GitHub](https://github.com/veliovgroup/flow-router/releases)

================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@veliovgroup.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
### I'm having an issue:
 1. Search [issues](https://github.com/veliovgroup/flow-router/issues?utf8=✓&q=is%3Aissue), maybe your issue is already solved
 2. Before submitting an issue make sure it's only related to `flow-router-extra` package
 3. If your issue is not solved:
     - Give an expressive description of what is went wrong
     - Version of `flow-router-extra` you're experiencing this issue
     - Version of `Meteor` you're experiencing this issue
     - Browser name and its version (Chrome, Firefox, Safari, etc.)?
     - Platform name and its version (Win, Mac, Linux)?
     - If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot

### I have a suggestion:
 1. PRs are always welcome - [send a PR](https://github.com/veliovgroup/flow-router/pulls)
 2. If you're can not send a PR for some reason:
     - Create a new issue ticket
     - Describe your feature / request
     - How you're going to use it? Give a usage example(s)

### Documentation is missing something or incorrect (have typos, etc.):
 1. PRs are always welcome - [send a PR](https://github.com/veliovgroup/flow-router/pulls)
 2. If you're can not send a PR to docs for some reason:
     - Create a new issue ticket
     - Give an expressive description what you have changed/added and why
     - Make sure you're using correct markdown markup
     - Make sure all code blocks starts with triple ``` (*backtick*) and have a syntax tag, for more read [this docs](https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting)
     - Post your addition/changes as issue ticket, we will manage it

================================================
FILE: HISTORY.md
================================================
For full package history, please see [releases on GitHub](https://github.com/veliovgroup/flow-router/releases)

================================================
FILE: LICENSE
================================================
Copyright (c) 2026, dr.dimitru (Dmitry A.; Veliov Group, LLC)
All rights reserved.

Redistribution and use in source and binary forms, 
with or without modification, are permitted provided 
that the following conditions are met:

1. Redistributions of source code must retain the 
above copyright notice, this list of conditions 
and the following disclaimer.

2. Redistributions in binary form must reproduce the 
above copyright notice, this list of conditions and 
the following disclaimer in the documentation and/or 
other materials provided with the distribution.

3. Neither the name of the copyright holder nor the 
names of its contributors may be used to endorse or 
promote products derived from this software without 
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: ORIGINAL FlowRouter LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015 MeteorHacks Pvt Ltd (Sri Lanka). <hello@meteorhacks.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: README.md
================================================
[![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru)
[![support](https://img.shields.io/badge/support-PayPal-white)](https://paypal.me/veliovgroup)
<a href="https://ostr.io/info/built-by-developers-for-developers?ref=github-flowrouter-repo-top"><img src="https://ostr.io/apple-touch-icon-60x60.png" height="20"></a>
<a href="https://meteor-files.com/?ref=github-flowrouter-repo-top"><img src="https://meteor-files.com/apple-touch-icon-60x60.png" height="20"></a>

# FlowRouter Extra

Carefully extended `flow-router` package. FlowRouter is a very simple router for [Meteor.js](https://docs.meteor.com/?utm_source=dr.dimitru&utm_medium=online&utm_campaign=Q2-2022-Ambassadors). It does routing for client-side apps and compatible with React, Vue, Svelte, and Blaze.

It exposes a great API for changing the URL and getting data from the URL. However, inside the router, it's not reactive. Most importantly, FlowRouter is designed with performance in mind and it focuses on what it does best: __routing__.

## Features:

- 📦 Not dependent on Blaze, ready for [__React.js__](https://github.com/veliovgroup/flow-router/blob/master/docs/react.md) and other templating/components engines/libs;
- 📦 No `underscore` package dependency;
- 👨‍💻 TypeScript definition [`index.d.ts`](https://github.com/veliovgroup/flow-router/blob/master/index.d.ts)
- 👨‍🔬 Great [tests coverage](https://github.com/veliovgroup/flow-router/tree/master/test);
- 🥑 Up-to-date [dependencies](https://github.com/veliovgroup/flow-router/blob/master/package.js);
- 📦 Support of [Fast Render](https://github.com/veliovgroup/flow-router/blob/master/docs/fast-render-integration.md) and [other great packages](https://github.com/veliovgroup/flow-router#related-packages);
- 📋 Following semver with regular [releases](https://github.com/veliovgroup/flow-router/releases);
- 📋 Great [wiki](https://github.com/veliovgroup/flow-router/wiki);
- 📋 Great [quick start tutorial](https://github.com/veliovgroup/flow-router/blob/master/docs/quick-start.md).

## Install

```shell
meteor add ostrio:flow-router-extra
```

### Compatibility

- Meteor `>=1.4`, including latest Meteor `3.4`;
- Compatible with `ostrio:flow-router-title@3.5.0` and `ostrio:flow-router-meta@2.4.0`.

### ES6 Import

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
// Full list of available classes and instances:
// { FlowRouter, Router, Route, Group, Triggers, BlazeRenderer, RouterHelpers }
```

### Usage

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// DISABLE QUERY STRING COMPATIBILITY
// WITH OLDER FlowRouter AND Meteor RELEASES
FlowRouter.decodeQueryParamsOnce = true;

FlowRouter.route('/', {
  name: 'index',
  action() {
    // Render a template using Blaze
    this.render('layoutName', 'index');

    // Can be used with BlazeLayout,
    // and ReactLayout for React-based apps
  },
  waitOn() {
    // Dynamically load JS per route
    return [import('/imports/client/index.js')];
  }
});

// Create 404 route (catch-all)
FlowRouter.route('*', {
  action() {
    // Show 404 error page using Blaze
    this.render('notFound');

    // Can be used with BlazeLayout,
    // and ReactLayout for React-based apps
  }
});
```

> [!TIP]
> TypeScript: add [`zodern:types`](https://github.com/zodern/meteor-types) to the app (`meteor add zodern:types`), enable TypeScript per [Meteor docs](https://docs.meteor.com/guide/typescript.html), then run a build so `.meteor/local/types` is generated. This package ships [`index.d.ts`](https://github.com/veliovgroup/flow-router/blob/master/index.d.ts) and [`package-types.json`](https://github.com/veliovgroup/flow-router/blob/master/package-types.json) for `meteor/ostrio:flow-router-extra` imports.

## Documentation

- Continue with our [wiki](https://github.com/veliovgroup/flow-router/wiki) or [README index](https://github.com/veliovgroup/flow-router/blob/master/docs/README.md);
- [Quick start](https://github.com/veliovgroup/flow-router/blob/master/docs/quick-start.md) tutorial;
- All docs as [single document](https://github.com/veliovgroup/flow-router/blob/master/docs/full.md).

### AGENTS.md

The repo ships [`AGENTS.md`](https://github.com/veliovgroup/flow-router/blob/master/AGENTS.md): a compact **implementation map** for `ostrio:flow-router-extra` (routes, groups, catch-all, hooks, globals, `RouterHelpers`, debugging, testing). It complements narrative API docs under `docs/` with file pointers and conventions maintainers rely on.

### SKILLS.md

- This repo ships a bundled skill at **`.agents/skills/meteor-flow-router/SKILL.md`** (covers **`ostrio:flow-router-extra`**, **`ostrio:flow-router-meta`**, **`ostrio:flow-router-title`**). Install into your project with the [Skills CLI](https://www.npmjs.com/package/skills) (`npx skills`):

```bash
# From a Meteor app repo (install into that app’s .agents/skills for Cursor, etc.)
npx skills add veliovgroup/flow-router --skill meteor-flow-router --agent cursor --yes

# Only list skills discovered in the Flow Router repo (no install)
npx skills add veliovgroup/flow-router --list

# Local clone of flow-router (path can be absolute or ./relative)
npx skills add ./flow-router --skill meteor-flow-router --agent cursor --yes

# User-global Cursor skills dir (~/.cursor/skills)
npx skills add veliovgroup/flow-router --skill meteor-flow-router --agent cursor --global --yes
```

### Related packages:

- [`ostrio:flow-router-title`](https://github.com/veliovgroup/Meteor-flow-router-title) - Reactive page title (`document.title`)
- [`ostrio:flow-router-meta`](https://github.com/veliovgroup/Meteor-flow-router-meta) - Per route `meta` tags, `script` and `link` (CSS), set per-route stylesheets and scripts
- [`communitypackages:fast-render`](https://github.com/Meteor-Community-Packages/meteor-fast-render) - Fast Render can improve the initial load time of your app, giving you 2-10 times faster initial page loads. [`fast-render` integration tutorial](https://github.com/veliovgroup/flow-router/blob/master/docs/fast-render-integration.md)
- [`communitypackages:inject-data`](https://github.com/Meteor-Community-Packages/meteor-inject-data) - This is the package used by `fast-render` to push data to the client with the initial HTML
- [`flean:flow-router-autoscroll`](https://github.com/flean/flow-router-autoscroll) - Autoscroll for Flow Router
- [`mealsunite:flow-routing-extra`](https://github.com/MealsUnite/flow-routing) - Add-on for User Accounts
- [`nxcong:flow-routing`](https://github.com/cafe4it/flow-routing) - Add-on for User Accounts (alternative)
- [`forwarder:autoform-wizard-flow-router-extra`](https://atmospherejs.com/forwarder/autoform-wizard-flow-router-extra) - Flow Router bindings for AutoForm Wizard
- [`nicolaslopezj:router-layer`](https://github.com/nicolaslopezj/meteor-router-layer) - Helps package authors to support multiple routers
- [`krishaamer:flow-router-breadcrumb`](https://github.com/krishaamer/flow-router-breadcrumb) - Easy way to add a breadcrumb with enough flexibility to your project (`flow-router-extra` edition)
- [`krishaamer:body-class`](https://github.com/krishaamer/body-class) - Easily scope CSS by automatically adding the current template and layout names as classes on the body element

## Running Tests

1. Clone this package
2. In Terminal (*Console*) go to directory where package is cloned
3. Then run:

### Meteor/Tinytest

```shell
# Default
meteor test-packages ./

# With custom port
meteor test-packages ./ --port 8888

# With local MongoDB and custom port
MONGO_URL="mongodb://127.0.0.1:27017/flow-router-tests" meteor test-packages ./ --port 8888
```

### Running Typescript Test

1. Install dev dependencies with `meteor npm install`;
2. Run `meteor npm run test:tsd` (or `meteor npm exec tsd`) from package root. `tsd` will find `index.test-d.ts` and report type errors.

## Support this project:

- Upload and share files using [☄️ meteor-files.com](https://meteor-files.com/?ref=github-flowrouter-repo-footer) — Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
- Use [▲ ostr.io](https://ostr.io?ref=github-flowrouter-repo-footer) for [Server Monitoring](https://snmp-monitoring.com), [Web Analytics](https://ostr.io/info/web-analytics?ref=github-flowrouter-repo-footer), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [SEO Pre-rendering](https://prerendering.com) of a website
- Star on [GitHub](https://github.com/veliovgroup/flow-router)
- Star on [Atmosphere](https://atmospherejs.com/ostrio/flow-router-extra)
- [Sponsor via GitHub](https://github.com/sponsors/dr-dimitru)
- [Support via PayPal](https://paypal.me/veliovgroup)


================================================
FILE: client/_init.js
================================================
import { Meteor }    from 'meteor/meteor';
import Router        from './router.js';
import Route         from './route.js';
import Group         from './group.js';
import Triggers      from './triggers.js';
import BlazeRenderer from './renderer.js';
import helpersInit   from './active.route.js';

if (Package['zimme:active-route']) {
  Meteor._debug('Please remove `zimme:active-route` package, as its features is build into flow-router-extra, and will interfere.');
  Meteor._debug('meteor remove zimme:active-route');
}

if (Package['arillo:flow-router-helpers']) {
  Meteor._debug('Please remove `arillo:flow-router-helpers` package, as its features is build into flow-router-extra, and will interfere.');
  Meteor._debug('meteor remove arillo:flow-router-helpers');
}

if (Package['meteorhacks:inject-data']) {
  Meteor._debug('`meteorhacks:inject-data` is deprecated, please remove it and install its successor - `communitypackages:inject-data`');
  Meteor._debug('meteor remove meteorhacks:inject-data');
  Meteor._debug('meteor add communitypackages:inject-data');
}

if (Package['meteorhacks:fast-render']) {
  Meteor._debug('`meteorhacks:fast-render` is deprecated, please remove it and install its successor - `communitypackages:fast-render`');
  Meteor._debug('meteor remove meteorhacks:fast-render');
  Meteor._debug('meteor add communitypackages:fast-render');
}

if (Package['staringatlights:inject-data']) {
  Meteor._debug('`staringatlights:inject-data` is deprecated, please remove it and install its successor - `communitypackages:inject-data`');
  Meteor._debug('meteor remove staringatlights:inject-data');
  Meteor._debug('meteor add communitypackages:inject-data');
}

if (Package['staringatlights:fast-render']) {
  Meteor._debug('`staringatlights:fast-render` is deprecated, please remove it and install its successor - `communitypackages:fast-render`');
  Meteor._debug('meteor remove staringatlights:fast-render');
  Meteor._debug('meteor add communitypackages:fast-render');
}

const FlowRouter = new Router();
FlowRouter.Router = Router;
FlowRouter.Route = Route;

// Initialize FlowRouter
Meteor.startup(() => {
  if(!FlowRouter._askedToWait && !FlowRouter._initialized) {
    FlowRouter.initialize();
  }
});

const RouterHelpers = helpersInit(FlowRouter);

export { MAX_WAIT_FOR_MS } from '../lib/constants.js';
export { FlowRouter, Router, Route, Group, Triggers, BlazeRenderer, RouterHelpers };


================================================
FILE: client/active.route.js
================================================
import { Meteor }       from 'meteor/meteor';
import { _helpers }     from './../lib/_helpers.js';
import { check, Match } from 'meteor/check';
import { ReactiveDict } from 'meteor/reactive-dict';
import { qs }           from './../lib/qs.js';

let Template;
if (Package.templating) {
  Template = Package.templating.Template;
}

const init = (FlowRouter) => {
  // Active Route
  // https://github.com/meteor-activeroute/legacy
  // zimme:active-route
  // License (MIT License): https://github.com/meteor-activeroute/legacy/blob/master/LICENSE.md
  // Lib
  const errorMessages = {
    noSupportedRouter: 'No supported router installed. Please install flow-router.',
    invalidRouteNameArgument: 'Invalid argument, must be String or RegExp.',
    invalidRouteParamsArgument: 'Invalid argument, must be Object.'
  };

  const checkRouteOrPath = (arg) => {
    try {
      return check(arg, Match.OneOf(RegExp, String));
    } catch (_e) {
      throw new Error(errorMessages.invalidRouteNameArgument);
    }
  };

  const checkParams = (arg) => {
    try {
      return check(arg, Object);
    } catch (_e) {
      throw new Error(errorMessages.invalidRouteParamsArgument);
    }
  };

  const config = new ReactiveDict('activeRouteConfig');
  config.setDefault({
    activeClass: 'active',
    caseSensitive: true,
    disabledClass: 'disabled'
  });

  const test = (_value, _pattern) => {
    let value = _value;
    let pattern = _pattern;
    if (!value) {
      return false;
    }

    if (Match.test(pattern, RegExp)) {
      return value.search(pattern) > -1;
    }

    if (Match.test(pattern, String)) {
      if (config.equals('caseSensitive', false)) {
        value = value.toLowerCase();
        pattern = pattern.toLowerCase();
      }
      return (value === pattern);
    }

    return false;
  };

  const ActiveRoute = {
    config() {
      return this.configure.apply(this, arguments);
    },
    configure(options) {
      if (!Meteor.isServer) {
        config.set(options);
      }
    },
    name(routeName, routeParams = {}) {
      if (Meteor.isServer) {
        return void 0;
      }
      checkRouteOrPath(routeName);
      checkParams(routeParams);

      let currentPath;
      let currentRouteName;
      let path;
      if (!_helpers.isEmpty(routeParams) && Match.test(routeName, String)) {
        FlowRouter.watchPathChange();
        currentPath = FlowRouter.current().path;
        path = FlowRouter.path(routeName, routeParams);
      } else {
        currentRouteName = FlowRouter.getRouteName();
      }
      return test(currentPath || currentRouteName, path || routeName);
    },
    path(path) {
      if (Meteor.isServer) {
        return void 0;
      }
      checkRouteOrPath(path);

      FlowRouter.watchPathChange();
      return test(FlowRouter.current().path, path);
    }
  };

  // Client
  const isActive = (type, inverse = false) => {
    let helperName;

    helperName = 'is';
    if (inverse) {
      helperName += 'Not';
    }
    helperName += 'Active' + type;

    return (_options = {}, _attributes = {}) => {
      let options    = (_helpers.isObject(_options)) ? (_options.hash || _options) : _options;
      let attributes = (_helpers.isObject(_attributes)) ? (_attributes.hash || _attributes) : _attributes;

      if (Match.test(options, String)) {
        if (config.equals('regex', true)) {
          options = {
            regex: options
          };
        } else if (type === 'Path') {
          options = {
            path: options
          };
        } else {
          options = {
            name: options
          };
        }
      }
      options = _helpers.extend(options, attributes);
      const pattern = Match.ObjectIncluding({
        class: Match.Optional(String),
        className: Match.Optional(String),
        regex: Match.Optional(Match.OneOf(RegExp, String)),
        name: Match.Optional(String),
        path: Match.Optional(String)
      });
      check(options, pattern);

      let regex     = options.regex;
      let name      = options.name;
      let path      = options.path;
      let className = options.class ? options.class : options.className;
      if (type === 'Path') {
        name = null;
      } else {
        path = null;
      }

      if (!(regex || name || path)) {
        const t = (type === 'Route' ? 'name' : type).toLowerCase();
        Meteor._debug(('Invalid argument, ' + helperName + ' takes "' + t + '", ') + (t + '="' + t + '" or regex="regex"'));
        return false;
      }

      if (Match.test(regex, String)) {
        if (config.equals('caseSensitive', false)) {
          regex = new RegExp(regex, 'i');
        } else {
          regex = new RegExp(regex);
        }
      }

      if (!_helpers.isRegExp(regex)) {
        regex = name || path;
      }

      if (inverse) {
        if (!_helpers.isString(className)) {
          className = config.get('disabledClass');
        }
      } else {
        if (!_helpers.isString(className)) {
          className = config.get('activeClass');
        }
      }

      let isPath;
      let result;
      if (type === 'Path') {
        isPath = true;
      }

      if (isPath) {
        result = ActiveRoute.path(regex);
      } else {
        options = _helpers.extend(attributes.data, attributes);
        result = ActiveRoute.name(regex, _helpers.omit(options, ['class', 'className', 'data', 'regex', 'name', 'path']));
      }

      if (inverse) {
        result = !result;
      }

      if (result) {
        return className;
      }
      return false;
    };
  };

  const arHelpers = {
    isActiveRoute: isActive('Route'),
    isActivePath: isActive('Path'),
    isNotActiveRoute: isActive('Route', true),
    isNotActivePath: isActive('Path', true)
  };

  // If blaze is in use, register global helpers
  if (Template) {
    for (const [name, helper] of Object.entries(arHelpers)) {
      Template.registerHelper(name, helper);
    }
  }

  // FlowRouter Helpers
  // arillo:flow-router-helpers
  // https://github.com/arillo/meteor-flow-router-helpers
  // License (MIT License): https://github.com/arillo/meteor-flow-router-helpers/blob/master/LICENCE
  const subsReady = (..._subs) => {
    let subs = _subs.slice(0, -1);
    if (subs.length === 1) {
      return FlowRouter.subsReady();
    }

    return subs.filter((memo, sub) => {
      if (_helpers.isString(sub)) {
        return memo && FlowRouter.subsReady(sub);
      }
    }, true);
  };

  const pathFor = (_path, _view = {hash: {}}) => {
    let path = _path;
    let view = _view;
    if (!path) {
      throw new Error('no path defined');
    }

    if (!view.hash) {
      view = {
        hash: view
      };
    }

    if (path.hash && path.hash.route) {
      view = path;
      path = view.hash.route;
      delete view.hash.route;
    }
    let query = {};
    if (_helpers.isString(view.hash.query)) {
      query = qs.parse(view.hash.query);
    } else if (_helpers.isObject(view.hash.query)) {
      query = view.hash.query;
    }
    const hashBang = view.hash.hash ? view.hash.hash : '';
    return FlowRouter.path(path, view.hash, query) + (hashBang ? '#' + hashBang : '');
  };

  const urlFor = (path, view) => {
    return Meteor.absoluteUrl(pathFor(path, view).substr(1));
  };

  const param = (name) => {
    return FlowRouter.getParam(name);
  };

  const queryParam = (key) => {
    return FlowRouter.getQueryParam(key);
  };

  const currentRouteName = () => {
    return FlowRouter.getRouteName();
  };

  const currentRouteOption = (optionName) => {
    return FlowRouter.current().route.options[optionName];
  };

  const isSubReady = (sub) => {
    if (sub) {
      return FlowRouter.subsReady(sub);
    }
    return FlowRouter.subsReady();
  };

  const frHelpers = {
    subsReady: subsReady,
    pathFor: pathFor,
    urlFor: urlFor,
    param: param,
    queryParam: queryParam,
    currentRouteName: currentRouteName,
    isSubReady: isSubReady,
    currentRouteOption: currentRouteOption
  };

  let FlowRouterHelpers;

  if (Meteor.isServer) {
    FlowRouterHelpers = {
      pathFor: pathFor,
      urlFor: urlFor
    };
  } else {
    FlowRouterHelpers = frHelpers;
    // If blaze is in use, register global helpers
    if (Template) {
      for (const [name, helper] of Object.entries(frHelpers)) {
        Template.registerHelper(name, helper);
      }
    }
  }

  return Object.assign({}, ActiveRoute, FlowRouterHelpers);
};

export default init;


================================================
FILE: client/group.js
================================================
import { GroupBase } from '../lib/group-base.js';

export default GroupBase;


================================================
FILE: client/modules.js
================================================
const requestAnimFrame = (() => {
  return window.requestAnimationFrame
    || window.webkitRequestAnimationFrame
    || window.mozRequestAnimationFrame
    || function (callback) { setTimeout(callback, 1000 / 60); };
})();

export { requestAnimFrame };


================================================
FILE: client/renderer.js
================================================
import { Meteor } from 'meteor/meteor';
import { _helpers } from './../lib/_helpers.js';
import { requestAnimFrame } from './modules.js';

let Blaze;
let Template;

if (Package.templating && Package.blaze) {
  Blaze = Package.blaze.Blaze;
  Template = Package.templating.Template;
}

const _BlazeRemove = function (view) {
  try {
    Blaze.remove(view);
  } catch (_e) {
    Meteor._debug('[flow-router] [_BlazeRemove] exception:', _e);
  }
};

class BlazeRenderer {
  constructor(opts = {}) {
    if (!Blaze || !Template) {
      return;
    }

    this.rootElement = opts.rootElement || function () {
      return document.body;
    };

    const self = this;
    this.isRendering = false;
    this.queue = [];
    this.yield = null;
    this.cache = {};
    this.old = this.newState();
    this.old.materialized = true;

    this.router = opts.router || false;
    this.inMemoryRendering = opts.inMemoryRendering || false;
    this.getMemoryElement = opts.getMemoryElement || function () {
      return document.createElement('div');
    };

    if (!this.getMemoryElement || !_helpers.isFunction(this.getMemoryElement)) {
      throw new Meteor.Error(400, '{getMemoryElement} must be a function, which returns new DOM element');
    }

    if (!this.rootElement || !_helpers.isFunction(this.rootElement)) {
      throw new Meteor.Error(400, 'You must pass function into BlazeRenderer constructor, which returns DOM element');
    }

    Template.yield = new Template('yield', function () {});

    Template.yield.onCreated(function () {
      self.yield = this;
    });

    Template.yield.onRendered(function () {
      self.yield = this;
      self.materialize(self.old);
    });

    Template.yield.onDestroyed(function () {
      if (self.old.template.view) {
        _BlazeRemove(self.old.template.view);
        self.old.template.view = null;
        self.old.materialized = false;
      }
      self.yield = null;
    });
  }

  render(__layout, __template = false, __data = {}, __callback) {
    if (!Blaze || !Template) {
      throw new Meteor.Error(400, '`.render()` - Requires `blaze` and `templating`, or `blaze-html-templates` packages to be installed');
    }

    if (!__layout) {
      throw new Meteor.Error(400, '`.render()` - Requires at least one argument');
    } else if (!_helpers.isString(__layout) && !(__layout instanceof Blaze.Template)) {
      throw new Meteor.Error(400, '`.render()` - First argument must be a String or instance of Blaze.Template');
    }

    this.queue.push([__layout, __template, __data, __callback]);
    this.startQueue();
  }

  startQueue() {
    if (this.queue.length) {
      if (!this.isRendering) {
        this.isRendering = true;
        const task = this.queue.shift();
        this.proceed.apply(this, task);
        if (this.queue.length) {
          requestAnimFrame(() => this.startQueue());
        }
      } else {
        requestAnimFrame(() => this.startQueue());
      }
    }
  }

  proceed(__layout, __template = false, __data = {}, __callback) {
    if (!Blaze || !Template) {
      return;
    }

    let data      = __data;
    let layout    = __layout;
    let _layout   = false;
    let template  = __template;
    let _template = false;
    let callback  = __callback || (() => {});

    if (_helpers.isString(layout)) {
      _layout = typeof Template !== 'undefined' && Template !== null ? Template[layout] : void 0;
    } else if (layout instanceof Blaze.Template) {
      _layout = layout;
      layout  = layout.viewName.replace('Template.', '');
    } else {
      layout = false;
    }

    if (_helpers.isString(template)) {
      _template = typeof Template !== 'undefined' && Template !== null ? Template[template] : void 0;
    } else if (template instanceof Blaze.Template) {
      _template = template;
      template  = template.viewName.replace('Template.', '');
    } else if (_helpers.isObject(template)) {
      data     = template;
      template = false;
    } else if (_helpers.isFunction(template)) {
      callback = template;
      template = false;
    } else {
      template = false;
    }

    if (_helpers.isFunction(data)) {
      callback = data;
      data = {};
    } else if (!_helpers.isObject(data)) {
      data = {};
    }

    if (!_helpers.isFunction(callback)) {
      callback = () => {};
    }

    if (!_layout) {
      this.old.materialized = true;
      this.isRendering = false;
      const error = new Meteor.Error(404, `No such layout template: ${layout}`);
      this.router.onRenderError && _helpers.isFunction(this.router.onRenderError) && this.router.onRenderError.call(this, error);
      callback(error);
      throw error;
    }

    const current = this.newState(layout, template);
    current.data = data;
    current.callback = callback;
    let updateTemplate = true;

    const forceReRender = !!(
      this.router &&
      this.router._current &&
      this.router._current.route &&
      this.router._current.route.conf &&
      this.router._current.route.conf.forceReRender === true
    );
    if (template) {
      if (!_template) {
        this.old.materialized = true;
        this.isRendering = false;
        const error = new Meteor.Error(404, `No such template: ${template}`);
        this.router.onRenderError && _helpers.isFunction(this.router.onRenderError) && this.router.onRenderError.call(this, error);
        current.callback(error);
        throw error;
      }

      if (forceReRender || this.old.template.name !== template) {
        current.template.name  = template;
        current.template.blaze = _template;
        this.newElement('template', current);

        if (this.old.template.view) {
          _BlazeRemove(this.old.template.view);
          this.old.template.view = null;
          this.old.materialized = false;
        }
        updateTemplate = false;
      } else {
        current.template = this.old.template;
      }
    }

    if (!template || this.old.layout.name !== layout) {
      current.layout.name = layout;
      current.layout.blaze = _layout;
      current.template.name = template;
      current.template.blaze = _template;
      this.newElement('layout', current);

      if (this.old.layout.view) {
        _BlazeRemove(this.old.layout.view);
        this.old.layout.view = null;
      }

      this._render(current);
    } else if (template) {
      current.layout = this.old.layout;
      current.template.name = template;
      current.template.blaze = _template;
      this._load(updateTemplate, true, current);
    } else {
      current.layout = this.old.layout;
      this.isRendering = false;
      current.materialized = true;
      current.callback();
      current.callback = () => {};
    }

    this.old = current;
  }

  _render(current) {
    if (!Blaze || !Template) {
      return;
    }

    const getData = () => {
      return current.data;
    };

    const rootElement = this.rootElement();
    if (!rootElement) {
      throw new Meteor.Error(400, 'BlazeRenderer can\'t find root element!');
    }

    if (this.inMemoryRendering) {
      current.layout.view = Blaze.renderWithData(current.layout.blaze, getData, current.layout.element);
      requestAnimFrame(() => {
        rootElement.appendChild(current.layout.element);
        this._load(false, false, current);
      });
    } else {
      current.layout.view = Blaze.renderWithData(current.layout.blaze, getData, rootElement);
      this._load(false, false, current);
    }
  }

  _load(updateTemplate, updateLayout, current) {
    if (updateLayout && current.layout.view) {
      const layoutDataVar = current.layout.view.dataVar.get();
      current.layout.view.dataVar.set(layoutDataVar && layoutDataVar.value ? { value: (current.data || {}) } : current.data);
    }

    if (current.template.view && updateTemplate) {
      const templateDataVar = current.template.view.dataVar.get();
      current.template.view.dataVar.set(templateDataVar && templateDataVar.value ? { value: (current.data || {}) } : current.data);
      this.isRendering = false;
      current.materialized = true;
      current.callback();
      current.callback = () => {};
    } else if (!current.template.name) {
      this.isRendering = false;
      current.materialized = true;
      current.callback();
      current.callback = () => {};
    } else if (current.template.name && !this.yield) {
      this.isRendering = false;
      current.materialized = false;
      current.callback();
      current.callback = () => {};
    } else if (current.template.name && this.yield) {
      this.materialize(current);
    }
  }

  newElement(type, current) {
    if (!this.inMemoryRendering) {
      return;
    }

    current[type].parent = current[type].parent ? current[type].parent : document.createElement('div');
    if (!current[type].element) {
      current[type].element = this.getMemoryElement();
      current[type].parent.appendChild(current[type].element);
      current[type].element._parentElement = current[type].parent;
    }
    return;
  }

  newState(layout = false, template = false) {
    const base = {
      materialized: false,
      data: null,
      callback: function () {},
      layout: {
        view: null,
        name: '',
        blaze: null,
        parent: null,
        element: null
      },
      template: {
        view: null,
        name: '',
        blaze: null,
        parent: null,
        element: null
      }
    };

    if (!this.inMemoryRendering || (!layout && !template)) {
      return base;
    }

    if (layout && this.cache[layout]) {
      base.layout = this.cache[layout];
    }

    if (template && this.cache[template]) {
      base.template = this.cache[template];
    }

    this.cache[template] = base;
    return base;
  }

  materialize(current) {
    if (!Blaze || !Template) {
      return;
    }

    if (current.template.name && !current.materialized) {
      const getData = () => {
        return current.data;
      };

      if (!this.yield) {
        current.materialized = false;
        return;
      }

      current.materialized = true;
      if (this.inMemoryRendering) {
        current.template.view = Blaze.renderWithData(current.template.blaze, getData, current.template.element, this.yield.view);
        if (this.yield) {
          this.yield.view._domrange.parentElement.appendChild(current.template.element);
          this.isRendering = false;
          current.materialized = true;
          current.callback();
          current.callback = () => {};
        } else {
          current.materialized = false;
        }
      } else {
        if (this.yield) {
          current.template.view = Blaze.renderWithData(current.template.blaze, getData, this.yield.view._domrange.parentElement, this.yield.view);
          this.isRendering = false;
          current.materialized = true;
          current.callback();
          current.callback = () => {};
        } else {
          current.materialized = false;
        }
      }
    }
  }
}

export default BlazeRenderer;


================================================
FILE: client/route.js
================================================
import { Router }       from './_init.js';
import { Meteor }       from 'meteor/meteor';
import { Promise }      from 'meteor/promise';
import { Tracker }      from 'meteor/tracker';
import { _helpers }     from './../lib/_helpers.js';
import { ReactiveDict } from 'meteor/reactive-dict';
import { MAX_WAIT_FOR_MS } from './../lib/constants.js';

const makeTriggers = (triggers) => {
  if (_helpers.isFunction(triggers)) {
    return [triggers];
  } else if (!_helpers.isArray(triggers)) {
    return [];
  }

  return triggers;
};

class Route {
  constructor(router = new Router(), pathDef, options = {}, group) {
    this.render           = router.Renderer.render.bind(router.Renderer);
    this.options          = options;
    this.globals          = router.globals;
    this.pathDef          = pathDef;

    // Route.path is deprecated and will be removed in 3.0
    this.path             = pathDef;
    this.conf             = options.conf || {};
    this.group            = group;
    this._data            = options.data || null;
    this._router          = router;
    this._action          = options.action || Function.prototype;
    this._waitOn          = options.waitOn || null;
    this._waitFor         = _helpers.isArray(options.waitFor) ? options.waitFor : [];
    this._subsMap         = {};
    this._onNoData        = options.onNoData || null;
    this._endWaiting      = options.endWaiting || null;
    this._currentData     = null;
    this._triggersExit    = options.triggersExit ? makeTriggers(options.triggersExit) : [];
    this._whileWaiting    = options.whileWaiting || null;
    this._triggersEnter   = options.triggersEnter ? makeTriggers(options.triggersEnter) : [];
    this._subscriptions   = options.subscriptions || Function.prototype;
    this._waitOnResources = options.waitOnResources || null;
    if (options.maxWaitFor !== undefined) {
      this._maxWaitFor = options.maxWaitFor;
    }

    this._params          = new ReactiveDict();
    this._queryParams     = new ReactiveDict();
    this._routeCloseDep   = new Tracker.Dependency();
    this._pathChangeDep   = new Tracker.Dependency();

    if (options.name) {
      this.name = options.name;
    }
  }

  clearSubscriptions() {
    this._subsMap = {};
  }

  register(name, sub) {
    this._subsMap[name] = sub;
  }

  getSubscription(name) {
    return this._subsMap[name];
  }

  getAllSubscriptions() {
    return this._subsMap;
  }

  checkSubscriptions(subscriptions) {
    const results = [];
    for (let i = 0; i < subscriptions.length; i++) {
      results.push((subscriptions[i] && subscriptions[i].ready) ? subscriptions[i].ready() : false);
    }

    return !results.includes(false);
  }

  async waitOn(current = {}, next) {
    let _data         = null;
    let _isWaiting    = false;
    let _preloaded    = 0;
    let _resources    = false;
    let timer;
    let waitFor       = [];
    let promises      = [];
    let subscriptions = [];
    let trackers      = [];
    let waitOnAborted = false;
    let pollTimer     = null;
    let subscriptionWaitFinish = null;

    const abortWaitOn = () => {
      waitOnAborted = true;
      if (pollTimer) {
        Meteor.clearTimeout(pollTimer);
        pollTimer = null;
      }
      if (subscriptionWaitFinish) {
        const finish = subscriptionWaitFinish;
        subscriptionWaitFinish = null;
        finish();
      }
    };

    const placeIn = (d) => {
      if (Object.prototype.toString.call(d) === '[object Promise]' || d.then && Object.prototype.toString.call(d.then) === '[object Function]') {
        promises.push(d);
      } else if (d.flush) {
        trackers.push(d);
      } else if (d.ready) {
        subscriptions.push(d);
      }
    };

    const whileWaitingAction = () => {
      if (!_isWaiting) {
        this._whileWaiting && this._whileWaiting(current.params, current.queryParams);
        _isWaiting = true;
      }
    };

    const subWait = (delay) => {
      timer = Meteor.setTimeout(async () => {
        if (this.checkSubscriptions(subscriptions)) {
          Meteor.clearTimeout(timer);
          _data = await getData();
          if (_resources) {
            whileWaitingAction();
            getResources();
          } else {
            next(current, _data);
          }
        } else {
          wait(24);
        }
      }, delay);
    };

    let waitFails = 0;
    const wait = (delay) => {
      if (promises.length) {
        const pendingPromises = promises.slice();
        promises = [];

        Promise.all(pendingPromises).then((resultSet) => {
          resultSet.forEach((result) => {
            processSubData(result);
          });
          waitFails = 0;
          wait(delay);
        }).catch((error) => {
          promises = pendingPromises.concat(promises);
          if (waitFails > 9) {
            subWait(256);
            waitFails = 0;
            promises = [];
          } else {
            wait(128);
            waitFails++;
            Meteor._debug('[ostrio:flow-router-extra] [route.wait] Promise not resolved', error);
          }
        });
      } else {
        subWait(delay);
      }
    };

    const processSubData = (subData) => {
      if (subData instanceof Array) {
        for (let i = subData.length - 1; i >= 0; i--) {
          if (subData[i] !== null && typeof subData[i] === 'object') {
            placeIn(subData[i]);
          }
        }
      } else if (subData !== null && typeof subData === 'object') {
        placeIn(subData);
      }
    };

    const stopSubs = () => {
      for (let i = subscriptions.length - 1; i >= 0; i--) {
        if (subscriptions[i].stop) {
          subscriptions[i].stop();
        }
        delete subscriptions[i];
      }
      subscriptions = [];
    };

    const done = (subscription) => {
      processSubData(_helpers.isFunction(subscription) ? subscription() : subscription);
    };

    if (current.route.globals.length) {
      for (let i = 0; i < current.route.globals.length; i++) {
        if (typeof current.route.globals[i] === 'object') {
          if (current.route.globals[i].waitOnResources) {
            if (!_resources) { _resources = []; }
            _resources.push(current.route.globals[i].waitOnResources);
          }

          if (current.route.globals[i].waitOn && _helpers.isFunction(current.route.globals[i].waitOn)) {
            waitFor.unshift(current.route.globals[i].waitOn);
          }
        }
      }
    }

    if (this._waitOnResources) {
      if (!_resources) { _resources = []; }
      _resources.push(this._waitOnResources);
    }

    const preload = (len, __data) => {
      _preloaded++;
      if (_preloaded >= len) {
        next(current, __data);
      }
    };

    const getData = async () => {
      if (this._data) {
        if (!_data) {
          _data = this._currentData = await this._data(current.params, current.queryParams);
        } else {
          _data = this._currentData;
        }
      }
      return _data;
    };

    const getResources = async () => {
      _data      = await getData();
      let len    = 0;
      let items;
      let images = [];
      let other  = [];
      for (let i = _resources.length - 1; i >= 0; i--) {
        items = _resources[i].call(this, current.params, current.queryParams, _data);
        if (items) {
          if (items.images && items.images.length) {
            images = images.concat(items.images);
          }
          if (items.other && items.other.length) {
            other = other.concat(items.other);
          }
        }
      }

      if ((other && other.length) || (images && images.length)) {
        if (other && other.length && typeof XMLHttpRequest !== 'undefined') {
          other = other.filter((elem, index, self) => {
            return index === self.indexOf(elem);
          });
          len += other.length;
          const prefetch = {};
          for (let k = other.length - 1; k >= 0; k--) {
            prefetch[k] = new XMLHttpRequest();
            prefetch[k].onload  = () => { preload(len, _data); };
            prefetch[k].onerror = () => { preload(len, _data); };
            prefetch[k].open('GET', other[k]);
            prefetch[k].send(null);
          }
        }

        if (images && images.length){
          images = images.filter((elem, index, self) => {
            return index === self.indexOf(elem);
          });
          len += images.length;
          const imgs = {};
          for (let j = images.length - 1; j >= 0; j--) {
            imgs[j]         = new Image();
            imgs[j].onload  = () => { preload(len, _data); };
            imgs[j].onerror = () => { preload(len, _data); };
            imgs[j].src     = images[j];
          }
        }
      } else {
        next(current, _data);
      }
    };

    if (this._waitFor.length) {
      waitFor = waitFor.concat(this._waitFor);
    }

    if (_helpers.isFunction(this._waitOn)) {
      waitFor.push(this._waitOn);
    }

    if (waitFor.length) {
      waitFor.forEach((wo) => {
        processSubData(wo.call(this, current.params, current.queryParams, done));
      });

      let triggerExitIndex = this._triggersExit.push(() => {
        abortWaitOn();
        stopSubs();
        for (let i = trackers.length - 1; i >= 0; i--) {
          if (trackers[i].stop) {
            trackers[i].stop();
          }
          delete trackers[i];
        }
        trackers = [];
        promises = [];
        subscriptions = [];
        _data = this._currentData = null;
        this._triggersExit.splice(triggerExitIndex - 1, 1);
      });

      whileWaitingAction();

      let maxSubWaitMs = MAX_WAIT_FOR_MS;
      if (typeof this._maxWaitFor === 'number' && this._maxWaitFor >= 0) {
        maxSubWaitMs = this._maxWaitFor;
      } else if (typeof this._router.maxWaitFor === 'number' && this._router.maxWaitFor >= 0) {
        maxSubWaitMs = this._router.maxWaitFor;
      }

      // Wait for promises; each resolution may yield subs/trackers/more promises (same as legacy wait())
      const promiseWaitStart = Date.now();
      while (promises.length) {
        if (waitOnAborted) {
          return;
        }
        const remaining = maxSubWaitMs - (Date.now() - promiseWaitStart);
        if (remaining <= 0) {
          Meteor._debug('[ostrio:flow-router-extra] [route.waitOn] Promise wait timed out');
          break;
        }
        const pendingPromises = promises.slice();
        promises = [];
        let timeoutId;
        try {
          const timeoutPromise = new Promise((_, reject) => {
            timeoutId = Meteor.setTimeout(() => {
              reject(Object.assign(new Error('timeout'), { code: 'WAITON_TIMEOUT' }));
            }, remaining);
          });
          const resultSet = await Promise.race([
            Promise.all(pendingPromises).then((r) => {
              if (timeoutId) {
                Meteor.clearTimeout(timeoutId);
              }
              return r;
            }),
            timeoutPromise,
          ]);
          if (waitOnAborted) {
            return;
          }
          resultSet.forEach((result) => {
            processSubData(result);
          });
        } catch (error) {
          if (timeoutId) {
            Meteor.clearTimeout(timeoutId);
          }
          if (error && error.code === 'WAITON_TIMEOUT') {
            Meteor._debug('[ostrio:flow-router-extra] [route.waitOn] Promise wait timed out');
            break;
          }
          Meteor._debug('[ostrio:flow-router-extra] [route.waitOn] Promise rejected:', error);
          break;
        }
      }

      if (waitOnAborted) {
        return;
      }

      // Wait until every handle reports ready (legacy subWait polled; plain { ready() } is not reactive)
      if (subscriptions.length) {
        const pollStartedAt = Date.now();

        await new Promise((resolve) => {
          let settled = false;
          const finish = () => {
            if (settled) {
              return;
            }
            settled = true;
            subscriptionWaitFinish = null;
            if (pollTimer) {
              Meteor.clearTimeout(pollTimer);
              pollTimer = null;
            }
            resolve();
          };

          subscriptionWaitFinish = finish;

          const poll = () => {
            if (waitOnAborted) {
              finish();
              return;
            }
            if (Date.now() - pollStartedAt > maxSubWaitMs) {
              Meteor._debug('[ostrio:flow-router-extra] [route.waitOn] Subscription wait timed out (stale or never ready)');
              finish();
              return;
            }
            if (this.checkSubscriptions(subscriptions)) {
              finish();
              return;
            }
            pollTimer = Meteor.setTimeout(poll, 24);
          };
          poll();
        });
      }

      if (waitOnAborted) {
        return;
      }

      _data = await getData();
      if (_resources) {
        getResources();
      } else {
        next(current, _data);
      }
    } else if (_resources) {
      whileWaitingAction();
      getResources();
    } else if (this._data) {
      next(current, await getData());
    } else {
      next(current);
    }
  }

  async callAction(current) {
    this._endWaiting && this._endWaiting();
    if (this._data) {
      if (this._onNoData && !this._currentData) {
        await this._onNoData(current.params, current.queryParams);
      } else {
        await this._action(current.params, current.queryParams, this._currentData);
      }
    } else {
      await this._action(current.params, current.queryParams, this._currentData);
    }
  }

  callSubscriptions(current) {
    this.clearSubscriptions();
    if (this.group) {
      this.group.callSubscriptions(current);
    }

    this._subscriptions(current.params, current.queryParams);
  }

  getRouteName() {
    this._routeCloseDep.depend();
    return this.name;
  }

  getParam(key) {
    this._routeCloseDep.depend();
    return this._params.get(key);
  }

  getQueryParam(key) {
    this._routeCloseDep.depend();
    return this._queryParams.get(key);
  }

  watchPathChange() {
    this._pathChangeDep.depend();
  }

  registerRouteClose() {
    this._params = new ReactiveDict();
    this._queryParams = new ReactiveDict();
    this._routeCloseDep.changed();
    this._pathChangeDep.changed();
  }

  registerRouteChange(currentContext, routeChanging) {
    // register params
    this._updateReactiveDict(this._params, currentContext.params);

    // register query params
    this._updateReactiveDict(this._queryParams, currentContext.queryParams);

    // if the route is changing, we need to defer triggering path changing
    // if we did this, old route's path watchers will detect this
    // Real issue is, above watcher will get removed with the new route
    // So, we don't need to trigger it now
    // We are doing it on the route close event. So, if they exists they'll
    // get notify that
    if(!routeChanging) {
      this._pathChangeDep.changed();
    }
  }

  _updateReactiveDict(dict, newValues) {
    const currentKeys = Object.keys(newValues);
    const oldKeys = Object.keys(dict.keyDeps);

    // set new values
    //  params is an array. So, currentKeys.forEach() does not works
    //  to iterate params
    currentKeys.forEach((key) => {
      dict.set(key, newValues[key]);
    });

    // remove keys which does not exisits here
    oldKeys.filter((i) => {
      return currentKeys.indexOf(i) < 0;
    }).forEach((key) => {
      dict.set(key, undefined);
    });
  }
}

export default Route;


================================================
FILE: client/router.js
================================================
import { FlowRouter, Route, Group, Triggers, BlazeRenderer } from './_init.js';
import { EJSON }    from 'meteor/ejson';
import { Meteor }   from 'meteor/meteor';
import { Tracker }  from 'meteor/tracker';
import { _helpers } from './../lib/_helpers.js';
import { qs }       from './../lib/qs.js';
import { MicroRouter } from '../lib/micro-router.js';
import { MAX_WAIT_FOR_MS } from '../lib/constants.js';

class Router {
  constructor() {
    this.pathRegExp = /(:[\w\(\)\\\+\*\.\?\[\]\-]+)+/g;
    this.queryRegExp = /\?([^\/\r\n].*)/;
    this.globals = [];
    this.subscriptions = Function.prototype;
    this.Renderer = new BlazeRenderer({ router: this });

    this._microRouter = new MicroRouter();
    this._tracker = this._buildTracker();
    this._current = {};
    this._onEveryPath = new Tracker.Dependency();

    this.maxWaitFor = MAX_WAIT_FOR_MS;
    this._globalRoute = new Route(this);
    this._onRouteCallbacks = [];

    this._askedToWait = false;
    this._initialized = false;
    this._triggersEnter = [];
    this._triggersExit = [];
    this._routes = [];
    this._routesMap = {};
    this._updateCallbacks();
    this._notFound = null;
    this.notfound = this.notFound;
    this.safeToRun = 0;

    this._basePath = window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
    this._oldRouteChain = [];

    this.env = {
      replaceState: new Meteor.EnvironmentVariable(),
      reload: new Meteor.EnvironmentVariable(),
      trailingSlash: new Meteor.EnvironmentVariable()
    };

    const reactiveApis = ['getParam', 'getQueryParam', 'getRouteName', 'watchPathChange'];
    reactiveApis.forEach((api) => {
      this[api] = function (arg1) {
        const currentRoute = this._current.route;
        if (!currentRoute) {
          this._onEveryPath.depend();
          return void 0;
        }
        return currentRoute[api].call(currentRoute, arg1);
      };
    });

    this._redirectFn = (pathDef, fields, queryParams) => {
      if (/^http(s)?:\/\//.test(pathDef)) {
        throw new Error("Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead");
      }
      this.withReplaceState(() => {
        this._microRouter.redirect(this._stripBase(FlowRouter.path(pathDef, fields, queryParams)));
      });
    };
    this._initTriggersAPI();
  }

  set notFound(opts) {
    Meteor.deprecate('FlowRouter.notFound is deprecated, use FlowRouter.route(\'*\', { /*...*/ }) instead!');
    opts.name = opts.name || '__notFound';
    this._notFound = this.route('*', opts);
  }

  get notFound() {
    return this._notFound;
  }

  route(pathDef, options = {}, group) {
    if (!/^\//.test(pathDef) && pathDef !== '*') {
      throw new Error("route's path must start with '/'");
    }

    const route = new Route(this, pathDef, options, group);

    route._actionHandle = (context) => {
      const oldRoute = this._current.route;
      this._oldRouteChain.push(oldRoute);

      const queryParams = qs.parse(context.querystring || '');
      // Reconstruct the full path (with base) so idempotency check in go() works
      const base = this._basePath ? `/${this._basePath}`.replace(/\/\/+/g, '/') : '';
      const fullPath = (base + context.path).replace(/\/\/+/g, '/');
      this._current = {
        path: fullPath,
        params: context.params,
        route,
        context,
        oldRoute,
        queryParams
      };

      const afterAllTriggersRan = () => {
        this._invalidateTracker();
      };

      route.waitOn(this._current, (_current, data) => {
        Triggers.runTriggers(
          this._triggersEnter.concat(route._triggersEnter),
          this._current,
          this._redirectFn,
          afterAllTriggersRan,
          data
        );
      });
    };

    route._exitHandle = (_context, next) => {
      Triggers.runTriggers(
        this._triggersExit.concat(route._triggersExit),
        this._current,
        this._redirectFn,
        next
      );
    };

    this._routes.push(route);
    if (options.name) {
      this._routesMap[options.name] = route;
    }

    this._updateCallbacks();
    this._triggerRouteRegister(route);
    return route;
  }

  group(options) {
    return new Group(this, options);
  }

  path(_pathDef, fields = {}, _queryParams = {}) {
    let pathDef = _pathDef || '';
    let queryParams = _queryParams;
    const hashIndex = pathDef.indexOf('#');
    const hash = hashIndex >= 0 ? pathDef.slice(hashIndex + 1) : '';
    if (hashIndex >= 0) {
      pathDef = pathDef.slice(0, hashIndex);
    }

    if (this._routesMap[pathDef]) {
      pathDef = _helpers.clone(this._routesMap[pathDef].pathDef);
    }

    if (this.queryRegExp.test(pathDef)) {
      const pathDefParts = pathDef.split(this.queryRegExp);
      pathDef = pathDefParts[0];
      if (pathDefParts[1]) {
        queryParams = qs.merge(qs.parse(pathDefParts[1]), queryParams);
      }
    }

    let path = '';

    if (this._basePath) {
      path += `/${this._basePath}/`;
    }

    path += pathDef.replace(this.pathRegExp, (_key) => {
      const firstRegexpChar = _key.indexOf('(');
      let key = _key.substring(1, firstRegexpChar > 0 ? firstRegexpChar : undefined);
      key = key.replace(/[\+\*\?]+/g, '');

      if (fields[key]) {
        return encodeURIComponent(`${fields[key]}`);
      }
      return '';
    });

    path = path.replace(/\/\/+/g, '/');
    path = path.match(/^\/{1}$/) ? path : path.replace(/\/$/, '');

    if (this.env.trailingSlash.get() && path[path.length - 1] !== '/') {
      path += '/';
    }

    const strQueryParams = qs.stringify(queryParams || {});
    if (strQueryParams) {
      path += `?${strQueryParams}`;
    }

    path = path.replace(/\/\/+/g, '/');

    if (hash) {
      path += `#${hash}`;
    }

    return path;
  }

  go(pathDef, fields, queryParams) {
    const path = this.path(pathDef, fields, queryParams);
    if (!this.env.reload.get() && path === this._current.path) {
      return;
    }

    try {
      // MicroRouter expects paths without base; strip it before passing
      const routerPath = this._stripBase(path);
      if (!this.env.reload.get() && this._microRouter._isHashOnlyChange(routerPath)) {
        window.location.hash = this._microRouter._createContext(routerPath).hash;
        return;
      }

      if (this.env.replaceState.get()) {
        this._microRouter.replace(routerPath);
      } else {
        this._microRouter.show(routerPath);
      }
    } catch (e) {
      Meteor._debug('Malformed URI!', path, e);
    }
  }

  reload() {
    this.env.reload.withValue(true, () => {
      this._microRouter.replace(this._stripBase(this._current.path));
    });
  }

  redirect(path) {
    this._microRouter.redirect(this._stripBase(path));
  }

  // Strip the base path prefix before passing to MicroRouter.
  // FlowRouter.path() includes the base path for link generation,
  // but MicroRouter works with app-relative paths and adds the base in pushState.
  _stripBase(path) {
    if (!this._basePath) return path;
    const base = `/${this._basePath}`.replace(/\/\/+/g, '/').replace(/\/$/, '');
    if (path.startsWith(base + '/') || path === base) {
      return path.slice(base.length) || '/';
    }
    return path;
  }

  setParams(newParams) {
    if (!this._current.route) { return false; }

    const pathDef = this._current.route.pathDef;
    const existingParams = this._current.params;
    let params = {};
    Object.keys(existingParams).forEach((key) => {
      params[key] = existingParams[key];
    });

    params = _helpers.extend(params, newParams);
    const queryParams = this._current.queryParams;
    this.go(pathDef, params, queryParams);
    return true;
  }

  setQueryParams(newParams) {
    if (!this._current.route) { return false; }

    const queryParams = _helpers.extend(_helpers.clone(this._current.queryParams), newParams);
    for (const k in queryParams) {
      if (queryParams[k] === null || queryParams[k] === undefined) {
        delete queryParams[k];
      }
    }

    const pathDef = this._current.route.pathDef;
    const params = this._current.params;
    this.go(pathDef, params, queryParams);
    return true;
  }

  current() {
    const current = _helpers.clone(this._current);
    current.queryParams = EJSON.clone(current.queryParams);
    current.params = EJSON.clone(current.params);
    return current;
  }

  track(reactiveMapper) {
    return (props, onData, env) => {
      let trackerCleanup = null;
      const handler = Tracker.nonreactive(() => {
        return Tracker.autorun(() => {
          trackerCleanup = reactiveMapper(props, onData, env);
        });
      });

      return () => {
        if (typeof trackerCleanup === 'function') {
          trackerCleanup();
        }
        return handler.stop();
      };
    };
  }

  mapper(props, onData, env) {
    if (typeof onData === 'function') {
      onData(null, { route: this.current(), props, env });
    }
  }

  trackMapper() {
    return this.track(this.mapper);
  }

  subsReady() {
    let callback = null;
    const args = Array.from(arguments);

    if (typeof args[args.length - 1] === 'function') {
      callback = args.pop();
    }

    const currentRoute = this.current().route;
    const globalRoute = this._globalRoute;

    this._onEveryPath.depend();

    if (!currentRoute) {
      return false;
    }

    let subscriptions;
    if (args.length === 0) {
      subscriptions = Object.values(globalRoute.getAllSubscriptions());
      subscriptions = subscriptions.concat(Object.values(currentRoute.getAllSubscriptions()));
    } else {
      subscriptions = args.map((subName) => {
        return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
      });
    }

    const isReady = () => {
      return subscriptions.every((sub) => sub && sub.ready());
    };

    if (callback) {
      Tracker.autorun((c) => {
        if (isReady()) {
          callback();
          c.stop();
        }
      });
      return true;
    }
    return isReady();
  }

  withReplaceState(fn) {
    return this.env.replaceState.withValue(true, fn);
  }

  withTrailingSlash(fn) {
    return this.env.trailingSlash.withValue(true, fn);
  }

  initialize(options = {}) {
    if (this._initialized) {
      throw new Error('FlowRouter is already initialized');
    }

    if (options.maxWaitFor !== undefined) {
      this.maxWaitFor = options.maxWaitFor;
    }

    this._updateCallbacks();
    this._microRouter.base(this._basePath);
    this._microRouter.start({
      click: options.click !== undefined ? options.click : true,
      popstate: options.popstate !== undefined ? options.popstate : true,
      dispatch: true
    });

    this._initialized = true;
  }

  wait() {
    if (this._initialized) {
      throw new Error("can't wait after FlowRouter has been initialized");
    }
    this._askedToWait = true;
  }

  onRouteRegister(cb) {
    this._onRouteCallbacks.push(cb);
  }

  _triggerRouteRegister(currentRoute) {
    const routePublicApi = _helpers.pick(currentRoute, ['name', 'pathDef', 'path']);
    routePublicApi.options = _helpers.omit(currentRoute.options, [
      'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name'
    ]);

    this._onRouteCallbacks.forEach((cb) => {
      cb(routePublicApi);
    });
  }

  url() {
    return Meteor.absoluteUrl(
      this.path.apply(this, arguments).replace(
        new RegExp('^' + (`/${this._basePath || ''}/`).replace(/\/\/+/g, '/')),
        ''
      )
    );
  }

  _buildTracker() {
    const tracker = Tracker.autorun(() => {
      if (!this._current || !this._current.route) {
        return;
      }

      const currentContext = this._current;
      const route = currentContext.route;
      const path = currentContext.path;

      if (this.safeToRun === 0) {
        throw new Error("You can't use reactive data sources like Session inside the `.subscriptions` method!");
      }

      this._globalRoute.clearSubscriptions();
      this.subscriptions.call(this._globalRoute, path);
      route.callSubscriptions(currentContext);

      Tracker.nonreactive(() => {
        let isRouteChange = currentContext.oldRoute !== currentContext.route;
        if (!currentContext.oldRoute) {
          isRouteChange = false;
        }

        const oldestRoute = this._oldRouteChain[0];
        this._oldRouteChain = [];

        currentContext.route.registerRouteChange(currentContext, isRouteChange);
        route.callAction(currentContext);

        Tracker.afterFlush(() => {
          this._onEveryPath.changed();
          if (isRouteChange) {
            if (oldestRoute && oldestRoute.registerRouteClose) {
              oldestRoute.registerRouteClose();
            }
          }
        });
      });

      this.safeToRun--;
    });

    return tracker;
  }

  _invalidateTracker() {
    this.safeToRun++;
    this._tracker.invalidate();

    if (!Tracker.currentComputation) {
      try {
        Tracker.flush();
      } catch(ex) {
        if (!/Tracker\.flush while flushing/.test(ex.message)) {
          return;
        }

        Meteor.defer(() => {
          const path = this._nextPath;
          if (!path) {
            return;
          }
          delete this._nextPath;
          this.env.reload.withValue(true, () => {
            this.go(path);
          });
        });
      }
    }
  }

  _updateCallbacks() {
    this._microRouter.reset();
    let catchAll = null;

    this._routes.forEach((route) => {
      if (route.pathDef === '*') {
        catchAll = route;
      } else {
        this._microRouter.route(route.pathDef, route._actionHandle);
        this._microRouter.exit(route.pathDef, route._exitHandle);
      }
    });

    if (catchAll) {
      this._microRouter.route(catchAll.pathDef, catchAll._actionHandle);
    }
  }

  _initTriggersAPI() {
    const self = this;
    this.triggers = {
      enter(_triggers, filter) {
        let triggers = Triggers.applyFilters(_triggers, filter);
        if (triggers.length) {
          self._triggersEnter = self._triggersEnter.concat(triggers);
        }
      },
      exit(_triggers, filter) {
        let triggers = Triggers.applyFilters(_triggers, filter);
        if (triggers.length) {
          self._triggersExit = self._triggersExit.concat(triggers);
        }
      }
    };
  }
}

export default Router;


================================================
FILE: client/triggers.js
================================================
// a set of utility functions for triggers

const Triggers = {};

// Apply filters for a set of triggers
// @triggers - a set of triggers
// @filter - filter with array fields with `only` and `except`
//           support only either `only` or `except`, but not both
Triggers.applyFilters = (_triggers, filter) => {
  let triggers = _triggers;
  if(!(triggers instanceof Array)) {
    triggers = [triggers];
  }

  if(!filter) {
    return triggers;
  }

  if(filter.only && filter.except) {
    throw new Error('Triggers don\'t support only and except filters at once');
  }

  if(filter.only && !(filter.only instanceof Array)) {
    throw new Error('only filters needs to be an array');
  }

  if(filter.except && !(filter.except instanceof Array)) {
    throw new Error('except filters needs to be an array');
  }

  if(filter.only) {
    return Triggers.createRouteBoundTriggers(triggers, filter.only);
  }

  if(filter.except) {
    return Triggers.createRouteBoundTriggers(triggers, filter.except, true);
  }

  throw new Error('Provided a filter but not supported');
};

//  create triggers by bounding them to a set of route names
//  @triggers - a set of triggers
//  @names - list of route names to be bound (trigger runs only for these names)
//  @negate - negate the result (triggers won't run for above names)
Triggers.createRouteBoundTriggers = (triggers, names, negate) => {
  const namesMap = {};
  names.forEach((name) => {
    namesMap[name] = true;
  });

  const filteredTriggers = triggers.map((originalTrigger) => {
    const modifiedTrigger = (context, next) => {
      let matched = (namesMap[context.route.name]) ? 1 : -1;
      matched = (negate) ? matched * -1 : matched;

      if(matched === 1) {
        originalTrigger(context, next);
      }
    };
    return modifiedTrigger;
  });

  return filteredTriggers;
};

//  run triggers and abort if redirected or callback stopped
//  @triggers - a set of triggers
//  @context - context we need to pass (it must have the route)
//  @redirectFn - function which used to redirect
//  @after - called after if only all the triggers runs
Triggers.runTriggers = (triggers, context, redirectFn, after, data) => {
  let abort = false;
  let inCurrentLoop = true;
  let alreadyRedirected = false;

  const doRedirect = (url, params, queryParams) => {
    if(alreadyRedirected) {
      throw new Error('already redirected');
    }

    if(!inCurrentLoop) {
      throw new Error('redirect needs to be done in sync');
    }

    if(!url) {
      throw new Error('trigger redirect requires an URL');
    }

    abort = true;
    alreadyRedirected = true;
    redirectFn(url, params, queryParams);
  };

  const doStop = () => {
    abort = true;
  };

  for (let lc = 0; lc < triggers.length; lc++) {
    triggers[lc](context, doRedirect, doStop, data);

    if (abort) {
      return;
    }
  }

  // mark that, we've exceeds the currentEventloop for
  // this set of triggers.
  inCurrentLoop = false;
  after();
};

export default Triggers;


================================================
FILE: docs/README.md
================================================
# Flow-Router Extra Docs Index

Client routing for [Meteor.js apps](https://docs.meteor.com/?utm_source=dr.dimitru&utm_medium=online&utm_campaign=Q2-2022-Ambassadors)

```shell
meteor add ostrio:flow-router-extra
```

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// DISABLE QUERY STRING COMPATIBILITY
// WITH OLDER FlowRouter AND Meteor RELEASES
FlowRouter.decodeQueryParamsOnce = true;

FlowRouter.route('/', {
  name: 'index',
  action() {
    // Render a template using Blaze
    this.render('templateName');

    // Can be used with BlazeLayout,
    // and ReactLayout for React-based apps
  }
});

// Create 404 route (catch-all)
FlowRouter.route('*', {
  action() {
    // Show 404 error page using Blaze
    this.render('notFound');

    // Can be used with BlazeLayout,
    // and ReactLayout for React-based apps
  }
});
```

> [!IMPORTANT]
> For the new apps it is recommended to set `decodeQueryParamsOnce` to `true`. This flag is here to fix [#78](https://github.com/veliovgroup/flow-router/issues/78). By default it is `false` due to its historical origin for compatibility purposes

## General tutorials:

- [Quick Start](https://github.com/veliovgroup/flow-router/blob/master/docs/quick-start.md)
- [Templating](https://github.com/veliovgroup/flow-router/blob/master/docs/templating.md)
- [Templating with "Regions"](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-regions.md)
- [Templating with Data](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-data.md)
- [Auto-scroll](https://github.com/veliovgroup/flow-router/blob/master/docs/auto-scroll.md)
- [React.js usage](https://github.com/veliovgroup/flow-router/blob/master/docs/react.md)
- [Usage in real application](https://github.com/veliovgroup/meteor-files-website/tree/master/imports/client/router)

## Hooks (*in execution order*):

- [`.whileWaiting()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/whileWaiting.md)
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [`.waitOnResources()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOnResources.md)
- [`.endWaiting()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/endWaiting.md)
- [`.data()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/data.md)
- [`.onNoData()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/onNoData.md)
- [`.triggersEnter()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
- [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.md)
- [`.triggersExit()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersExit.md)

## Helpers:

- [`isActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActiveRoute.md)
- [`isActivePath` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActivePath.md)
- [`isNotActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isNotActiveRoute.md)
- [`isNotActivePath` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isNotActivePath.md)
- [`pathFor` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/pathFor.md)
- [`urlFor` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/urlFor.md)
- [`param` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/param.md)
- [`queryParam` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/queryParam.md)
- [`currentRouteName` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/currentRouteName.md)
- [`currentRouteOption` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/currentRouteOption.md)
- [`isActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActiveRoute.md)
- [`RouterHelpers` class](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/RouterHelpers.md)
- [`templatehelpers` package](https://github.com/veliovgroup/Meteor-Template-helpers)

## API:

- __General Methods:__
  - [`.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md)
  - [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
  - [`.group()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/group.md)
  - [`.render()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/render.md)
  - [Global `.triggers`](https://github.com/veliovgroup/flow-router/blob/master/docs/api/triggers.md)
- __Workarounds:__
  - [`.refresh()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/refresh.md)
  - [`.reload()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/reload.md)
  - [`.pathRegExp` option](https://github.com/veliovgroup/flow-router/blob/master/docs/api/pathRegExp.md)
  - [`.decodeQueryParamsOnce` option](https://github.com/veliovgroup/flow-router/blob/master/docs/api/decodeQueryParamsOnce.md)
- __Manipulation:__
  - [`.getParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getParam.md)
  - [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
  - [`.setParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setParams.md)
  - [`.setQueryParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setQueryParams.md)
- __URLs and data:__
  - [`.url()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/url.md)
  - [`.path()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/path.md)
  - [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)
  - [`.getRouteName()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getRouteName.md)
- __Reactivity:__
  - [`.watchPathChange()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/watchPathChange.md)
  - [`.withReplaceState()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/withReplaceState.md)
- __For add-on developers:__
  - [`.onRouteRegister()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/onRouteRegister.md)
- __Tweaking:__
  - [`.wait()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/wait.md)
  - [`.initialize()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/initialize.md)

## Related packages:

- [`ostrio:flow-router-title`](https://github.com/veliovgroup/Meteor-flow-router-title) - Reactive page title (`document.title`)
- [`ostrio:flow-router-meta`](https://github.com/veliovgroup/Meteor-flow-router-meta) - Per route `meta` tags, `script` and `link` (CSS), set per-route stylesheets and scripts
- [`communitypackages:fast-render`](https://github.com/Meteor-Community-Packages/meteor-fast-render) - Fast Render can improve the initial load time of your app, giving you 2-10 times faster initial page loads. [`fast-render` integration tutorial](https://github.com/veliovgroup/flow-router/blob/master/docs/fast-render-integration.md)
- [`communitypackages:inject-data`](https://github.com/Meteor-Community-Packages/meteor-inject-data) - This is the package used by `fast-render` to push data to the client with the initial HTML
- [`flean:flow-router-autoscroll`](https://github.com/flean/flow-router-autoscroll) - Autoscroll for Flow Router
- [`mealsunite:flow-routing-extra`](https://github.com/MealsUnite/flow-routing) - Add-on for User Accounts
- [`nxcong:flow-routing`](https://github.com/cafe4it/flow-routing) - Add-on for User Accounts (alternative)
- [`forwarder:autoform-wizard-flow-router-extra`](https://atmospherejs.com/forwarder/autoform-wizard-flow-router-extra) - Flow Router bindings for AutoForm Wizard
- [`nicolaslopezj:router-layer`](https://github.com/nicolaslopezj/meteor-router-layer) - Helps package authors to support multiple routers
- [`krishaamer:flow-router-breadcrumb`](https://github.com/krishaamer/flow-router-breadcrumb) - Easy way to add a breadcrumb with enough flexibility to your project (`flow-router-extra` edition)
- [`krishaamer:body-class`](https://github.com/krishaamer/body-class) - Easily scope CSS by automatically adding the current template and layout names as classes on the body element

## Support this project:

- Upload and share files using [☄️ meteor-files.com](https://meteor-files.com/?ref=github-flowrouter-repo-footer) — Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
- Use [▲ ostr.io](https://ostr.io?ref=github-flowrouter-repo-footer) for [Server Monitoring](https://snmp-monitoring.com), [Web Analytics](https://ostr.io/info/web-analytics?ref=github-flowrouter-repo-footer), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [SEO Pre-rendering](https://prerendering.com) of a website
- Star on [GitHub](https://github.com/veliovgroup/flow-router)
- Star on [Atmosphere](https://atmospherejs.com/ostrio/flow-router-extra)
- [Sponsor via GitHub](https://github.com/sponsors/dr-dimitru)
- [Support via PayPal](https://paypal.me/veliovgroup)


================================================
FILE: docs/api/README.md
================================================
# API

- __General Methods:__
  - [`.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md)
  - [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
  - [`.group()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/group.md)
  - [`.render()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/render.md)
  - [Global `.triggers`](https://github.com/veliovgroup/flow-router/blob/master/docs/api/triggers.md)
- __Workarounds:__
  - [`.refresh()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/refresh.md)
  - [`.reload()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/reload.md)
  - [`.pathRegExp` option](https://github.com/veliovgroup/flow-router/blob/master/docs/api/pathRegExp.md)
  - [`.decodeQueryParamsOnce` option](https://github.com/veliovgroup/flow-router/blob/master/docs/api/decodeQueryParamsOnce.md)
- __Manipulation:__
  - [`.getParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getParam.md)
  - [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
  - [`.setParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setParams.md)
  - [`.setQueryParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setQueryParams.md)
- __URLs and data:__
  - [`.url()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/url.md)
  - [`.path()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/path.md)
  - [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)
  - [`.getRouteName()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getRouteName.md)
- __Reactivity:__
  - [`.watchPathChange()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/watchPathChange.md)
  - [`.withReplaceState()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/withReplaceState.md)
- __For add-on developers:__
  - [`.onRouteRegister()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/onRouteRegister.md)
- __Tweaking:__
  - [`.wait()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/wait.md)
  - [`.initialize()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/initialize.md)


================================================
FILE: docs/api/current.md
================================================
### current method

```js
FlowRouter.current();
```

- Returns {*Object*}

Get the current state of the router. **This API is not reactive**.
If you need to watch the changes in the path simply use `FlowRouter.watchPathChange()`.

#### Example

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

const current = FlowRouter.current();
console.log(current);

// prints following object
// {
//     path: "/apps/this-is-my-app?show=yes&color=red",
//     params: {appId: "this-is-my-app"},
//     queryParams: {show: "yes", color: "red"}
//     route: {pathDef: "/apps/:appId", name: "name-of-the-route"}
// }
```

#### Further reading

- [`.watchPathChange()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/watchPathChange.md)


================================================
FILE: docs/api/decodeQueryParamsOnce.md
================================================
### decodeQueryParamsOnce option

The current behavior of `FlowRouter.getQueryParam("...")` and `FlowRouter.current().queryParams` is to double-decode query params, but this can cause issues when, for example, you want to pass a URL with its own query parameters as a URI component, such as in an OAuth flow or a redirect after login.

To solve this, you can set this option to `true` to tell FlowRouter to only decode query params once.

```js
// Allows us to pass things like encoded URLs as query params (default = false)
FlowRouter.decodeQueryParamsOnce = true;
```

#### Example

Given the URL in the address bar:

```plain
http://localhost:3000/signin?after=%2Foauth%2Fauthorize%3Fclient_id%3D123%26redirect_uri%3Dhttps%253A%252F%252Fothersite.com%252F
```

If `decodeQueryParamsOnce` is not set or set to `false` ❌ ...

```js
FlowRouter.getQueryParam("after");
// returns: "/oauth/authorize?client_id=123"

FlowRouter.current().queryParams;
// returns: { after: "/oauth/authorize?client_id=123", redirect_uri: "https://othersite.com/" }
```

If `decodeQueryParamsOnce` is set to `true` ✔️ ...

```js
FlowRouter.getQueryParam("after");
// returns: "/oauth/authorize?client_id=123&redirect_uri=https%3A%2F%2Fothersite.com%2F"

FlowRouter.current().queryParams;
// returns: { after: "/oauth/authorize?client_id=123&redirect_uri=https%3A%2F%2Fothersite.com%2F" }
```

The former is no longer recommended, but to maintain compatibility with legacy apps, `false` is the default value for this flag. Enabling this flag manually with `true` is recommended for all new apps. For more info, see [#78](https://github.com/veliovgroup/flow-router/issues/78).

#### Further reading

- [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)


================================================
FILE: docs/api/getParam.md
================================================
### getParam method

```js
FlowRouter.getParam(paramName);
```

- `paramName` {*String*}
- Returns {*String*}

Reactive function which you can use to get a parameter from the URL.

#### Example

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app

const appId = FlowRouter.getParam('appId');
console.log(appId); // prints "this-is-my-app"
```


================================================
FILE: docs/api/getQueryParam.md
================================================
### getQueryParam method

```js
FlowRouter.getQueryParam(queryKey);
```

- `queryKey` {*String*}
- Returns {*String*}

Reactive function which you can use to get a value from the query string.

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

const color = FlowRouter.getQueryParam('color');
console.log(color); // prints "red"
```


================================================
FILE: docs/api/getRouteName.md
================================================
### getRouteName method

```js
FlowRouter.getRouteName();
```

- Returns {*String*}

Use to get the name of the route reactively.

#### Example

```js
Tracker.autorun(function () {
  const routeName = FlowRouter.getRouteName();
  console.log('Current route name is: ', routeName);
});
```

#### Further reading

- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)


================================================
FILE: docs/api/go.md
================================================
### go method

`.go(path, params, queryParams)`

- `path` {*String*} - Path or Route's name
- `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
- `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
- Returns {*true*}

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.route('/blog', { name: 'blog' /* ... */ });
FlowRouter.route('/blog/:_id', { name: 'blogPost' /* ... */ });

FlowRouter.go('/blog'); // <-- by path - /blog/
FlowRouter.go('blog'); // <-- by Route's name - /blog/
FlowRouter.go('blogPost', { _id: 'post_id' }); // /blog/post_id
FlowRouter.go('blogPost', { _id: 'post_id' }, { commentId: '123' }); // /blog/post_id?commentId=123
```

If only the hash changes for the current path and query, FlowRouter leaves route logic alone and lets the browser handle it like normal anchor navigation. The route action does not re-run. Use the browser `hashchange` event for tab switches, scrolling, or other fragment-specific behavior.

```js
window.addEventListener('hashchange', () => {
  const tab = window.location.hash.slice(1);
  if (tab) {
    document.getElementById(tab)?.scrollIntoView();
  }
});

FlowRouter.go('/profile#security'); // same /profile route, browser hash behavior
```

#### Further reading

- [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
- [`.getRouteName()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getRouteName.md)


================================================
FILE: docs/api/group.md
================================================
### group method

Use group routes for better route organization.

`.group(options)`

- `options` {*Object*} - [Optional]
- `options.name` {*String*} - [Optional] Route's name
- `options.prefix` {*String*} - [Optional] Route prefix
- `options[prop-name]` {*Any*} - [Optional] Any property which will be available inside route call
- `options[hook-name]` {*Function*} - [Optional] See [all available hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order)
- Returns {*Group*}

```js
const adminRoutes = FlowRouter.group({
  prefix: '/admin',
  name: 'admin',
  triggersEnter: [(context, redirect) => {
    console.log('running group triggers');
  }]
});

// handling /admin/ route
adminRoutes.route('/', {
  name: 'adminIndex',
  action() { /* ... */ }
});

// handling /admin/posts
adminRoutes.route('/posts', {
  name: 'adminPosts',
  action() { /* ... */ }
});
```

#### Nested Group

```js
const adminRoutes = FlowRouter.group({
    prefix: '/admin',
    name: 'admin'
});

const superAdminRoutes = adminRoutes.group({
    prefix: '/super',
    name: 'superadmin'
});

// handling /admin/super/post
superAdminRoutes.route('/post', {
    action() { /* ... */ }
});
```

#### Get group name

```js
FlowRouter.current().route.group.name
```

This can be useful for determining if the current route is in a specific group (e.g. *admin*, *public*, *loggedIn*) without needing to use prefixes if you don't want to. If it's a nested group, you can get the parent group's name with:

```js
FlowRouter.current().route.group.parent.name
```

#### Further reading

- [`.triggersEnter()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
- [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.md)
- [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)


================================================
FILE: docs/api/initialize.md
================================================
### initialize method

```js
FlowRouter.initialize(options);
```

- `options` {*Object*}
  - `hashbang` {*Boolean*} - Enable hashbang urls like `mydomain.com/#!/mypath`, default: `false`
  - `page` {*Object*} - Options for [page.js](https://github.com/visionmedia/page.js#pageoptions)
    - `click` {*Boolean*} - When false, `<a href>` tags in your app won't automatically call flow router and will do the browser's default page load instead. This is identical to how `react-router` behaves. You can create a `<Link />` component that calls `FlowRouter.go` in its `onClick` handler. This way, you have more control over your links. Default: `true`
    - Other options can be found in a [`page.js` docs](https://github.com/visionmedia/page.js#pageoptions)
- Returns {*void*}

By default, FlowRouter initializes the routing process in a `Meteor.startup()` callback. This works for most of the applications. But, some applications have custom initializations and FlowRouter needs to initialize after that.

So, that's where `FlowRouter.wait()` comes to save you. You need to call it directly inside your JavaScript file. After that, whenever your app is ready call `FlowRouter.initialize()`.

#### Example

```js
FlowRouter.wait();
WhenEverYourAppIsReady(() => {
  FlowRouter.initialize();
});
```

#### Further reading

- [`.wait()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/wait.md)


================================================
FILE: docs/api/onRouteRegister.md
================================================
### onRouteRegister method

```js
FlowRouter.onRouteRegister(callback);
```

- `callback` {*Function*}
- Returns {*void*}

This API is specially designed for add-on developers. They can listen for any registered route and add custom functionality to FlowRouter. This works on both server and client alike.

```js
FlowRouter.onRouteRegister((route) => {
  // do anything with the route object
  console.log(route);
});
```


================================================
FILE: docs/api/path.md
================================================
### path method

```js
FlowRouter.path(path, params, queryParams);
```

- `path` {*String*} - Path or Route's name
- `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
- `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
- Returns {*String*} - URI

```js
const pathDef = '/blog/:cat/:id';
const params = { cat: 'met eor', id: 'abc' };
const queryParams = {show: 'y+e=s', color: 'black'};

const path = FlowRouter.path(pathDef, params, queryParams);
console.log(path); // --> "/blog/met%20eor/abc?show=y%2Be%3Ds&color=black"
```

If there are no `params` or `queryParams`, it will simply return the path as it is.

#### Further reading

- [`.url()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/url.md)


================================================
FILE: docs/api/pathRegExp.md
================================================
### pathRegExp option

```js
// Use dashes as separators so `/:id-:slug/` isn't translated to `id-:slug` but to `:id`-`:slug`
FlowRouter.pathRegExp = /(:[\w\(\)\\\+\*\.\?\[\]]+)+/g;
```

- `pathRegExp` {*RegExp*}
- Default - `/(:[\w\(\)\\\+\*\.\?\[\]\-]+)+/g`

Use to change the URI RegEx parser used for `params`, for more info see [#25](https://github.com/veliovgroup/flow-router/issues/25).

#### Further reading

- [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
- [`.group()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/group.md)


================================================
FILE: docs/api/refresh.md
================================================
### refresh method

```js
FlowRouter.refresh('layout', 'template');
```

- `layout` {*String*} - [required] Name of the layout template
- `template` {*String*} - [required] Name of the intermediate template, simple `<template>Loading...</template>` might be a good option

`FlowRouter.refresh()` will force all route's rules and hooks to re-run, including subscriptions, waitOn(s) and template render.
Useful in cases where template logic depends from route's hooks, example:

```handlebars
{{#if currentUser}}
  {{> yield}}
{{else}}
  {{> loginForm}}
{{/if}}
```

in example above "yielded" template may loose data context after user login action, although user login will cause `yield` template to render - `data` and `waitOn` hooks will not fetch new data.

#### Login example

```js
Meteor.loginWithPassword({
  username: 'some@email.com'
}, 'password', error => {
  if (error) {
    /* show error */
  } else {
    /* If login form has its own `/login` route, redirect to root: */
    if (FlowRouter._current.route.name === 'login') {
      FlowRouter.go('/');
    } else {
      FlowRouter.refresh('_layout', '_loading');
    }
  }
});
```

#### Logout example

```js
Meteor.logout((error) => {
  if (error) {
    console.error(error);
  } else {
    FlowRouter.refresh('_layout', '_loading');
  }
});
```

#### Further reading

- [`.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md)
- [`.route()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/route.md)
- [`.group()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/group.md)


================================================
FILE: docs/api/reload.md
================================================
### reload method

```js
FlowRouter.reload();
```

- Returns {*void*}

FlowRouter routes are idempotent. That means, even if you call `FlowRouter.go()` to the same URL multiple times, it only activates in the first run. This is also true for directly clicking on paths.

So, if you really need to reload the route, this is the method you want.

#### Further reading

- [`.refresh()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/refresh.md)


================================================
FILE: docs/api/render.md
================================================
### render method

`this.render()` method is available only [inside hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order).

> [!NOTE]
> `this.render()` method is available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed

#### With Layout

`this.render(layout, template [, data, callback])`

- `layout` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of layout template (*which has* `yield`)
- `template` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of template (*which will be rendered into yield*)
- `data` {*Object*} - [Optional] Object of data context to use in template. Will be passed to both `layout` and `template`
- `callback` {*Function*} - [Optional] Callback triggered after template is rendered and placed into DOM. This callback has no context

#### Without Layout

`this.render(template [, data, callback])`

- `template` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of template (*which will be rendered into* `body` *element, or element defined in* `FlowRouter.Renderer.rootElement`)
- `data` {*Object*} - [Optional] Object of data context to use in template
- `callback` {*Function*} - [Optional] Callback triggered after template is rendered and placed into DOM. This callback has no context

#### Global catch-all rendering exception:

`FlowRouter.onRenderError = function (error) { /* ... */ };` this callback called with single `error` argument:

- `error` {*Meteor.Error*} — Reason.

Use `FlowRouter.onRenderError` to set global callback to catch errors like `No such layout template` and `No such template`. It's great workaround for dynamically loaded routes and templates, and might be triggered upon broken Internet connection, or when template not loaded for other reason. Here's recommended usage:

```html
<template name="templatingError">
  <h1>Oops, something went wrong</h1>
  <p>Network or other error occurred, please try to <a href="#" onclick="window.location.href=window.location.href">reload this page</a> or go back to <a href="/">home page</a>.</p>
</template>
```

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.onRenderError = function (error) {
  console.error('[onRenderError]', error);
  this.render('templatingError');
};
```

#### Features:

- Made with animation performance in mind, all DOM interactions are wrapped into `requestAnimationFrame`
- In-memory rendering (*a.k.a. off-screen rendering, virtual DOM*), disabled by default, can be activated with `FlowRouter.Renderer.inMemoryRendering = true;`

#### Settings (*Experimental!*):

- Settings below is experimental, targeted to reduce on-screen DOM layout re-flow, speed up rendering on slower devices and Phones in first place, by moving DOM computation to off-screen (*a.k.a. In-Memory DOM, Virtual DOM*)
- `FlowRouter.Renderer.rootElement` {*Function*} - Function which returns root DOM element where layout will be rendered, default: `document.body`
- `FlowRouter.Renderer.inMemoryRendering` {*Boolean*} - Enable/Disable in-memory rendering, default: `false`
- `FlowRouter.Renderer.getMemoryElement` {*Function*} - Function which returns default in-memory element, default: `document.createElement('div')`. Use `document.createDocumentFragment()` to avoid extra parent elements
  - The default `document.createElement('div')` will cause extra wrapping `div` element
  - `document.createDocumentFragment()` won't cause extra wrapping `div` element but may lead to exceptions in Blaze engine, depends from your app implementation

#### Further reading

- [Templating](https://github.com/veliovgroup/flow-router/blob/master/docs/templating.md)
- [Templating with "Regions"](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-regions.md)
- [Templating with Data](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-data.md)


================================================
FILE: docs/api/route.md
================================================
### route method

```js
FlowRouter.route(path, options);
```

- `path` {*String*} - Path with placeholders
- `options` {*Object*}
- `options.name` {*String*} - Route's name
- `options[prop-name]` {*Any*} - [Optional] Any property which will be available inside route call
- `options[hook-name]` {*Function*} - [Optional] See [all available hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order)
- Returns {*Route*}

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.route('/blog/:cat/:id', {
  name: 'blogPostRoute'
})

const params = {cat: "meteor", id: "abc"};
const queryParams = {show: "yes", color: "black"};

const path = FlowRouter.path("blogPostRoute", params, queryParams);
console.log(path); // prints "/blog/meteor/abc?show=yes&color=black"
```

#### Catch-all route

```js
// Create 404 route (catch-all)
FlowRouter.route('*', {
  action() {
    // Show 404 error page
  }
});
```

#### Further reading

- [`.path()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/path.md)


================================================
FILE: docs/api/setParams.md
================================================
### setParams method

```js
FlowRouter.setParams(params);
```

- `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
- Returns {*true*}

Change the current Route's `params` with the new values and re-route to the new path.

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

FlowRouter.setParams({appId: 'new-id'});
// Then the user will be redirected to the following path
//      /apps/new-id?show=yes&color=red
```

#### Further reading

- [`.setQueryParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setQueryParams.md)
- [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
- [`.getParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getParam.md)


================================================
FILE: docs/api/setQueryParams.md
================================================
### setQueryParams method

```js
FlowRouter.setQueryParams(queryParams);
```

- `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
- Returns {*true*}

#### Unset parameter

To remove a query param set it to `null`:

```js
FlowRouter.setQueryParams({ paramToRemove: null });
```

#### Further reading

- [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
- [`.setParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setParams.md)
- [`.getParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getParam.md)


================================================
FILE: docs/api/triggers.md
================================================
### Global Triggers

```js
FlowRouter.triggers.enter([cb1, cb2]);
FlowRouter.triggers.exit([cb1, cb2]);

// filtering
FlowRouter.triggers.enter([trackRouteEntry], {only: ["home"]});
FlowRouter.triggers.exit([trackRouteExit], {except: ["home"]});
```

To filter routes use `only` or `except` keywords.
You can't use both `only` and `except` at once.

#### Further reading

- [`.triggersEnter()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
- [`.triggersExit()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersExit.md)


================================================
FILE: docs/api/url.md
================================================
### url method

```js
FlowRouter.url(path, params, queryParams);
```

- `path` {*String*} - Path or Route's name
- `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
- `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
- Returns {*String*} - Absolute URL using `Meteor.absoluteUrl`

#### Further reading

- [`.path()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/path.md)


================================================
FILE: docs/api/wait.md
================================================
### wait method

```js
FlowRouter.wait();
```

- Returns {*void*}

By default, FlowRouter initializes the routing process in a `Meteor.startup()` callback. This works for most of the applications. But, some applications have custom initializations and FlowRouter needs to initialize after that.

So, that's where `FlowRouter.wait()` comes to save you. You need to call it directly inside your JavaScript file. After that, whenever your app is ready call `FlowRouter.initialize()`.

#### Example

```js
FlowRouter.wait();
WhenEverYourAppIsReady(() => {
  FlowRouter.initialize();
});
```

#### Further reading

- [`.initialize()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/initialize.md)


================================================
FILE: docs/api/watchPathChange.md
================================================
### watchPathChange method

```js
FlowRouter.watchPathChange();
```

- Returns {*void*}

Reactively watch the changes in the path. If you need to simply get the `params` or `queryParams` use methods like `FlowRouter.getQueryParam()`.

```js
Tracker.autorun(() => {
  FlowRouter.watchPathChange();
  const currentContext = FlowRouter.current();
  // do something with the current context
});
```

#### Further reading

- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)
- [`.getQueryParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getQueryParam.md)
- [`.getParam()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getParam.md)
- [`.getRouteName()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/getRouteName.md)


================================================
FILE: docs/api/withReplaceState.md
================================================
### withReplaceState method

```js
FlowRouter.withReplaceState(callback);
```

- `callback` {*Function*}
- Returns {*void*}

Normally, all the route changes made via APIs like `FlowRouter.go` and `FlowRouter.setParams()` add a URL item to the browser history. For example, run the following code:

```js
FlowRouter.setParams({id: 'the-id-1'});
FlowRouter.setParams({id: 'the-id-2'});
FlowRouter.setParams({id: 'the-id-3'});
```

Now you can hit the back button of your browser two times. This is normal behavior since users may click the back button and expect to see the previous state of the app.

But sometimes, this is not something you want. You don't need to pollute the browser history. Then, you can use the following syntax.

```js
FlowRouter.withReplaceState(() => {
  FlowRouter.setParams({id: 'the-id-1'});
  FlowRouter.setParams({id: 'the-id-2'});
  FlowRouter.setParams({id: 'the-id-3'});
});
```

#### Further reading

- [`.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md)
- [`.setParams()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/setParams.md)



================================================
FILE: docs/auto-scroll.md
================================================
### Auto-scroll to the top of the page after navigation

*FlowRouter* causes the page to remain at the same scroll position on navigation between routes (which people are often surprised by). Little snipped below would fix this behavior to more common, when each page loaded at the top of the scrolling position.

Originated from [`#9`](https://github.com/veliovgroup/flow-router/issues/9).

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

const scrollToTop = () => {
  setTimeout(() => {
    if (!window.location.hash) {
      (window.scroll || window.scrollTo || function (){})(0, 0);
    }
  }, 25);
};

FlowRouter.triggers.enter([scrollToTop]);
```

With jQuery animation:

```js
import { $ } from 'meteor/jquery';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

const scrollToTop = () => {
  setTimeout(() => {
    if (!window.location.hash) {
      $('html, body').animate({scrollTop: 100});
    }
  }, 25);
};

FlowRouter.triggers.enter([scrollToTop]);
```


================================================
FILE: docs/fast-render-integration.md
================================================
### Fast-Render Integration

To get the most out of Flow-Router Extra and [Fast-Render](https://github.com/abecks/meteor-fast-render) use combination of `subscriptions` and `waitOn`.

#### Install fast-render library:

```shell
meteor add communitypackages:fast-render
```

__Note: make sure `communitypackages:fast-render` placed above `ostrio:flow-router-extra` in `meteor-app/.meteor/packages` file. For package developers: Make sure `communitypackages:fast-render` placed before `ostrio:flow-router-extra` in `api.use()` method:__

```plaintext
# meteor-app/.meteor/packages
communitypackages:fast-render
ostrio:flow-router-extra
```

```js
// meteor-package/package.js
Package.onUse((api) => {
  api.use(['communitypackages:fast-render', 'ostrio:flow-router-extra', /*...*/]);
});
```

__To utilize features of Fast-Render place routes definition into `lib` or any other isomorphic location/import.__

```js
// meteor-app/lib/routes.js
import { Meteor }     from 'meteor/meteor';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.route('/:_id', {
  name: 'file',
  action(params, queryParams, data) {
    // this.render(/*...*/);
  },
  waitOn(params) {
    if (Meteor.isClient) {
      return Meteor.subscribe('data', params._id);
    }
  },
  subscriptions(params) {
    if (Meteor.isServer) {
      this.register('data', Meteor.subscribe('data', params._id));
    }
  },
  fastRender: true,
  data(params) {
    // Get subscribed data
    return MyCollection.findOne(params._id) || false;
  }
});
```

#### Further Reading

- [Fast Render Repository](https://github.com/abecks/meteor-fast-render)
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [`.subscriptions()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/original-readme.md#subscription-management)
- [`.data()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/data.md)
- [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.md)
- [`.render()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/render.md)


================================================
FILE: docs/full.md
================================================
# Docs as single file

## Quick Start

#### Install

```shell
# Remove original FlowRouter
meteor remove kadira:flow-router

# Install FR-Extra
meteor add ostrio:flow-router-extra
```

__Note:__ *This package is meant to replace original FlowRouter,* `kadira:flow-router` *should be removed to avoid interference and unexpected behavior.*

#### ES6 Import

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
```

#### Create your first route

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// Create index route
FlowRouter.route('/', {
  name: 'index',
  action() {
    // Do something here
    // After route is followed
    this.render('templateName');
  }
});

// Create 404 route (catch-all)
FlowRouter.route('*', {
  action() {
    // Show 404 error page
    this.render('notFound');
  }
});
```

#### Force template re-rendering

*Introduced in `v3.7.1`*

By default if same template is rendered when user navigates to a different route, including parameters or query-string change/update rendering engine will smoothly __only update__ template's data. In case if you wish to force full template rendering executing all hooks and callbacks use `{ conf: { forceReRender: true } }`, like:

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import '/imports/client/layout/layout.js';

FlowRouter.route('/item/:_id', {
  name: 'item',
  conf: {
    // without this option template won't be re-rendered
    // upon navigation between different "item" routes
    // e.g. when navigating from `/item/1` to `/item/2`
    forceReRender: true
  },
  waitOn() {
    return import('/imports/client/item/item.js');
  },
  action(params) {
    this.render('layout', 'item', params);
  }
});
```

#### Create a route with parameters
```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// Going to: /article/article_id/article-slug
FlowRouter.route('/article/:_id/:slug', {
  name: 'article',
  action(params) {
    // All passed parameters is available as Object:
    console.log(params);
    // { _id: 'article_id', slug: 'article-slug' }

    // Pass params to Template's context
    this.render('article', params);
  },
  waitOn(params) {
    return Meteor.subscribe('article', params._id);
  }
});
```

#### Create a route with query string
```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

// Going to: /article/article_id?comment=123
FlowRouter.route('/article/:_id', {
  name: 'article',
  action(params, queryParams) {
    // All passed parameters and query string
    // are available as Objects:
    console.log(params);
    // { _id: 'article_id' }
    console.log(queryParams);
    // { comment: '123' }

    // Pass params and query string to Template's context
    this.render('article', Object.assign({}, params, queryParams));
  }
});
```

__Note:__ *if you're using any package which requires original FR namespace, throws an error, you can solve it with next code:*
```js
// in /lib/ directory
Package['kadira:flow-router'] = Package['ostrio:flow-router-extra'];
```


-------


## API
### current method

```js
FlowRouter.current();
```
 - Returns {*Object*}

Get the current state of the router. **This API is not reactive**.
If you need to watch the changes in the path simply use `FlowRouter.watchPathChange()`.

#### Example
```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

const current = FlowRouter.current();
console.log(current);

// prints following object
// {
//     path: "/apps/this-is-my-app?show=yes&color=red",
//     params: {appId: "this-is-my-app"},
//     queryParams: {show: "yes", color: "red"}
//     route: {pathDef: "/apps/:appId", name: "name-of-the-route"}
// }
```

-------

### getParam method

```js
FlowRouter.getParam(paramName);
```
 - `paramName` {*String*}
 - Returns {*String*}

Reactive function which you can use to get a parameter from the URL.

#### Example
```js
// route def: /apps/:appId
// url: /apps/this-is-my-app

const appId = FlowRouter.getParam('appId');
console.log(appId); // prints "this-is-my-app"
```

-------

### getQueryParam method

```js
FlowRouter.getQueryParam(queryKey);
```
 - `queryKey` {*String*}
 - Returns {*String*}

Reactive function which you can use to get a value from the query string.

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

const color = FlowRouter.getQueryParam('color');
console.log(color); // prints "red"
```

-------

### getRouteName method

```js
FlowRouter.getRouteName();
```
 - Returns {*String*}

Use to get the name of the route reactively.

#### Example
```js
Tracker.autorun(function () {
  const routeName = FlowRouter.getRouteName();
  console.log('Current route name is: ', routeName);
});
```

-------

### go method

`.go(path, params, queryParams)`
 - `path` {*String*} - Path or Route's name
 - `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
 - Returns {*true*}

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.route('/blog', { name: 'blog' /* ... */ });
FlowRouter.route('/blog/:_id', { name: 'blogPost' /* ... */ });

FlowRouter.go('/blog'); // <-- by path - /blog/
FlowRouter.go('blog'); // <-- by Route's name - /blog/
FlowRouter.go('blogPost', { _id: 'post_id' }); // /blog/post_id
FlowRouter.go('blogPost', { _id: 'post_id' }, { commentId: '123' }); // /blog/post_id?commentId=123
```

If only the hash changes for the current path and query, FlowRouter leaves route logic alone and lets the browser handle it like normal anchor navigation. The route action does not re-run. Use the browser `hashchange` event for tab switches, scrolling, or other fragment-specific behavior.

```js
window.addEventListener('hashchange', () => {
  const tab = window.location.hash.slice(1);
  if (tab) {
    document.getElementById(tab)?.scrollIntoView();
  }
});

FlowRouter.go('/profile#security'); // same /profile route, browser hash behavior
```

-------

### group method

Use group routes for better route organization.

`.group(options)`
 - `options` {*Object*} - [Optional]
 - `options.name` {*String*} - [Optional] Route's name
 - `options.prefix` {*String*} - [Optional] Route prefix
 - `options[prop-name]` {*Any*} - [Optional] Any property which will be available inside route call
 - `options[hook-name]` {*Function*} - [Optional] See [all available hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order)
 - Returns {*Group*}

```js
const adminRoutes = FlowRouter.group({
  prefix: '/admin',
  name: 'admin',
  triggersEnter: [(context, redirect) => {
    console.log('running group triggers');
  }]
});

// handling /admin/ route
adminRoutes.route('/', {
  name: 'adminIndex',
  action() { /* ... */ }
});

// handling /admin/posts
adminRoutes.route('/posts', {
  name: 'adminPosts',
  action() { /* ... */ }
});
```

#### Nested Group
```js
const adminRoutes = FlowRouter.group({
    prefix: '/admin',
    name: 'admin'
});

const superAdminRoutes = adminRoutes.group({
    prefix: '/super',
    name: 'superadmin'
});

// handling /admin/super/post
superAdminRoutes.route('/post', {
    action() { /* ... */ }
});
```

#### Get group name
```js
FlowRouter.current().route.group.name
```

This can be useful for determining if the current route is in a specific group (e.g. *admin*, *public*, *loggedIn*) without needing to use prefixes if you don't want to. If it's a nested group, you can get the parent group's name with:
```js
FlowRouter.current().route.group.parent.name
```

-------

### initialize method

```js
FlowRouter.initialize();
```
 - Returns {*void*}

By default, FlowRouter initializes the routing process in a `Meteor.startup()` callback. This works for most of the applications. But, some applications have custom initializations and FlowRouter needs to initialize after that.

So, that's where `FlowRouter.wait()` comes to save you. You need to call it directly inside your JavaScript file. After that, whenever your app is ready call `FlowRouter.initialize()`.

#### Example
```js
FlowRouter.wait();
WhenEverYourAppIsReady(() => {
  FlowRouter.initialize();
});
```

-------

### onRouteRegister method

```js
FlowRouter.onRouteRegister(callback);
```
 - `callback` {*Function*}
 - Returns {*void*}

This API is specially designed for add-on developers. They can listen for any registered route and add custom functionality to FlowRouter. This works on both server and client alike.

```js
FlowRouter.onRouteRegister((route) => {
  // do anything with the route object
  console.log(route);
});
```

-------

### path method

```js
FlowRouter.path(path, params, queryParams);
```
 - `path` {*String*} - Path or Route's name
 - `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
 - Returns {*String*} - URI

```js
const pathDef = '/blog/:cat/:id';
const params = { cat: 'met eor', id: 'abc' };
const queryParams = {show: 'y+e=s', color: 'black'};

const path = FlowRouter.path(pathDef, params, queryParams);
console.log(path); // --> "/blog/met%20eor/abc?show=y%2Be%3Ds&color=black"
```

If there are no `params` or `queryParams`, it will simply return the path as it is.

-------

### pathRegExp option

```js
// Use dashes as separators so `/:id-:slug/` isn't translated to `id-:slug` but to `:id`-`:slug`
FlowRouter.pathRegExp = /(:[\w\(\)\\\+\*\.\?\[\]]+)+/g;
```
 - `pathRegExp` {*RegExp*}
 - Default - `/(:[\w\(\)\\\+\*\.\?\[\]\-]+)+/g`

Use to change the URI RegEx parser used for `params`, for more info see [#25](https://github.com/veliovgroup/flow-router/issues/25).

-------

### decodeQueryParamsOnce option

The current behavior of `FlowRouter.getQueryParam("...")` and `FlowRouter.current().queryParams` is to double-decode query params, but this can cause issues when, for example, you want to pass a URL with its own query parameters as a URI component, such as in an OAuth flow or a redirect after login.

To solve this, you can set this option to `true` to tell FlowRouter to only decode query params once.

```js
// Allows us to pass things like encoded URLs as query params (default = false)
FlowRouter.decodeQueryParamsOnce = true;
```

#### Example

Given the URL in the address bar:
```
http://localhost:3000/signin?after=%2Foauth%2Fauthorize%3Fclient_id%3D123%26redirect_uri%3Dhttps%253A%252F%252Fothersite.com%252F
```

If `decodeQueryParamsOnce` is not set or set to `false` ❌ ...
```js
FlowRouter.getQueryParam("after");
// returns: "/oauth/authorize?client_id=123"

FlowRouter.current().queryParams;
// returns: { after: "/oauth/authorize?client_id=123", redirect_uri: "https://othersite.com/" }
```

If `decodeQueryParamsOnce` is set to `true` ✔️ ...
```js
FlowRouter.getQueryParam("after");
// returns: "/oauth/authorize?client_id=123&redirect_uri=https%3A%2F%2Fothersite.com%2F"

FlowRouter.current().queryParams;
// returns: { after: "/oauth/authorize?client_id=123&redirect_uri=https%3A%2F%2Fothersite.com%2F" }
```

The former is no longer recommended, but to maintain compatibility with legacy apps, `false` is the default value for this flag. Enabling this flag manually with `true` is recommended for all new apps. For more info, see [#78](https://github.com/veliovgroup/flow-router/issues/78).

-------

### refresh method

```js
FlowRouter.refresh('layout', 'template');
```
 - `layout` {*String*} - [required] Name of the layout template
 - `template` {*String*} - [required] Name of the intermediate template, simple `<template>Loading...</template>` might be a good option


`FlowRouter.refresh()` will force all route's rules and hooks to re-run, including subscriptions, waitOn(s) and template render.
Useful in cases where template logic depends from route's hooks, example:
```handlebars
{{#if currentUser}}
  {{> yield}}
{{else}}
  {{> loginForm}}
{{/if}}
```
in example above "yielded" template may loose data context after user login action, although user login will cause `yield` template to render - `data` and `waitOn` hooks will not fetch new data.

#### Login example
```js
Meteor.loginWithPassword({
  username: 'some@email.com'
}, 'password', error => {
  if (error) {
    /* show error */
  } else {
    /* If login form has its own `/login` route, redirect to root: */
    if (FlowRouter._current.route.name === 'login') {
      FlowRouter.go('/');
    } else {
      FlowRouter.refresh('_layout', '_loading');
    }
  }
});
```

#### Logout example
```js
Meteor.logout((error) => {
  if (error) {
    console.error(error);
  } else {
    FlowRouter.refresh('_layout', '_loading');
  }
});
```

-------

### reload method

```js
FlowRouter.reload();
```
 - Returns {*void*}

FlowRouter routes are idempotent. That means, even if you call `FlowRouter.go()` to the same URL multiple times, it only activates in the first run. This is also true for directly clicking on paths.

So, if you really need to reload the route, this is the method you want.

-------

### render method

`this.render()` method is available only [inside hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order).

> [!NOTE]
> `this.render()` method is available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed

#### With Layout
`this.render(layout, template [, data, callback])`
 - `layout` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of layout template (*which has* `yield`)
 - `template` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of template (*which will be rendered into yield*)
 - `data` {*Object*} - [Optional] Object of data context to use in template. Will be passed to both `layout` and `template`
 - `callback` {*Function*} - [Optional] Callback triggered after template is rendered and placed into DOM. This callback has no context

#### Without Layout
`this.render(template [, data, callback])`
 - `template` {*String*|*Blaze.Template*} - *Blaze.Template* instance or a name of template (*which will be rendered into* `body` *element, or element defined in* `FlowRouter.Renderer.rootElement`)
 - `data` {*Object*} - [Optional] Object of data context to use in template
 - `callback` {*Function*} - [Optional] Callback triggered after template is rendered and placed into DOM. This callback has no context

#### Features:
 - Made with animation performance in mind, all DOM interactions are wrapped into `requestAnimationFrame`
 - In-memory rendering (*a.k.a. off-screen rendering, virtual DOM*), disabled by default, can be activated with `FlowRouter.Renderer.inMemoryRendering = true;`

#### Settings (*Experimental!*):
 - Settings below is experimental, targeted to reduce on-screen DOM layout reflow, speed up rendering on slower devices and Phones in first place, by moving DOM computation to off-screen (*a.k.a. In-Memory DOM, Virtual DOM*)
 - `FlowRouter.Renderer.rootElement` {*Function*} - Function which returns root DOM element where layout will be rendered, default: `document.body`
 - `FlowRouter.Renderer.inMemoryRendering` {*Boolean*} - Enable/Disable in-memory rendering, default: `false`
 - `FlowRouter.Renderer.getMemoryElement` {*Function*} - Function which returns default in-memory element, default: `document.createElement('div')`. Use `document.createDocumentFragment()` to avoid extra parent elements
     * The default `document.createElement('div')` will cause extra wrapping `div` element
     * `document.createDocumentFragment()` won't cause extra wrapping `div` element but may lead to exceptions in Blaze engine, depends from your app implementation

-------

### route method

`.route(path, options)`
 - `path` {*String*} - Path with placeholders
 - `options` {*Object*}
 - `options.name` {*String*} - Route's name
 - `options[prop-name]` {*Any*} - [Optional] Any property which will be available inside route call
 - `options[hook-name]` {*Function*} - [Optional] See [all available hooks](https://github.com/veliovgroup/flow-router/tree/master/docs#hooks-in-execution-order)
 - Returns {*Route*}

```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';

FlowRouter.route('/blog/:cat/:id', {
  name: 'blogPostRoute'
})

const params = {cat: "meteor", id: "abc"};
const queryParams = {show: "yes", color: "black"};

const path = FlowRouter.path("blogPostRoute", params, queryParams);
console.log(path); // prints "/blog/meteor/abc?show=yes&color=black"
```

#### Catch-all route
```js
// Create 404 route (catch-all)
FlowRouter.route('*', {
  action() {
    // Show 404 error page
  }
});
```

-------

### setParams method

```js
FlowRouter.setParams(params);
```
 - `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
 - Returns {*true*}

Change the current Route's `params` with the new values and re-route to the new path.

```js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red

FlowRouter.setParams({appId: 'new-id'});
// Then the user will be redirected to the following path
//      /apps/new-id?show=yes&color=red
```

-------

### setQueryParams method

```js
FlowRouter.setQueryParams(queryParams);
```
 - `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
 - Returns {*true*}


#### Unset parameter
To remove a query param set it to `null`:
```js
FlowRouter.setQueryParams({ paramToRemove: null });
```

-------

### Global Triggers

```js
FlowRouter.triggers.enter([cb1, cb2]);
FlowRouter.triggers.exit([cb1, cb2]);

// filtering
FlowRouter.triggers.enter([trackRouteEntry], {only: ["home"]});
FlowRouter.triggers.exit([trackRouteExit], {except: ["home"]});
```

To filter routes use `only` or `except` keywords.
You can't use both `only` and `except` at once.

-------

### url method

```js
FlowRouter.url(path, params, queryParams);
```
 - `path` {*String*} - Path or Route's name
 - `params` {*Object*} - Serialized route parameters, `{ _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `{ key: 'val' }`
 - Returns {*String*} - Absolute URL using `Meteor.absoluteUrl`

-------

### wait method

```js
FlowRouter.wait();
```
 - Returns {*void*}

By default, FlowRouter initializes the routing process in a `Meteor.startup()` callback. This works for most of the applications. But, some applications have custom initializations and FlowRouter needs to initialize after that.

So, that's where `FlowRouter.wait()` comes to save you. You need to call it directly inside your JavaScript file. After that, whenever your app is ready call `FlowRouter.initialize()`.

#### Example
```js
FlowRouter.wait();
WhenEverYourAppIsReady(() => {
  FlowRouter.initialize();
});
```

-------

### watchPathChange method

```js
FlowRouter.watchPathChange();
```
 - Returns {*void*}

Reactively watch the changes in the path. If you need to simply get the `params` or `queryParams` use methods like `FlowRouter.getQueryParam()`.

```js
Tracker.autorun(() => {
  FlowRouter.watchPathChange();
  const currentContext = FlowRouter.current();
  // do something with the current context
});
```

-------

### withReplaceState method

```js
FlowRouter.withReplaceState(callback);
```
 - `callback` {*Function*}
 - Returns {*void*}

Normally, all the route changes made via APIs like `FlowRouter.go` and `FlowRouter.setParams()` add a URL item to the browser history. For example, run the following code:

```js
FlowRouter.setParams({id: 'the-id-1'});
FlowRouter.setParams({id: 'the-id-2'});
FlowRouter.setParams({id: 'the-id-3'});
```

Now you can hit the back button of your browser two times. This is normal behavior since users may click the back button and expect to see the previous state of the app.

But sometimes, this is not something you want. You don't need to pollute the browser history. Then, you can use the following syntax.

```js
FlowRouter.withReplaceState(() => {
  FlowRouter.setParams({id: 'the-id-1'});
  FlowRouter.setParams({id: 'the-id-2'});
  FlowRouter.setParams({id: 'the-id-3'});
});
```


-----------


## Hooks

### action hook

`action(params, queryParams, data)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - `data` {*Mix*} - Value returned from `.data()` hook
 - Return: {*void*}

`.action()` hook is triggered right after page is navigated to route, or after (*exact order, if any of those is defined*):
 - [`.whileWaiting()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/whileWaiting.md)
 - [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
 - [`.waitOnResources()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOnResources.md)
 - [`.triggersEnter()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
 - [`.data()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/data.md)

-------

### data hook

`data(params, queryParams)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - Return: {*Mongo.Cursor*|*Object*|[*Object*]|*false*|*null*|*void*}


`.data()` is triggered right after all resources in `.waitOn()` and `.waitOnResources()` hooks are ready.

```js
FlowRouter.route('/post/:_id', {
  name: 'post',
  waitOn(params) {
    return Meteor.subscribe('post', params._id);
  },
  async data(params, queryParams) {
    return await PostsCollection.findOneAsync({_id: params._id});
  }
});
```

#### Passing data into a *Template*
```js
FlowRouter.route('/post/:_id', {
  name: 'post',
  action(params, queryParams, post) {
    this.render('_layout', 'post', { post });
  },
  waitOn(params) {
    return Meteor.subscribe('post', params._id);
  },
  async data(params, queryParams) {
    return await PostsCollection.findOneAsync({_id: params._id});
  }
});
```

```html
<!-- in a template -->
<template name="post">
  <h1>{{post.title}}</h1>
  <p>{{post.text}}</p>
</template>
```

#### Data in other hooks
Returned value from `data` hook, will be passed into all other hooks as third argument and to `triggersEnter` hooks as fourth argument
```jsx
FlowRouter.route('/post/:_id', {
  name: 'post',
  async data(params) {
    return await PostsCollection.findOneAsync({_id: params._id});
  },
  triggersEnter: [(context, redirect, stop, data) => {
    console.log(data);
  }]
});
```

-------

### endWaiting hook

`endWaiting()` - Called with no arguments
 - Return: {*void*}

`.endWaiting()` hook is triggered right after all resources in `.waitOn()` and `.waitOnResources()` hooks are ready.

-------

### onNoData hook

`onNoData(params, queryParams)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - Return: {*void*}


`.onNoData()` hook is triggered instead of `.action()` in case when `.data()` hook returns "falsy" value. Run any JavaScript code inside `.onNoData()` hook, for example render *404* template or redirect user somewhere else.

```js
FlowRouter.route('/post/:_id', {
  name: 'post',
  async data(params) {
    return await PostsCollection.findOneAsync({_id: params._id});
  },
  async onNoData(params, queryParams){
    await import('/imports/client/page-404.js');
    this.render('_layout', '_404');
  }
});
```

-------

### triggersEnter

`triggersEnter` is option (*not actually a hook*), it accepts array of *Function*s, each function will be called with next arguments:
 - `context` {*Route*} - Output of `FlowRouter.current()`
 - `redirect` {*Function*} - Use to redirect to another route, same as [`FlowRouter.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md)
 - `stop` {*Function*} - Use to abort current route execution
 - `data` {*Mix*} - Value returned from `.data()` hook
 - Return: {*void*}

#### Scroll to top:
```js
const scrollToTop = () => {
  (window.scroll || window.scrollTo || function (){})(0, 0);
};

FlowRouter.route('/', {
  name: 'index',
  triggersEnter: [scrollToTop]
});

// Apply to every route:
FlowRouter.triggers.enter([scrollToTop]);
```

#### Logging:
```js
FlowRouter.route('/', {
  name: 'index',
  triggersEnter: [() => {
    console.log('triggersEnter');
  }]
});
```

#### Redirect:
```js
FlowRouter.route('/', {
  name: 'index',
  triggersEnter: [(context, redirect) => {
    redirect('/other/route');
  }]
});
```

#### Global
```js
FlowRouter.triggers.enter([cb1, cb2]);
```

-------

### triggersExit hooks

`triggersExit` is option (*not actually a hook*), it accepts array of *Function*s, each function will be called with one argument:
 - `context` {*Route*} - Output of `FlowRouter.current()`
 - Return: {*void*}

```js
const trackRouteEntry = (context) => {
  // context is the output of `FlowRouter.current()`
  console.log("visit-to-home", context.queryParams);
};

const trackRouteClose = (context) => {
  console.log("move-from-home", context.queryParams);
};

FlowRouter.route('/home', {
  // calls just before the action
  triggersEnter: [trackRouteEntry],
  action() {
    // do something you like
  },
  // calls when when we decide to move to another route
  // but calls before the next route started
  triggersExit: [trackRouteClose]
});
```

#### Global
```js
FlowRouter.triggers.exit([cb1, cb2]);
```

-------

### waitOn hook

`waitOn(params, queryParams, ready)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - `ready` {*Function*} - Call when computation is ready using *Tracker*
 - Return: {*Promise*|[*Promise*]|*Subscription*|[*Subscription*]|*Tracker*|[*Tracker*]}

`.waitOn()` hook is triggered before `.action()` hook, allowing to load necessary data before rendering a template.

#### `maxWaitFor` (route and router)

- **Per route:** `FlowRouter.route(path, { maxWaitFor, waitOn, action, … })` — max time in **milliseconds** for resolving **`waitOn` promises** (including `async waitOn`) and for waiting until every returned subscription-like handle’s **`ready()`** is true (polled every 24ms).
- **Router default:** `FlowRouter.maxWaitFor` defaults to **`120000`** (same as package export **`MAX_WAIT_FOR_MS`**). Set via **`FlowRouter.initialize({ maxWaitFor })`** or assign **`FlowRouter.maxWaitFor = …`**. Routes **without** an explicit **`maxWaitFor`** use **`FlowRouter.maxWaitFor`** at the time **`waitOn`** runs.
- If **`maxWaitFor`** elapses while promises or subscriptions are still pending, **`waitOn` ends** and the route still runs **`triggersEnter`** / **`action`** (timeout is logged). **Navigation away** aborts `waitOn` and skips **`action`** for the route being left.

#### Subscriptions
```js
FlowRouter.route('/post/:_id', {
  name: 'post',
  waitOn(params) {
    return [Meteor.subscribe('post', params._id), Meteor.subscribe('suggestedPosts', params._id)];
  }
});
```

#### *Tracker*
Use reactive data sources inside `waitOn` hook. To make `waitOn` rerun on reactive data changes, wrap it to `Tracker.autorun` and return Tracker Computation object or an *Array* of Tracker Computation objects. Note: the third argument of `waitOn` is `ready` callback.
```js
FlowRouter.route('/posts', {
  name: 'post',
  waitOn(params, queryParams, ready) {
    return Tracker.autorun(() => {
      ready(() => {
        return Meteor.subscribe('posts', search.get(), page.get());
      });
    });
  }
});
```

#### Array of *Trackers*
```js
FlowRouter.route('/posts', {
  name: 'post',
  waitOn(params, queryParams, ready) {
    const tracks = [];
    tracks.push(Tracker.autorun(() => {
      ready(() => {
        return Meteor.subscribe('posts', search.get(), page.get());
      });
    }));

    tracks.push(Tracker.autorun(() => {
      ready(() => {
        return Meteor.subscribe('comments', postId.get());
      });
    }));
    return tracks;
  }
});
```

#### *Promises*
```js
FlowRouter.route('/posts', {
  name: 'posts',
  waitOn() {
    return new Promise((resolve, reject) => {
      loadPosts((err) => {
        (err) ? reject() : resolve();
      });
    });
  }
});
```

#### Array of *Promises*

```js
FlowRouter.route('/posts', {
  name: 'posts',
  waitOn() {
    return [new Promise({/*..*/}), new Promise({/*..*/}), new Promise({/*..*/})];
  }
});
```

#### Meteor method via *Promise*

*Deprecated, since v3.12.0 `Meteor.callAsync` can get called inside `async data()` hook to retrieve data from a method*

```js
FlowRouter.route('/posts', {
  name: 'posts',
  conf: {
    posts: false
  },
  action(params, queryParams, data) {
    this.render('layout', 'posts', data);
  },
  waitOn() {
    return new Promise((resolve, reject) => {
      Meteor.call('posts.get', (error, posts) => {
        if (error) {
          reject();
        } else {
          // Use `conf` as shared object to
          // pass it from `data()` hook to
          // `action()` hook`
          this.conf.posts = posts;
          resolve();
        }
      });
    });
  },
  data() {
    return this.conf.posts;
  }
});
```

#### Dynamic `import`

```js
FlowRouter.route('/posts', {
  name: 'posts',
  waitOn() {
    return import('/imports/client/posts.js');
  }
});
```

#### Array of dynamic `import`(s)
```js
FlowRouter.route('/posts', {
  name: 'posts',
  waitOn() {
    return [
      import('/imports/client/posts.js'),
      import('/imports/client/sidebar.js'),
      import('/imports/client/footer.js')
    ];
  }
});
```

#### Dynamic `import` and Subscription
```js
FlowRouter.route('/posts', {
  name: 'posts',
  waitOn() {
    return [import('/imports/client/posts.js'), Meteor.subscribe('Posts')];
  }
});
```

-------

### waitOnResources hook

`waitOnResources(params, queryParams)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - Return: {*Object*} `{ images: ['url'], other: ['url'] }`

`.waitOnResources()` hook is triggered before `.action()` hook, allowing to load necessary files, images, fonts before rendering a template.

#### Preload images
```js
FlowRouter.route('/images', {
  name: 'images',
  waitOnResources() {
    return {
      images:[
        '/imgs/1.png',
        '/imgs/2.png',
        '/imgs/3.png'
      ]
    };
  },
});
```

#### Global
Useful to preload background images and other globally used resources
```js
FlowRouter.globals.push({
  waitOnResources() {
    return {
      images: [
        '/imgs/background/jpg',
        '/imgs/icon-sprite.png',
        '/img/logo.png'
      ]
    };
  }
});
```

#### Preload Resources
This method will work only for __cacheble__ resources, if URLs returns non-cacheble resources (*dynamic resources*) it will be useless.

*Why Images and Other resources is separated? What the difference?* - Images can be prefetched via `Image()` constructor, all other resources will use `XMLHttpRequest` to cache resources. Thats why important to make sure requested URLs returns cacheble response.


```js
FlowRouter.route('/', {
  name: 'index',
  waitOnResources() {
    return {
      other:[
        '/fonts/OpenSans-Regular.eot',
        '/fonts/OpenSans-Regular.svg',
        '/fonts/OpenSans-Regular.ttf',
        '/fonts/OpenSans-Regular.woff',
        '/fonts/OpenSans-Regular.woff2'
      ]
    };
  }
});
```

#### Global
Useful to prefetch Fonts and other globally used resources
```js
FlowRouter.globals.push({
  waitOnResources() {
    return {
      other:[
        '/fonts/OpenSans-Regular.eot',
        '/fonts/OpenSans-Regular.svg',
        '/fonts/OpenSans-Regular.ttf',
        '/fonts/OpenSans-Regular.woff',
        '/fonts/OpenSans-Regular.woff2'
      ]
    };
  }
});
```

-------

### whileWaiting hook

`whileWaiting(params, queryParams)`
 - `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: 'str' }`
 - `queryParams` {*Object*} - Query params object, `/route/?key=val => { key: 'val' }`
 - Return: {*void*}

`.whileWaiting()` hook is triggered before `.waitOn()` hook, allowing to display/render text or animation saying `Loading...`.

```js
FlowRouter.route('/post/:_id', {
  name: 'post',
  whileWaiting() {
    this.render('loading');
  },
  waitOn(params) {
    return Meteor.subscribe('post', params._id);
  }
});
```


-----------


## Template Helpers

> [!NOTE]
> Template helpers are available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed

### `currentRouteName` Template Helper

Returns the name of the current route

```handlebars
<div class={{currentRouteName}}>
  ...
</div>
```

-------

### `currentRouteOption` Template Helper

This adds support to get options from flow router

```javascript
FlowRouter.route('name', {
  name: 'routeName',
  action() {
    this.render('layoutTemplate', 'main');
  },
  coolOption: "coolOptionValue"
});
```

```handlebars
<div class={{currentRouteOption 'coolOption'}}>
  ...
</div>
```

-------

### `isActivePath` Template Helper

Template helper to check if the supplied path matches the currently active route's path.

Returns either a configurable `String`, which defaults to `'active'`, or `false`.

```handlebars
<li class="{{isActivePath '/home'}}">...</li>
<li class="{{isActivePath path='/home'}}">...</li>
<li class="{{isActivePath regex='home|dashboard'}}">...</li>
{{#if isActivePath '/home'}}
  <span>Show only if '/home' is the current route's path</span>
{{/if}}
{{#if isActivePath regex='^\\/products'}}
  <span>Show only if current route's path begins with '/products'</span>
{{/if}}

<li class="{{isActivePath class='is-selected' path='/home'}}">...</li>
<li class="{{isActivePath '/home' class='is-selected'}}">...</li>
```

-------

### `isActiveRoute` Template Helper

Template helper to check if the supplied route name matches the currently active route's name.

Returns either a configurable `String`, which defaults to `'active'`, or `false`.

```handlebars
<li class="{{isActiveRoute 'home'}}">...</li>
<li class="{{isActiveRoute name='home'}}">...</li>
<li class="{{isActiveRoute regex='home|dashboard'}}">...</li>
{{#if isActiveRoute 'home'}}
  <span>Show only if 'home' is the current route's name</span>
{{/if}}
{{#if isActiveRoute regex='^products'}}
  <span>Show only if the current route's name begins with 'products'</span>
{{/if}}

<li class="{{isActiveRoute class='is-selected' name='home'}}">...</li>
<li class="{{isActiveRoute 'home' class='is-selected'}}">...</li>
```

-------

### `isNotActivePath` Template Helper

Template helper to check if the supplied path doesn't match the currently active route's path.

Returns either a configurable `String`, which defaults to `'disabled'`, or `false`.

```handlebars
<li class="{{isNotActivePath '/home'}}">...</li>
<li class="{{isNotActivePath path='/home'}}">...</li>
<li class="{{isNotActivePath regex='home|dashboard'}}">...</li>
{{#if isNotActivePath '/home'}}
  <span>Show only if '/home' isn't the current route's path</span>
{{/if}}
{{#if isNotActivePath regex='^\\/products'}}
  <span>Show only if current route's path doesn't begin with '/products'</span>
{{/if}}

<li class="{{isNotActivePath class='is-disabled' path='/home'}}">...</li>
<li class="{{isNotActivePath '/home' class='is-disabled'}}">...</li>
```

-------

### `isNotActiveRoute` Template Helper

Template helper to check if the supplied route name doesn't match the currently active route's name.

Returns either a configurable `String`, which defaults to `'disabled'`, or `false`.

```handlebars
<li class="{{isNotActiveRoute 'home'}}">...</li>
<li class="{{isNotActiveRoute name='home'}}">...</li>
<li class="{{isNotActiveRoute regex='home|dashboard'}}">...</li>
{{#if isNotActiveRoute 'home'}}
  <span>Show only if 'home' isn't the current route's name</span>
{{/if}}
{{#if isNotActiveRoute regex='^products'}}
  <span>
    Show only if the current route's name doesn't begin with 'products'
  </span>
{{/if}}

<li class="{{isNotActiveRoute class='is-disabled' name='home'}}">...</li>
<li class="{{isNotActiveRoute 'home' class='is-disabled'}}">...</li>
```

#### Arguments
The following can be used by as arguments in `isNotActivePath`, `isNotActiveRoute`, `isActivePath` and `isActiveRoute` helpers.

 - Data context, Optional. `String` or `Object` with `name`, `path` or `regex`
 - `name` {*String*} - Only available for `isActiveRoute` and `isNotActiveRoute`
 - `path` {*String*} - Only available for `isActivePath` and `isNotActivePath`
 - `regex` {*String|RegExp*}

-------

### `param` Template Helper

Returns the value for a URL parameter

```handlebars
<div>ID of this post is <em>{{param 'id'}}</em></div>
```

-------

### `pathFor` Template Helper

Used to build a path to your route. First parameter can be either the path definition or name you assigned to the route. After that you can pass the params needed to construct the path. Query parameters can be passed with the `query` parameter. Hash is supported via `hash` parameter.

```handlebars
<a href="{{pathFor '/post/:id' id=_id}}">Link to post</a>
<a href="{{pathFor 'postRouteName' id=_id}}">Link to post</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id cid=comment._id}}">Link to comment in post</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id hash=comment._id}}">Jump to comment</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id cid=comment._id query='back=yes&more=true'}}">Link to comment in post with query params</a>
```

Same-route hash links use browser fragment behavior. FlowRouter does not run route hooks or actions when only the hash changes; handle fragment-specific UI with `hashchange`.

```js
window.addEventListener('hashchange', () => {
  const commentId = window.location.hash.slice(1);
  document.getElementById(commentId)?.scrollIntoView();
});
```

-------

### `queryParam` Template Helper

Returns the value for a query parameter

```handlebars
<input placeholder="Search" value="{{queryParam 'query'}}">
```

-------

### `urlFor` Template Helper

Used to build an absolute URL to your route. First parameter can be either the path definition or name you assigned to the route. After that you can pass the params needed to construct the URL. Query parameters can be passed with the `query` parameter. Hash is supported via `hash` parameter.

```handlebars
<a href="{{urlFor '/post/:id' id=_id}}">Link to post</a>
<a href="{{urlFor 'postRouteName' id=_id}}">Link to post</a>
<a href="{{urlFor '/post/:id/comments/:cid' id=_id cid=comment._id}}">Link to comment in post</a>
<a href="{{urlFor '/post/:id/comments/:cid' id=_id hash=comment._id}}">Jump to comment</a>
<a href="{{urlFor '/post/:id/comments/:cid' id=_id cid=comment._id query='back=yes&more=true'}}">Link to comment in post with query params</a>
```


================================================
FILE: docs/helpers/README.md
================================================
# Helpers:

- [`isActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActiveRoute.md)
- [`isActivePath` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActivePath.md)
- [`isNotActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isNotActiveRoute.md)
- [`isNotActivePath` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isNotActivePath.md)
- [`pathFor` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/pathFor.md)
- [`urlFor` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/urlFor.md)
- [`param` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/param.md)
- [`queryParam` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/queryParam.md)
- [`currentRouteName` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/currentRouteName.md)
- [`currentRouteOption` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/currentRouteOption.md)
- [`isActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isActiveRoute.md)
- [`RouterHelpers` class](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/RouterHelpers.md)
- [`templatehelpers` package](https://github.com/veliovgroup/Meteor-Template-helpers)


================================================
FILE: docs/helpers/RouterHelpers.md
================================================
### RouterHelpers Class

Use template helpers right from JavaScript code.

```js
import { RouterHelpers } from 'meteor/ostrio:flow-router-extra';

RouterHelpers.name('home');
// Returns true if current route's name is 'home'.
RouterHelpers.name(new RegExp('home|dashboard'));
// Returns true if current route's name contains 'home' or 'dashboard'.
RouterHelpers.name(/^products/);
// Returns true if current route's name starts with 'products'.
RouterHelpers.path('/home');
// Returns true if current route's path is '/home'.
RouterHelpers.path(new RegExp('users'));
// Returns true if current route's path contains 'users'.
RouterHelpers.path(/\/edit$/i);
// Returns true if current route's path ends with '/edit', matching is
// case-insensitive

RouterHelpers.pathFor('/post/:id', {id: '12345'});

RouterHelpers.configure({
  activeClass: 'active',
  caseSensitive: true,
  disabledClass: 'disabled',
  regex: 'false'
});
```


================================================
FILE: docs/helpers/currentRouteName.md
================================================
### `currentRouteName` Template Helper

Returns the name of the current route

```handlebars
<div class={{currentRouteName}}>
  ...
</div>
```


================================================
FILE: docs/helpers/currentRouteOption.md
================================================
### `currentRouteOption` Template Helper

This adds support to get options from flow router

```javascript
FlowRouter.route('name', {
  name: 'routeName',
  action() {
    this.render('layoutTemplate', 'main');
  },
  coolOption: "coolOptionValue"
});
```

```handlebars
<div class={{currentRouteOption 'coolOption'}}>
  ...
</div>
```


================================================
FILE: docs/helpers/isActivePath.md
================================================
### `isActivePath` Template Helper

Template helper to check if the supplied path matches the currently active route's path.

Returns either a configurable `String`, which defaults to `'active'`, or `false`.

```handlebars
<li class="{{isActivePath '/home'}}">...</li>
<li class="{{isActivePath path='/home'}}">...</li>
<li class="{{isActivePath regex='home|dashboard'}}">...</li>
{{#if isActivePath '/home'}}
  <span>Show only if '/home' is the current route's path</span>
{{/if}}
{{#if isActivePath regex='^\\/products'}}
  <span>Show only if current route's path begins with '/products'</span>
{{/if}}

<li class="{{isActivePath class='is-selected' path='/home'}}">...</li>
<li class="{{isActivePath '/home' class='is-selected'}}">...</li>
```

#### Arguments
The following can be used by as arguments in `isNotActivePath`, `isNotActiveRoute`, `isActivePath` and `isActiveRoute` helpers.

 - Data context, Optional. `String` or `Object` with `name`, `path` or `regex`
 - `name` {*String*} - Only available for `isActiveRoute` and `isNotActiveRoute`
 - `path` {*String*} - Only available for `isActivePath` and `isNotActivePath`
 - `regex` {*String|RegExp*}


================================================
FILE: docs/helpers/isActiveRoute.md
================================================
### `isActiveRoute` Template Helper

Template helper to check if the supplied route name matches the currently active route's name.

Returns either a configurable `String`, which defaults to `'active'`, or `false`.

```handlebars
<li class="{{isActiveRoute 'home'}}">...</li>
<li class="{{isActiveRoute name='home'}}">...</li>
<li class="{{isActiveRoute regex='home|dashboard'}}">...</li>
{{#if isActiveRoute 'home'}}
  <span>Show only if 'home' is the current route's name</span>
{{/if}}
{{#if isActiveRoute regex='^products'}}
  <span>Show only if the current route's name begins with 'products'</span>
{{/if}}

<li class="{{isActiveRoute class='is-selected' name='home'}}">...</li>
<li class="{{isActiveRoute 'home' class='is-selected'}}">...</li>
```

#### Arguments
The following can be used by as arguments in `isNotActivePath`, `isNotActiveRoute`, `isActivePath` and `isActiveRoute` helpers.

 - Data context, Optional. `String` or `Object` with `name`, `path` or `regex`
 - `name` {*String*} - Only available for `isActiveRoute` and `isNotActiveRoute`
 - `path` {*String*} - Only available for `isActivePath` and `isNotActivePath`
 - `regex` {*String|RegExp*}


================================================
FILE: docs/helpers/isNotActivePath.md
================================================
### `isNotActivePath` Template Helper

Template helper to check if the supplied path doesn't match the currently active route's path.

Returns either a configurable `String`, which defaults to `'disabled'`, or `false`.

```handlebars
<li class="{{isNotActivePath '/home'}}">...</li>
<li class="{{isNotActivePath path='/home'}}">...</li>
<li class="{{isNotActivePath regex='home|dashboard'}}">...</li>
{{#if isNotActivePath '/home'}}
  <span>Show only if '/home' isn't the current route's path</span>
{{/if}}
{{#if isNotActivePath regex='^\\/products'}}
  <span>Show only if current route's path doesn't begin with '/products'</span>
{{/if}}

<li class="{{isNotActivePath class='is-disabled' path='/home'}}">...</li>
<li class="{{isNotActivePath '/home' class='is-disabled'}}">...</li>
```

#### Arguments
The following can be used by as arguments in `isNotActivePath`, `isNotActiveRoute`, `isActivePath` and `isActiveRoute` helpers.

 - Data context, Optional. `String` or `Object` with `name`, `path` or `regex`
 - `name` {*String*} - Only available for `isActiveRoute` and `isNotActiveRoute`
 - `path` {*String*} - Only available for `isActivePath` and `isNotActivePath`
 - `regex` {*String|RegExp*}


================================================
FILE: docs/helpers/isNotActiveRoute.md
================================================
### `isNotActiveRoute` Template Helper

Template helper to check if the supplied route name doesn't match the currently active route's name.

Returns either a configurable `String`, which defaults to `'disabled'`, or `false`.

```handlebars
<li class="{{isNotActiveRoute 'home'}}">...</li>
<li class="{{isNotActiveRoute name='home'}}">...</li>
<li class="{{isNotActiveRoute regex='home|dashboard'}}">...</li>
{{#if isNotActiveRoute 'home'}}
  <span>Show only if 'home' isn't the current route's name</span>
{{/if}}
{{#if isNotActiveRoute regex='^products'}}
  <span>
    Show only if the current route's name doesn't begin with 'products'
  </span>
{{/if}}

<li class="{{isNotActiveRoute class='is-disabled' name='home'}}">...</li>
<li class="{{isNotActiveRoute 'home' class='is-disabled'}}">...</li>
```

#### Arguments
The following can be used by as arguments in `isNotActivePath`, `isNotActiveRoute`, `isActivePath` and `isActiveRoute` helpers.

 - Data context, Optional. `String` or `Object` with `name`, `path` or `regex`
 - `name` {*String*} - Only available for `isActiveRoute` and `isNotActiveRoute`
 - `path` {*String*} - Only available for `isActivePath` and `isNotActivePath`
 - `regex` {*String|RegExp*}


================================================
FILE: docs/helpers/param.md
================================================
### `param` Template Helper

Returns the value for a URL parameter

```handlebars
<div>ID of this post is <em>{{param 'id'}}</em></div>
```


================================================
FILE: docs/helpers/pathFor.md
================================================
### `pathFor` Template Helper

Used to build a path to your route. First parameter can be either the path definition or name you assigned to the route. After that you can pass the params needed to construct the path. Query parameters can be passed with the `query` parameter. Hash is supported via `hash` parameter.

```handlebars
<a href="{{pathFor '/post/:id' id=_id}}">Link to post</a>
<a href="{{pathFor 'postRouteName' id=_id}}">Link to post</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id cid=comment._id}}">Link to comment in post</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id hash=comment._id}}">Jump to comment</a>
<a href="{{pathFor '/post/:id/comments/:cid' id=_id cid=comment._id query='back=yes&more=true'}}">Link to comment in post with query params</a>
```

Same-route hash links use browser fragment behavior. FlowRouter does not run route hooks or actions when only the hash changes; handle fragment-specific UI with `hashchange`.

```js
window.addEventListener('hashchange', () => {
  const commentId = window.location.hash.slice(1);
  document.getElementById(commentId)?.scrollIntoView();
});
```


================================================
FILE: docs/helpers/queryParam.md
================================================
### `queryParam` Template Helper

Returns the value for a query parameter

```handlebars
<input placeholder="Search" value="{{queryParam 'query'}}">
```


================================================
FILE: docs/helpers/urlFor.md
================================================
### `urlFor` Template Helper

Used to build an absolute URL to your route. First parameter can be either the path definition or name you assigned to the route. After that you can pass the params needed to construct the URL. Query parameters can be passed with the `query` parameter. Hash is supported via `hash` parameter.

```handlebars
<a href="{{urlFor '/post/:id' id=_id}}">Link to post</a>
<a href="{{urlFor 'postRouteName' id=_id}}">Link to post</a>
<a hre
Download .txt
gitextract_0qjsbjv_/

├── .agents/
│   └── skills/
│       └── meteor-flow-router/
│           ├── SKILL.md
│           └── reference.md
├── .cursorignore
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   ├── PULL_REQUEST_TEMPLATE
│   └── workflows/
│       └── test_suite.yml
├── .gitignore
├── .meteorignore
├── .versions
├── AGENTS.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── HISTORY.md
├── LICENSE
├── ORIGINAL FlowRouter LICENSE
├── README.md
├── client/
│   ├── _init.js
│   ├── active.route.js
│   ├── group.js
│   ├── modules.js
│   ├── renderer.js
│   ├── route.js
│   ├── router.js
│   └── triggers.js
├── docs/
│   ├── README.md
│   ├── api/
│   │   ├── README.md
│   │   ├── current.md
│   │   ├── decodeQueryParamsOnce.md
│   │   ├── getParam.md
│   │   ├── getQueryParam.md
│   │   ├── getRouteName.md
│   │   ├── go.md
│   │   ├── group.md
│   │   ├── initialize.md
│   │   ├── onRouteRegister.md
│   │   ├── path.md
│   │   ├── pathRegExp.md
│   │   ├── refresh.md
│   │   ├── reload.md
│   │   ├── render.md
│   │   ├── route.md
│   │   ├── setParams.md
│   │   ├── setQueryParams.md
│   │   ├── triggers.md
│   │   ├── url.md
│   │   ├── wait.md
│   │   ├── watchPathChange.md
│   │   └── withReplaceState.md
│   ├── auto-scroll.md
│   ├── fast-render-integration.md
│   ├── full.md
│   ├── helpers/
│   │   ├── README.md
│   │   ├── RouterHelpers.md
│   │   ├── currentRouteName.md
│   │   ├── currentRouteOption.md
│   │   ├── isActivePath.md
│   │   ├── isActiveRoute.md
│   │   ├── isNotActivePath.md
│   │   ├── isNotActiveRoute.md
│   │   ├── param.md
│   │   ├── pathFor.md
│   │   ├── queryParam.md
│   │   └── urlFor.md
│   ├── hooks/
│   │   ├── README.md
│   │   ├── action.md
│   │   ├── data.md
│   │   ├── endWaiting.md
│   │   ├── onNoData.md
│   │   ├── triggersEnter.md
│   │   ├── triggersExit.md
│   │   ├── waitOn.md
│   │   ├── waitOnResources.md
│   │   └── whileWaiting.md
│   ├── original-readme.md
│   ├── quick-start.md
│   ├── react.md
│   ├── templating-with-data.md
│   ├── templating-with-regions.md
│   └── templating.md
├── index.d.ts
├── index.test-d.ts
├── lib/
│   ├── _helpers.js
│   ├── constants.js
│   ├── group-base.js
│   ├── micro-router.js
│   ├── qs.js
│   ├── route-base.js
│   └── router-base.js
├── package-types.json
├── package.js
├── package.json
├── server/
│   ├── _init.js
│   ├── group.js
│   ├── plugins/
│   │   └── fast-render.js
│   ├── route.js
│   └── router.js
├── test/
│   ├── client/
│   │   ├── _helpers.js
│   │   ├── group.spec.js
│   │   ├── loader.spec.js
│   │   ├── route.reactivity.spec.js
│   │   ├── router.core.spec.js
│   │   ├── router.reactivity.spec.js
│   │   ├── router.subs_ready.spec.js
│   │   ├── trigger.spec.js
│   │   └── triggers.js
│   ├── common/
│   │   ├── fast_render_route.js
│   │   ├── group.spec.js
│   │   ├── route.spec.js
│   │   ├── router.addons.spec.js
│   │   ├── router.path.spec.js
│   │   └── router.url.spec.js
│   └── server/
│       ├── _helpers.js
│       └── plugins/
│           └── fast_render.js
└── tsconfig.json
Download .txt
SYMBOL INDEX (222 symbols across 20 files)

FILE: client/active.route.js
  method config (line 70) | config() {
  method configure (line 73) | configure(options) {
  method name (line 78) | name(routeName, routeParams = {}) {
  method path (line 97) | path(path) {

FILE: client/renderer.js
  class BlazeRenderer (line 21) | class BlazeRenderer {
    method constructor (line 22) | constructor(opts = {}) {
    method render (line 74) | render(__layout, __template = false, __data = {}, __callback) {
    method startQueue (line 89) | startQueue() {
    method proceed (line 104) | proceed(__layout, __template = false, __data = {}, __callback) {
    method _render (line 227) | _render(current) {
    method _load (line 253) | _load(updateTemplate, updateLayout, current) {
    method newElement (line 281) | newElement(type, current) {
    method newState (line 295) | newState(layout = false, template = false) {
    method materialize (line 332) | materialize(current) {

FILE: client/route.js
  class Route (line 19) | class Route {
    method constructor (line 20) | constructor(router = new Router(), pathDef, options = {}, group) {
    method clearSubscriptions (line 58) | clearSubscriptions() {
    method register (line 62) | register(name, sub) {
    method getSubscription (line 66) | getSubscription(name) {
    method getAllSubscriptions (line 70) | getAllSubscriptions() {
    method checkSubscriptions (line 74) | checkSubscriptions(subscriptions) {
    method waitOn (line 83) | async waitOn(current = {}, next) {
    method callAction (line 440) | async callAction(current) {
    method callSubscriptions (line 453) | callSubscriptions(current) {
    method getRouteName (line 462) | getRouteName() {
    method getParam (line 467) | getParam(key) {
    method getQueryParam (line 472) | getQueryParam(key) {
    method watchPathChange (line 477) | watchPathChange() {
    method registerRouteClose (line 481) | registerRouteClose() {
    method registerRouteChange (line 488) | registerRouteChange(currentContext, routeChanging) {
    method _updateReactiveDict (line 506) | _updateReactiveDict(dict, newValues) {

FILE: client/router.js
  class Router (line 10) | class Router {
    method constructor (line 11) | constructor() {
    method notFound (line 70) | set notFound(opts) {
    method notFound (line 76) | get notFound() {
    method route (line 80) | route(pathDef, options = {}, group) {
    method group (line 138) | group(options) {
    method path (line 142) | path(_pathDef, fields = {}, _queryParams = {}) {
    method go (line 201) | go(pathDef, fields, queryParams) {
    method reload (line 225) | reload() {
    method redirect (line 231) | redirect(path) {
    method _stripBase (line 238) | _stripBase(path) {
    method setParams (line 247) | setParams(newParams) {
    method setQueryParams (line 263) | setQueryParams(newParams) {
    method current (line 279) | current() {
    method track (line 286) | track(reactiveMapper) {
    method mapper (line 304) | mapper(props, onData, env) {
    method trackMapper (line 310) | trackMapper() {
    method subsReady (line 314) | subsReady() {
    method withReplaceState (line 357) | withReplaceState(fn) {
    method withTrailingSlash (line 361) | withTrailingSlash(fn) {
    method initialize (line 365) | initialize(options = {}) {
    method wait (line 385) | wait() {
    method onRouteRegister (line 392) | onRouteRegister(cb) {
    method _triggerRouteRegister (line 396) | _triggerRouteRegister(currentRoute) {
    method url (line 407) | url() {
    method _buildTracker (line 416) | _buildTracker() {
    method _invalidateTracker (line 462) | _invalidateTracker() {
    method _updateCallbacks (line 488) | _updateCallbacks() {
    method _initTriggersAPI (line 506) | _initTriggersAPI() {

FILE: index.d.ts
  type Trigger (line 5) | type Trigger = (context: ReturnType<Router["current"]>, redirect: Router...
  type TriggerFilterParam (line 7) | type TriggerFilterParam = { only: string[] } | { except: string[] };
  type DynamicImport (line 9) | type DynamicImport = Promise<string>;
  type QueryValue (line 11) | type QueryValue = string | number | boolean | null | undefined | QueryPa...
  type QueryParams (line 12) | type QueryParams = {
  type Hook (line 16) | type Hook = (params: Param, queryParams: QueryParams) => void | Promise<...
  type waitOn (line 18) | type waitOn = (
  type waitOnResources (line 31) | type waitOnResources = (
  type data (line 39) | type data = (params: Param, queryParams: QueryParams) => Mongo.CursorSta...
  type action (line 41) | type action = (params: Param, queryParams: QueryParams, data: any) => void;
  type Param (line 43) | type Param = {
  type NewParams (line 47) | type NewParams = {
  type Router (line 51) | interface Router {
  type Route (line 121) | interface Route {
  type RouterConstructor (line 133) | interface RouterConstructor {
  type RouteConstructor (line 138) | interface RouteConstructor {
  type GroupConstructor (line 143) | interface GroupConstructor {
  type FlowRouterSingleton (line 151) | type FlowRouterSingleton = Router & {
  type Context (line 156) | type Context = {
  type Helpers (line 167) | interface Helpers {

FILE: lib/_helpers.js
  method isEmpty (line 4) | isEmpty(obj) { // 1
  method isObject (line 15) | isObject(obj) {
  method omit (line 19) | omit(obj, keys) { // 10
  method pick (line 37) | pick(obj, keys) { // 2
  method isArray (line 55) | isArray(obj) {
  method extend (line 58) | extend(...objs) { // 4
  method clone (line 61) | clone(obj) {

FILE: lib/constants.js
  constant MAX_WAIT_FOR_MS (line 2) | const MAX_WAIT_FOR_MS = 120000;

FILE: lib/group-base.js
  class GroupBase (line 26) | class GroupBase {
    method constructor (line 27) | constructor(router, options = {}, parent) {
    method route (line 52) | route(_pathDef, options = {}, _group) {
    method group (line 74) | group(options) {
    method callSubscriptions (line 78) | callSubscriptions(current) {

FILE: lib/micro-router.js
  function pathToRegExp (line 28) | function pathToRegExp(pathDef) {
  function matchPath (line 59) | function matchPath(compiledRoute, path) {
  class MicroRouter (line 75) | class MicroRouter {
    method constructor (line 76) | constructor() {
    method base (line 91) | base(path) {
    method route (line 98) | route(pathDef, handler) {
    method exit (line 106) | exit(pathDef, handler) {
    method reset (line 114) | reset() {
    method start (line 122) | start(options = {}) {
    method stop (line 145) | stop() {
    method show (line 155) | show(path, state, dispatch, push) {
    method replace (line 175) | replace(path, state, dispatch) {
    method redirect (line 194) | redirect(path) {
    method _getPath (line 202) | _getPath() {
    method _createContext (line 214) | _createContext(fullPath) {
    method _isHashOnlyChange (line 234) | _isHashOnlyChange(path) {
    method _dispatch (line 242) | _dispatch(ctx) {
    method _runExits (line 259) | _runExits(newCtx, callback) {
    method _pushState (line 286) | _pushState(ctx, state) {
    method _replaceState (line 295) | _replaceState(ctx, state) {
    method _onPopState (line 304) | _onPopState() {
    method _onClick (line 314) | _onClick(event) {

FILE: lib/qs.js
  method parse (line 178) | parse(query = '') {
  method stringify (line 209) | stringify(queryParams = {}) {
  method merge (line 222) | merge(baseQueryParams = {}, queryParams = {}) {

FILE: lib/route-base.js
  class RouteBase (line 1) | class RouteBase {
    method constructor (line 2) | constructor(router, pathDef, options = {}, group) {
    method clearSubscriptions (line 16) | clearSubscriptions() {
    method register (line 20) | register(name, sub) {
    method getSubscription (line 24) | getSubscription(name) {
    method getAllSubscriptions (line 28) | getAllSubscriptions() {
    method callSubscriptions (line 32) | callSubscriptions(current) {

FILE: lib/router-base.js
  class RouterBase (line 5) | class RouterBase {
    method constructor (line 6) | constructor() {
    method path (line 40) | path(_pathDef, fields = {}, _queryParams = {}) {
    method url (line 99) | url() {
    method group (line 108) | group(options) {
    method onRouteRegister (line 113) | onRouteRegister(cb) {
    method _triggerRouteRegister (line 117) | _triggerRouteRegister(currentRoute) {
    method _getGroupClass (line 129) | _getGroupClass() {
    method go (line 134) | go()             {}
    method setParams (line 135) | setParams()      {}
    method setQueryParams (line 136) | setQueryParams() {}
    method initialize (line 137) | initialize()     {}
    method wait (line 138) | wait()           {}
    method getRouteName (line 139) | getRouteName()   {}
    method getParam (line 140) | getParam()       {}
    method getQueryParam (line 141) | getQueryParam()  {}
    method watchPathChange (line 142) | watchPathChange() {}
    method current (line 144) | current() {

FILE: server/router.js
  class Router (line 7) | class Router extends RouterBase {
    method constructor (line 8) | constructor() {
    method _getGroupClass (line 14) | _getGroupClass() {
    method matchPath (line 18) | matchPath(path) {
    method setCurrent (line 35) | setCurrent(current) {
    method route (line 39) | route(pathDef, options = {}, group) {

FILE: test/client/group.spec.js
  method action (line 27) | action() {
  method action (line 46) | action() {
  method waitOn (line 65) | async waitOn() {
  method action (line 77) | action() {
  method subscriptions (line 97) | subscriptions() {

FILE: test/client/route.reactivity.spec.js
  function checkAfterNormalRouteChange (line 121) | function checkAfterNormalRouteChange() {
  function checkAfterLastRouteChange (line 128) | function checkAfterLastRouteChange() {
  function checkAfterRouteClose (line 149) | function checkAfterRouteClose() {

FILE: test/client/router.core.spec.js
  method action (line 11) | action() {
  method action (line 30) | action() {
  method action (line 54) | action() {
  method action (line 85) | action() {
  method action (line 117) | action() {
  method ready (line 146) | ready() {
  method waitOn (line 152) | async waitOn() {
  method action (line 166) | action() {
  method ready (line 190) | ready() { return false; }
  method waitOn (line 195) | waitOn() {
  method action (line 198) | action() {
  method action (line 204) | action() {
  method waitOn (line 230) | waitOn() {
  method action (line 233) | action() {
  method waitOn (line 251) | waitOn() {
  method action (line 254) | action() {
  method action (line 271) | action() {
  method action (line 299) | action() {
  method action (line 327) | action() {
  method action (line 355) | action() {
  method action (line 384) | action(params) {
  method action (line 404) | action(params) {
  method action (line 424) | action(_params) {
  method action (line 446) | action() {
  method action (line 453) | action() {
  method action (line 475) | action(params) {
  method action (line 511) | action(params) {
  function validate (line 524) | function validate() {
  function validate (line 553) | function validate() {
  method action (line 578) | action(params, queryParams) {
  function validate (line 608) | function validate() {
  method action (line 621) | action(params, queryParams) {
  function validate (line 633) | function validate() {
  method action (line 646) | action(params, queryParams) {
  function validate (line 658) | function validate() {
  method action (line 672) | action(params, queryParams) {
  function validate (line 686) | function validate() {
  method subscriptions (line 710) | subscriptions() {
  method action (line 713) | action() {
  method action (line 737) | action(params) {
  method action (line 765) | action(params) {
  method action (line 781) | action() {
  method action (line 803) | action() {
  method action (line 860) | action() {
  method action (line 942) | action() {}
  method action (line 957) | action() {
  function setBasePath (line 988) | function setBasePath(path) {
  function resetBasePath (line 995) | function resetBasePath() {

FILE: test/client/trigger.spec.js
  method action (line 122) | action() {
  method action (line 128) | action() {
  method action (line 200) | action() {
  method action (line 207) | action() {

FILE: test/client/triggers.js
  function MakeTriggers (line 286) | function MakeTriggers(count, store) {

FILE: test/common/fast_render_route.js
  method subscriptions (line 8) | subscriptions() {
  method waitOn (line 14) | waitOn() {
  method subscriptions (line 22) | subscriptions(params, queryParams) {
  method waitOn (line 28) | waitOn(params, queryParams) {
  method subscriptions (line 36) | subscriptions() {
  method waitOn (line 42) | waitOn() {
  method subscriptions (line 54) | subscriptions() {
  method waitOn (line 60) | waitOn() {

FILE: test/common/router.addons.spec.js
  method action (line 24) | action() {}
  method subscriptions (line 25) | subscriptions() {}
  method triggersEnter (line 26) | triggersEnter() {}
  method triggersExit (line 27) | triggersExit() {}
Condensed preview — 116 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (390K chars).
[
  {
    "path": ".agents/skills/meteor-flow-router/SKILL.md",
    "chars": 12317,
    "preview": "---\nname: meteor-flow-router\ndescription: Guides Meteor routing with ostrio:flow-router-extra plus head/title companions"
  },
  {
    "path": ".agents/skills/meteor-flow-router/reference.md",
    "chars": 656,
    "preview": "# Canonical `AGENTS.md` sources\n\nRead these for full upstream wording, file-level maps, and maintainer notes.\n\n| Package"
  },
  {
    "path": ".cursorignore",
    "chars": 159,
    "preview": ".build*\n.DS_Store\n.eslintcache\n.npm\n.github\nnode_modules\nCHANGELOG.md\nCODE_OF_CONDUCT.md\nCONTRIBUTING.md\nHISTORY.md\nLICE"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 104,
    "preview": "# These are supported funding model platforms\n\ngithub: dr-dimitru\ncustom: https://paypal.me/veliovgroup\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE",
    "chars": 1074,
    "preview": "### I'm having an issue:\n  - Give an expressive description of what is went wrong\n  - Version of `flow-router-extra` you"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE",
    "chars": 521,
    "preview": "Thank you for contribution. Before you go:\n  1. Make sure you're using `spaces` for indentation\n  2. Make sure all new c"
  },
  {
    "path": ".github/workflows/test_suite.yml",
    "chars": 915,
    "preview": "name: Test suite\n\n# run ci on direct pushes to master\n# or on any pull request update\non:\n  push:\n    branches:\n      - "
  },
  {
    "path": ".gitignore",
    "chars": 119,
    "preview": "*.browserify.js.cached\n*.browserify.js.map\n.build*\n.DS_Store\n.eslintcache\n.npm\n/client/tmp\nnode_modules\n/.cursor/hooks\n"
  },
  {
    "path": ".meteorignore",
    "chars": 281,
    "preview": ".github\n.agents/\n.cursorignore\n.gitignore\n.npm\nnode_modules/\n.DS_Store\n.eslintcache\n.eslintrc\nAGENTS.md\nCHANGELOG.md\nCOD"
  },
  {
    "path": ".versions",
    "chars": 1062,
    "preview": "allow-deny@2.1.0\nbabel-compiler@7.14.0\nbabel-runtime@1.5.2\nbase64@1.0.13\nbinary-heap@1.0.12\nboilerplate-generator@2.1.0\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 16064,
    "preview": "# Agent notes: `ostrio:flow-router-extra`\n\nUse when **editing this repo**, **shipping Atmosphere releases**, or **implem"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 110,
    "preview": "For full package history, please see [releases on GitHub](https://github.com/veliovgroup/flow-router/releases)"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3217,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1657,
    "preview": "### I'm having an issue:\n 1. Search [issues](https://github.com/veliovgroup/flow-router/issues?utf8=✓&q=is%3Aissue), may"
  },
  {
    "path": "HISTORY.md",
    "chars": 110,
    "preview": "For full package history, please see [releases on GitHub](https://github.com/veliovgroup/flow-router/releases)"
  },
  {
    "path": "LICENSE",
    "chars": 1531,
    "preview": "Copyright (c) 2026, dr.dimitru (Dmitry A.; Veliov Group, LLC)\nAll rights reserved.\n\nRedistribution and use in source and"
  },
  {
    "path": "ORIGINAL FlowRouter LICENSE",
    "chars": 1122,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 MeteorHacks Pvt Ltd (Sri Lanka). <hello@meteorhacks.com>\n\nPermission is hereby"
  },
  {
    "path": "README.md",
    "chars": 8788,
    "preview": "[![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru)\n[![support](http"
  },
  {
    "path": "client/_init.js",
    "chars": 2429,
    "preview": "import { Meteor }    from 'meteor/meteor';\nimport Router        from './router.js';\nimport Route         from './route.j"
  },
  {
    "path": "client/active.route.js",
    "chars": 8464,
    "preview": "import { Meteor }       from 'meteor/meteor';\nimport { _helpers }     from './../lib/_helpers.js';\nimport { check, Match"
  },
  {
    "path": "client/group.js",
    "chars": 77,
    "preview": "import { GroupBase } from '../lib/group-base.js';\n\nexport default GroupBase;\n"
  },
  {
    "path": "client/modules.js",
    "chars": 254,
    "preview": "const requestAnimFrame = (() => {\n  return window.requestAnimationFrame\n    || window.webkitRequestAnimationFrame\n    ||"
  },
  {
    "path": "client/renderer.js",
    "chars": 11020,
    "preview": "import { Meteor } from 'meteor/meteor';\nimport { _helpers } from './../lib/_helpers.js';\nimport { requestAnimFrame } fro"
  },
  {
    "path": "client/route.js",
    "chars": 15617,
    "preview": "import { Router }       from './_init.js';\nimport { Meteor }       from 'meteor/meteor';\nimport { Promise }      from 'm"
  },
  {
    "path": "client/router.js",
    "chars": 14388,
    "preview": "import { FlowRouter, Route, Group, Triggers, BlazeRenderer } from './_init.js';\nimport { EJSON }    from 'meteor/ejson';"
  },
  {
    "path": "client/triggers.js",
    "chars": 3012,
    "preview": "// a set of utility functions for triggers\n\nconst Triggers = {};\n\n// Apply filters for a set of triggers\n// @triggers - "
  },
  {
    "path": "docs/README.md",
    "chars": 9617,
    "preview": "# Flow-Router Extra Docs Index\n\nClient routing for [Meteor.js apps](https://docs.meteor.com/?utm_source=dr.dimitru&utm_m"
  },
  {
    "path": "docs/api/README.md",
    "chars": 2483,
    "preview": "# API\n\n- __General Methods:__\n  - [`.go()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/go.md"
  },
  {
    "path": "docs/api/current.md",
    "chars": 781,
    "preview": "### current method\n\n```js\nFlowRouter.current();\n```\n\n- Returns {*Object*}\n\nGet the current state of the router. **This A"
  },
  {
    "path": "docs/api/decodeQueryParamsOnce.md",
    "chars": 1888,
    "preview": "### decodeQueryParamsOnce option\n\nThe current behavior of `FlowRouter.getQueryParam(\"...\")` and `FlowRouter.current().qu"
  },
  {
    "path": "docs/api/getParam.md",
    "chars": 353,
    "preview": "### getParam method\n\n```js\nFlowRouter.getParam(paramName);\n```\n\n- `paramName` {*String*}\n- Returns {*String*}\n\nReactive "
  },
  {
    "path": "docs/api/getQueryParam.md",
    "chars": 365,
    "preview": "### getQueryParam method\n\n```js\nFlowRouter.getQueryParam(queryKey);\n```\n\n- `queryKey` {*String*}\n- Returns {*String*}\n\nR"
  },
  {
    "path": "docs/api/getRouteName.md",
    "chars": 412,
    "preview": "### getRouteName method\n\n```js\nFlowRouter.getRouteName();\n```\n\n- Returns {*String*}\n\nUse to get the name of the route re"
  },
  {
    "path": "docs/api/go.md",
    "chars": 1487,
    "preview": "### go method\n\n`.go(path, params, queryParams)`\n\n- `path` {*String*} - Path or Route's name\n- `params` {*Object*} - Seri"
  },
  {
    "path": "docs/api/group.md",
    "chars": 2001,
    "preview": "### group method\n\nUse group routes for better route organization.\n\n`.group(options)`\n\n- `options` {*Object*} - [Optional"
  },
  {
    "path": "docs/api/initialize.md",
    "chars": 1412,
    "preview": "### initialize method\n\n```js\nFlowRouter.initialize(options);\n```\n\n- `options` {*Object*}\n  - `hashbang` {*Boolean*} - En"
  },
  {
    "path": "docs/api/onRouteRegister.md",
    "chars": 422,
    "preview": "### onRouteRegister method\n\n```js\nFlowRouter.onRouteRegister(callback);\n```\n\n- `callback` {*Function*}\n- Returns {*void*"
  },
  {
    "path": "docs/api/path.md",
    "chars": 759,
    "preview": "### path method\n\n```js\nFlowRouter.path(path, params, queryParams);\n```\n\n- `path` {*String*} - Path or Route's name\n- `pa"
  },
  {
    "path": "docs/api/pathRegExp.md",
    "chars": 609,
    "preview": "### pathRegExp option\n\n```js\n// Use dashes as separators so `/:id-:slug/` isn't translated to `id-:slug` but to `:id`-`:"
  },
  {
    "path": "docs/api/refresh.md",
    "chars": 1617,
    "preview": "### refresh method\n\n```js\nFlowRouter.refresh('layout', 'template');\n```\n\n- `layout` {*String*} - [required] Name of the "
  },
  {
    "path": "docs/api/reload.md",
    "chars": 467,
    "preview": "### reload method\n\n```js\nFlowRouter.reload();\n```\n\n- Returns {*void*}\n\nFlowRouter routes are idempotent. That means, eve"
  },
  {
    "path": "docs/api/render.md",
    "chars": 3957,
    "preview": "### render method\n\n`this.render()` method is available only [inside hooks](https://github.com/veliovgroup/flow-router/tr"
  },
  {
    "path": "docs/api/route.md",
    "chars": 1078,
    "preview": "### route method\n\n```js\nFlowRouter.route(path, options);\n```\n\n- `path` {*String*} - Path with placeholders\n- `options` {"
  },
  {
    "path": "docs/api/setParams.md",
    "chars": 816,
    "preview": "### setParams method\n\n```js\nFlowRouter.setParams(params);\n```\n\n- `params` {*Object*} - Serialized route parameters, `{ _"
  },
  {
    "path": "docs/api/setQueryParams.md",
    "chars": 633,
    "preview": "### setQueryParams method\n\n```js\nFlowRouter.setQueryParams(queryParams);\n```\n\n- `queryParams` {*Object*} - Query params "
  },
  {
    "path": "docs/api/triggers.md",
    "chars": 596,
    "preview": "### Global Triggers\n\n```js\nFlowRouter.triggers.enter([cb1, cb2]);\nFlowRouter.triggers.exit([cb1, cb2]);\n\n// filtering\nFl"
  },
  {
    "path": "docs/api/url.md",
    "chars": 430,
    "preview": "### url method\n\n```js\nFlowRouter.url(path, params, queryParams);\n```\n\n- `path` {*String*} - Path or Route's name\n- `para"
  },
  {
    "path": "docs/api/wait.md",
    "chars": 716,
    "preview": "### wait method\n\n```js\nFlowRouter.wait();\n```\n\n- Returns {*void*}\n\nBy default, FlowRouter initializes the routing proces"
  },
  {
    "path": "docs/api/watchPathChange.md",
    "chars": 842,
    "preview": "### watchPathChange method\n\n```js\nFlowRouter.watchPathChange();\n```\n\n- Returns {*void*}\n\nReactively watch the changes in"
  },
  {
    "path": "docs/api/withReplaceState.md",
    "chars": 1129,
    "preview": "### withReplaceState method\n\n```js\nFlowRouter.withReplaceState(callback);\n```\n\n- `callback` {*Function*}\n- Returns {*voi"
  },
  {
    "path": "docs/auto-scroll.md",
    "chars": 1000,
    "preview": "### Auto-scroll to the top of the page after navigation\n\n*FlowRouter* causes the page to remain at the same scroll posit"
  },
  {
    "path": "docs/fast-render-integration.md",
    "chars": 2149,
    "preview": "### Fast-Render Integration\n\nTo get the most out of Flow-Router Extra and [Fast-Render](https://github.com/abecks/meteor"
  },
  {
    "path": "docs/full.md",
    "chars": 39022,
    "preview": "# Docs as single file\n\n## Quick Start\n\n#### Install\n\n```shell\n# Remove original FlowRouter\nmeteor remove kadira:flow-rou"
  },
  {
    "path": "docs/helpers/README.md",
    "chars": 1528,
    "preview": "# Helpers:\n\n- [`isActiveRoute` template helper](https://github.com/veliovgroup/flow-router/blob/master/docs/helpers/isAc"
  },
  {
    "path": "docs/helpers/RouterHelpers.md",
    "chars": 929,
    "preview": "### RouterHelpers Class\n\nUse template helpers right from JavaScript code.\n\n```js\nimport { RouterHelpers } from 'meteor/o"
  },
  {
    "path": "docs/helpers/currentRouteName.md",
    "chars": 143,
    "preview": "### `currentRouteName` Template Helper\n\nReturns the name of the current route\n\n```handlebars\n<div class={{currentRouteNa"
  },
  {
    "path": "docs/helpers/currentRouteOption.md",
    "chars": 336,
    "preview": "### `currentRouteOption` Template Helper\n\nThis adds support to get options from flow router\n\n```javascript\nFlowRouter.ro"
  },
  {
    "path": "docs/helpers/isActivePath.md",
    "chars": 1160,
    "preview": "### `isActivePath` Template Helper\n\nTemplate helper to check if the supplied path matches the currently active route's p"
  },
  {
    "path": "docs/helpers/isActiveRoute.md",
    "chars": 1168,
    "preview": "### `isActiveRoute` Template Helper\n\nTemplate helper to check if the supplied route name matches the currently active ro"
  },
  {
    "path": "docs/helpers/isNotActivePath.md",
    "chars": 1202,
    "preview": "### `isNotActivePath` Template Helper\n\nTemplate helper to check if the supplied path doesn't match the currently active "
  },
  {
    "path": "docs/helpers/isNotActiveRoute.md",
    "chars": 1218,
    "preview": "### `isNotActiveRoute` Template Helper\n\nTemplate helper to check if the supplied route name doesn't match the currently "
  },
  {
    "path": "docs/helpers/param.md",
    "chars": 140,
    "preview": "### `param` Template Helper\n\nReturns the value for a URL parameter\n\n```handlebars\n<div>ID of this post is <em>{{param 'i"
  },
  {
    "path": "docs/helpers/pathFor.md",
    "chars": 1134,
    "preview": "### `pathFor` Template Helper\n\nUsed to build a path to your route. First parameter can be either the path definition or "
  },
  {
    "path": "docs/helpers/queryParam.md",
    "chars": 153,
    "preview": "### `queryParam` Template Helper\n\nReturns the value for a query parameter\n\n```handlebars\n<input placeholder=\"Search\" val"
  },
  {
    "path": "docs/helpers/urlFor.md",
    "chars": 792,
    "preview": "### `urlFor` Template Helper\n\nUsed to build an absolute URL to your route. First parameter can be either the path defini"
  },
  {
    "path": "docs/hooks/README.md",
    "chars": 1002,
    "preview": "# Hooks\n\n*Hooks below are listed in execution order*\n\n- [`.whileWaiting()` hook](https://github.com/veliovgroup/flow-rou"
  },
  {
    "path": "docs/hooks/action.md",
    "chars": 954,
    "preview": "### action hook\n\n`action(params, queryParams, data)`\n\n- `params` {*Object*} - Serialized route parameters, `/route/:_id "
  },
  {
    "path": "docs/hooks/data.md",
    "chars": 2337,
    "preview": "### data hook\n\n`data(params, queryParams)`\n\n- `params` {*Object*} - Serialized route parameters, `/route/:_id => { _id: "
  },
  {
    "path": "docs/hooks/endWaiting.md",
    "chars": 432,
    "preview": "### endWaiting hook\n\n`endWaiting()` - Called with no arguments\n\n- Return: {*void*}\n\n`.endWaiting()` hook is triggered ri"
  },
  {
    "path": "docs/hooks/onNoData.md",
    "chars": 899,
    "preview": "### onNoData hook\n\n`onNoData(params, queryParams)`\n\n- `params` {*Object*} - Serialized route parameters, `/route/:_id =>"
  },
  {
    "path": "docs/hooks/triggersEnter.md",
    "chars": 1563,
    "preview": "### triggersEnter\n\n`triggersEnter` is option (*not actually a hook*), it accepts array of *Function*s, each function wil"
  },
  {
    "path": "docs/hooks/triggersExit.md",
    "chars": 1269,
    "preview": "### triggersExit hooks\n\n`triggersExit` is option (*not actually a hook*), it accepts array of *Function*s, each function"
  },
  {
    "path": "docs/hooks/waitOn.md",
    "chars": 4797,
    "preview": "### waitOn hook\n\n`waitOn(params, queryParams, ready)`\n\n- `params` {*Object*} - Serialized route parameters, `/route/:_id"
  },
  {
    "path": "docs/hooks/waitOnResources.md",
    "chars": 2254,
    "preview": "### waitOnResources hook\n\n`waitOnResources(params, queryParams)`\n\n- `params` {*Object*} - Serialized route parameters, `"
  },
  {
    "path": "docs/hooks/whileWaiting.md",
    "chars": 797,
    "preview": "### whileWaiting hook\n\n`whileWaiting(params, queryParams)`\n\n- `params` {*Object*} - Serialized route parameters, `/route"
  },
  {
    "path": "docs/original-readme.md",
    "chars": 25986,
    "preview": "## Meteor Routing Guide\n\n[Meteor Routing Guide](https://kadira.io/academy/meteor-routing-guide) is a completed guide int"
  },
  {
    "path": "docs/quick-start.md",
    "chars": 3906,
    "preview": "### Quick Start\n\nLearn how to create routes and pull data from Method or Subscription\n\n#### Install\n\n```shell\n# Remove o"
  },
  {
    "path": "docs/react.md",
    "chars": 681,
    "preview": "### React + react-mounter\n\nUse flow router with beloved `React` library. For more info read docs of [`react-mounter`](ht"
  },
  {
    "path": "docs/templating-with-data.md",
    "chars": 3165,
    "preview": "### Templating with Data\n\n> [!NOTE]\n> Blaze templating is available only if application has `templating` and `blaze`, or"
  },
  {
    "path": "docs/templating-with-regions.md",
    "chars": 2331,
    "preview": "### Templating with \"Regions\"\n\n> [!NOTE]\n> Blaze templating is available only if application has `templating` and `blaze"
  },
  {
    "path": "docs/templating.md",
    "chars": 3722,
    "preview": "### Templating\n\n> [!NOTE]\n> Blaze templating is available only if application has `templating` and `blaze`, or `blaze-ht"
  },
  {
    "path": "index.d.ts",
    "chars": 6596,
    "preview": "import type { Meteor } from \"meteor/meteor\";\nimport type { Mongo } from \"meteor/mongo\";\nimport type { Tracker } from \"me"
  },
  {
    "path": "index.test-d.ts",
    "chars": 2382,
    "preview": "/* eslint-disable no-new -- tsd exercises constructor signatures */\nimport { expectAssignable, expectError, expectType }"
  },
  {
    "path": "lib/_helpers.js",
    "chars": 1909,
    "preview": "import { Meteor } from 'meteor/meteor';\n\nconst _helpers = {\n  isEmpty(obj) { // 1\n    if (obj == null) {\n      return tr"
  },
  {
    "path": "lib/constants.js",
    "chars": 124,
    "preview": "/** Default for `FlowRouter.maxWaitFor` and per-route `maxWaitFor` fallback (ms). */\nexport const MAX_WAIT_FOR_MS = 1200"
  },
  {
    "path": "lib/group-base.js",
    "chars": 2521,
    "preview": "import { _helpers } from './_helpers.js';\n\nconst makeTrigger = (trigger) => {\n  if (_helpers.isFunction(trigger)) {\n    "
  },
  {
    "path": "lib/micro-router.js",
    "chars": 8806,
    "preview": "/**\n * MicroRouter — A minimal client-side router replacing page.js.\n *\n * Features:\n * - Path matching via path-to-rege"
  },
  {
    "path": "lib/qs.js",
    "chars": 5510,
    "preview": "import { _helpers } from './_helpers.js';\n\nconst decodeQueryPart = (value = '') => {\n  try {\n    return decodeURICompone"
  },
  {
    "path": "lib/route-base.js",
    "chars": 1018,
    "preview": "class RouteBase {\n  constructor(router, pathDef, options = {}, group) {\n    this.options      = options;\n    this.pathDe"
  },
  {
    "path": "lib/router-base.js",
    "chars": 3835,
    "preview": "import { Meteor }   from 'meteor/meteor';\nimport { _helpers } from './_helpers.js';\nimport { qs } from './qs.js';\n\nclass"
  },
  {
    "path": "package-types.json",
    "chars": 33,
    "preview": "{\n  \"typesEntry\": \"index.d.ts\"\n}\n"
  },
  {
    "path": "package.js",
    "chars": 2451,
    "preview": "Package.describe({\n  name: 'ostrio:flow-router-extra',\n  summary: 'The router for modern JavaScript apps, with support f"
  },
  {
    "path": "package.json",
    "chars": 433,
    "preview": "{\n  \"name\": \"ostrio-flow-router-extra\",\n  \"private\": true,\n  \"types\": \"index.d.ts\",\n  \"scripts\": {\n    \"test:once\": \"mte"
  },
  {
    "path": "server/_init.js",
    "chars": 1669,
    "preview": "import { Meteor } from 'meteor/meteor';\n\nimport Router from './router.js';\nimport Route from './route.js';\nimport Group "
  },
  {
    "path": "server/group.js",
    "chars": 77,
    "preview": "import { GroupBase } from '../lib/group-base.js';\n\nexport default GroupBase;\n"
  },
  {
    "path": "server/plugins/fast-render.js",
    "chars": 1236,
    "preview": "import { Meteor } from 'meteor/meteor';\nimport { _helpers } from './../../lib/_helpers.js';\nimport { FlowRouter } from '"
  },
  {
    "path": "server/route.js",
    "chars": 73,
    "preview": "import RouteBase from '../lib/route-base.js';\n\nexport default RouteBase;\n"
  },
  {
    "path": "server/router.js",
    "chars": 1483,
    "preview": "import Route                            from './route.js';\nimport Group                            from './group.js';\nim"
  },
  {
    "path": "test/client/_helpers.js",
    "chars": 519,
    "preview": "import { Meteor }     from 'meteor/meteor';\nimport { FlowRouter } from 'meteor/ostrio:flow-router-extra';\n\nPackage['ostr"
  },
  {
    "path": "test/client/group.spec.js",
    "chars": 3449,
    "preview": "import { Meteor }            from 'meteor/meteor';\nimport { Random }            from 'meteor/random';\nimport { GetSub } "
  },
  {
    "path": "test/client/loader.spec.js",
    "chars": 579,
    "preview": "import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.add('Client - import page.js', (test) =>"
  },
  {
    "path": "test/client/route.reactivity.spec.js",
    "chars": 3533,
    "preview": "import { Route }   from 'meteor/ostrio:flow-router-extra';\nimport { Meteor }  from 'meteor/meteor';\nimport { Tracker } f"
  },
  {
    "path": "test/client/router.core.spec.js",
    "chars": 25221,
    "preview": "import { GetSub }     from './_helpers.js';\nimport { Meteor }     from 'meteor/meteor';\nimport { Random }     from 'mete"
  },
  {
    "path": "test/client/router.reactivity.spec.js",
    "chars": 4762,
    "preview": "import { FlowRouter } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.addAsync(\n'Client - Router - Reactivity - detect"
  },
  {
    "path": "test/client/router.subs_ready.spec.js",
    "chars": 6674,
    "preview": "import { FlowRouter } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.addAsync('Client - Router - subsReady - with no "
  },
  {
    "path": "test/client/trigger.spec.js",
    "chars": 16649,
    "preview": "import { FlowRouter, Route } from 'meteor/ostrio:flow-router-extra';\nimport { Random } from 'meteor/random';\n\nTinytest.a"
  },
  {
    "path": "test/client/triggers.js",
    "chars": 6893,
    "preview": "import { Triggers } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.addAsync(\n'Triggers - runTriggers - run all and af"
  },
  {
    "path": "test/common/fast_render_route.js",
    "chars": 1565,
    "preview": "import { Mongo }      from 'meteor/mongo';\nimport { Meteor }     from 'meteor/meteor';\nimport { FlowRouter } from 'meteo"
  },
  {
    "path": "test/common/group.spec.js",
    "chars": 992,
    "preview": "import { Random }     from 'meteor/random';\nimport { FlowRouter } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.add("
  },
  {
    "path": "test/common/route.spec.js",
    "chars": 461,
    "preview": "import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra';\n\nPackage['kadira:flow-router'] = Package['ostrio:f"
  },
  {
    "path": "test/common/router.addons.spec.js",
    "chars": 728,
    "preview": "import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.addAsync('Common - Addons - onRouteRegis"
  },
  {
    "path": "test/common/router.path.spec.js",
    "chars": 7042,
    "preview": "import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.addAsync('Common - Router - validate pat"
  },
  {
    "path": "test/common/router.url.spec.js",
    "chars": 429,
    "preview": "import { Meteor }     from 'meteor/meteor';\nimport { FlowRouter } from 'meteor/ostrio:flow-router-extra';\n\nTinytest.add("
  },
  {
    "path": "test/server/_helpers.js",
    "chars": 1372,
    "preview": "import { HTTP }   from 'meteor/http';\nimport { Meteor } from 'meteor/meteor';\n\nPackage['ostrio:flow-router-extra'].FlowR"
  },
  {
    "path": "test/server/plugins/fast_render.js",
    "chars": 1721,
    "preview": "import { Meteor }         from 'meteor/meteor';\nimport { GetFRData }      from '../_helpers.js';\nimport { FastRenderColl"
  },
  {
    "path": "tsconfig.json",
    "chars": 193,
    "preview": "{\n  \"compilerOptions\": {\n    \"preserveSymlinks\": true,\n    \"paths\": {\n      \"meteor/*\": [\n        \"node_modules/@types/m"
  }
]

About this extraction

This page contains the full source code of the VeliovGroup/flow-router GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 116 files (362.3 KB), approximately 97.8k tokens, and a symbol index with 222 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!