Repository: VeliovGroup/flow-router Branch: master Commit: 1c8eedef079e Files: 116 Total size: 362.3 KB Directory structure: gitextract_0qjsbjv_/ ├── .agents/ │ └── skills/ │ └── meteor-flow-router/ │ ├── SKILL.md │ └── reference.md ├── .cursorignore ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ ├── PULL_REQUEST_TEMPLATE │ └── workflows/ │ └── test_suite.yml ├── .gitignore ├── .meteorignore ├── .versions ├── AGENTS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── ORIGINAL FlowRouter LICENSE ├── README.md ├── client/ │ ├── _init.js │ ├── active.route.js │ ├── group.js │ ├── modules.js │ ├── renderer.js │ ├── route.js │ ├── router.js │ └── triggers.js ├── docs/ │ ├── README.md │ ├── api/ │ │ ├── README.md │ │ ├── current.md │ │ ├── decodeQueryParamsOnce.md │ │ ├── getParam.md │ │ ├── getQueryParam.md │ │ ├── getRouteName.md │ │ ├── go.md │ │ ├── group.md │ │ ├── initialize.md │ │ ├── onRouteRegister.md │ │ ├── path.md │ │ ├── pathRegExp.md │ │ ├── refresh.md │ │ ├── reload.md │ │ ├── render.md │ │ ├── route.md │ │ ├── setParams.md │ │ ├── setQueryParams.md │ │ ├── triggers.md │ │ ├── url.md │ │ ├── wait.md │ │ ├── watchPathChange.md │ │ └── withReplaceState.md │ ├── auto-scroll.md │ ├── fast-render-integration.md │ ├── full.md │ ├── helpers/ │ │ ├── README.md │ │ ├── RouterHelpers.md │ │ ├── currentRouteName.md │ │ ├── currentRouteOption.md │ │ ├── isActivePath.md │ │ ├── isActiveRoute.md │ │ ├── isNotActivePath.md │ │ ├── isNotActiveRoute.md │ │ ├── param.md │ │ ├── pathFor.md │ │ ├── queryParam.md │ │ └── urlFor.md │ ├── hooks/ │ │ ├── README.md │ │ ├── action.md │ │ ├── data.md │ │ ├── endWaiting.md │ │ ├── onNoData.md │ │ ├── triggersEnter.md │ │ ├── triggersExit.md │ │ ├── waitOn.md │ │ ├── waitOnResources.md │ │ └── whileWaiting.md │ ├── original-readme.md │ ├── quick-start.md │ ├── react.md │ ├── templating-with-data.md │ ├── templating-with-regions.md │ └── templating.md ├── index.d.ts ├── index.test-d.ts ├── lib/ │ ├── _helpers.js │ ├── constants.js │ ├── group-base.js │ ├── micro-router.js │ ├── qs.js │ ├── route-base.js │ └── router-base.js ├── package-types.json ├── package.js ├── package.json ├── server/ │ ├── _init.js │ ├── group.js │ ├── plugins/ │ │ └── fast-render.js │ ├── route.js │ └── router.js ├── test/ │ ├── client/ │ │ ├── _helpers.js │ │ ├── group.spec.js │ │ ├── loader.spec.js │ │ ├── route.reactivity.spec.js │ │ ├── router.core.spec.js │ │ ├── router.reactivity.spec.js │ │ ├── router.subs_ready.spec.js │ │ ├── trigger.spec.js │ │ └── triggers.js │ ├── common/ │ │ ├── fast_render_route.js │ │ ├── group.spec.js │ │ ├── route.spec.js │ │ ├── router.addons.spec.js │ │ ├── router.path.spec.js │ │ └── router.url.spec.js │ └── server/ │ ├── _helpers.js │ └── plugins/ │ └── fast_render.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .agents/skills/meteor-flow-router/SKILL.md ================================================ --- name: meteor-flow-router description: Guides Meteor routing with ostrio:flow-router-extra plus head/title companions ostrio:flow-router-meta and ostrio:flow-router-title — registration, hooks, globals, triggers, RouterHelpers, document.title, and meta/link/script merge rules. Use when editing Flow Router ecosystem packages, wiring Meteor client routes, SEO head tags, JSON-LD, 404 routes, TypeScript imports, or debugging navigation and head sync. --- # Meteor Flow Router (ostrio stack) ## When this applies - Implementing or debugging **client** routing in Meteor with **`ostrio:flow-router-extra`** (not legacy `kadira:flow-router`). - **`ostrio:flow-router-meta`**: `` **`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 ================================================ [![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru) [![support](https://img.shields.io/badge/support-PayPal-white)](https://paypal.me/veliovgroup) # 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 `` 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 `` 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 ``` #### 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 post Link to post Link to comment in post Jump to comment Link 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 post Link to post Link to comment in post Jump to comment Link 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 post Link to post Link to comment in post Jump to comment Link 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 post Link to post Link to comment in post Jump to comment Link 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 ``` #### 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. [![Meteor Routing Guide](https://cldup.com/AxlPfoxXmR.png)](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: >![FlowRouter's Subscription Management](https://cldup.com/esLzM8cjEL.gif) #### 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 ``` ```js // /imports/client/layout/layout.js import { Template } from 'meteor/templating'; import './layout.html'; /* ... */ ``` #### Create notFound (404) template ```handlebars ``` #### Create article template ```handlebars ``` #### Create loading template ```handlebars ``` #### 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 ``` ```js // /imports/client/layout/layout.js import { Template } from 'meteor/templating'; import './layout.html'; /* ... */ ``` #### Create index template ```handlebars ``` ```js // /imports/client/index/index.js import { Template } from 'meteor/templating'; import './index.html'; /* ... */ ``` #### Create sidebar template ```handlebars ``` #### Create footer template ```handlebars ``` #### Create index route ```js import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; // Import layout template statically as it will be used a lot import '/imports/client/layout/layout.js'; // Create index route FlowRouter.route('/', { name: 'index', waitOn() { return [ import('/imports/client/index/index.js'), import('/imports/client/sidebar/sidebar.html'), import('/imports/client/footer/footer.html') ]; }, action() { this.render('layout', 'index', { sidebar: 'sidebar', footer: 'footer' }); } }); ``` #### Further Reading - [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.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/templating.md ================================================ ### Templating > [!NOTE] > Blaze templating is available only if application has `templating` and `blaze`, or `blaze-html-templates` packages installed #### Create layout ```handlebars ``` ```js // /imports/client/layout/layout.js import { Template } from 'meteor/templating'; import './layout.html'; // As example all available Template's callbacks // layout should be considered as a regular template Template.layout.onCreated(function () { /* ... */ }); Template.layout.onRendered(function () { /* ... */ }); Template.layout.onDestroyed(function () { /* ... */ }); Template.layout.helpers({ /* ... */ }); Template.layout.events({ /* ... */ }); ``` #### Create index template ```handlebars ``` ```js // /imports/client/index/index.js import { Template } from 'meteor/templating'; import './index.html'; // As example all available Template's callbacks Template.index.onCreated(function () { /* ... */ }); Template.index.onRendered(function () { /* ... */ }); Template.index.onDestroyed(function () { /* ... */ }); Template.index.helpers({ /* ... */ }); Template.index.events({ /* ... */ }); ``` #### Create loading template ```handlebars ``` #### Create index route 1. Create index route 2. Using `waitOn` hook wait for template to load from server 3. Using `action` hook to render template into layout ```js import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; // Import layout and loading templates statically as it will be used a lot import '/imports/client/layout/layout.js'; import '/imports/client/loading/loading.html'; // Create index route FlowRouter.route('/', { name: 'index', waitOn() { return import('/imports/client/index/index.js'); }, whileWaiting() { this.render('layout', 'loading'); }, action() { this.render('layout', 'index'); } }); ``` #### 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); } }); ``` #### Further Reading - [`.action()` hook](https://github.com/veliovgroup/flow-router/blob/master/docs/hooks/action.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: index.d.ts ================================================ import type { Meteor } from "meteor/meteor"; import type { Mongo } from "meteor/mongo"; import type { Tracker } from "meteor/tracker"; type Trigger = (context: ReturnType, redirect: Router["go"], stop: () => void, data: any) => void; type TriggerFilterParam = { only: string[] } | { except: string[] }; type DynamicImport = Promise; type QueryValue = string | number | boolean | null | undefined | QueryParams | QueryValue[]; type QueryParams = { [key: string]: QueryValue; }; type Hook = (params: Param, queryParams: QueryParams) => void | Promise; type waitOn = ( params: Param, queryParams: QueryParams, ready: (func: () => ReturnType) => void ) => | Promise | Array> | Meteor.SubscriptionHandle | Tracker.Computation | Array | DynamicImport | Array; type waitOnResources = ( params: Param, queryParams: QueryParams ) => { images: string[]; other: string[]; }; type data = (params: Param, queryParams: QueryParams) => Mongo.CursorStatic | Object | Object[] | false | null | void | Promise; type action = (params: Param, queryParams: QueryParams, data: any) => void; type Param = { [key: string]: string; }; type NewParams = { [key: string]: string | null }; export interface Router { /** Max time (ms) for each `waitOn` promise phase and subscription poll phase; default `120000`. Overridable per route via `maxWaitFor`. When exceeded, `waitOn` stops waiting but `action` still runs; navigating away aborts `waitOn` and skips `action` for the left route. */ maxWaitFor: number; go: (path: string, params?: NewParams, queryParams?: QueryParams) => boolean; route: ( path: string, options?: { name?: string; whileWaiting?: Hook; waitOn?: waitOn; waitOnResources?: waitOnResources; endWaiting?: () => void; data?: data; onNoData?: Hook; triggersEnter?: Array; action?: action; triggersExit?: Array; conf?: { [key: string]: any; forceReRender?: boolean }; /** Max time (ms) for this route’s `waitOn` promise and subscription waits; defaults to `FlowRouter.maxWaitFor`. On timeout, `action` still runs. */ maxWaitFor?: number; [key: string]: any; } ) => Route; group: (options: { name: string; prefix?: string; [key: string]: any }) => any; render: (layout: string, template: string, data?: { [key: string]: any }, callback?: () => void) => void; refresh: (layout: string, template: string) => void; reload: () => void; redirect: (path: string) => void; pathRegExp: RegExp; decodeQueryParamsOnce: boolean; getParam: (param: string) => string; getQueryParam: (param: string) => string; setParams: (params: NewParams) => boolean; setQueryParams: (params: QueryParams) => boolean; url: (path: string, params?: NewParams, queryParams?: QueryParams) => string; path: (path: string, params?: NewParams, queryParams?: QueryParams) => string; current: () => { context: Context; oldRoute: Route; params: Param; path: string; queryParams: QueryParams; route: Route; }; getRouteName: () => string; watchPathChange: () => void; withReplaceState: (callback: () => void) => void; onRouteRegister: (callback: (route: Route) => void) => void; wait: () => void; initialize: (options: { hashbang?: boolean; page?: { click: boolean }; click?: boolean; popstate?: boolean; /** Sets `FlowRouter.maxWaitFor` (ms) for routes that do not set `maxWaitFor`. Default `120000`. */ maxWaitFor?: number; }) => void; triggers: { enter: (triggers: Trigger[], filter?: TriggerFilterParam) => void; exit: (triggers: Trigger[], filter?: TriggerFilterParam) => void; }; } interface Route { conf: { [key: string]: string | boolean }; globals: Array; group: string; name: string; options: { name: string }; path: string; pathDef: string; render: () => void; } /** `new Router()` instance API (same object shape as `FlowRouter` before `.Router` / `.Route` are attached). */ export interface RouterConstructor { new (): Router; } /** Route class instance (see `FlowRouter.route()` return type). */ export interface RouteConstructor { new (...args: any[]): Route; } /** Route group constructor (see `FlowRouter.group()`). */ export interface GroupConstructor { new (...args: any[]): any; } /** * Client singleton: full router + `FlowRouter.Router` / `FlowRouter.Route` constructors * (used by companion packages such as `ostrio:flow-router-meta`). */ export type FlowRouterSingleton = Router & { Router: RouterConstructor; Route: RouteConstructor; }; type Context = { canonicalPath: string; hash: string; params: Param; path: string; pathname: string; querystring: string; state: { [key: string]: string }; title: string; }; interface Helpers { name: (routeName: string | RegExp) => boolean; path: (pathName: string | RegExp) => boolean; pathFor: (pathName: string, params: Param) => string; configure: (options: { activeClass: string; caseSensitive: boolean; disabledClass: string; regex: string }) => void; } /** Router class (instantiate only inside the package; apps use `FlowRouter`). */ export const Router: RouterConstructor; /** Route class (for advanced / package authors). */ export const Route: RouteConstructor; /** Group class. */ export const Group: GroupConstructor; /** Trigger helpers (`applyFilters`, etc.). Server build exposes an empty object. */ export const Triggers: Record any>; /** * Blaze renderer when `templating` + `blaze` are present; otherwise a no-op stub. * Server build exports an empty object — use only from client router code. */ export const BlazeRenderer: new (opts?: Record) => unknown; /** Default `FlowRouter.maxWaitFor` and route `maxWaitFor` fallback (ms). */ export declare const MAX_WAIT_FOR_MS: number; export const FlowRouter: FlowRouterSingleton; /** Client-only: not exported from server `mainModule`. Import from isomorphic modules only if guarded with `Meteor.isClient`. */ export const RouterHelpers: Helpers; ================================================ FILE: index.test-d.ts ================================================ /* eslint-disable no-new -- tsd exercises constructor signatures */ import { expectAssignable, expectError, expectType } from 'tsd'; import type { FlowRouterSingleton, GroupConstructor, RouteConstructor, Router as RouterApi, RouterConstructor, } from '.'; import { BlazeRenderer, FlowRouter, Group, MAX_WAIT_FOR_MS, Route, Router, RouterHelpers, Triggers, } from '.'; expectType(MAX_WAIT_FOR_MS); expectType(FlowRouter.Router); expectType(FlowRouter.Route); expectType(Router); expectType(Route); expectType(Group); expectAssignable(FlowRouter); expectType(FlowRouter); expectType(new FlowRouter.Router()); const routeInstance = new FlowRouter.Route(); expectType(routeInstance.name); new Group(); new BlazeRenderer(); expectType(Triggers); expectType(FlowRouter.maxWaitFor); FlowRouter.initialize({ hashbang: false, maxWaitFor: 60_000 }); FlowRouter.route('/', { name: 'home', }); FlowRouter.go('/'); expectType(FlowRouter.current().route.name); expectType(FlowRouter.current().path); expectType>(FlowRouter.current().params); FlowRouter.route('/post/:id', { name: 'singlePost', }); FlowRouter.go('singlePost', { id: '12345' }); expectType(FlowRouter.path('singlePost', { id: 'a' })); expectType(FlowRouter.url('singlePost', { id: 'a' })); expectType(FlowRouter.path('singlePost', { id: 'a' }, { filter: { tags: ['flow', 'router'] }, page: 2 })); expectType(FlowRouter.url('singlePost', { id: 'a' }, { includeDrafts: false })); expectType(FlowRouter.setParams({ id: '1' })); expectType(FlowRouter.setQueryParams({ q: null })); expectType(FlowRouter.setQueryParams({ filter: { status: 'active' }, tags: ['a', 'b'] })); // getParam is always string at type level expectType(FlowRouter.getParam('id')); expectType(FlowRouter.getQueryParam('q')); expectType(RouterHelpers.name('home')); expectType(RouterHelpers.pathFor('home', {})); // Should error when a number is given instead of string | null expectError(FlowRouter.go('singlePost', { id: 12345 })); expectError(FlowRouter.path('singlePost', { id: 'a' }, { bad: Symbol('x') })); ================================================ FILE: lib/_helpers.js ================================================ import { Meteor } from 'meteor/meteor'; const _helpers = { isEmpty(obj) { // 1 if (obj == null) { return true; } if (this.isArray(obj) || this.isString(obj) || this.isArguments(obj)) { return obj.length === 0; } return Object.keys(obj).length === 0; }, isObject(obj) { const type = typeof obj; return type === 'function' || type === 'object' && !!obj; }, omit(obj, keys) { // 10 if (!this.isObject(obj)) { Meteor._debug('[ostrio:flow-router-extra] [_helpers.omit] First argument must be an Object'); return obj; } if (!this.isArray(keys)) { Meteor._debug('[ostrio:flow-router-extra] [_helpers.omit] Second argument must be an Array'); return obj; } const copy = this.clone(obj); keys.forEach((key) => { delete copy[key]; }); return copy; }, pick(obj, keys) { // 2 if (!this.isObject(obj)) { Meteor._debug('[ostrio:flow-router-extra] [_helpers.omit] First argument must be an Object'); return obj; } if (!this.isArray(keys)) { Meteor._debug('[ostrio:flow-router-extra] [_helpers.omit] Second argument must be an Array'); return obj; } const picked = {}; keys.forEach((key) => { picked[key] = obj[key]; }); return picked; }, isArray(obj) { return Array.isArray(obj); }, extend(...objs) { // 4 return Object.assign({}, ...objs); }, clone(obj) { if (!this.isObject(obj)) return obj; return this.isArray(obj) ? obj.slice() : this.extend(obj); } }; ['Arguments', 'Function', 'String', 'RegExp'].forEach((name) => { _helpers['is' + name] = function (obj) { const tag = Object.prototype.toString.call(obj) if (name === 'Function') { return tag === '[object Function]' || tag === '[object AsyncFunction]' } return tag === '[object ' + name + ']' } }); export { _helpers }; ================================================ FILE: lib/constants.js ================================================ /** Default for `FlowRouter.maxWaitFor` and per-route `maxWaitFor` fallback (ms). */ export const MAX_WAIT_FOR_MS = 120000; ================================================ FILE: lib/group-base.js ================================================ import { _helpers } from './_helpers.js'; const makeTrigger = (trigger) => { if (_helpers.isFunction(trigger)) { return [trigger]; } else if (!_helpers.isArray(trigger)) { return []; } return trigger; }; const makeWaitFor = (func) => { if (_helpers.isFunction(func)) { return [func]; } return []; }; const makeTriggers = (_base, _triggers) => { if (!_base && !_triggers) { return []; } return makeTrigger(_base).concat(makeTrigger(_triggers)); }; class GroupBase { constructor(router, options = {}, parent) { if (options.prefix && !/^\//.test(options.prefix)) { throw new Error('group\'s prefix must start with "/"'); } this._waitFor = makeWaitFor(options.waitOn); this._router = router; this.prefix = options.prefix || ''; this.name = options.name; this.options = options; this._triggersEnter = makeTriggers(options.triggersEnter, this._triggersEnter); this._triggersExit = makeTriggers(this._triggersExit, options.triggersExit); this._subscriptions = options.subscriptions || Function.prototype; this.parent = parent; if (this.parent) { this.prefix = parent.prefix + this.prefix; this._triggersEnter = makeTriggers(parent._triggersEnter, this._triggersEnter); this._triggersExit = makeTriggers(this._triggersExit, parent._triggersExit); this._waitFor = this.parent._waitFor.concat(this._waitFor); } } route(_pathDef, options = {}, _group) { if (!/^\//.test(_pathDef)) { throw new Error('route\'s path must start with "/"'); } const group = _group || this; const pathDef = this.prefix + _pathDef; options.triggersEnter = makeTriggers(this._triggersEnter, options.triggersEnter); options.triggersExit = makeTriggers(options.triggersExit, this._triggersExit); options.waitFor = this._waitFor.concat([]); return this._router.route( pathDef, _helpers.extend( _helpers.omit(this.options, ['triggersEnter', 'triggersExit', 'subscriptions', 'prefix', 'waitOn', 'name', 'title', 'titlePrefix', 'link', 'script', 'meta']), options ), group ); } group(options) { return new this.constructor(this._router, options, this); } callSubscriptions(current) { if (this.parent) { this.parent.callSubscriptions(current); } this._subscriptions.call(current.route, current.params, current.queryParams); } } export { GroupBase, makeTrigger, makeTriggers }; ================================================ FILE: lib/micro-router.js ================================================ /** * MicroRouter — A minimal client-side router replacing page.js. * * Features: * - Path matching via path-to-regexp style patterns * - History API integration (pushState, replaceState, popstate) * - Click interception on elements * - Base path support * - No double-decoding bugs * * This module is client-only. */ // --- Path matching engine --- /** * Parse a path definition like '/users/:id/posts/:postId' * into a regex and a list of parameter names. * * Supports: * - :param — named parameter * - :param? — optional parameter * - :param* — zero or more segments * - :param+ — one or more segments * - * — catch-all * - :param(\\d+) — parameter with custom regex constraint */ function pathToRegExp(pathDef) { if (pathDef === '*') { return { regexp: /^(.*)$/, keys: [] }; } const keys = []; // Single-pass replacement to preserve parameter order. // Matches all param styles: :name(regex), :name*, :name+, :name?, :name const pattern = pathDef.replace(/:(\w+)(?:\(([^)]+)\)|([+*?]))?/g, (_, name, customRegex, modifier) => { keys.push(name); if (customRegex) { return `(${customRegex})`; } switch (modifier) { case '*': return '(.*)'; case '+': return '(.+)'; case '?': return '(?:([^/]+))?'; default: return '([^/]+)'; } }); // Make trailing slash optional so /posts/ matches /posts and vice versa const regexp = new RegExp(`^${pattern.replace(/\/+$/, '')}\\/?$`); return { regexp, keys }; } /** * Match a path against a compiled route pattern. * Returns params object or null if no match. */ function matchPath(compiledRoute, path) { const match = compiledRoute.regexp.exec(path); if (!match) return null; const params = {}; for (let i = 0; i < compiledRoute.keys.length; i++) { const key = compiledRoute.keys[i]; const val = match[i + 1]; params[key] = val ? decodeURIComponent(val) : undefined; } return params; } // --- MicroRouter class --- class MicroRouter { constructor() { this._routes = []; this._exits = []; this._basePath = ''; this._running = false; this._currentContext = null; this._isRedirecting = false; this._onPopState = this._onPopState.bind(this); this._onClick = this._onClick.bind(this); this._options = {}; } /** * Set the base path prefix. */ base(path) { this._basePath = (path || '').replace(/\/+$/, ''); } /** * Register a route handler. */ route(pathDef, handler) { const compiled = pathToRegExp(pathDef); this._routes.push({ pathDef, compiled, handler }); } /** * Register an exit handler for a path. */ exit(pathDef, handler) { const compiled = pathToRegExp(pathDef); this._exits.push({ pathDef, compiled, handler }); } /** * Clear all registered routes and exits. */ reset() { this._routes = []; this._exits = []; } /** * Start the router — listen to popstate and intercept clicks. */ start(options = {}) { if (this._running) return; this._options = options; this._running = true; if (options.popstate !== false) { window.addEventListener('popstate', this._onPopState); } if (options.click !== false) { document.addEventListener('click', this._onClick); } // Dispatch the current URL if (options.dispatch !== false) { this.show(this._getPath(), null, true, false); } } /** * Stop the router. */ stop() { if (!this._running) return; this._running = false; window.removeEventListener('popstate', this._onPopState); document.removeEventListener('click', this._onClick); } /** * Navigate to a path using pushState. */ show(path, state, dispatch, push) { if (!path) return; const ctx = this._createContext(path); // Run exit triggers for the current route this._runExits(ctx, () => { if (push !== false) { this._pushState(ctx, state); } if (dispatch !== false) { this._dispatch(ctx); } }); } /** * Navigate using replaceState instead of pushState. */ replace(path, state, dispatch) { if (!path) return; const ctx = this._createContext(path); this._runExits(ctx, () => { this._replaceState(ctx, state); if (dispatch !== false) { this._dispatch(ctx); } }); } /** * Redirect — replaceState + dispatch. * Sets _isRedirecting to prevent exit triggers from re-running * for the current route when redirect() is called from within an exit trigger. */ redirect(path) { this._isRedirecting = true; this.replace(path); this._isRedirecting = false; } // --- Internal methods --- _getPath() { const { pathname, search, hash } = window.location; let path = pathname + search + hash; // Strip base path if (this._basePath && path.startsWith(this._basePath)) { path = path.slice(this._basePath.length) || '/'; } return path; } _createContext(fullPath) { const hashIndex = fullPath.indexOf('#'); const beforeHash = hashIndex >= 0 ? fullPath.slice(0, hashIndex) : fullPath; const hash = hashIndex >= 0 ? fullPath.slice(hashIndex + 1) : ''; // Split path and querystring const [pathPart, ...qsParts] = beforeHash.split('?'); const path = pathPart || '/'; const querystring = qsParts.join('?'); return { path: fullPath, pathname: path, querystring, hash, params: {}, state: null }; } _isHashOnlyChange(path) { const current = this._currentContext || this._createContext(this._getPath()); const next = this._createContext(path); return current.pathname === next.pathname && current.querystring === next.querystring && current.hash !== next.hash; } _dispatch(ctx) { const pathname = ctx.pathname; for (const route of this._routes) { const params = matchPath(route.compiled, pathname); if (params) { ctx.params = params; this._currentContext = ctx; route.handler(ctx); return; } } // No match — check for catch-all this._currentContext = ctx; } _runExits(newCtx, callback) { if (!this._currentContext || this._isRedirecting) { callback(); return; } const oldPathname = this._currentContext.pathname; let index = 0; const next = () => { if (index >= this._exits.length) { callback(); return; } const exitRoute = this._exits[index++]; const params = matchPath(exitRoute.compiled, oldPathname); if (params) { exitRoute.handler(this._currentContext, next); } else { next(); } }; next(); } _pushState(ctx, state) { const url = this._basePath + ctx.path; try { window.history.pushState(state || {}, '', url); } catch (e) { window.location.href = url; } } _replaceState(ctx, state) { const url = this._basePath + ctx.path; try { window.history.replaceState(state || {}, '', url); } catch (e) { window.location.href = url; } } _onPopState() { if (!this._running) return; const path = this._getPath(); if (this._isHashOnlyChange(path)) { this._currentContext = this._createContext(path); return; } this.show(path, null, true, false); } _onClick(event) { // Only handle left clicks without modifiers if (event.defaultPrevented) return; if (event.button !== 0) return; if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; // Find the closest element let el = event.target; while (el && el.nodeName !== 'A') { el = el.parentNode; } if (!el || el.nodeName !== 'A') return; // Skip links with specific attributes if (el.hasAttribute('download')) return; if (el.hasAttribute('target') && el.target !== '_self') return; if (el.getAttribute('rel') === 'external') return; // Only handle same-origin links const href = el.getAttribute('href'); if (!href) return; if (/^(mailto:|tel:|javascript:)/.test(href)) return; // Check same origin try { const linkUrl = new URL(href, window.location.origin); if (linkUrl.origin !== window.location.origin) return; let path = linkUrl.pathname + linkUrl.search + linkUrl.hash; // Strip base path if (this._basePath && path.startsWith(this._basePath)) { path = path.slice(this._basePath.length) || '/'; } if (this._isHashOnlyChange(path)) return; event.preventDefault(); this.show(path); } catch (e) { // Invalid URL, let browser handle it } } } export { MicroRouter, pathToRegExp, matchPath }; ================================================ FILE: lib/qs.js ================================================ import { _helpers } from './_helpers.js'; const decodeQueryPart = (value = '') => { try { return decodeURIComponent(value.replace(/\+/g, ' ')); } catch (_error) { return value; } }; const encodeQueryPart = (value = '') => { try { return encodeURIComponent(`${value}`); } catch (_error) { return `${value}`; } }; const getPathTokens = (key = '') => { const tokens = []; const matcher = /([^[\]]+)|\[(.*?)\]/g; let match = matcher.exec(key); while (match !== null) { tokens.push(match[1] !== undefined ? match[1] : match[2]); match = matcher.exec(key); } return tokens; }; const isArrayToken = (token) => token === '' || /^\d+$/.test(token); const ensureContainer = (value, nextToken) => { if (value && _helpers.isObject(value)) { return value; } if (Array.isArray(value)) { return value; } return isArrayToken(nextToken) ? [] : {}; }; const mergeLeaf = (target, key, value) => { if (!Object.prototype.hasOwnProperty.call(target, key)) { target[key] = value; return; } if (Array.isArray(target[key])) { target[key].push(value); return; } target[key] = [target[key], value]; }; const setDeepValue = (target, tokens, value) => { if (!tokens.length) { return; } if (tokens.length === 1 && tokens[0] !== '') { mergeLeaf(target, tokens[0], value); return; } let cursor = target; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; const isLast = i === tokens.length - 1; const nextToken = tokens[i + 1]; if (token === '__proto__' || token === 'constructor' || token === 'prototype') { return; } if (isLast) { if (token === '') { if (!Array.isArray(cursor)) { return; } cursor.push(value); } else if (Array.isArray(cursor) && /^\d+$/.test(token)) { const index = Number(token); if (cursor[index] !== undefined) { cursor.push(value); } else { cursor[index] = value; } } else if (_helpers.isObject(cursor)) { mergeLeaf(cursor, token, value); } return; } if (token === '') { if (!Array.isArray(cursor)) { return; } const container = ensureContainer(undefined, nextToken); cursor.push(container); cursor = container; continue; } if (Array.isArray(cursor) && /^\d+$/.test(token)) { const index = Number(token); let current = cursor[index]; if (!_helpers.isObject(current) && !Array.isArray(current)) { current = ensureContainer(current, nextToken); cursor[index] = current; } cursor = current; continue; } if (!_helpers.isObject(cursor)) { return; } let current = cursor[token]; if (!Array.isArray(current) && !_helpers.isObject(current)) { const prev = current; current = ensureContainer(current, nextToken); if (Array.isArray(current) && typeof prev !== 'undefined') { current.push(prev); } cursor[token] = current; } cursor = current; } }; const buildPairs = (value, key, pairs) => { if (typeof value === 'undefined') { return; } if (Array.isArray(value)) { value.forEach((item, idx) => { buildPairs(item, `${key}[${idx}]`, pairs); }); return; } if (_helpers.isObject(value)) { Object.keys(value).forEach((nestedKey) => { buildPairs(value[nestedKey], `${key}[${nestedKey}]`, pairs); }); return; } pairs.push(`${encodeQueryPart(key)}=${encodeQueryPart(value)}`); }; const mergeObjects = (base, override) => { const result = _helpers.clone(base); Object.keys(override || {}).forEach((key) => { const left = result[key]; const right = override[key]; const leftIsObj = _helpers.isObject(left) && !Array.isArray(left); const rightIsObj = _helpers.isObject(right) && !Array.isArray(right); if (leftIsObj && rightIsObj) { result[key] = mergeObjects(left, right); } else { result[key] = right; } }); return result; }; const qs = { parse(query = '') { if (!query || !_helpers.isString(query)) { return {}; } const cleanQuery = query.replace(/^\?/, ''); if (!cleanQuery) { return {}; } return cleanQuery.split('&').reduce((acc, pair) => { if (!pair) { return acc; } const [rawKey, ...rawValueParts] = pair.split('='); const decodedKey = decodeQueryPart(rawKey); if (!decodedKey) { return acc; } const value = decodeQueryPart(rawValueParts.join('=')); const tokens = getPathTokens(decodedKey); if (!tokens.length) { return acc; } setDeepValue(acc, tokens, value); return acc; }, {}); }, stringify(queryParams = {}) { if (!queryParams || !_helpers.isObject(queryParams)) { return ''; } const pairs = []; Object.keys(queryParams).forEach((key) => { buildPairs(queryParams[key], key, pairs); }); return pairs.join('&'); }, merge(baseQueryParams = {}, queryParams = {}) { if (!_helpers.isObject(baseQueryParams) || Array.isArray(baseQueryParams)) { return _helpers.isObject(queryParams) ? _helpers.clone(queryParams) : {}; } if (!_helpers.isObject(queryParams) || Array.isArray(queryParams)) { return _helpers.clone(baseQueryParams); } return mergeObjects(baseQueryParams, queryParams); } }; export { qs }; ================================================ FILE: lib/route-base.js ================================================ class RouteBase { constructor(router, pathDef, options = {}, group) { this.options = options; this.pathDef = pathDef; // Route.path is deprecated and will be removed in a future version this.path = pathDef; this.name = options.name; this.conf = options.conf || {}; this.group = group; this._router = router; this._action = options.action || Function.prototype; this._subsMap = {}; this._subscriptions = options.subscriptions || Function.prototype; } clearSubscriptions() { this._subsMap = {}; } register(name, sub) { this._subsMap[name] = sub; } getSubscription(name) { return this._subsMap[name]; } getAllSubscriptions() { return this._subsMap; } callSubscriptions(current) { this.clearSubscriptions(); if (this.group) { this.group.callSubscriptions(current); } this._subscriptions(current.params, current.queryParams); } } export default RouteBase; ================================================ FILE: lib/router-base.js ================================================ import { Meteor } from 'meteor/meteor'; import { _helpers } from './_helpers.js'; import { qs } from './qs.js'; class RouterBase { constructor() { this.pathRegExp = /(:[\w\(\)\\\+\*\.\?\[\]\-]+)+/g; this.queryRegExp = /\?([^\/\r\n].*)/; this.globals = []; this._routes = []; this._routesMap = {}; this._current = {}; this._specialChars = ['/', '%', '+']; this._encodeParam = (param) => { const paramArr = param.split(''); let _param = ''; for (let i = 0; i < paramArr.length; i++) { if (this._specialChars.includes(paramArr[i])) { _param += encodeURIComponent(encodeURIComponent(paramArr[i])); } else { try { _param += encodeURIComponent(paramArr[i]); } catch (_e) { _param += paramArr[i]; } } } return _param; }; this.subscriptions = Function.prototype; this._onRouteCallbacks = []; this.triggers = { enter() { /* client only */ }, exit() { /* client only */ } }; } 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 this._encodeParam(`${fields[key]}`); } return ''; }); path = path.replace(/\/\/+/g, '/'); path = path.match(/^\/{1}$/) ? path : path.replace(/\/$/, ''); if (this.env && this.env.trailingSlash && 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; } url() { return Meteor.absoluteUrl( this.path.apply(this, arguments).replace( new RegExp('^' + (`/${this._basePath || ''}/`).replace(/\/\/+/g, '/')), '' ) ); } group(options) { const GroupClass = this._getGroupClass(); return new GroupClass(this, options); } 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); }); } // Subclasses must implement _getGroupClass() { throw new Error('Subclass must implement _getGroupClass()'); } // Client-only stubs go() {} setParams() {} setQueryParams() {} initialize() {} wait() {} getRouteName() {} getParam() {} getQueryParam() {} watchPathChange() {} current() { return this._current; } } export { RouterBase, qs }; ================================================ FILE: package-types.json ================================================ { "typesEntry": "index.d.ts" } ================================================ FILE: package.js ================================================ Package.describe({ name: 'ostrio:flow-router-extra', summary: 'The router for modern JavaScript apps, with support for Blaze, Vue, React, Svelte', version: '3.15.0', git: 'https://github.com/veliovgroup/flow-router', documentation: 'README.md', }); Package.onUse((api) => { api.versionsFrom(['1.4', '2.8.0', '3.0.1', '3.4']); api.use(['modules', 'ecmascript', 'promise', 'tracker', 'reactive-dict', 'reactive-var', 'ejson', 'check'], ['client', 'server']); api.use(['zodern:types@1.0.13', 'typescript'], ['client', 'server'], { weak: true }); api.use(['templating', 'blaze@2.0.0 || 3.0.0'], 'client', { weak: true }); api.mainModule('client/_init.js', 'client'); api.mainModule('server/_init.js', 'server'); // For zodern:types to pick up our published types. api.addAssets('index.d.ts', ['client', 'server']); }); Package.onTest((api) => { api.use(['ecmascript', 'tinytest', 'underscore', 'check', 'mongo', 'http', 'random', 'ostrio:flow-router-extra', 'zodern:types', 'typescript'], ['client', 'server']); api.use(['reactive-var', 'tracker'], 'client'); // Temporary disable `fast-render` tests as not compatible with meteor@3 // once fast-render, meteorx, and inject-data are compatible with meteor@3, add the next packages: // 'communitypackages:fast-render', 'communitypackages:inject-data', 'montiapm:meteorx' // api.addFiles('test/common/fast_render_route.js', ['client', 'server']); // api.addFiles('test/server/plugins/fast_render.js', 'server'); api.addFiles('test/client/_helpers.js', 'client'); api.addFiles('test/server/_helpers.js', 'server'); api.addFiles('test/client/loader.spec.js', 'client'); api.addFiles('test/client/route.reactivity.spec.js', 'client'); api.addFiles('test/client/router.core.spec.js', 'client'); api.addFiles('test/client/router.subs_ready.spec.js', 'client'); api.addFiles('test/client/router.reactivity.spec.js', 'client'); api.addFiles('test/client/group.spec.js', 'client'); api.addFiles('test/client/trigger.spec.js', 'client'); api.addFiles('test/client/triggers.js', 'client'); api.addFiles('test/common/router.path.spec.js', ['client', 'server']); api.addFiles('test/common/router.url.spec.js', ['client', 'server']); api.addFiles('test/common/router.addons.spec.js', ['client', 'server']); api.addFiles('test/common/route.spec.js', ['client', 'server']); api.addFiles('test/common/group.spec.js', ['client', 'server']); }); ================================================ FILE: package.json ================================================ { "name": "ostrio-flow-router-extra", "private": true, "types": "index.d.ts", "scripts": { "test:once": "mtest --package ./ --port=8888 --once", "test:watch": "mtest --package ./ --port=8888", "test:browser": "meteor test-packages ./ --port 8888", "test:types": "tsd", "test": "npm run test:once && npm run test:types" }, "devDependencies": { "@zodern/mtest": "^0.5.1", "tsd": "^0.31.2" } } ================================================ FILE: server/_init.js ================================================ import { Meteor } from 'meteor/meteor'; import Router from './router.js'; import Route from './route.js'; import Group from './group.js'; import './plugins/fast-render.js'; 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 Triggers = {}; const BlazeRenderer = {}; const FlowRouter = new Router(); FlowRouter.Router = Router; FlowRouter.Route = Route; export { MAX_WAIT_FOR_MS } from '../lib/constants.js'; export { FlowRouter, Router, Route, Group, Triggers, BlazeRenderer }; ================================================ FILE: server/group.js ================================================ import { GroupBase } from '../lib/group-base.js'; export default GroupBase; ================================================ FILE: server/plugins/fast-render.js ================================================ import { Meteor } from 'meteor/meteor'; import { _helpers } from './../../lib/_helpers.js'; import { FlowRouter } from '../_init.js'; const setupFastRender = () => { if(!Package['communitypackages:fast-render']) { return; } const FastRender = Package['communitypackages:fast-render'].FastRender; FlowRouter._routes.forEach((route) => { if (route.pathDef === '*') { return; } FastRender.route(route.pathDef, function (routeParams, path) { // anyone using Meteor.subscribe for something else? const meteorSubscribe = Meteor.subscribe; Meteor.subscribe = function () { return Array.from(arguments); }; route._subsMap = {}; FlowRouter.subscriptions.call(route, path); if (route.subscriptions) { route.subscriptions(_helpers.omit(routeParams, ['query']), routeParams.query); } Object.keys(route._subsMap).forEach((key) => { this.subscribe.apply(this, route._subsMap[key]); }); // restore Meteor.subscribe, ... on server side Meteor.subscribe = meteorSubscribe; }); }); }; // hack to run after everything else on startup Meteor.startup(() => { Meteor.startup(() => { setupFastRender(); }); }); ================================================ FILE: server/route.js ================================================ import RouteBase from '../lib/route-base.js'; export default RouteBase; ================================================ FILE: server/router.js ================================================ import Route from './route.js'; import Group from './group.js'; import { _helpers } from '../lib/_helpers.js'; import { RouterBase } from '../lib/router-base.js'; import { pathToRegExp, matchPath } from '../lib/micro-router.js'; class Router extends RouterBase { constructor() { super(); // Pre-compiled route patterns cache (populated lazily in matchPath) this._compiledRoutes = new WeakMap(); } _getGroupClass() { return Group; } matchPath(path) { for (const route of this._routes) { if (!this._compiledRoutes.has(route)) { this._compiledRoutes.set(route, pathToRegExp(route.pathDef)); } const compiled = this._compiledRoutes.get(route); const params = matchPath(compiled, path); if (params) { return { params: _helpers.clone(params), route: _helpers.clone(route), }; } } return null; } setCurrent(current) { this._current = current; } 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); this._routes.push(route); if (options.name) { this._routesMap[options.name] = route; } this._triggerRouteRegister(route); return route; } } export default Router; ================================================ FILE: test/client/_helpers.js ================================================ import { Meteor } from 'meteor/meteor'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Package['ostrio:flow-router-extra'].FlowRouter.decodeQueryParamsOnce = true; Package['kadira:flow-router'] = Package['ostrio:flow-router-extra']; const GetSub = (name) => { for(let id in Meteor.connection._subscriptions) { if(name === Meteor.connection._subscriptions[id].name) { return Meteor.connection._subscriptions[id]; } } return void 0; }; FlowRouter.route('/'); export { GetSub }; ================================================ FILE: test/client/group.spec.js ================================================ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { GetSub } from './_helpers.js'; import { FlowRouter, Group } from 'meteor/ostrio:flow-router-extra'; Tinytest.add('Client - Group - validate path definition', (test) => { // path & prefix must start with '/' test.throws(function() { new Group(null, {prefix: Random.id()}); }); const group = FlowRouter.group({prefix: '/' + Random.id()}); test.throws(function() { group.route(Random.id()); }); }); Tinytest.addAsync('Client - Group - define and go to route with prefix', (test, next) => { const prefix = Random.id(); const rand = Random.id(); let rendered = 0; const group = FlowRouter.group({prefix: '/' + prefix}); group.route('/' + rand, { action() { rendered++; } }); FlowRouter.go('/' + prefix + '/' + rand); setTimeout(() => { test.equal(rendered, 1); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Group - define and go to route without prefix', (test, next) => { const rand = Random.id(); let rendered = 0; const group = FlowRouter.group(); group.route('/' + rand, { action() { rendered++; } }); FlowRouter.go('/' + rand); setTimeout(() => { test.equal(rendered, 1); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Group - async waitOn delays route action', (test, next) => { const prefix = Random.id(); const rand = Random.id(); const events = []; const group = FlowRouter.group({ prefix: '/' + prefix, async waitOn() { events.push('waitOn:start'); await new Promise((resolve) => { Meteor.setTimeout(() => { events.push('waitOn:end'); resolve(); }, 40); }); } }); group.route('/' + rand, { action() { events.push('action'); } }); FlowRouter.go('/' + prefix + '/' + rand); Meteor.setTimeout(() => { test.equal(events, ['waitOn:start']); Meteor.setTimeout(() => { test.equal(events, ['waitOn:start', 'waitOn:end', 'action']); next(); }, 100); }, 10); }); Tinytest.addAsync('Client - Group - subscribe', (test, next) => { const rand = Random.id(); const group = FlowRouter.group({ subscriptions() { this.register('baz', Meteor.subscribe('baz')); } }); group.route('/' + rand); FlowRouter.go('/' + rand); setTimeout(() => { test.isTrue(!!GetSub('baz')); next(); }, 100); }); Tinytest.addAsync('Client - Group - set and retrieve group name', (test, next) => { const rand = Random.id(); const name = Random.id(); const group = FlowRouter.group({ name: name }); group.route('/' + rand); FlowRouter.go('/' + rand); setTimeout(() => { test.isTrue((FlowRouter.current().route.group || {}).name === name); next(); }, 100); }); Tinytest.add('Client - Group - expose group options on a route', (test) => { const pathDef = '/' + Random.id(); const name = Random.id(); const groupName = Random.id(); const data = {aa: 10}; const layout = 'blah'; const group = FlowRouter.group({ name: groupName, prefix: '/admin', layout: layout, someData: data }); group.route(pathDef, { name }); const route = FlowRouter._routesMap[name]; test.equal(route.group.options.someData, data); test.equal(route.group.options.layout, layout); }); ================================================ FILE: test/client/loader.spec.js ================================================ import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra'; Tinytest.add('Client - import page.js', (test) => { // page.js has been replaced by MicroRouter — verify it is no longer present test.isFalse(!!window.page); test.isTrue(!!FlowRouter._microRouter); }); Tinytest.add('Client - import query.js', (test) => { // qs is used internally — verify the router can generate query strings test.isTrue(!!FlowRouter.path('/', {}, { foo: 'bar' }).includes('foo=bar')); }); Tinytest.add('Client - create FlowRouter', (test) => { test.isTrue(!!FlowRouter); }); ================================================ FILE: test/client/route.reactivity.spec.js ================================================ import { Route } from 'meteor/ostrio:flow-router-extra'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; Tinytest.addAsync('Client - Route - Reactivity - getParam', (test, done) => { const r = new Route(); Tracker.autorun((c) => { const param = r.getParam('id'); if(param) { test.equal(param, 'hello'); c.stop(); Meteor.defer(done); } }); setTimeout(() => { const context = { params: {id: 'hello'}, queryParams: {} }; r.registerRouteChange(context); }, 10); }); Tinytest.addAsync('Client - Route - Reactivity - getParam on route close', (test, done) => { const r = new Route(); let closeTriggered = false; Tracker.autorun((c) => { const param = r.getParam('id'); if(closeTriggered) { test.equal(param, undefined); c.stop(); Meteor.defer(done); } }); setTimeout(() => { closeTriggered = true; r.registerRouteClose(); }, 10); }); Tinytest.addAsync('Client - Route - Reactivity - getQueryParam', (test, done) => { const r = new Route(); Tracker.autorun((c) => { const param = r.getQueryParam('id'); if(param) { test.equal(param, 'hello'); c.stop(); Meteor.defer(done); } }); setTimeout(() => { const context = { params: {}, queryParams: {id: 'hello'} }; r.registerRouteChange(context); }, 10); }); Tinytest.addAsync('Client - Route - Reactivity - getQueryParam on route close', (test, done) => { const r = new Route(); let closeTriggered = false; Tracker.autorun((c) => { const param = r.getQueryParam('id'); if(closeTriggered) { test.equal(param, undefined); c.stop(); Meteor.defer(done); } }); setTimeout(() => { closeTriggered = true; r.registerRouteClose(); }, 10); }); Tinytest.addAsync('Client - Route - Reactivity - getRouteName rerun when route closed', (test, done) => { const r = new Route(); r.name = 'my-route'; let closeTriggered = false; Tracker.autorun((c) => { const name = r.getRouteName(); test.equal(name, r.name); if(closeTriggered) { c.stop(); Meteor.defer(done); } }); setTimeout(() => { closeTriggered = true; r.registerRouteClose(); }, 10); }); Tinytest.addAsync('Client - Route - Reactivity - watchPathChange when routeChange', (test, done) => { const r = new Route(); let pathChangeCounts = 0; const c = Tracker.autorun(() => { r.watchPathChange(); pathChangeCounts++; }); const context = { params: {}, queryParams: {} }; setTimeout(() => { r.registerRouteChange(context); setTimeout(checkAfterNormalRouteChange, 50); }, 10); function checkAfterNormalRouteChange() { test.equal(pathChangeCounts, 2); const lastRouteChange = true; r.registerRouteChange(context, lastRouteChange); setTimeout(checkAfterLastRouteChange, 10); } function checkAfterLastRouteChange() { test.equal(pathChangeCounts, 2); c.stop(); Meteor.defer(done); } }); Tinytest.addAsync('Client - Route - Reactivity - watchPathChange when routeClose', (test, done) => { const r = new Route(); let pathChangeCounts = 0; const c = Tracker.autorun(() => { r.watchPathChange(); pathChangeCounts++; }); setTimeout(() => { r.registerRouteClose(); setTimeout(checkAfterRouteClose, 10); }, 10); function checkAfterRouteClose() { test.equal(pathChangeCounts, 2); c.stop(); Meteor.defer(done); } }); ================================================ FILE: test/client/router.core.spec.js ================================================ import { GetSub } from './_helpers.js'; import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync('Client - Router - define and go to route', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go('/' + rand); setTimeout(() => { test.equal(rendered, 1); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Router - route matches path with hash fragment', (test, next) => { const rand = Random.id(); let rendered = 0; let currentContext; FlowRouter.route('/' + rand, { action() { rendered++; currentContext = FlowRouter.current().context; } }); FlowRouter.go('/' + rand + '#security'); Meteor.setTimeout(() => { test.equal(rendered, 1); test.equal(currentContext && currentContext.pathname, '/' + rand); test.equal(currentContext && currentContext.path, '/' + rand + '#security'); test.equal(currentContext && currentContext.hash, 'security'); test.isTrue(location.href.endsWith('/' + rand + '#security')); next(); }, 100); }); Tinytest.addAsync('Client - Router - route matches path with query and hash fragment', (test, next) => { const rand = Random.id(); let rendered = 0; let current; FlowRouter.route('/' + rand, { action() { rendered++; current = FlowRouter.current(); } }); FlowRouter.go('/' + rand + '?tab=account#security'); Meteor.setTimeout(() => { test.equal(rendered, 1); test.equal(current && current.context.pathname, '/' + rand); test.equal(current && current.context.path, '/' + rand + '?tab=account#security'); test.equal(current && current.context.querystring, 'tab=account'); test.equal(current && current.context.hash, 'security'); test.equal(current && current.queryParams.tab, 'account'); test.isTrue(location.href.endsWith('/' + rand + '?tab=account#security')); next(); }, 100); }); Tinytest.addAsync('Client - Router - hash-only go does not rerun same route', (test, next) => { const rand = Random.id(); let rendered = 0; let hashChanges = 0; const onHashChange = () => { hashChanges++; }; window.addEventListener('hashchange', onHashChange); FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go('/' + rand); Meteor.setTimeout(() => { FlowRouter.go('/' + rand + '#security'); Meteor.setTimeout(() => { test.equal(rendered, 1); test.equal(window.location.hash, '#security'); test.equal(hashChanges, 1); window.removeEventListener('hashchange', onHashChange); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - same-route hash link uses browser default', (test, next) => { const rand = Random.id(); let rendered = 0; let hashChanges = 0; const link = document.createElement('a'); const onHashChange = () => { hashChanges++; }; window.addEventListener('hashchange', onHashChange); FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go('/' + rand); Meteor.setTimeout(() => { link.href = '/' + rand + '#security-link'; link.textContent = 'Security'; document.body.appendChild(link); link.click(); Meteor.setTimeout(() => { test.equal(rendered, 1); test.equal(window.location.hash, '#security-link'); test.equal(hashChanges, 1); document.body.removeChild(link); window.removeEventListener('hashchange', onHashChange); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route with async waitOn', (test, next) => { const rand = Random.id(); const events = []; let isReady = false; const handle = { ready() { return isReady; } }; FlowRouter.route('/' + rand, { async waitOn() { events.push('waitOn:start'); await new Promise((resolve) => { Meteor.setTimeout(() => { events.push('waitOn:resolved'); resolve(); }, 20); }); Meteor.setTimeout(() => { isReady = true; events.push('subscription:ready'); }, 40); return handle; }, action() { events.push('action'); } }); FlowRouter.go('/' + rand); Meteor.setTimeout(() => { test.equal(events, ['waitOn:start']); Meteor.setTimeout(() => { test.equal(events, ['waitOn:start', 'waitOn:resolved']); }, 30); Meteor.setTimeout(() => { test.equal(events, ['waitOn:start', 'waitOn:resolved', 'subscription:ready', 'action']); next(); }, 120); }, 10); }); Tinytest.addAsync('Client - Router - waitOn aborts when navigating away', (test, next) => { const randA = Random.id(); const randB = Random.id(); const awaited = { ready() { return false; } }; let aAction = 0; let bAction = 0; FlowRouter.route('/' + randA, { waitOn() { return awaited; }, action() { aAction++; } }); FlowRouter.route('/' + randB, { action() { bAction++; } }); Meteor.setTimeout(() => { awaited.ready = () => { return true; }; }, 50); FlowRouter.go('/' + randA); Meteor.setTimeout(() => { FlowRouter.go('/' + randB); Meteor.setTimeout(() => { test.equal(aAction, 0); test.equal(bAction, 1); next(); }, 150); }, 1); }); Tinytest.addAsync('Client - Router - waitOn stale subscription times out (maxWaitFor)', (test, next) => { const rand = Random.id(); let actionCount = 0; FlowRouter.route('/' + rand, { maxWaitFor: 80, waitOn() { return { ready() { return false; } }; }, action() { actionCount++; } }); FlowRouter.go('/' + rand); Meteor.setTimeout(() => { test.equal(actionCount, 1); next(); }, 250); }); Tinytest.addAsync('Client - Router - waitOn stale promise times out (maxWaitFor)', (test, next) => { const rand = Random.id(); let actionCount = 0; FlowRouter.route('/' + rand, { maxWaitFor: 80, waitOn() { return new Promise(() => {}); }, action() { actionCount++; } }); FlowRouter.go('/' + rand); Meteor.setTimeout(() => { test.equal(actionCount, 1); next(); }, 250); }); Tinytest.addAsync('Client - Router - define and go to route (issue #93; query string repetition) - go', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go(`/${rand}?test=1`); setTimeout(() => { test.equal(rendered, 1); test.isTrue(location.href.endsWith(`/${rand}?test=1`)); const route = FlowRouter.current(); const qs = {...route.queryParams}; qs.test = 2; FlowRouter.go(route.path, {...route.params}, qs); setTimeout(() => { test.isTrue(location.href.endsWith(`/${rand}?test=2`)); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route (issue #93; query string repetition) - setQueryParams', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go(`/${rand}?test=1`); setTimeout(() => { test.equal(rendered, 1); test.isTrue(location.href.endsWith(`/${rand}?test=1`)); const route = FlowRouter.current(); const qs = {...route.queryParams}; qs.test = 2; FlowRouter.setQueryParams(qs); setTimeout(() => { test.isTrue(location.href.endsWith(`/${rand}?test=2`)); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route (issue #93; query string repetition) - go - empty arguments', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go(`/${rand}?D`); setTimeout(() => { test.equal(rendered, 1); test.isTrue(location.href.endsWith(`/${rand}?D=`)); const route = FlowRouter.current(); const qs = {...route.queryParams}; qs.test = 2; FlowRouter.go(route.path, {...route.params}, qs); setTimeout(() => { test.isTrue(location.href.endsWith(`/${rand}?D=&test=2`)); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route (issue #93; query string repetition) - setQueryParams - empty arguments', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.go(`/${rand}?D`); setTimeout(() => { test.equal(rendered, 1); test.isTrue(location.href.endsWith(`/${rand}?D=`)); const route = FlowRouter.current(); const qs = {...route.queryParams}; qs.test = 2; FlowRouter.setQueryParams(qs); setTimeout(() => { test.isTrue(location.href.endsWith(`/${rand}?D=&test=2`)); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route with fields', (test, next) => { const rand = Random.id(); const pathDef = '/' + rand + '/:key'; let rendered = 0; FlowRouter.route(pathDef, { action(params) { test.equal(params.key, 'abc +@%'); rendered++; } }); FlowRouter.go(pathDef, {key: 'abc +@%'}); setTimeout(() => { test.equal(rendered, 1); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Router - define and go to route with UTF-8 fields', (test, next) => { const rand = Random.id(); const pathDef = '/' + rand + '/:key'; let rendered = 0; FlowRouter.route(pathDef, { action(params) { test.equal(params.key, '𒀨𒀭'); rendered++; } }); FlowRouter.go(pathDef, {key: '𒀨𒀭'}); setTimeout(() => { test.equal(rendered, 1); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Router - parse params and query', (test, next) => { const rand = Random.id(); let rendered = 0; let params = {}; FlowRouter.route('/' + rand + '/:foo', { action(_params) { rendered++; params = _params; } }); FlowRouter.go('/' + rand + '/bar'); setTimeout(() => { test.equal(rendered, 1); test.equal(params.foo, 'bar'); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Router - redirect using FlowRouter.go', (test, next) => { const rand = Random.id(); const rand2 = Random.id(); const log = []; const paths = ['/' + rand2, '/' + rand]; FlowRouter.route(paths[0], { action() { log.push(1); FlowRouter.go(paths[1]); } }); FlowRouter.route(paths[1], { action() { log.push(2); } }); FlowRouter.go(paths[0]); setTimeout(() => { test.equal(log, [1, 2]); next(); }, 100); }); Tinytest.addAsync('Client - Router - get current route path', (test, next) => { const value = Random.id(); const randomValue = Random.id(); const pathDef = '/' + randomValue + '/:_id'; const path = '/' + randomValue + '/' + value; let detectedValue = null; FlowRouter.route(pathDef, { action(params) { detectedValue = params._id; } }); FlowRouter.go(path); Meteor.setTimeout(() => { test.equal(detectedValue, value); test.equal(FlowRouter.current().path, path); next(); }, 50); }); Tinytest.addAsync('Client - Router - subscribe to global subs', (test, next) => { const rand = Random.id(); FlowRouter.route('/' + rand); FlowRouter.subscriptions = function (path) { test.equal(path, '/' + rand); this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); setTimeout(() => { test.isTrue(!!GetSub('baz')); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - setParams - generic', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}/:cat/:id`; const paramsList = []; FlowRouter.route(pathDef, { action(params) { paramsList.push(params); } }); FlowRouter.go(pathDef, {cat: 'meteor', id: '200'}); setTimeout(() => { // return done(); const success = FlowRouter.setParams({id: '700'}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(paramsList.length, 2); test.equal({ id: (paramsList[0] || {}).id, cat: (paramsList[0] || {}).cat }, {cat: 'meteor', id: '200'}); test.equal({ id: (paramsList[1] || {}).id, cat: (paramsList[1] || {}).cat }, {cat: 'meteor', id: '700'}); done(); } }); Tinytest.addAsync('Client - Router - setParams - preserve query strings', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}/:cat/:id`; const paramsList = []; const queryParamsList = []; FlowRouter.route(pathDef, { action: function(params, queryParams) { paramsList.push(params); queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {cat: 'meteor', id: '200 +% / ad'}, {aa: '20 +%'}); setTimeout(function() { // return done(); const success = FlowRouter.setParams({id: '700 +% / ad'}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(paramsList.length, 2, 'paramsList.length'); test.equal(queryParamsList.length, 2, 'queryParamsList.length'); test.equal({ id: (paramsList[0] || {}).id, cat: (paramsList[0] || {}).cat }, {cat: 'meteor', id: '200 +% / ad'}); test.equal({ id: (paramsList[1] || {}).id, cat: (paramsList[1] || {}).cat }, {cat: 'meteor', id: '700 +% / ad'}); test.equal(queryParamsList, [{aa: '20 +%'}, {aa: '20 +%'}]); done(); } }); Tinytest.add('Client - Router - setParams - no route selected', (test) => { const originalRoute = FlowRouter._current.route; FlowRouter._current.route = undefined; const success = FlowRouter.setParams({id: '800'}); test.isFalse(success); FlowRouter._current.route = originalRoute; }); Tinytest.addAsync('Client - Router - setQueryParams - using check', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}`; const queryParamsList = []; FlowRouter.route(pathDef, { action(params, queryParams) { queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {}, {cat: 'meteor', id: '200'}); setTimeout(() => { test.equal(FlowRouter.current().queryParams, {cat: 'meteor', id: '200'}); done(); }, 50); }); Tinytest.addAsync('Client - Router - setQueryParams - generic', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}`; const queryParamsList = []; FlowRouter.route(pathDef, { action: function(params, queryParams) { queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {}, {cat: 'meteor', id: '200'}); setTimeout(() => { // return done(); const success = FlowRouter.setQueryParams({id: '700'}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(queryParamsList.length, 2); test.equal({ id: (queryParamsList[0] || {}).id, cat: (queryParamsList[0] || {}).cat }, {cat: 'meteor', id: '200'}); test.equal({ id: (queryParamsList[1] || {}).id, cat: (queryParamsList[1] || {}).cat }, {cat: 'meteor', id: '700'}); done(); } }); Tinytest.addAsync('Client - Router - setQueryParams - remove query param null', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}`; const queryParamsList = []; FlowRouter.route(pathDef, { action(params, queryParams) { queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {}, {cat: 'meteor', id: '200'}); setTimeout(() => { const success = FlowRouter.setQueryParams({id: '700', cat: null}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(queryParamsList.length, 2); test.equal({ id: (queryParamsList[0] || {}).id, cat: (queryParamsList[0] || {}).cat }, {cat: 'meteor', id: '200'}); test.equal(queryParamsList[1], {id: '700'}); done(); } }); Tinytest.addAsync('Client - Router - setQueryParams - remove query param undefined', (test, done) => { const randomKey = Random.id(); const pathDef = `/${randomKey}`; const queryParamsList = []; FlowRouter.route(pathDef, { action(params, queryParams) { queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {}, {cat: 'meteor', id: '200'}); setTimeout(() => { const success = FlowRouter.setQueryParams({id: '700', cat: undefined}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(queryParamsList.length, 2); test.equal({ id: (queryParamsList[0] || {}).id, cat: (queryParamsList[0] || {}).cat }, {cat: 'meteor', id: '200'}); test.equal(queryParamsList[1], {id: '700'}); done(); } }); Tinytest.addAsync('Client - Router - setQueryParams - preserve params', (test, done) => { const randomKey = Random.id(); const pathDef = '/' + randomKey + '/:abc'; const queryParamsList = []; const paramsList = []; FlowRouter.route(pathDef, { action(params, queryParams) { paramsList.push(params); queryParamsList.push(queryParams); } }); FlowRouter.go(pathDef, {abc: '20'}, {cat: 'meteor', id: '200'}); setTimeout(() => { // return done(); const success = FlowRouter.setQueryParams({id: '700'}); test.isTrue(success); setTimeout(validate, 50); }, 50); function validate() { test.equal(queryParamsList.length, 2); test.equal(queryParamsList, [ {cat: 'meteor', id: '200'}, {cat: 'meteor', id: '700'} ]); test.equal(paramsList.length, 2); test.equal({ abc: (paramsList[0] || {}).abc }, {abc: '20'}); test.equal({ abc: (paramsList[1] || {}).abc }, {abc: '20'}); done(); } }); Tinytest.add('Client - Router - setQueryParams - no route selected', (test) => { const originalRoute = FlowRouter._current.route; FlowRouter._current.route = undefined; const success = FlowRouter.setQueryParams({id: '800'}); test.isFalse(success); FlowRouter._current.route = originalRoute; }); Tinytest.addAsync('Client - Router - notFound', (test, done) => { const data = []; FlowRouter.route('*', { subscriptions() { data.push('subscriptions'); }, action() { data.push('action'); } }); FlowRouter.go('/' + Random.id()); setTimeout(() => { test.equal(data, ['subscriptions', 'action']); done(); }, 50); }); Tinytest.addAsync('Client - Router - withReplaceState - enabled', (test, done) => { const pathDef = '/' + Random.id() + '/:id'; const name = Random.id(); const originalReplace = FlowRouter._microRouter.replace.bind(FlowRouter._microRouter); let callCount = 0; FlowRouter._microRouter.replace = function(path) { callCount++; originalReplace(path); }; FlowRouter.route(pathDef, { name: name, action(params) { test.equal(params.id, 'awesome'); test.equal(callCount, 1); FlowRouter._microRouter.replace = originalReplace; // We don't use Meteor.defer here since it carries // Meteor.Environment vars too // Which breaks our test below setTimeout(done, 0); } }); FlowRouter.withReplaceState(function() { FlowRouter.go(pathDef, {id: 'awesome'}); }); }); Tinytest.addAsync('Client - Router - withReplaceState - disabled', (test, done) => { const pathDef = '/' + Random.id() + '/:id'; const name = Random.id(); const originalReplace = FlowRouter._microRouter.replace.bind(FlowRouter._microRouter); let callCount = 0; FlowRouter._microRouter.replace = function(path) { callCount++; originalReplace(path); }; FlowRouter.route(pathDef, { name: name, action(params) { test.equal(params.id, 'awesome'); test.equal(callCount, 0); FlowRouter._microRouter.replace = originalReplace; Meteor.defer(done); } }); FlowRouter.go(pathDef, {id: 'awesome'}); }); Tinytest.addAsync('Client - Router - withTrailingSlash - enabled', (test, next) => { const rand = Random.id(); let rendered = 0; FlowRouter.route('/' + rand, { action() { rendered++; } }); FlowRouter.withTrailingSlash(function() { FlowRouter.go('/' + rand); }); setTimeout(() => { test.equal(rendered, 1); test.equal(location.href[location.href.length - 1], '/'); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Router - idempotent routing - action', (test, done) => { const rand = Random.id(); const pathDef = `/${rand}`; let rendered = 0; FlowRouter.route(pathDef, { action() { rendered++; } }); FlowRouter.go(pathDef); Meteor.defer(() => { FlowRouter.go(pathDef); Meteor.defer(() => { test.equal(rendered, 1); done(); }); }); }); Tinytest.addAsync('Client - Router - idempotent routing - triggers', (test, next) => { const rand = Random.id(); const pathDef = `/${rand}`; let runnedTriggers = 0; let done = false; const triggerFns = [function() { if (done) return; runnedTriggers++; }]; FlowRouter.triggers.enter(triggerFns); FlowRouter.route(pathDef, { triggersEnter: triggerFns, triggersExit: triggerFns }); FlowRouter.go(pathDef); FlowRouter.triggers.exit(triggerFns); Meteor.defer(() => { FlowRouter.go(pathDef); Meteor.defer(() => { test.equal(runnedTriggers, 2); done = true; next(); }); }); }); Tinytest.addAsync('Client - Router - reload - action', (test, done) => { const rand = Random.id(); const pathDef = `/${rand}`; let rendered = 0; FlowRouter.route(pathDef, { action() { rendered++; } }); FlowRouter.go(pathDef); Meteor.defer(() => { FlowRouter.reload(); Meteor.defer(() => { test.equal(rendered, 2); done(); }); }); }); Tinytest.addAsync('Client - Router - reload - triggers', (test, next) => { const rand = Random.id(); const pathDef = `/${rand}`; let runnedTriggers = 0; let done = false; const triggerFns = [function() { if (done) return; runnedTriggers++; }]; FlowRouter.triggers.enter(triggerFns); FlowRouter.route(pathDef, { triggersEnter: triggerFns, triggersExit: triggerFns }); FlowRouter.go(pathDef); FlowRouter.triggers.exit(triggerFns); Meteor.defer(() => { FlowRouter.reload(); Meteor.defer(() => { test.equal(runnedTriggers, 6); done = true; next(); }); }); }); Tinytest.addAsync('Client - Router - wait - before initialize', (test, done) => { FlowRouter._initialized = false; FlowRouter.wait(); test.equal(FlowRouter._askedToWait, true); FlowRouter._initialized = true; FlowRouter._askedToWait = false; done(); }); Tinytest.addAsync('Client - Router - wait - after initialized', (test, done) => { try { FlowRouter.wait(); } catch(ex) { test.isTrue(/can't wait/.test(ex.message)); done(); } }); Tinytest.addAsync('Client - Router - initialize - after initialized', (test, done) => { try { FlowRouter.initialize(); } catch(ex) { test.isTrue(/already initialized/.test(ex.message)); done(); } }); Tinytest.addAsync('Client - Router - base path - url updated', (test, done) => { const simulatedBasePath = '/flow'; const rand = Random.id(); FlowRouter.route('/' + rand, { action() {} }); setBasePath(simulatedBasePath); FlowRouter.go('/' + rand); setTimeout(() => { test.equal(location.pathname, simulatedBasePath + '/' + rand); resetBasePath(); done(); }, 100); }); Tinytest.addAsync('Client - Router - base path - route action called', (test, done) => { const simulatedBasePath = '/flow'; const rand = Random.id(); FlowRouter.route('/' + rand, { action() { resetBasePath(); done(); } }); setBasePath(simulatedBasePath); FlowRouter.go('/' + rand); }); Tinytest.add('Client - Router - base path - path generation', (test) => { ['/flow', '/flow/', 'flow/', 'flow'].forEach((simulatedBasePath) => { const rand = Random.id(); setBasePath(simulatedBasePath); test.equal(FlowRouter.path('/' + rand), '/flow/' + rand); }); resetBasePath(); }); Tinytest.add('Client - Router - base path - url generation', (test) => { ['/flow', '/flow/', 'flow/', 'flow'].forEach((simulatedBasePath) => { const rand = Random.id(); setBasePath(simulatedBasePath); Meteor.absoluteUrl.defaultOptions.rootUrl = 'http://example.com/flow'; test.equal(FlowRouter.url(`/${rand}`), 'http://example.com/flow/' + rand); }); resetBasePath(); }); function setBasePath(path) { FlowRouter._initialized = false; FlowRouter._basePath = path; FlowRouter.initialize(); } const defaultBasePath = FlowRouter._basePath; function resetBasePath() { setBasePath(defaultBasePath); } // function bind(obj, method) { // return function() { // obj[method].apply(obj, arguments); // }; // } ================================================ FILE: test/client/router.reactivity.spec.js ================================================ import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync( 'Client - Router - Reactivity - detectChange only once', function (test, done) { var route = "/" + Random.id(); var name = Random.id(); FlowRouter.route(route, {name: name}); var ranCount = 0; var pickedId = null; var c = Tracker.autorun(function() { ranCount++; pickedId = FlowRouter.getQueryParam("id"); if (pickedId) { test.equal(pickedId, "hello"); test.equal(ranCount, 2); c.stop(); Meteor.defer(done); } }); setTimeout(function() { FlowRouter.go(name, {}, {id: "hello"}); }, 2); }); Tinytest.addAsync( 'Client - Router - Reactivity - detectChange in the action', function (test, done) { var route = "/" + Random.id(); var name = Random.id(); FlowRouter.route(route, { name: name, action: function() { var id = FlowRouter.getQueryParam("id"); test.equal(id, "hello"); Meteor.defer(done); } }); setTimeout(function() { FlowRouter.go(name, {}, {id: "hello"}); }, 2); }); Tinytest.addAsync( 'Client - Router - Reactivity - detect prev routeChange after new action', function (test, done) { var route1 = "/" + Random.id(); var name1 = Random.id(); var pickedName1 = null; var route2 = "/" + Random.id(); var name2 = Random.id(); var pickedName2 = Random.id(); FlowRouter.route(route1, { name: name1, action: function() { Tracker.autorun(function(c) { pickedName1 = FlowRouter.getRouteName(); if(pickedName1 == name2) { test.equal(pickedName1, pickedName2); c.stop(); Meteor.defer(done); } }); } }); FlowRouter.route(route2, { name: name2, action: function() { pickedName2 = FlowRouter.getRouteName(); test.equal(pickedName1, name1); test.equal(pickedName2, name2); } }); FlowRouter.go(name1); Meteor.setTimeout(function() { FlowRouter.go(name2); }, 10); }); Tinytest.addAsync( 'Client - Router - Reactivity - defer watchPathChange until new route rendered', function(test, done) { var route1 = "/" + Random.id(); var name1 = Random.id(); var pickedName1 = null; var route2 = "/" + Random.id(); var name2 = Random.id(); var pickedName2 = Random.id(); FlowRouter.route(route1, { name: name1, action: function() { Tracker.autorun(function(c) { FlowRouter.watchPathChange(); pickedName1 = FlowRouter.current().route.name; if(pickedName1 == name2) { test.equal(pickedName1, pickedName2); c.stop(); Meteor.defer(done); } }); } }); FlowRouter.route(route2, { name: name2, action: function() { pickedName2 = FlowRouter.current().route.name; test.equal(pickedName1, name1); test.equal(pickedName2, name2); } }); FlowRouter.go(name1); Meteor.setTimeout(function() { FlowRouter.go(name2); }, 10); }); Tinytest.addAsync( 'Client - Router - Reactivity - reactive changes and trigger redirects', function(test, done) { var name1 = Random.id(); var route1 = "/" + name1; FlowRouter.route(route1, { name: name1 }); var name2 = Random.id(); var route2 = "/" + name2; FlowRouter.route(route2, { name: name2, triggersEnter: [function(context, redirect) { redirect(name3); }] }); var name3 = Random.id(); var route3 = "/" + name3; FlowRouter.route(route3, { name: name3 }); var routeNamesFired = []; FlowRouter.go(name1); var c = null; setTimeout(function() { c = Tracker.autorun(function(c) { routeNamesFired.push(FlowRouter.getRouteName()); }); FlowRouter.go(name2); }, 50); setTimeout(function() { c.stop(); test.equal(routeNamesFired, [name1, name3]); Meteor.defer(done); }, 250); }); Tinytest.addAsync( 'Client - Router - Reactivity - watchPathChange for every route change', function(test, done) { var route1 = "/" + Random.id(); var name1 = Random.id(); var pickedName1 = null; var route2 = "/" + Random.id(); var name2 = Random.id(); var pickedName2 = Random.id(); FlowRouter.route(route1, { name: name1 }); FlowRouter.route(route2, { name: name2 }); var ids = []; var c = Tracker.autorun(function() { FlowRouter.watchPathChange(); ids.push(FlowRouter.current().queryParams['id']); }); FlowRouter.go(name1, {}, {id: "one"}); Meteor.setTimeout(function() { FlowRouter.go(name1, {}, {id: "two"}); }, 10); Meteor.setTimeout(function() { FlowRouter.go(name2, {}, {id: "three"}); }, 20); Meteor.setTimeout(function() { test.equal(ids, [undefined, "one", "two", "three"]); c.stop(); done(); }, 40); }); ================================================ FILE: test/client/router.subs_ready.spec.js ================================================ import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync('Client - Router - subsReady - with no args - all subscriptions ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('foo', Meteor.subscribe('foo')); } }); FlowRouter.subscriptions = function () { this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); Tracker.autorun(function(c) { if(FlowRouter.subsReady()) { FlowRouter.subscriptions = Function.prototype; next(); c.stop(); } }); }); Tinytest.addAsync('Client - Router - subsReady - with no args - all subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('fooNotReady', Meteor.subscribe('fooNotReady')); } }); FlowRouter.subscriptions = function () { this.register('bazNotReady', Meteor.subscribe('bazNotReady')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady()); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with no args - global subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('foo', Meteor.subscribe('foo')); } }); FlowRouter.subscriptions = function () { this.register('bazNotReady', Meteor.subscribe('bazNotReady')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady()); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with no args - current subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('fooNotReady', Meteor.subscribe('fooNotReady')); } }); FlowRouter.subscriptions = function () { this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady()); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with args - all subscriptions ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('foo', Meteor.subscribe('foo')); } }); FlowRouter.subscriptions = function () { this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); Tracker.autorun(function(c) { if(FlowRouter.subsReady('foo', 'baz')) { FlowRouter.subscriptions = Function.prototype; next(); c.stop(); } }); }); Tinytest.addAsync('Client - Router - subsReady - with args - all subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('fooNotReady', Meteor.subscribe('fooNotReady')); } }); FlowRouter.subscriptions = function () { this.register('bazNotReady', Meteor.subscribe('bazNotReady')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady('fooNotReady', 'bazNotReady')); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with args - global subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('foo', Meteor.subscribe('foo')); } }); FlowRouter.subscriptions = function () { this.register('bazNotReady', Meteor.subscribe('bazNotReady')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady('foo', 'bazNotReady')); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with args - current subscriptions does not ready', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); this.register('fooNotReady', Meteor.subscribe('fooNotReady')); } }); FlowRouter.subscriptions = function () { this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady('fooNotReady', 'baz')); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with args - subscribe with wrong name', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, { subscriptions: function(params) { this.register('bar', Meteor.subscribe('bar')); } }); FlowRouter.subscriptions = function () { this.register('baz', Meteor.subscribe('baz')); }; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(!FlowRouter.subsReady('baz', 'xxx', 'baz')); FlowRouter.subscriptions = Function.prototype; next(); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - with args - same route two different subs', function (test, next) { var rand = Random.id(); var count = 0; FlowRouter.route('/' + rand, { subscriptions: function(params) { if(++count == 1) { this.register('not-exisitng', Meteor.subscribe('not-exisitng')); } } }); FlowRouter.subscriptions = Function.prototype; FlowRouter.go('/' + rand); setTimeout(function() { test.isFalse(FlowRouter.subsReady()); FlowRouter.go('/' + rand, {}, {param: "111"}); setTimeout(function() { test.isTrue(FlowRouter.subsReady()); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Router - subsReady - no subscriptions - simple', function (test, next) { var rand = Random.id(); FlowRouter.route('/' + rand, {}); FlowRouter.subscriptions = Function.prototype; FlowRouter.go('/' + rand); setTimeout(function() { test.isTrue(FlowRouter.subsReady()); next(); }, 100); }); ================================================ FILE: test/client/trigger.spec.js ================================================ import { FlowRouter, Route } from 'meteor/ostrio:flow-router-extra'; import { Random } from 'meteor/random'; Tinytest.addAsync('Client - Triggers - global enter triggers', function(test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var paths = ['/' + rand2, '/' + rand]; var done = false; FlowRouter.route('/' + rand, { action: function(_params) { log.push(1); } }); FlowRouter.route('/' + rand2, { action: function(_params) { log.push(2); } }); FlowRouter.triggers.enter([function(context) { if(done) return; test.equal(context.path, paths.pop()); log.push(0); }]); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [0, 1, 0, 2]); done = true; setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - global enter triggers with "only"', function (test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var done = false; FlowRouter.route('/' + rand, { action: function(_params) { log.push(1); } }); FlowRouter.route('/' + rand2, { name: 'foo', action: function(_params) { log.push(2); } }); FlowRouter.triggers.enter([function(context) { if(done) return; test.equal(context.path, '/' + rand2); log.push(8); }], {only: ['foo']}); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [1, 8, 2]); done = true; setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - global enter triggers with "except"', function (test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var done = false; FlowRouter.route('/' + rand, { action: function(_params) { log.push(1); } }); FlowRouter.route('/' + rand2, { name: 'foo', action: function(_params) { log.push(2); } }); FlowRouter.triggers.enter([function(context) { if(done) return; test.equal(context.path, '/' + rand); log.push(8); }], {except: ['foo']}); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [8, 1, 2]); done = true; setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - global exit triggers', function (test, next) { const rand = Random.id(); const rand2 = Random.id(); const log = []; let done = false; FlowRouter.route('/' + rand, { action() { log.push(1); } }); FlowRouter.route('/' + rand2, { action() { log.push(2); } }); FlowRouter.go('/' + rand); FlowRouter.triggers.exit([function(context) { if (done) return; test.equal(context.path, '/' + rand); log.push(0); }]); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [1, 0, 2]); done = true; setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - global exit triggers with "only"', function (test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var done = false; FlowRouter.route('/' + rand, { action: function(_params) { log.push(1); } }); FlowRouter.route('/' + rand2, { name: 'foo', action: function(_params) { log.push(2); } }); FlowRouter.triggers.exit([function(context) { if(done) return; test.equal(context.path, '/' + rand2); log.push(8); }], {only: ['foo']}); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [1, 2, 8, 1]); done = true; setTimeout(next, 100); }, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - global exit triggers with "except"', function (test, next) { const rand = Random.id(); const rand2 = Random.id(); const log = []; let done = false; FlowRouter.route('/' + rand, { action() { log.push(1); } }); FlowRouter.route('/' + rand2, { name: 'foo', action() { log.push(2); } }); FlowRouter.go('/' + rand); FlowRouter.triggers.exit([function(context) { if (done) return; test.equal(context.path, '/' + rand); log.push(9); }], {except: ['foo']}); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [1, 9, 2, 1]); done = true; setTimeout(next, 100); }, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - route enter triggers', function (test, next) { var rand = Random.id(); var log = []; var triggerFn = function (context) { test.equal(context.path, '/' + rand); log.push(5); }; FlowRouter.route('/' + rand, { triggersEnter: [triggerFn], action: function(_params) { log.push(1); } }); FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [5, 1]); setTimeout(next, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - router exit triggers', function (test, next) { var rand = Random.id(); var log = []; var triggerFn = function (context) { test.equal(context.path, '/' + rand); log.push(6); }; FlowRouter.route('/' + rand, { triggersExit: [triggerFn], action: function(_params) { log.push(1); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + Random.id()); setTimeout(function() { test.equal(log, [1, 6]); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - group enter triggers', function (test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var paths = ['/' + rand2, '/' + rand]; var triggerFn = function (context) { test.equal(context.path, paths.pop()); log.push(3); }; var group = FlowRouter.group({ triggersEnter: [triggerFn] }); group.route('/' + rand, { action: function(_params) { log.push(1); } }); group.route('/' + rand2, { action: function(_params) { log.push(2); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [3, 1, 3, 2]); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - group exit triggers', function (test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var triggerFn = function (context) { log.push(4); }; var group = FlowRouter.group({ triggersExit: [triggerFn] }); group.route('/' + rand, { action: function(_params) { log.push(1); } }); group.route('/' + rand2, { action: function(_params) { log.push(2); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [1, 4, 2]); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group enter triggers', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersEnter: function (context) { log.push('root'); } }); var group = rootGroup.group({ triggersEnter: [function (context) { log.push('group'); }] }); let route = group.route('/' + rand, { action: function(_params) { log.push('route'); }, triggersEnter: function (context) { test.equal(context.path, paths.pop()); log.push('route trigger'); } }); setTimeout(function() { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, ['root', 'group', 'route trigger', 'route']); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group enter triggers no route triggers', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersEnter: [function (context) { log.push('root'); }] }); var group = rootGroup.group({ triggersEnter: function (context) { log.push('group'); } }); group.route('/' + rand, { action: function(_params) { log.push('route'); } }); setTimeout(function() { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, ['root', 'group', 'route']); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group enter triggers, no route action', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersEnter: function (context) { log.push('root'); } }); var group = rootGroup.group({ triggersEnter: [function (context) { log.push('group'); }] }); group.route('/' + rand, { triggersEnter: [function (context) { test.equal(context.path, paths.pop()); log.push('route trigger'); }] }); setTimeout(function() { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, ['root', 'group', 'route trigger']); setTimeout(next, 100); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group exit triggers', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersExit: function (context) { log.push('root'); } }); var group = rootGroup.group({ triggersExit: [function (context) { log.push('group'); }] }); let route = group.route('/' + rand, { action: function(_params) { log.push('route'); }, triggersExit: function (context) { test.equal(context.path, paths.pop()); log.push('route trigger'); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/'); setTimeout(function() { test.equal(log, ['route', 'route trigger', 'group', 'root']); setTimeout(next, 150); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group exit triggers no route triggers', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersExit: [function (context) { log.push('root'); }] }); var group = rootGroup.group({ triggersExit: function (context) { log.push('group'); } }); group.route('/' + rand, { action: function(_params) { log.push('route'); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/'); setTimeout(function() { test.equal(log, ['route', 'group', 'root']); setTimeout(next, 150); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - nested group exit triggers, no route action', function (test, next) { var rand = Random.id(); var log = []; var paths = ['/' + rand]; var rootGroup = FlowRouter.group({ triggersExit: function (context) { log.push('root'); } }); var group = rootGroup.group({ triggersExit: [function (context) { log.push('group'); }] }); group.route('/' + rand, { triggersExit: [function (context) { test.equal(context.path, paths.pop()); log.push('route trigger'); }] }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/'); setTimeout(function() { test.equal(log, ['route trigger', 'group', 'root']); setTimeout(next, 150); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - redirect from enter', function(test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; FlowRouter.route('/' + rand, { triggersEnter: [function(context, redirect) { redirect("/" + rand2); }, function() { throw new Error("should not execute this trigger"); }], action: function(_params) { log.push(1); }, name: rand }); FlowRouter.route('/' + rand2, { action: function(_params) { log.push(2); }, name: rand2 }); FlowRouter.go('/'); FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [2]); next(); }, 300); }); Tinytest.addAsync('Client - Triggers - redirect by routeName', function(test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; FlowRouter.route('/' + rand, { name: rand, triggersEnter: [function(context, redirect) { redirect(rand2, null, {aa: "bb"}); }, function() { throw new Error("should not execute this trigger"); }], action: function(_params) { log.push(1); }, name: rand }); FlowRouter.route('/' + rand2, { name: rand2, action: function(_params, queryParams) { log.push(2); test.equal(queryParams, {aa: "bb"}); }, name: rand2 }); FlowRouter.go('/'); FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [2]); next(); }, 300); }); Tinytest.addAsync('Client - Triggers - redirect from exit', function(test, next) { var rand = Random.id(), rand2 = Random.id(), rand3 = Random.id(); var log = []; FlowRouter.route('/' + rand, { action: function() { log.push(1); }, triggersExit: [ function(context, redirect) { redirect('/' + rand3); }, function() { throw new Error("should not call this trigger"); } ] }); FlowRouter.route('/' + rand2, { action: function() { log.push(2); } }); FlowRouter.route('/' + rand3, { action: function() { log.push(3); } }); FlowRouter.go('/' + rand); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [1, 3]); next(); }, 100); }, 100); }); Tinytest.addAsync('Client - Triggers - redirect to external URL fails', function(test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; // testing "http://" URLs FlowRouter.route('/' + rand, { triggersEnter: [function(context, redirect) { test.throws(function() { redirect("http://example.com/"); }, "Redirects to URLs outside of the app are not supported"); }], action: function(_params) { log.push(1); }, name: rand }); // testing "https://" URLs FlowRouter.route('/' + rand2, { triggersEnter: [function(context, redirect) { test.throws(function() { redirect("https://example.com/"); }) }], action: function(_params) { log.push(2); }, name: rand2 }); FlowRouter.go('/'); FlowRouter.go('/' + rand); FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, []); next(); }, 300); }); Tinytest.addAsync('Client - Triggers - stop callback from enter', function(test, next) { var rand = Random.id(); var log = []; FlowRouter.route('/' + rand, { triggersEnter: [function(context, redirect, stop) { log.push(10); stop(); }, function() { throw new Error("should not execute this trigger"); }], action: function(_params) { throw new Error("should not execute the action"); } }); FlowRouter.go('/'); FlowRouter.go('/' + rand); setTimeout(function() { test.equal(log, [10]); next(); }, 100); }); Tinytest.addAsync( 'Client - Triggers - invalidate inside an autorun', function(test, next) { var rand = Random.id(), rand2 = Random.id(); var log = []; var paths = ['/' + rand2, '/' + rand]; var done = false; FlowRouter.route('/' + rand, { action: function(_params) { log.push(1); } }); FlowRouter.route('/' + rand2, { action: function(_params) { log.push(2); } }); FlowRouter.triggers.enter([function(context) { if(done) return; test.equal(context.path, paths.pop()); log.push(0); }]); Tracker.autorun(function(c) { FlowRouter.go('/' + rand); }); setTimeout(function() { FlowRouter.go('/' + rand2); setTimeout(function() { test.equal(log, [0, 1, 0, 2]); done = true; setTimeout(next, 100); }, 100); }, 100); }); ================================================ FILE: test/client/triggers.js ================================================ import { Triggers } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync( 'Triggers - runTriggers - run all and after', function(test, done) { var store = []; var triggers = MakeTriggers(2, store); Triggers.runTriggers(triggers, null, null, function() { test.equal(store, [0, 1]); done(); }); }); Tinytest.addAsync( 'Triggers - runTriggers - redirect with url', function(test, done) { var store = []; var url = "http://google.com"; var triggers = MakeTriggers(2, store); triggers.splice(1, 0, function(context, redirect) { redirect(url); }); Triggers.runTriggers(triggers, null, function(u) { test.equal(store, [0]); test.equal(u, url); done(); }, null); }); Tinytest.addAsync( 'Triggers - runTriggers - redirect without url', function(test, done) { var store = []; var url = "http://google.com"; var triggers = MakeTriggers(2, store); triggers.splice(1, 0, function(context, redirect) { try { redirect(); } catch(ex) { test.isTrue(/requires an URL/.test(ex.message)); test.equal(store, [0]); done(); } }); Triggers.runTriggers(triggers, null, null, null); }); Tinytest.addAsync( 'Triggers - runTriggers - redirect in a different event loop', function(test, done) { var store = []; var url = "http://google.com"; var triggers = MakeTriggers(2, store); var doneCalled = false; triggers.splice(1, 0, function(context, redirect) { setTimeout(function() { try { redirect(url); } catch(ex) { test.isTrue(/sync/.test(ex.message)); test.equal(store, [0, 1]); test.isTrue(doneCalled); done(); } }, 0); }); Triggers.runTriggers(triggers, null, null, function() { doneCalled = true; }); }); Tinytest.addAsync( 'Triggers - runTriggers - redirect called multiple times', function(test, done) { var store = []; var url = "http://google.com"; var triggers = MakeTriggers(2, store); var redirectCalled = false; triggers.splice(1, 0, function(context, redirect) { redirect(url); try { redirect(url); } catch(ex) { test.isTrue(/already redirected/.test(ex.message)); test.equal(store, [0]); test.isTrue(redirectCalled); done(); } }); Triggers.runTriggers(triggers, null, function() { redirectCalled = true; }, null); }); Tinytest.addAsync( 'Triggers - runTriggers - stop callback', function(test, done) { var store = []; var triggers = MakeTriggers(2, store); triggers.splice(1, 0, function(context, redirect, stop) { stop(); }); Triggers.runTriggers(triggers, null, null, function() { store.push(2); }); test.equal(store, [0]); done(); }); Tinytest.addAsync( 'Triggers - runTriggers - get context', function(test, done) { var context = {}; var trigger = function(c) { test.equal(c, context); done(); }; Triggers.runTriggers([trigger], context, function() {}, function() {}); }); Tinytest.addAsync( 'Triggers - createRouteBoundTriggers - matching trigger', function(test, done) { var context = {route: {name: "abc"}}; var redirect = function() {}; var trigger = function(c, r) { test.equal(c, context); test.equal(r, redirect); done(); }; var triggers = Triggers.createRouteBoundTriggers([trigger], ["abc"]); triggers[0](context, redirect); }); Tinytest.addAsync( 'Triggers - createRouteBoundTriggers - multiple matching triggers', function(test, done) { var context = {route: {name: "abc"}}; var redirect = function() {}; var doneCount = 0; var trigger = function(c, r) { test.equal(c, context); test.equal(r, redirect); doneCount++; }; var triggers = Triggers.createRouteBoundTriggers([trigger, trigger], ["abc"]); triggers[0](context, redirect); triggers[1](context, redirect); test.equal(doneCount, 2); done(); }); Tinytest.addAsync( 'Triggers - createRouteBoundTriggers - no matching trigger', function(test, done) { var context = {route: {name: "some-other-route"}}; var redirect = function() {}; var doneCount = 0; var trigger = function(c, r) { test.equal(c, context); test.equal(r, redirect); doneCount++; }; var triggers = Triggers.createRouteBoundTriggers([trigger], ["abc"]); triggers[0](context, redirect); test.equal(doneCount, 0); done(); }); Tinytest.addAsync( 'Triggers - createRouteBoundTriggers - negate logic', function(test, done) { var context = {route: {name: "some-other-route"}}; var redirect = function() {}; var doneCount = 0; var trigger = function(c, r) { test.equal(c, context); test.equal(r, redirect); doneCount++; }; var triggers = Triggers.createRouteBoundTriggers([trigger], ["abc"], true); triggers[0](context, redirect); test.equal(doneCount, 1); done(); }); Tinytest.addAsync( 'Triggers - applyFilters - no filters', function(test, done) { var original = []; test.equal(Triggers.applyFilters(original), original); done(); }); Tinytest.addAsync( 'Triggers - applyFilters - single trigger to array', function(test, done) { var original = function() {}; test.equal(Triggers.applyFilters(original)[0], original); done(); }); Tinytest.addAsync( 'Triggers - applyFilters - only and except both', function(test, done) { var original = []; try { Triggers.applyFilters(original, {only: [], except: []}); } catch(ex) { test.isTrue(/only and except/.test(ex.message)); done(); } }); Tinytest.addAsync( 'Triggers - applyFilters - only is not an array', function(test, done) { var original = []; try { Triggers.applyFilters(original, {only: "name"}); } catch(ex) { test.isTrue(/to be an array/.test(ex.message)); done(); } }); Tinytest.addAsync( 'Triggers - applyFilters - except is not an array', function(test, done) { var original = []; try { Triggers.applyFilters(original, {except: "name"}); } catch(ex) { test.isTrue(/to be an array/.test(ex.message)); done(); } }); Tinytest.addAsync( 'Triggers - applyFilters - unsupported filter', function(test, done) { var original = []; try { Triggers.applyFilters(original, {wowFilter: []}); } catch(ex) { test.isTrue(/not supported/.test(ex.message)); done(); } }); Tinytest.addAsync( 'Triggers - applyFilters - just only filter', function(test, done) { var bounded = Triggers.applyFilters(done, {only: ["abc"]}); bounded[0]({route: {name: "abc"}}); }); Tinytest.addAsync( 'Triggers - applyFilters - just except filter', function(test, done) { var bounded = Triggers.applyFilters(done, {except: ["abc"]}); bounded[0]({route: {name: "some-other"}}); }); function MakeTriggers(count, store) { var triggers = []; function addTrigger(no) { triggers.push(function() { store.push(no); }); } for(var lc = 0; lc < count; lc++) { addTrigger(lc); } return triggers; } ================================================ FILE: test/common/fast_render_route.js ================================================ import { Mongo } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; const FastRenderColl = new Mongo.Collection('fast-render-coll'); FlowRouter.route('/the-fast-render-route', { subscriptions() { if (Meteor.isServer) { this.register('data', Meteor.subscribe('fast-render-data')); } }, fastRender: true, waitOn() { if (Meteor.isClient) { return Meteor.subscribe('fast-render-data'); } }, }); FlowRouter.route('/the-fast-render-route-params/:id', { subscriptions(params, queryParams) { if (Meteor.isServer) { this.register('data', Meteor.subscribe('fast-render-data-params', params, queryParams)); } }, fastRender: true, waitOn(params, queryParams) { if (Meteor.isClient) { return Meteor.subscribe('fast-render-data-params', params, queryParams); } }, }); FlowRouter.route('/no-fast-render', { subscriptions() { if(Meteor.isClient) { this.register('data', Meteor.subscribe('fast-render-data')); } }, fastRender: true, waitOn() { if (Meteor.isClient) { return Meteor.subscribe('fast-render-data'); } } }); const frGroup = FlowRouter.group({ prefix: '/fr' }); frGroup.route('/have-fr', { subscriptions() { if (Meteor.isServer) { this.register('data', Meteor.subscribe('fast-render-data')); } }, fastRender: true, waitOn() { if (Meteor.isClient) { return Meteor.subscribe('fast-render-data'); } } }); export { FastRenderColl }; ================================================ FILE: test/common/group.spec.js ================================================ import { Random } from 'meteor/random'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Tinytest.add('Common - Group - expose group options', (test) => { const name = Random.id(); const data = {aa: 10}; const layout = 'blah'; const group = FlowRouter.group({ name: name, prefix: '/admin', layout: layout, someData: data }); test.equal(group.options.someData, data); test.equal(group.options.layout, layout); }); Tinytest.add('Common - Group - define route with nested prefix', (test) => { const firstPrefix = Random.id(); const secondPrefix = Random.id(); const routePath = Random.id(); const routeName = Random.id(); const firstGroup = FlowRouter.group({prefix: '/' + firstPrefix}); const secondGroup = firstGroup.group({prefix: '/' + secondPrefix}); secondGroup.route('/' + routePath, {name: routeName}); test.equal(FlowRouter.path(routeName), '/' + firstPrefix + '/' + secondPrefix + '/' + routePath); }); ================================================ FILE: test/common/route.spec.js ================================================ import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra'; Package['kadira:flow-router'] = Package['ostrio:flow-router-extra']; Tinytest.addAsync('Common - Route - expose route options', function (test, next) { var pathDef = '/' + Random.id(); var name = Random.id(); var data = {aa: 10}; FlowRouter.route(pathDef, { name: name, someData: data }); test.equal(FlowRouter._routesMap[name].options.someData, data); next(); }); ================================================ FILE: test/common/router.addons.spec.js ================================================ import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync('Common - Addons - onRouteRegister basic usage', function (test, done) { var name = Random.id(); var customField = Random.id(); var pathDef = '/' + name; FlowRouter.onRouteRegister(function(route) { test.equal(route, { pathDef: pathDef, // Route.path is deprecated and will be removed in 3.0 path: pathDef, name: name, options: {customField: customField} }); FlowRouter._onRouteCallbacks = []; done(); }); FlowRouter.route(pathDef, { name: name, action() {}, subscriptions() {}, triggersEnter() {}, triggersExit() {}, customField: customField }); }); ================================================ FILE: test/common/router.path.spec.js ================================================ import { FlowRouter, Router } from 'meteor/ostrio:flow-router-extra'; Tinytest.addAsync('Common - Router - validate path definition', function (test, next) { // path must start with '/' try { FlowRouter.route(Random.id()); } catch(ex) { next(); } }); Tinytest.add('Common - Router - path - generic', function (test) { var pathDef = "/blog/:blogId/some/:name"; var fields = { blogId: "1001", name: "superb" }; var expectedPath = "/blog/1001/some/superb"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - queryParams', function (test) { var pathDef = "/blog/:blogId/some/:name"; var fields = { blogId: "1001", name: "superb" }; var queryParams = { aa: "100", bb: "200" }; var expectedPath = "/blog/1001/some/superb?aa=100&bb=200"; var path = FlowRouter.path(pathDef, fields, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - just queryParams', function (test) { var pathDef = "/blog/abc"; var queryParams = { aa: "100", bb: "200" }; var expectedPath = "/blog/abc?aa=100&bb=200"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - query from pathDef', function (test) { var pathDef = "/blog/abc?aa=100&bb=200"; var expectedPath = "/blog/abc?aa=100&bb=200"; var path = FlowRouter.path(pathDef); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - hash from pathDef', function (test) { var pathDef = "/blog/abc#security"; var expectedPath = "/blog/abc#security"; var path = FlowRouter.path(pathDef); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - query and hash from pathDef', function (test) { var pathDef = "/blog/abc?aa=100#security"; var expectedPath = "/blog/abc?aa=100#security"; var path = FlowRouter.path(pathDef); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - merge query from pathDef and queryParams', function (test) { var pathDef = "/blog/abc?aa=100&bb=200"; var queryParams = { bb: "300", cc: "400" }; var expectedPath = "/blog/abc?aa=100&bb=300&cc=400"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - encode query values', function (test) { var pathDef = "/blog/abc"; var queryParams = { q: "flow router", amp: "a&b", eq: "x=y" }; var expectedPath = "/blog/abc?q=flow%20router&=a%26b&eq=x%3Dy"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - array query values', function (test) { var pathDef = "/blog/abc"; var queryParams = { tags: ["flow", "router"] }; var expectedPath = "/blog/abc?tags%5B0%5D=flow&tags%5B1%5D=router"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - nested query object values', function (test) { var pathDef = "/blog/abc"; var queryParams = { filter: { status: "active", owner: "meteor" } }; var expectedPath = "/blog/abc?filter%5Bstatus%5D=active&filter%5Bowner%5D=meteor"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - merge nested query from pathDef and queryParams', function (test) { var pathDef = "/blog/abc?filter%5Bstatus%5D=active"; var queryParams = { filter: { owner: "meteor" } }; var expectedPath = "/blog/abc?filter%5Bstatus%5D=active&filter%5Bowner%5D=meteor"; var path = FlowRouter.path(pathDef, null, queryParams); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - mixed scalar and array query values are stable', function (test) { var pathDef = "/blog/abc?a=1&a%5B0%5D=2"; var expectedPath = "/blog/abc?a%5B0%5D=1&a%5B1%5D=2"; var path = FlowRouter.path(pathDef); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - missing fields', function (test) { var pathDef = "/blog/:blogId/some/:name"; var fields = { blogId: "1001", }; var expectedPath = "/blog/1001/some"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - no fields', function (test) { var pathDef = "/blog/blogId/some/name"; var path = FlowRouter.path(pathDef); test.equal(path, pathDef); }); Tinytest.add('Common - Router - path - complex route', function (test) { var pathDef = "/blog/:blogId/some/:name(\\d*)+"; var fields = { blogId: "1001", name: 20 }; var expectedPath = "/blog/1001/some/20"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - optional last param missing', function (test) { var pathDef = "/blog/:blogId/some/:name?"; var fields = { blogId: "1001" }; var expectedPath = "/blog/1001/some"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - both optional last param missing', function (test) { var pathDef = "/blog/:id?/:action?"; var fields = { id: "6135cb32d14df059605901fd", action: '' }; var expectedPath = "/blog/6135cb32d14df059605901fd"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - both optional last param exists', function (test) { var pathDef = "/blog/:id?/:action?"; var fields = { id: "6135cb32d14df059605901fd", action: 'view' }; var expectedPath = "/blog/6135cb32d14df059605901fd/view"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - optional last param exists', function (test) { var pathDef = "/blog/:blogId/some/:name?"; var fields = { blogId: "1001", name: 20 }; var expectedPath = "/blog/1001/some/20"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - remove trailing slashes', function (test) { var pathDef = "/blog/:blogId/some/:name//"; var fields = { blogId: "1001", name: "superb" }; var expectedPath = "/blog/1001/some/superb"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - handle multiple slashes', function (test) { var pathDef = "/blog///some/hi////"; var expectedPath = "/blog/some/hi"; var path = FlowRouter.path(pathDef); test.equal(path, expectedPath); }); Tinytest.add('Common - Router - path - keep the root slash', function (test) { var pathDef = "/"; var fields = {}; var expectedPath = "/"; var path = FlowRouter.path(pathDef, fields); test.equal(path, expectedPath); }); ================================================ FILE: test/common/router.url.spec.js ================================================ import { Meteor } from 'meteor/meteor'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; Tinytest.add('Common - Router - url - generic', (test) => { const pathDef = '/blog/:blogId/some/:name'; const fields = { blogId: '1001', name: 'superb' }; const expectedUrl = Meteor.absoluteUrl('blog/1001/some/superb'); const path = FlowRouter.url(pathDef, fields); test.equal(path, expectedUrl); }); ================================================ FILE: test/server/_helpers.js ================================================ import { HTTP } from 'meteor/http'; import { Meteor } from 'meteor/meteor'; Package['ostrio:flow-router-extra'].FlowRouter.decodeQueryParamsOnce = true; Package['kadira:flow-router'] = Package['ostrio:flow-router-extra']; Meteor.publish('foo', function () { this.ready(); }); Meteor.publish('fooNotReady', function () { }); Meteor.publish('bar', function () { this.ready(); }); // use this only to test global subs Meteor.publish('baz', function () { this.ready(); }); Meteor.publish('bazNotReady', function () { }); Meteor.publish('readyness', function (doIt) { if(doIt) { this.ready(); } }); let GetFRData; if (Package['communitypackages:inject-data']) { const InjectData = Package['communitypackages:inject-data'].InjectData; const urlResolve = require('url').resolve; GetFRData = function (path) { const url = urlResolve(process.env.ROOT_URL, path); // FastRender only servers if there is a accept header with html in it const options = { headers: {'accept': 'html'} }; const res = HTTP.get(url, options); if (res.content) { const encodedData = res.content.match(/data">(.*)<\/script/); if (encodedData && encodedData[1]) { return InjectData._decode(encodedData[1])['fast-render-data']; } } return {collectionData: {'fast-render-coll': {}}}; }; } export { GetFRData }; ================================================ FILE: test/server/plugins/fast_render.js ================================================ import { Meteor } from 'meteor/meteor'; import { GetFRData } from '../_helpers.js'; import { FastRenderColl } from '../../common/fast_render_route.js'; if (!FastRenderColl.findOne()) { FastRenderColl.insert({_id: 'one', aa: 10}); FastRenderColl.insert({_id: 'two', aa: 20}); } Meteor.publish('fast-render-data', () => { return FastRenderColl.find({}, {sort: {aa: -1}}); }); Meteor.publish('fast-render-data-params', function (params, queryParams) { this.added('fast-render-coll', 'one', { params, queryParams }); this.ready(); }); Tinytest.add('Server - Fast Render - fast render supported route', (test) => { const expectedFastRenderCollData = [ [{_id: 'two', aa: 20}, {_id: 'one', aa: 10}] ]; const data = GetFRData('/the-fast-render-route'); test.equal(data.collectionData['fast-render-coll'], expectedFastRenderCollData); }); Tinytest.add('Server - Fast Render - fast render supported route with params', (test) => { const expectedFastRenderCollData = [ [{ _id: 'one', params: {id: 'the-id'}, queryParams: {aa: '20'} }] ]; const data = GetFRData('/the-fast-render-route-params/the-id?aa=20'); test.equal(data.collectionData['fast-render-coll'], expectedFastRenderCollData); }); Tinytest.add('Server - Fast Render - no fast render supported route', (test) => { const data = GetFRData('/no-fast-render'); test.equal(data.collectionData, {}); }); Tinytest.add('Server - Fast Render - with group routes', (test) => { const expectedFastRenderCollData = [ [{_id: 'two', aa: 20}, {_id: 'one', aa: 10}] ]; const data = GetFRData('/fr/have-fr'); test.equal(data.collectionData['fast-render-coll'], expectedFastRenderCollData); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "preserveSymlinks": true, "paths": { "meteor/*": [ "node_modules/@types/meteor/*", ".meteor/local/types/packages.d.ts" ] } } }