` **`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=""`**.
### Attribute shorthand
- **`meta`:** string → **`name=""`**, **`content=""`**; object spreads attrs.
- **`link`:** string → **`rel=""`**, **`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 **`/// `** 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).
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
================================================
[](https://github.com/sponsors/dr-dimitru)
[](https://paypal.me/veliovgroup)
# 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, `` 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 `` 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 `Loading...` 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
```
```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 `Loading...` 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
{{post.title}}
{{post.text}}
```
#### 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
...
```
-------
### `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
...
```
-------
### `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
...
...
...
{{#if isActivePath '/home'}}
Show only if '/home' is the current route's path
{{/if}}
{{#if isActivePath regex='^\\/products'}}
Show only if current route's path begins with '/products'
{{/if}}
...
...
```
-------
### `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
...
...
...
{{#if isActiveRoute 'home'}}
Show only if 'home' is the current route's name
{{/if}}
{{#if isActiveRoute regex='^products'}}
Show only if the current route's name begins with 'products'
{{/if}}
...
...
```
-------
### `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
...
...
...
{{#if isNotActivePath '/home'}}
Show only if '/home' isn't the current route's path
{{/if}}
{{#if isNotActivePath regex='^\\/products'}}
Show only if current route's path doesn't begin with '/products'
{{/if}}
...
...
```
-------
### `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
...
...
...
{{#if isNotActiveRoute 'home'}}
Show only if 'home' isn't the current route's name
{{/if}}
{{#if isNotActiveRoute regex='^products'}}
Show only if the current route's name doesn't begin with 'products'
{{/if}}
...
...
```
#### 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
ID of this post is {{param 'id'}}
```
-------
### `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
Link to postLink to postLink to comment in postJump to commentLink to comment in post with query params
```
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
```
-------
### `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
Link to postLink to postLink to comment in postJump to commentLink to comment in post with query params
```
================================================
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
...
```
================================================
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
...
```
================================================
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
...
...
...
{{#if isActivePath '/home'}}
Show only if '/home' is the current route's path
{{/if}}
{{#if isActivePath regex='^\\/products'}}
Show only if current route's path begins with '/products'
{{/if}}
...
...
```
#### 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
...
...
...
{{#if isActiveRoute 'home'}}
Show only if 'home' is the current route's name
{{/if}}
{{#if isActiveRoute regex='^products'}}
Show only if the current route's name begins with 'products'
{{/if}}
...
...
```
#### 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
...
...
...
{{#if isNotActivePath '/home'}}
Show only if '/home' isn't the current route's path
{{/if}}
{{#if isNotActivePath regex='^\\/products'}}
Show only if current route's path doesn't begin with '/products'
{{/if}}
...
...
```
#### 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
...
...
...
{{#if isNotActiveRoute 'home'}}
Show only if 'home' isn't the current route's name
{{/if}}
{{#if isNotActiveRoute regex='^products'}}
Show only if the current route's name doesn't begin with 'products'
{{/if}}
...
...
```
#### 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
ID of this post is {{param 'id'}}
```
================================================
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
Link to postLink to postLink to comment in postJump to commentLink to comment in post with query params
```
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
```
================================================
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
Link to postLink to postLink to comment in postJump to commentLink to comment in post with query params
```
================================================
FILE: docs/hooks/README.md
================================================
# Hooks
*Hooks below are listed 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)
================================================
FILE: docs/hooks/action.md
================================================
### 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)
================================================
FILE: 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. __This hook can be async__
```js
// USE data() HOOK WITH PUBLISH / SUBSCRIBE
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 });
}
});
// USE data() HOOK WITH METEOR METHOD
FlowRouter.route('/post/:_id', {
name: 'post',
async data(params, queryParams) {
return await Meteor.callAsync('post.get', 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
{{post.title}}
{{post.text}}
```
#### 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
```js
FlowRouter.route('/post/:_id', {
name: 'post',
async data(params) {
return await PostsCollection.findOneAsync({ _id: params._id });
},
triggersEnter: [(context, redirect, stop, data) => {
console.log(data);
}]
});
```
#### Further reading
- [`.triggersEnter()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
- [`.onNoData()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/onNoData.md)
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [`.render()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/render.md)
- [Templating with Data](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-data.md)
================================================
FILE: docs/hooks/endWaiting.md
================================================
### endWaiting hook
`endWaiting()` - Called with no arguments
- Return: {*void*}
`.endWaiting()` hook is triggered right after all resources in `.waitOn()` and `.waitOnResources()` hooks are ready.
#### Further reading
- [`.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)
================================================
FILE: docs/hooks/onNoData.md
================================================
### 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. __This hook can be async__
```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');
}
});
```
#### Further reading
- [`.data()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/data.md)
================================================
FILE: docs/hooks/triggersEnter.md
================================================
### 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]);
```
#### Further reading
- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)
- [Global `.triggers`](https://github.com/veliovgroup/flow-router/blob/master/docs/api/triggers.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)
================================================
FILE: docs/hooks/triggersExit.md
================================================
### 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]);
```
#### Further reading
- [`.current()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/current.md)
- [Global `.triggers`](https://github.com/veliovgroup/flow-router/blob/master/docs/api/triggers.md)
- [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.md)
- [`.triggersEnter()` hooks](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/triggersEnter.md)
================================================
FILE: docs/hooks/waitOn.md
================================================
### 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 arrays of promises), and
- 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 = …`** on the singleton. Routes **without** an explicit **`maxWaitFor`** use **`FlowRouter.maxWaitFor`** at the time **`waitOn`** runs (so a later **`initialize`** / assignment still applies).
- 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')];
}
});
```
#### *async* support
```js
FlowRouter.route('/posts', {
name: 'posts',
async waitOn() {
await import('/imports/client/posts.js');
return Meteor.subscribe('Posts');
}
});
```
#### Further reading
- [`.waitOnResources()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOnResources.md)
================================================
FILE: docs/hooks/waitOnResources.md
================================================
### 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 images preload
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 __cacheable__ resources, if URLs returns non-cacheable resources (*dynamic resources*) it will be useless.
> [!TIP]
> Why Images and Other resources are separated? What the difference?
>
> - Images can be prefetched via `Image()` constructor, all other resources will use `XMLHttpRequest` or `fetch()` to cache resources. That's why important to make sure requested URLs returns cacheable 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 resources preload
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'
]
};
}
});
```
#### Further reading
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
================================================
FILE: docs/hooks/whileWaiting.md
================================================
### 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);
}
});
```
#### Further reading
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [Templating with Data](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-data.md)
================================================
FILE: docs/original-readme.md
================================================
## Meteor Routing Guide
[Meteor Routing Guide](https://kadira.io/academy/meteor-routing-guide) is a completed guide into **routing** and related topics in Meteor. It talks about how to use FlowRouter properly and use it with **Blaze and React**. It also shows how to manage **subscriptions** and implement **auth logic** in the view layer.
[](https://kadira.io/academy/meteor-routing-guide)
## Getting Started
Add FlowRouter to your app:
~~~shell
meteor add kadira:flow-router
~~~
Let's write our first route (add this file to `lib/router.js`):
~~~js
FlowRouter.route('/blog/:postId', {
action: function (params, queryParams) {
console.log("Yeah! We are on the post:", params.postId);
}
});
~~~
Then visit `/blog/my-post-id` from the browser or invoke the following command from the browser console:
~~~js
FlowRouter.go('/blog/my-post-id');
~~~
Then you can see some messages printed in the console.
## Routes Definition
FlowRouter routes are very simple and based on the syntax of [path-to-regexp](https://github.com/pillarjs/path-to-regexp) which is used in both [Express](http://expressjs.com/) and `iron:router`.
Here's the syntax for a simple route:
~~~js
FlowRouter.route('/blog/:postId', {
// do some action for this route
action: function (params, queryParams) {
console.log("Params:", params);
console.log("Query Params:", queryParams);
},
name: "" // optional
});
~~~
So, this route will be activated when you visit a url like below:
~~~js
FlowRouter.go('/blog/my-post?comments=on&color=dark');
~~~
After you've visit the route, this will be printed in the console:
~~~
Params: {postId: "my-post"}
Query Params: {comments: "on", color: "dark"}
~~~
For a single interaction, the router only runs once. That means, after you've visit a route, first it will call `triggers`, then `subscriptions` and finally `action`. After that happens, none of those methods will be called again for that route visit.
You can define routes anywhere in the `client` directory. But, we recommend to add them in the `lib` directory. Then `fast-render` can detect subscriptions and send them for you (we'll talk about this is a moment).
### Group Routes
You can group routes for better route organization. Here's an example:
~~~js
var adminRoutes = FlowRouter.group({
prefix: '/admin',
name: 'admin',
triggersEnter: [function (context, redirect) {
console.log('running group triggers');
}]
});
// handling /admin route
adminRoutes.route('/', {
action: function () {
BlazeLayout.render('componentLayout', {content: 'admin'});
},
triggersEnter: [function (context, redirect) {
console.log('running /admin trigger');
}]
});
// handling /admin/posts
adminRoutes.route('/posts', {
action: function () {
BlazeLayout.render('componentLayout', {content: 'posts'});
}
});
~~~
**All of the options for the `FlowRouter.group()` are optional.**
You can even have nested group routes as shown below:
~~~js
var adminRoutes = FlowRouter.group({
prefix: "/admin",
name: "admin"
});
var superAdminRoutes = adminRoutes.group({
prefix: "/super",
name: "superadmin"
});
// handling /admin/super/post
superAdminRoutes.route('/post', {
action: function () {
}
});
~~~
You can determine which group the current route is in using:
~~~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
~~~
As with all current route properties, these are not reactive, but can be combined with `FlowRouter.watchPathChange()` to get group names reactively.
## Rendering and Layout Management
FlowRouter does not handle rendering or layout management. For that, you can use:
* [Blaze Layout for Blaze](https://github.com/kadirahq/blaze-layout)
* [React Layout for React](https://github.com/kadirahq/meteor-react-layout)
Then you can invoke the layout manager inside the `action` method in the router.
~~~js
FlowRouter.route('/blog/:postId', {
action: function (params) {
BlazeLayout.render("mainLayout", {area: "blog"});
}
});
~~~
## Triggers
Triggers are the way FlowRouter allows you to perform tasks before you **enter** into a route and after you **exit** from a route.
#### Defining triggers for a route
Here's how you can define triggers for a route:
~~~js
FlowRouter.route('/home', {
// calls just before the action
triggersEnter: [trackRouteEntry],
action: function () {
// do something you like
},
// calls when when we decide to move to another route
// but calls before the next route started
triggersExit: [trackRouteClose]
});
function trackRouteEntry(context) {
// context is the output of `FlowRouter.current()`
Mixpanel.track("visit-to-home", context.queryParams);
}
function trackRouteClose(context) {
Mixpanel.track("move-from-home", context.queryParams);
}
~~~
#### Defining triggers for a group route
This is how you can define triggers on a group definition.
~~~js
var adminRoutes = FlowRouter.group({
prefix: '/admin',
triggersEnter: [trackRouteEntry],
triggersExit: [trackRouteEntry]
});
~~~
> You can add triggers to individual routes in the group too.
#### Defining Triggers Globally
You can also define triggers globally. Here's how to do it:
~~~js
FlowRouter.triggers.enter([cb1, cb2]);
FlowRouter.triggers.exit([cb1, cb2]);
// filtering
FlowRouter.triggers.enter([trackRouteEntry], {only: ["home"]});
FlowRouter.triggers.exit([trackRouteExit], {except: ["home"]});
~~~
As you can see from the last two examples, you can filter routes using the `only` or `except` keywords. But, you can't use both `only` and `except` at once.
> If you'd like to learn more about triggers and design decisions, visit [here](https://github.com/meteorhacks/flow-router/pull/59).
#### Redirecting With Triggers
You can redirect to a different route using triggers. You can do it from both enter and exit triggers. See how to do it:
~~~js
FlowRouter.route('/', {
triggersEnter: [function (context, redirect) {
redirect('/some-other-path');
}],
action: function (_params) {
throw new Error("this should not get called");
}
});
~~~
Every trigger callback comes with a second argument: a function you can use to redirect to a different route. Redirect also has few properties to make sure it's not blocking the router.
* redirect must be called with an URL
* redirect must be called within the same event loop cycle (no async or called inside a Tracker)
* redirect cannot be called multiple times
Check this [PR](https://github.com/meteorhacks/flow-router/pull/172) to learn more about our redirect API.
#### Stopping the Callback With Triggers
In some cases, you may need to stop the route callback from firing using triggers. You can do this in **before** triggers, using the third argument: the `stop` function. For example, you can check the prefix and if it fails, show the notFound layout and stop before the action fires.
```js
var localeGroup = FlowRouter.group({
prefix: '/:locale?',
triggersEnter: [localeCheck]
});
localeGroup.route('/login', {
action: function (params, queryParams) {
BlazeLayout.render('componentLayout', {content: 'login'});
}
});
function localeCheck(context, redirect, stop) {
var locale = context.params.locale;
if (locale !== undefined && locale !== 'fr') {
BlazeLayout.render('notFound');
stop();
}
}
```
> **Note**: When using the stop function, you should always pass the second **redirect** argument, even if you won't use it.
## Not Found Routes
You can configure Not Found routes like this:
~~~js
FlowRouter.notFound = {
// Subscriptions registered here don't have Fast Render support.
subscriptions: function () {
},
action: function () {
}
};
~~~
## API
FlowRouter has a rich API to help you to navigate the router and reactively get information from the router.
#### FlowRouter.getParam(paramName);
Reactive function which you can use to get a parameter from the URL.
~~~js
// route def: /apps/:appId
// url: /apps/this-is-my-app
var appId = FlowRouter.getParam("appId");
console.log(appId); // prints "this-is-my-app"
~~~
#### FlowRouter.getQueryParam(queryStringKey);
Reactive function which you can use to get a value from the queryString.
~~~js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red
var color = FlowRouter.getQueryParam("color");
console.log(color); // prints "red"
~~~
#### FlowRouter.path(pathDef, params, queryParams)
Generate a path from a path definition. Both `params` and `queryParams` are optional.
Special characters in `params` and `queryParams` will be URL encoded.
~~~js
var pathDef = "/blog/:cat/:id";
var params = {cat: "met eor", id: "abc"};
var queryParams = {show: "y+e=s", color: "black"};
var path = FlowRouter.path(pathDef, params, queryParams);
console.log(path); // prints "/blog/met%20eor/abc?show=y%2Be%3Ds&color=black"
~~~
If there are no params or queryParams, this will simply return the pathDef as it is.
##### Using Route name instead of the pathDef
You can also use the route's name instead of the pathDef. Then, FlowRouter will pick the pathDef from the given route. See the following example:
~~~js
FlowRouter.route("/blog/:cat/:id", {
name: "blogPostRoute",
action: function (params) {
//...
}
})
var params = {cat: "meteor", id: "abc"};
var queryParams = {show: "yes", color: "black"};
var path = FlowRouter.path("blogPostRoute", params, queryParams);
console.log(path); // prints "/blog/meteor/abc?show=yes&color=black"
~~~
#### FlowRouter.go(pathDef, params, queryParams);
This will get the path via `FlowRouter.path` based on the arguments and re-route to that path.
You can call `FlowRouter.go` like this as well:
~~~js
FlowRouter.go("/blog");
~~~
#### FlowRouter.url(pathDef, params, queryParams)
Just like `FlowRouter.path`, but gives the absolute url. (Uses `Meteor.absoluteUrl` behind the scenes.)
#### FlowRouter.setParams(newParams)
This will change the current params with the newParams 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
~~~
#### FlowRouter.setQueryParams(newQueryParams)
Just like `FlowRouter.setParams`, but for queryString params.
To remove a query param set it to `null` like below:
~~~js
FlowRouter.setQueryParams({paramToRemove: null});
~~~
#### FlowRouter.getRouteName()
To get the name of the route reactively.
~~~js
Tracker.autorun(function () {
var routeName = FlowRouter.getRouteName();
console.log("Current route name is: ", routeName);
});
~~~
#### FlowRouter.current()
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()`.
This gives an object like this:
~~~js
// route def: /apps/:appId
// url: /apps/this-is-my-app?show=yes&color=red
var 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"}
// }
~~~
#### FlowRouter.watchPathChange()
Reactively watch the changes in the path. If you need to simply get the params or queryParams use dedicated APIs like `FlowRouter.getQueryParam()`.
~~~js
Tracker.autorun(function () {
FlowRouter.watchPathChange();
var currentContext = FlowRouter.current();
// do anything with the current context
// or anything you wish
});
~~~
#### FlowRouter.withReplaceState(fn)
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(function () {
FlowRouter.setParams({id: "the-id-1"});
FlowRouter.setParams({id: "the-id-2"});
FlowRouter.setParams({id: "the-id-3"});
});
~~~
Now, there is no item in the browser history. Just like `FlowRouter.setParams`, you can use any FlowRouter API inside `FlowRouter.withReplaceState`.
> We named this function as `withReplaceState` because, replaceState is the underline API used for this functionality. Read more about [replace state & the history API](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history).
#### FlowRouter.reload()
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 API you want.
#### FlowRouter.wait() and FlowRouter.initialize()
By default, FlowRouter initializes the routing process in a `Meteor.startup()` callback. This works for most of the apps. But, some apps 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()`.
eg:-
~~~js
// file: app.js
FlowRouter.wait();
WhenEverYourAppIsReady(function () {
FlowRouter.initialize();
});
~~~
For more information visit [issue #180](https://github.com/meteorhacks/flow-router/issues/180).
#### FlowRouter.onRouteRegister(cb)
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(function (route) {
// do anything with the route object
console.log(route);
});
~~~
Let's say a user defined a route like this:
~~~js
FlowRouter.route('/blog/:post', {
name: 'postList',
triggersEnter: [function () {}],
subscriptions: function () {},
action: function () {},
triggersExit: [function () {}],
customField: 'customName'
});
~~~
Then the route object will be something like this:
~~~js
{
pathDef: '/blog/:post',
name: 'postList',
options: {customField: 'customName'}
}
~~~
So, it's not the internal route object we are using.
## Subscription Management
For Subscription Management, we highly suggest you to follow [Template/Component level subscriptions](https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management). Visit this [guide](https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management) for that.
FlowRouter also has it's own subscription registration mechanism. We will remove this in version 3.0. We don't remove or deprecate it in version 2.x because this is the easiest way to implement FastRender support for your app. In 3.0 we've better support for FastRender with Server Side Rendering.
FlowRouter only deals with registration of subscriptions. It does not wait until subscription becomes ready. This is how to register a subscription.
~~~js
FlowRouter.route('/blog/:postId', {
subscriptions: function (params, queryParams) {
this.register('myPost', Meteor.subscribe('blogPost', params.postId));
}
});
~~~
We can also register global subscriptions like this:
~~~js
FlowRouter.subscriptions = function () {
this.register('myCourses', Meteor.subscribe('courses'));
};
~~~
All these global subscriptions run on every route. So, pay special attention to names when registering subscriptions.
After you've registered your subscriptions, you can reactively check for the status of those subscriptions like this:
~~~js
Tracker.autorun(function () {
console.log("Is myPost ready?:", FlowRouter.subsReady("myPost"));
console.log("Are all subscriptions ready?:", FlowRouter.subsReady());
});
~~~
So, you can use `FlowRouter.subsReady` inside template helpers to show the loading status and act accordingly.
### FlowRouter.subsReady() with a callback
Sometimes, we need to use `FlowRouter.subsReady()` in places where an autorun is not available. One such example is inside an event handler. For such places, we can use the callback API of `FlowRouter.subsReady()`.
~~~js
Template.myTemplate.events({
"click #id": function(){
FlowRouter.subsReady("myPost", function() {
// do something
});
}
});
~~~
> Arunoda has discussed more about Subscription Management in FlowRouter in [this](https://meteorhacks.com/flow-router-and-subscription-management.html#subscription-management) blog post about [FlowRouter and Subscription Management](https://meteorhacks.com/flow-router-and-subscription-management.html).
> He's showing how to build an app like this:
>
#### Fast Render
FlowRouter has built in support for [Fast Render](https://github.com/abecks/meteor-fast-render).
- `meteor add communitypackages:fast-render`
- Put `router.js` in a shared location. We suggest `lib/router.js`.
You can exclude Fast Render support by wrapping the subscription registration in an `isClient` block:
~~~js
FlowRouter.route('/blog/:postId', {
subscriptions: function (params, queryParams) {
// using Fast Render
this.register('myPost', Meteor.subscribe('blogPost', params.postId));
// not using Fast Render
if(Meteor.isClient) {
this.register('data', Meteor.subscribe('bootstrap-data');
}
}
});
~~~
#### Subscription Caching
You can also use [Subs Manager](https://github.com/meteorhacks/subs-manager) for caching subscriptions on the client. We haven't done anything special to make it work. It should work as it works with other routers.
## IE9 Support
FlowRouter has IE9 support. But it does not ship the **HTML5 history polyfill** out of the box. That's because most apps do not require it.
If you need to support IE9, add the **HTML5 history polyfill** with the following package.
~~~shell
meteor add tomwasd:history-polyfill
~~~
## Hashbang URLs
To enable hashbang urls like `mydomain.com/#!/mypath` simple set the `hashbang` option to `true` in the initialize function:
~~~js
// file: app.js
FlowRouter.wait();
WhenEverYourAppIsReady(function () {
FlowRouter.initialize({hashbang: true});
});
~~~
## Prefixed paths
In cases you wish to run multiple web application on the same domain name, you’ll probably want to serve your particular meteor application under a sub-path (eg `example.com/myapp`). In this case simply include the path prefix in the meteor `ROOT_URL` environment variable and FlowRouter will handle it transparently without any additional configuration.
## Add-ons
Router is a base package for an app. Other projects like [useraccounts](http://useraccounts.meteor.com/) should have support for FlowRouter. Otherwise, it's hard to use FlowRouter in a real project. Now a lot of packages have [started to support FlowRouter](https://kadira.io/blog/meteor/addon-packages-for-flowrouter).
So, you can use your your favorite package with FlowRouter as well. If not, there is an [easy process](https://kadira.io/blog/meteor/addon-packages-for-flowrouter#what-if-project-xxx-still-doesn-t-support-flowrouter-) to convert them to FlowRouter.
**Add-on API**
We have also released a [new API](https://github.com/kadirahq/flow-router#flowrouteronrouteregistercb) to support add-on developers. With that add-on packages can get a notification, when the user created a route in their app.
If you've more ideas for the add-on API, [let us know](https://github.com/kadirahq/flow-router/issues).
## Difference with Iron Router
FlowRouter and Iron Router are two different routers. Iron Router tries to be a full featured solution. It tries to do everything including routing, subscriptions, rendering and layout management.
FlowRouter is a minimalistic solution focused on routing with UI performance in mind. It exposes APIs for related functionality.
Let's learn more about the differences:
### Rendering
FlowRouter doesn't handle rendering. By decoupling rendering from the router it's possible to use any rendering framework, such as [Blaze Layout](https://github.com/kadirahq/blaze-layout) to render with Blaze's Dynamic Templates. Rendering calls are made in the the route's action. We have a layout manager for [React](https://github.com/kadirahq/meteor-react-layout) as well.
### Subscriptions
With FlowRouter, we highly suggest using template/component layer subscriptions. But, if you need to do routing in the router layer, FlowRouter has [subscription registration](#subscription-management) mechanism. Even with that, FlowRouter never waits for the subscriptions and view layer to do it.
### Reactive Content
In Iron Router you can use reactive content inside the router, but any hook or method can re-run in an unpredictable manner. FlowRouter limits reactive data sources to a single run; when it is first called.
We think that's the way to go. Router is just a user action. We can work with reactive content in the rendering layer.
### router.current() is evil
`Router.current()` is evil. Why? Let's look at following example. Imagine we have a route like this in our app:
~~~
/apps/:appId/:section
~~~
Now let's say, we need to get `appId` from the URL. Then we will do, something like this in Iron Router.
~~~js
Templates['foo'].helpers({
"someData": function () {
var appId = Router.current().params.appId;
return doSomething(appId);
}
});
~~~
Let's say we changed `:section` in the route. Then the above helper also gets rerun. If we add a query param to the URL, it gets rerun. That's because `Router.current()` looks for changes in the route(or URL). But in any of above cases, `appId` didn't get changed.
Because of this, a lot parts of our app get re-run and re-rendered. This creates unpredictable rendering behavior in our app.
FlowRouter fixes this issue by providing the `Router.getParam()` API. See how to use it:
~~~js
Templates['foo'].helpers({
"someData": function () {
var appId = FlowRouter.getParam('appId');
return doSomething(appId);
}
});
~~~
### No data context
FlowRouter does not have a data context. Data context has the same problem as reactive `.current()`. We believe, it'll possible to get data directly in the template (component) layer.
### Built in Fast Render Support
FlowRouter has built in [Fast Render](https://github.com/abecks/meteor-fast-render) support. Just add Fast Render to your app and it'll work. Nothing to change in the router.
For more information check [docs](#fast-render).
### Server Side Routing
FlowRouter is a client side router and it **does not** support server side routing at all. But `subscriptions` run on the server to enable Fast Render support.
#### Reason behind that
Meteor is not a traditional framework where you can send HTML directly from the server. Meteor needs to send a special set of HTML to the client initially. So, you can't directly send something to the client yourself.
Also, in the server we need look for different things compared with the client. For example:
* In the server we have to deal with headers.
* In the server we have to deal with methods like `GET`, `POST`, etc.
* In the server we have Cookies.
So, it's better to use a dedicated server-side router like [`meteorhacks:picker`](https://github.com/meteorhacks/picker). It supports connect and express middlewares and has a very easy to use route syntax.
### Server Side Rendering
FlowRouter 3.0 will have server side rendering support. We've already started the initial version and check our [`ssr`](https://github.com/meteorhacks/flow-router/tree/ssr) branch for that.
It's currently very usable and Kadira already using it for
### Better Initial Loading Support
In Meteor, we have to wait until all the JS and other resources send before rendering anything. This is an issue. In 3.0, with the support from Server Side Rendering we are going to fix it.
## Migrating into 2.0
Migrating into version 2.0 is easy and you don't need to change any application code since you are already using 2.0 features and the APIs. In 2.0, we've changed names and removed some deprecated APIs.
Here are the steps to migrate your app into 2.0.
#### Use the New FlowRouter Package
* Now FlowRouter comes as `kadira:flow-router`
* So, remove `meteorhacks:flow-router` with : `meteor remove meteorhacks:flow-router`
* Then, add `kadira:flow-router` with `meteor add kadira:flow-router`
#### Change FlowLayout into BlazeLayout
* We've also renamed FlowLayout as [BlazeLayout](https://github.com/kadirahq/blaze-layout).
* So, remove `meteorhacks:flow-layout` and add `kadira:blaze-layout` instead.
* You need to use `BlazeLayout.render()` instead of `FlowLayout.render()`
#### Stop using deprecated Apis
* There is no middleware support. Use triggers instead.
* There is no API called `.reactiveCurrent()`, use `.watchPathChange()` instead.
* Earlier, you can access query params with `FlowRouter.current().params.query`. But, now you can't do that. Use `FlowRouter.current().queryParams` instead.
================================================
FILE: docs/quick-start.md
================================================
### Quick Start
Learn how to create routes and pull data from Method or Subscription
#### 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 package `kadira:flow-router`, it should be removed to avoid interference and unexpected behavior
#### ES6 Import
```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
```
#### Create your first route
Create the first route and `*` catch all route to serve "404" page
```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');
}
});
```
#### Pull data from a Subscription
Create a route with parameters and pull data from Subscription
```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, queryParams, articleObject) {
// Pass fetched article data to template
this.render('article', articleObject);
},
waitOn(params) {
// All passed parameters is available as Object:
// { _id: 'article_id', slug: 'article-slug' }
console.log(params);
return Meteor.subscribe('article', params._id);
},
async data(params) {
// All passed parameters is available as Object:
// { _id: 'article_id', slug: 'article-slug' }
console.log(params);
return await ArticleCollection.findOneAsync(params._id)
}
});
```
#### Pull data from a Method
Create a route with parameters and pull data from Method
```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, queryParams, articleObject) {
// Pass fetched article data to template
this.render('article', articleObject);
},
async data(params) {
// All passed parameters is available as Object:
// { _id: 'article_id', slug: 'article-slug' }
console.log(params);
return await Meteor.callAsync('article.get', params._id);
}
});
```
#### Create a route with GET-query string
Use GET-parameters for conditional logic
```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', { ...params, ...queryParams });
}
});
```
> [!TIP]
> if you're using any package which require original FlowRouter namespace and throwing an error, you can solve it with the next code
```js
// in /lib/ directory
Package['kadira:flow-router'] = Package['ostrio:flow-router-extra'];
```
#### Further reading
- [Templating](https://github.com/veliovgroup/flow-router/blob/master/docs/templating.md)
- [Templating with Data](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-data.md)
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [`.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/react.md
================================================
### React + react-mounter
Use flow router with beloved `React` library. For more info read docs of [`react-mounter`](https://github.com/kadirahq/react-mounter).
```jsx
import React from 'react'
import { mount } from 'react-mounter';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import AboutMe from './AboutMe'; // <-- template to render
const MainLayout = ({content}) => (
This is our header
{content()}
);
FlowRouter.route('/about-me', {
name: 'about-me',
action() {
mount(MainLayout, {
content: () => ,
});
},
});
```
================================================
FILE: docs/templating-with-data.md
================================================
### Templating with Data
> [!NOTE]
> Blaze templating is available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed
#### Create layout
```handlebars
{{> yield}}
```
```js
// /imports/client/layout/layout.js
import { Template } from 'meteor/templating';
import './layout.html';
/* ... */
```
#### Create notFound (404) template
```handlebars
404
No such page.
```
#### Create article template
```handlebars
{{article.title}}
{{article.headline}}
{{{article.text}}}
```
#### Create loading template
```handlebars
Loading...
```
#### Create article route
1. Create article route
2. Using `waitOn` hook wait for template and methods/subscription to be ready
3. Using `action` hook to render article template into layout
4. Using `data` hook fetch data from Collection
5. If article doesn't exists (*bad* `_id` *is provided*) - render 404 template using `onNoData` hook
```js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
// Import layout, loading and notFound templates statically as it will be used a lot
import '/imports/client/layout/layout.js';
import '/imports/client/loading/loading.html';
import '/imports/client/notFound/notFound.html';
// Create article route
FlowRouter.route('/article/:_id', {
name: 'article',
waitOn(params) {
return [
import('/imports/client/article/article.html'),
Meteor.subscribe('article', params._id) // OMIT IF METHOD IS USED TO FETCH ARTICLE
];
},
whileWaiting() {
this.render('layout', 'loading');
},
action(params, queryParams, article) {
this.render('layout', 'article', { article });
},
async data(params) {
// USE SUBSCRIPTION:
return await ArticlesCollection.findOneAsync({ _id: params._id });
// OR USE METHOD
return await Meteor.callAsync('article.get', params._id);
},
onNoData() {
this.render('notFound');
}
});
```
#### Further Reading
- [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.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)
- [`.waitOn()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/waitOn.md)
- [`.whileWaiting()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/whileWaiting.md)
- [`.render()` method](https://github.com/veliovgroup/flow-router/blob/master/docs/api/render.md)
- [Templating with "Regions"](https://github.com/veliovgroup/flow-router/blob/master/docs/templating-with-regions.md)
================================================
FILE: docs/templating-with-regions.md
================================================
### Templating with "Regions"
> [!NOTE]
> Blaze templating is available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed
#### Create layout
```handlebars
{{> yield}}
```
```js
// /imports/client/layout/layout.js
import { Template } from 'meteor/templating';
import './layout.html';
/* ... */
```
#### Create index template
```handlebars