Repository: vaadin/vaadin-router
Branch: main
Commit: 74684792d0bd
Files: 222
Total size: 655.0 KB
Directory structure:
gitextract_suxs7xzw/
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── docs.yml
│ └── validation.yml
├── .gitignore
├── .prettierrc.json
├── .run/
│ ├── All tests.run.xml
│ └── Template Karma.run.xml
├── .stylelintrc.json
├── LICENSE
├── README.md
├── analysis.json
├── demo/
│ ├── .eslintrc.json
│ ├── @debug/
│ │ ├── index.html
│ │ └── index.ts
│ ├── @helpers/
│ │ ├── common.css
│ │ ├── common.ts
│ │ ├── iframe.script.ts
│ │ ├── nested-styles.css
│ │ ├── page.css
│ │ ├── shared-styles.css
│ │ ├── theme-controller.ts
│ │ ├── vaadin-demo-code-snippet-file.css
│ │ ├── vaadin-demo-code-snippet-file.ts
│ │ ├── vaadin-demo-code-snippet.css
│ │ ├── vaadin-demo-code-snippet.ts
│ │ ├── vaadin-demo-layout.css
│ │ ├── vaadin-demo-layout.ts
│ │ ├── vaadin-presentation-addressbar.css
│ │ ├── vaadin-presentation-addressbar.ts
│ │ ├── vaadin-presentation.css
│ │ ├── vaadin-presentation.ts
│ │ ├── x-breadcrumbs.ts
│ │ ├── x-home-view.ts
│ │ ├── x-image-view.css
│ │ ├── x-image-view.ts
│ │ ├── x-knowledge-base.ts
│ │ ├── x-login-view.ts
│ │ ├── x-not-found-view.ts
│ │ ├── x-profile-view.ts
│ │ ├── x-user-list.ts
│ │ ├── x-user-not-found-view.ts
│ │ ├── x-user-numeric-view.ts
│ │ └── x-user-profile.ts
│ ├── animated-transitions/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── styles.css
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ ├── styles.css
│ │ │ └── x-wrapper.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── code-splitting/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── user.bundle.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── user-routes.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── getting-started/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── index.html
│ │ ├── index.ts
│ │ └── snippets/
│ │ ├── s1.html
│ │ ├── s2.ts
│ │ └── s4.ts
│ ├── index.html
│ ├── index.ts
│ ├── lifecycle-callback/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-countdown.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-friend.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ ├── x-user-deleted.ts
│ │ │ └── x-user-manage.ts
│ │ ├── d4/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ ├── x-autosave-view.ts
│ │ │ └── x-main-page.ts
│ │ ├── d5/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d6/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── index.html
│ │ ├── index.ts
│ │ └── snippets/
│ │ ├── my-view-with-after-enter.ts
│ │ ├── my-view-with-after-leave.ts
│ │ ├── my-view-with-before-enter.ts
│ │ └── my-view-with-before-leave.ts
│ ├── navigation-trigger/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d4/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── redirect/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-admin-view.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── index.html
│ │ ├── index.ts
│ │ └── snippets/
│ │ ├── s1.ts
│ │ ├── s2.ts
│ │ └── s3.ts
│ ├── route-actions/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d4/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d5/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── route-parameters/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-project-view.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d4/
│ │ │ ├── iframe.html
│ │ │ └── script.ts
│ │ ├── d5/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-page-number-view.ts
│ │ ├── d6/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-hash-view.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── types.t.ts
│ ├── url-generation/
│ │ ├── d1/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-main-layout.ts
│ │ ├── d2/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-main-layout.ts
│ │ ├── d3/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-user-layout-d3.ts
│ │ ├── d4/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-user-layout-d4.ts
│ │ ├── d5/
│ │ │ ├── iframe.html
│ │ │ ├── script.ts
│ │ │ └── x-pages-menu.ts
│ │ ├── index.html
│ │ └── index.ts
│ └── vite.config.ts
├── index.html
├── karma.config.cjs
├── package.json
├── polymer.json
├── scripts/
│ ├── build.ts
│ ├── codeSnippet.ts
│ ├── constructCss.ts
│ ├── copy-dts.ts
│ ├── loadRegisterJs.ts
│ ├── register.js
│ ├── resolveHTMLImports.ts
│ └── types.d.ts
├── src/
│ ├── index.ts
│ ├── mod.t.ts
│ ├── resolver/
│ │ ├── LICENSE.txt
│ │ ├── generateUrls.ts
│ │ ├── matchPath.ts
│ │ ├── matchRoute.ts
│ │ ├── resolveRoute.ts
│ │ ├── resolver.ts
│ │ ├── types.t.ts
│ │ └── utils.ts
│ ├── router-config.ts
│ ├── router-meta.ts
│ ├── router.ts
│ ├── transitions/
│ │ └── animate.ts
│ ├── triggers/
│ │ ├── click.ts
│ │ ├── navigation.ts
│ │ └── popstate.ts
│ ├── types.t.ts
│ ├── utils.ts
│ └── v1-compat.t.ts
├── test/
│ ├── resolver/
│ │ ├── LICENSE.txt
│ │ ├── generateUrls.spec.ts
│ │ ├── matchPath.spec.ts
│ │ ├── matchRoute.spec.ts
│ │ └── resolver.spec.ts
│ ├── router/
│ │ ├── dynamic-redirect.spec.ts
│ │ ├── lifecycle-events.spec.ts
│ │ ├── parent-layout.spec.ts
│ │ ├── router.spec.ts
│ │ ├── test-utils.ts
│ │ └── url-for.spec.ts
│ ├── setup.ts
│ ├── transitions/
│ │ └── animate.spec.ts
│ ├── triggers/
│ │ ├── click.spec.ts
│ │ ├── popstate.spec.ts
│ │ └── setNavigationTriggers.spec.ts
│ └── typescript/
│ └── compile_fixture.ts
├── tsconfig.build.json
├── tsconfig.json
├── tsdoc.json
├── typedoc.json
├── vite.config.ts
└── wct.conf.cjs
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{java,xml}]
indent_size = 4
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
src/documentation/*.js
================================================
FILE: .eslintrc.json
================================================
{
"extends": [
"vaadin/typescript-requiring-type-checking",
"vaadin/imports-typescript",
"vaadin/lit",
"vaadin/prettier",
"vaadin/testing"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["tsdoc"],
"rules": {
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/no-useless-template-literals": "off",
"import/no-unassigned-import": "off",
"max-params": "off",
"sort-keys": "off",
"tsdoc/syntax": "error",
"import/prefer-default-export": "off"
},
"ignorePatterns": ["*.cjs"]
}
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
versioning-strategy: increase
================================================
FILE: .github/workflows/docs.yml
================================================
name: Publish Docs
on:
push:
branches: ['main']
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Use NodeJS LTS
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install
run: npm ci
- name: Build Docs
run: npm run docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './.docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/validation.yml
================================================
name: Validation
on:
push:
branches:
- 'master'
pull_request:
permissions:
contents: read
jobs:
test:
name: Validation
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout Project Code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
- name: Install dependencies and build
run: npm ci
- name: Build TypeScript
run: npm run build
- name: Check TypeScript
run: npm run typecheck
- name: Test
run: npm test
================================================
FILE: .gitignore
================================================
bower_components
node_modules
build
dist
coverage
.idea
.vscode
.docs
================================================
FILE: .prettierrc.json
================================================
{
"bracketSpacing": true,
"printWidth": 120,
"trailingComma": "all",
"tabWidth": 2,
"singleQuote": true
}
================================================
FILE: .run/All tests.run.xml
================================================
================================================
FILE: .run/Template Karma.run.xml
================================================
================================================
FILE: .stylelintrc.json
================================================
{
"rules": {
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
"block-opening-brace-newline-after": "always-multi-line",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"custom-property-empty-line-before": "never",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [ true, {
"ignore": ["consecutive-duplicates-with-different-values"]
} ],
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"function-max-empty-lines": 0,
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-whitespace-after": "always",
"indentation": null,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"max-empty-lines": 1,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
"no-eol-whitespace": true,
"no-invalid-double-slash-comments": true,
"number-no-trailing-zeros": true,
"property-case": "lower",
"property-no-unknown": true,
"rule-empty-line-before": [ "always-multi-line", {
"except": ["first-nested"],
"ignore": ["after-comment"]
} ],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"shorthand-property-no-redundant-values": true,
"string-no-newline": true,
"unit-case": "lower",
"unit-no-unknown": true,
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0
}
}
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2017 Vaadin Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
[](https://www.npmjs.com/package/@vaadin/router)
[](https://bundlephobia.com/result?p=@vaadin/router)
# Vaadin Router is deprecated
This library is no longer actively maintained. Vaadin uses React Router as a primary client-side routing tool.
Consider [`@lit-labs/router`](https://www.npmjs.com/package/@lit-labs/router) as a more lightweight and modern alternative. Also, as the [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) API is universally available in modern browsers nowadays, we recommend using that for building customized client-side routing.
---
# Vaadin Router
[Live demo](https://vaadin.github.io/router/index.html)
| [API documentation](https://vaadin.github.io/router/API/index.html)
Vaadin Router is a small and powerful client-side router JS library. It uses the widely adopted express.js syntax for routes (`/users/:id`) to map URLs to Web Component views. All features one might expect from a modern router are supported: async route resolution, animated transitions, navigation guards, redirects, and more. It is framework-agnostic and works equally well with all Web Components regardless of how they are created (Polymer / SkateJS / Stencil / Angular / Vue / etc).
Vaadin Router is a good fit for developers that do not want to go all-in with one framework, and prefer to have freedom in picking the components that work best for their specific needs.
```
npm install --save @vaadin/router
```
```javascript
import {Router} from '@vaadin/router';
const router = new Router(document.getElementById('outlet'));
router.setRoutes([
{path: '/', component: 'x-home-view'},
{path: '/users', component: 'x-user-list'}
]);
```
## Browser support
A specific version of Vaadin Router supports the same browsers as the Vaadin platform major version which includes that version of Vaadin Router.
See [Vaadin platform release notes](https://github.com/vaadin/platform/releases) for details on included Vaadin Router version and supported technologies.
The Supported Technologies section is typically listed in the release notes of the first publicly available release of a Vaadin platform major version
(for example [Vaadin 18.0.1](https://github.com/vaadin/platform/releases/tag/18.0.1) since 18.0.0 was skipped).
### Desktop browsers
Evergreen versions of the following browsers
- Chrome, Firefox, Firefox ESR, Safari and Edge (Chromium)
### Mobile browsers
Built-in browsers in the following mobile operating systems:
- Safari starting from iOS 13 (Safari 13 or newer)
- Google Chrome evergreen on Android (requiring Android 4.4 or newer)
### Sauce Labs test status
[](https://saucelabs.com/u/vaadin-router)
### Big Thanks
Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com).
## Running demos and tests in the browser
1. Fork the `vaadin/router` GitHub repository and clone it locally.
1. Make sure you have [npm](https://www.npmjs.com/) installed.
1. In the router directory, run `npm install` to install dependencies.
1. Run `npm run build` to build the library.
1. Run `npm start`, and open [http://localhost:4173](http://127.0.0.1:8000/components/vaadin-router) in your browser to see the component live demos and API documentation.
## Running tests from the command line
1. In the router directory, run `npm test`
## Following the coding style
We are using [ESLint](http://eslint.org/) for linting JavaScript code. You can check if your code is following our standards by running `npm run lint`, which will automatically lint all `.js` files as well as JavaScript snippets inside `.html` files.
## Contributing
- Make sure your code is compliant with our code linters: `npm run lint`
- Check that tests are passing: `npm test`
- [Submit a pull request](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github) with detailed title and description
- Wait for response from one of Vaadin components team members
## License
Apache License 2.0
Vaadin collects development time usage statistics to improve this product. For details and to opt-out, see https://github.com/vaadin/vaadin-usage-statistics.
================================================
FILE: analysis.json
================================================
{
"schema_version": "1.0.0",
"namespaces": [
{
"name": "Router",
"description": "",
"summary": "",
"sourceRange": {
"file": "src/documentation/namespace.js",
"start": {
"line": 3,
"column": 0
},
"end": {
"line": 3,
"column": 18
}
},
"classes": [
{
"description": "This is a type declaration. It exists for build-time type checking and\ndocumentation purposes. It should not be used in any source code, and it\ncertainly does not exist at the run time.\n\n`Location` describes the state of a router at a given point in time. It is\navailable for your application code in several ways:\n - as the `router.location` property\n - as the `location` property set by Vaadin Router on every view Web Component\n - as the `location` argument passed by Vaadin Router into view Web Component\n lifecycle callbacks\n - as the `event.detail.location` of the global Vaadin Router events",
"summary": "Type declaration for the `router.location` property.",
"path": "src/documentation/location.js",
"properties": [
{
"name": "baseUrl",
"type": "string",
"description": "The base URL used in the router. See [the `baseUrl` property\n](#/classes/Router#property-baseUrl) in the Router.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 25,
"column": 4
},
"end": {
"line": 25,
"column": 17
}
},
"metadata": {}
},
{
"name": "pathname",
"type": "!string",
"description": "The pathname as entered in the browser address bar\n(e.g. `/users/42/messages/12/edit`). It always starts with a `/` (slash).",
"privacy": "public",
"sourceRange": {
"start": {
"line": 34,
"column": 4
},
"end": {
"line": 34,
"column": 18
}
},
"metadata": {}
},
{
"name": "search",
"type": "!string",
"description": "The query string portion of the current url.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 42,
"column": 4
},
"end": {
"line": 42,
"column": 16
}
},
"metadata": {}
},
{
"name": "searchParams",
"type": "URLSearchParams",
"description": "The query search parameters of the current url.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 50,
"column": 4
},
"end": {
"line": 50,
"column": 22
}
},
"metadata": {}
},
{
"name": "hash",
"type": "!string",
"description": "The fragment identifier (including hash character) for the current page.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 58,
"column": 4
},
"end": {
"line": 58,
"column": 14
}
},
"metadata": {}
},
{
"name": "redirectFrom",
"type": "?string",
"description": "(optional) The original pathname string in case if this location is a\nresult of a redirect.\n\nE.g. with the routes config as below a navigation to `/u/12` produces a\nlocation with `pathname: '/user/12'` and `redirectFrom: '/u/12'`.\n\n```javascript\nsetRoutes([\n {path: '/u/:id', redirect: '/user/:id'},\n {path: '/user/:id', component: 'x-user-view'},\n]);\n```\n\nSee the **Redirects** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 81,
"column": 4
},
"end": {
"line": 81,
"column": 22
}
},
"metadata": {}
},
{
"name": "route",
"type": "?Route",
"description": "(optional) The route object associated with `this` Web Component instance.\n\nThis property is defined in the `location` objects that are passed as\nparameters into Web Component lifecycle callbacks, and the `location`\nproperty set by Vaadin Router on the Web Components.\n\nThis property is undefined in the `location` objects that are available\nas `router.location`, and in the `location` that is included into the\nglobal router event details.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 97,
"column": 4
},
"end": {
"line": 97,
"column": 15
}
},
"metadata": {}
},
{
"name": "routes",
"type": "!Array.",
"description": "A list of route objects that match the current pathname. This list has\none element for each route that defines a parent layout, and then the\nelement for the route that defines the view.\n\nSee the **Getting Started** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails on child routes and nested layouts.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 111,
"column": 4
},
"end": {
"line": 111,
"column": 16
}
},
"metadata": {}
},
{
"name": "params",
"type": "!IndexedParams",
"description": "A bag of key-value pairs with parameters for the current location. Named\nparameters are available by name, unnamed ones - by index (e.g. for the\n`/users/:id` route the `:id` parameter is available as `location.params.id`).\n\nSee the **Route Parameters** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 125,
"column": 4
},
"end": {
"line": 125,
"column": 16
}
},
"metadata": {}
}
],
"methods": [
{
"name": "getUrl",
"description": "Returns a URL corresponding to the route path and the parameters of this\nlocation. When the parameters object is given in the arguments,\nthe argument parameters override the location ones.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 140,
"column": 2
},
"end": {
"line": 140,
"column": 20
}
},
"metadata": {},
"params": [
{
"name": "params",
"type": "Params=",
"description": "optional object with parameters to override.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)."
}
],
"return": {
"type": "string"
}
}
],
"staticMethods": [],
"demos": [],
"metadata": {},
"sourceRange": {
"start": {
"line": 16,
"column": 7
},
"end": {
"line": 141,
"column": 1
}
},
"privacy": "public",
"name": "Router.Location"
}
]
}
],
"classes": [
{
"description": "",
"summary": "",
"path": "dist/vaadin-router.js",
"properties": [],
"methods": [],
"staticMethods": [],
"demos": [],
"metadata": {},
"sourceRange": {
"start": {
"line": 89,
"column": 28
},
"end": {
"line": 89,
"column": 51
}
},
"privacy": "public",
"name": "NotFoundResult"
},
{
"description": "",
"summary": "",
"path": "dist/vaadin-router.js",
"properties": [
{
"name": "__effectiveBaseUrl",
"type": "?",
"description": "If the baseUrl property is set, transforms the baseUrl and returns the full\nactual `base` string for using in the `new URL(path, base);` and for\nprepernding the paths with. The returned base ends with a trailing slash.\n\nOtherwise, returns empty string.\n ",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1038,
"column": 2
},
"end": {
"line": 1045,
"column": 3
}
},
"metadata": {}
}
],
"methods": [
{
"name": "getRoutes",
"description": "Returns the current list of routes (as a shallow copy). Adding / removing\nroutes to / from the returned array does not affect the routing config,\nbut modifying the route objects does.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 895,
"column": 2
},
"end": {
"line": 897,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "!Array."
}
},
{
"name": "setRoutes",
"description": "Sets the routing config (replacing the existing one).",
"privacy": "public",
"sourceRange": {
"start": {
"line": 905,
"column": 2
},
"end": {
"line": 909,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "routes",
"type": "(!Array. | !Router.Route)",
"description": "a single route or an array of those\n (the array is shallow copied)"
}
],
"return": {
"type": "void"
}
},
{
"name": "addRoutes",
"description": "Appends one or several routes to the routing config and returns the\neffective routing config after the operation.",
"privacy": "protected",
"sourceRange": {
"start": {
"line": 920,
"column": 2
},
"end": {
"line": 924,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "routes",
"type": "(!Array. | !Router.Route)",
"description": "a single route or an array of those\n (the array is shallow copied)"
}
],
"return": {
"type": "!Array."
}
},
{
"name": "removeRoutes",
"description": "Removes all existing routes from the routing config.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 929,
"column": 2
},
"end": {
"line": 931,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
}
},
{
"name": "resolve",
"description": "Asynchronously resolves the given pathname, i.e. finds all routes matching\nthe pathname and tries resolving them one after another in the order they\nare listed in the routes config until the first non-null result.\n\nReturns a promise that is fulfilled with the return value of an object that consists of the first\nroute handler result that returns something other than `null` or `undefined` and context used to get this result.\n\nIf no route handlers return a non-null result, or if no route matches the\ngiven pathname the returned promise is rejected with a 'page not found'\n`Error`.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 950,
"column": 2
},
"end": {
"line": 1022,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "pathnameOrContext",
"type": "(!string | !{pathname: !string})",
"description": "the pathname to\n resolve or a context object with a `pathname` property and other\n properties to pass to the route resolver functions."
}
],
"return": {
"type": "!Promise."
}
},
{
"name": "__normalizePathname",
"description": "If the baseUrl is set, matches the pathname with the router’s baseUrl,\nand returns the local pathname with the baseUrl stripped out.\n\nIf the pathname does not match the baseUrl, returns undefined.\n\nIf the `baseUrl` is not set, returns the unmodified pathname argument.",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1055,
"column": 2
},
"end": {
"line": 1070,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "pathname"
}
]
}
],
"staticMethods": [
{
"name": "__createUrl",
"description": "URL constructor polyfill hook. Creates and returns an URL instance.",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1027,
"column": 2
},
"end": {
"line": 1029,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "args",
"rest": true
}
]
}
],
"demos": [],
"metadata": {},
"sourceRange": {
"start": {
"line": 874,
"column": 0
},
"end": {
"line": 1071,
"column": 1
}
},
"privacy": "public",
"name": "Resolver"
},
{
"description": "A simple client-side router for single-page applications. It uses\nexpress-style middleware and has a first-class support for Web Components and\nlazy-loading. Works great in Polymer and non-Polymer apps.\n\nUse `new Router(outlet, options)` to create a new Router instance.\n\n* The `outlet` parameter is a reference to the DOM node to render\n the content into.\n\n* The `options` parameter is an optional object with options. The following\n keys are supported:\n * `baseUrl` — the initial value for [\n the `baseUrl` property\n ](#/classes/Router#property-baseUrl)\n\nThe Router instance is automatically subscribed to navigation events\non `window`.\n\nSee [Live Examples](#/classes/Router/demos/demo/index.html) for the detailed usage demo and code snippets.\n\nSee also detailed API docs for the following methods, for the advanced usage:\n\n* [setOutlet](#/classes/Router#method-setOutlet) – should be used to configure the outlet.\n* [setTriggers](#/classes/Router#method-setTriggers) – should be used to configure the navigation events.\n* [setRoutes](#/classes/Router#method-setRoutes) – should be used to configure the routes.\n\nOnly `setRoutes` has to be called manually, others are automatically invoked when creating a new instance.",
"summary": "JavaScript class that renders different DOM content depending on\n a given path. It can re-render when triggered or automatically on\n 'popstate' and / or 'click' events.",
"path": "dist/vaadin-router.js",
"properties": [
{
"name": "__effectiveBaseUrl",
"type": "?",
"description": "If the baseUrl property is set, transforms the baseUrl and returns the full\nactual `base` string for using in the `new URL(path, base);` and for\nprepernding the paths with. The returned base ends with a trailing slash.\n\nOtherwise, returns empty string.\n ",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1038,
"column": 2
},
"end": {
"line": 1045,
"column": 3
}
},
"metadata": {},
"inheritedFrom": "Resolver"
},
{
"name": "baseUrl",
"type": "string",
"description": "The base URL for all routes in the router instance. By default,\nif the base element exists in the ``, vaadin-router\ntakes the `` attribute value, resolved against the current\n`document.URL`.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1410,
"column": 4
},
"end": {
"line": 1410,
"column": 17
}
},
"metadata": {}
},
{
"name": "ready",
"type": "!Promise.",
"description": "A promise that is settled after the current render cycle completes. If\nthere is no render cycle in progress the promise is immediately settled\nwith the last render cycle result.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1420,
"column": 4
},
"end": {
"line": 1420,
"column": 15
}
},
"metadata": {}
},
{
"name": "location",
"type": "!RouterLocation",
"description": "Contains read-only information about the current router location:\npathname, active routes, parameters. See the\n[Location type declaration](#/classes/RouterLocation)\nfor more details.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1432,
"column": 4
},
"end": {
"line": 1432,
"column": 18
}
},
"metadata": {}
}
],
"methods": [
{
"name": "getRoutes",
"description": "Returns the current list of routes (as a shallow copy). Adding / removing\nroutes to / from the returned array does not affect the routing config,\nbut modifying the route objects does.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 895,
"column": 2
},
"end": {
"line": 897,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "!Array."
},
"inheritedFrom": "Resolver"
},
{
"name": "setRoutes",
"description": "Sets the routing config (replacing the existing one) and triggers a\nnavigation event so that the router outlet is refreshed according to the\ncurrent `window.location` and the new routing config.\n\nEach route object may have the following properties, listed here in the processing order:\n* `path` – the route path (relative to the parent route if any) in the\n[express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths\").\n\n* `children` – an array of nested routes or a function that provides this\narray at the render time. The function can be synchronous or asynchronous:\nin the latter case the render is delayed until the returned promise is\nresolved. The `children` function is executed every time when this route is\nbeing rendered. This allows for dynamic route structures (e.g. backend-defined),\nbut it might have a performance impact as well. In order to avoid calling\nthe function on subsequent renders, you can override the `children` property\nof the route object and save the calculated array there\n(via `context.route.children = [ route1, route2, ...];`).\nParent routes are fully resolved before resolving the children. Children\n'path' values are relative to the parent ones.\n\n* `action` – the action that is executed before the route is resolved.\nThe value for this property should be a function, accepting `context`\nand `commands` parameters described below. If present, this function is\nalways invoked first, disregarding of the other properties' presence.\nThe action can return a result directly or within a `Promise`, which\nresolves to the result. If the action result is an `HTMLElement` instance,\na `commands.component(name)` result, a `commands.redirect(path)` result,\nor a `context.next()` result, the current route resolution is finished,\nand other route config properties are ignored.\nSee also **Route Actions** section in [Live Examples](#/classes/Router/demos/demo/index.html).\n\n* `redirect` – other route's path to redirect to. Passes all route parameters to the redirect target.\nThe target route should also be defined.\nSee also **Redirects** section in [Live Examples](#/classes/Router/demos/demo/index.html).\n\n* `component` – the tag name of the Web Component to resolve the route to.\nThe property is ignored when either an `action` returns the result or `redirect` property is present.\nIf route contains the `component` property (or an action that return a component)\nand its child route also contains the `component` property, child route's component\nwill be rendered as a light dom child of a parent component.\n\n* `name` – the string name of the route to use in the\n[`router.urlForName(name, params)`](#/classes/Router#method-urlForName)\nnavigation helper method.\n\nFor any route function (`action`, `children`) defined, the corresponding `route` object is available inside the callback\nthrough the `this` reference. If you need to access it, make sure you define the callback as a non-arrow function\nbecause arrow functions do not have their own `this` reference.\n\n`context` object that is passed to `action` function holds the following properties:\n* `context.pathname` – string with the pathname being resolved\n\n* `context.search` – search query string\n\n* `context.hash` – hash string\n\n* `context.params` – object with route parameters\n\n* `context.route` – object that holds the route that is currently being rendered.\n\n* `context.next()` – function for asynchronously getting the next route\ncontents from the resolution chain (if any)\n\n`commands` object that is passed to `action` function has\nthe following methods:\n\n* `commands.redirect(path)` – function that creates a redirect data\nfor the path specified.\n\n* `commands.component(component)` – function that creates a new HTMLElement\nwith current context. Note: the component created by this function is reused if visiting the same path twice in row.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1609,
"column": 2
},
"end": {
"line": 1617,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "routes",
"type": "(!Array. | !Route)",
"description": "a single route or an array of those"
},
{
"name": "skipRender",
"type": "?boolean",
"defaultValue": "false",
"description": "configure the router but skip rendering the\n route corresponding to the current `window.location` values"
}
],
"return": {
"type": "!Promise."
}
},
{
"name": "addRoutes",
"description": "Appends one or several routes to the routing config and returns the\neffective routing config after the operation.",
"privacy": "protected",
"sourceRange": {
"start": {
"line": 920,
"column": 2
},
"end": {
"line": 924,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "routes",
"type": "(!Array. | !Router.Route)",
"description": "a single route or an array of those\n (the array is shallow copied)"
}
],
"return": {
"type": "!Array."
},
"inheritedFrom": "Resolver"
},
{
"name": "removeRoutes",
"description": "Removes all existing routes from the routing config.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 929,
"column": 2
},
"end": {
"line": 931,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
},
"inheritedFrom": "Resolver"
},
{
"name": "resolve",
"description": "Asynchronously resolves the given pathname, i.e. finds all routes matching\nthe pathname and tries resolving them one after another in the order they\nare listed in the routes config until the first non-null result.\n\nReturns a promise that is fulfilled with the return value of an object that consists of the first\nroute handler result that returns something other than `null` or `undefined` and context used to get this result.\n\nIf no route handlers return a non-null result, or if no route matches the\ngiven pathname the returned promise is rejected with a 'page not found'\n`Error`.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 950,
"column": 2
},
"end": {
"line": 1022,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "pathnameOrContext",
"type": "(!string | !{pathname: !string})",
"description": "the pathname to\n resolve or a context object with a `pathname` property and other\n properties to pass to the route resolver functions."
}
],
"return": {
"type": "!Promise."
},
"inheritedFrom": "Resolver"
},
{
"name": "__normalizePathname",
"description": "If the baseUrl is set, matches the pathname with the router’s baseUrl,\nand returns the local pathname with the baseUrl stripped out.\n\nIf the pathname does not match the baseUrl, returns undefined.\n\nIf the `baseUrl` is not set, returns the unmodified pathname argument.",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1055,
"column": 2
},
"end": {
"line": 1070,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "pathname"
}
],
"inheritedFrom": "Resolver"
},
{
"name": "__resolveRoute",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1444,
"column": 2
},
"end": {
"line": 1501,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
}
]
},
{
"name": "setOutlet",
"description": "Sets the router outlet (the DOM node where the content for the current\nroute is inserted). Any content pre-existing in the router outlet is\nremoved at the end of each render pass.\n\nNOTE: this method is automatically invoked first time when creating a new Router instance.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1513,
"column": 2
},
"end": {
"line": 1518,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "outlet",
"type": "?Node",
"description": "the DOM node where the content for the current route\n is inserted."
}
],
"return": {
"type": "void"
}
},
{
"name": "getOutlet",
"description": "Returns the current router outlet. The initial value is `undefined`.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1525,
"column": 2
},
"end": {
"line": 1527,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "?Node",
"desc": "the current router outlet (or `undefined`)"
}
},
{
"name": "render",
"description": "Asynchronously resolves the given pathname and renders the resolved route\ncomponent into the router outlet. If no router outlet is set at the time of\ncalling this method, or at the time when the route resolution is completed,\na `TypeError` is thrown.\n\nReturns a promise that is fulfilled with the router outlet DOM Node after\nthe route component is created and inserted into the router outlet, or\nrejected if no route matches the given path.\n\nIf another render pass is started before the previous one is completed, the\nresult of the previous render pass is ignored.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 1640,
"column": 2
},
"end": {
"line": 1724,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "pathnameOrContext",
"type": "(!string | !{pathname: !string, search: ?string, hash: ?string})",
"description": "the pathname to render or a context object with a `pathname` property,\n optional `search` and `hash` properties, and other properties\n to pass to the resolver."
},
{
"name": "shouldUpdateHistory",
"type": "boolean=",
"description": "update browser history with the rendered location"
}
],
"return": {
"type": "!Promise."
}
},
{
"name": "__fullyResolveChain",
"description": "and 'redirect' callback results.",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1737,
"column": 2
},
"end": {
"line": 1785,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "topOfTheChainContextBeforeRedirects"
},
{
"name": "contextBeforeRedirects",
"defaultValue": "topOfTheChainContextBeforeRedirects"
}
]
},
{
"name": "__findComponentContextAfterAllRedirects",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1787,
"column": 2
},
"end": {
"line": 1807,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
}
]
},
{
"name": "__amendWithOnBeforeCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1809,
"column": 2
},
"end": {
"line": 1816,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "contextWithFullChain"
}
]
},
{
"name": "__runOnBeforeCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1818,
"column": 2
},
"end": {
"line": 1890,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "newContext"
}
]
},
{
"name": "__runOnBeforeLeaveCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1892,
"column": 2
},
"end": {
"line": 1904,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "callbacks"
},
{
"name": "newContext"
},
{
"name": "commands"
},
{
"name": "chainElement"
}
]
},
{
"name": "__runOnBeforeEnterCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1906,
"column": 2
},
"end": {
"line": 1914,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "callbacks"
},
{
"name": "newContext"
},
{
"name": "commands"
},
{
"name": "chainElement"
}
]
},
{
"name": "__isReusableElement",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1916,
"column": 2
},
"end": {
"line": 1923,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "element"
},
{
"name": "otherElement"
}
]
},
{
"name": "__isLatestRender",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1925,
"column": 2
},
"end": {
"line": 1927,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
}
]
},
{
"name": "__redirect",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1929,
"column": 2
},
"end": {
"line": 1943,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "redirectData"
},
{
"name": "counter"
},
{
"name": "renderId"
}
]
},
{
"name": "__ensureOutlet",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1945,
"column": 2
},
"end": {
"line": 1949,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "outlet",
"defaultValue": "this.__outlet"
}
],
"return": {
"type": "void"
}
},
{
"name": "__updateBrowserHistory",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1951,
"column": 2
},
"end": {
"line": 1960,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "{\n pathname,\n search = '',\n hash = ''\n}"
},
{
"name": "replace"
}
],
"return": {
"type": "void"
}
},
{
"name": "__copyUnchangedElements",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1962,
"column": 2
},
"end": {
"line": 1978,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
},
{
"name": "previousContext"
}
]
},
{
"name": "__addAppearingContent",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1980,
"column": 2
},
"end": {
"line": 2018,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
},
{
"name": "previousContext"
}
],
"return": {
"type": "void"
}
},
{
"name": "__removeDisappearingContent",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2020,
"column": 2
},
"end": {
"line": 2026,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
}
},
{
"name": "__removeAppearingContent",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2028,
"column": 2
},
"end": {
"line": 2034,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
}
},
{
"name": "__runOnAfterLeaveCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2036,
"column": 2
},
"end": {
"line": 2062,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "currentContext"
},
{
"name": "targetContext"
}
],
"return": {
"type": "void"
}
},
{
"name": "__runOnAfterEnterCallbacks",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2064,
"column": 2
},
"end": {
"line": 2077,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "currentContext"
}
],
"return": {
"type": "void"
}
},
{
"name": "__animateIfNeeded",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2079,
"column": 2
},
"end": {
"line": 2101,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "context"
}
]
},
{
"name": "subscribe",
"description": "Subscribes this instance to navigation events on the `window`.\n\nNOTE: beware of resource leaks. For as long as a router instance is\nsubscribed to navigation events, it won't be garbage collected.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2109,
"column": 2
},
"end": {
"line": 2111,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
}
},
{
"name": "unsubscribe",
"description": "Removes the subscription to navigation events created in the `subscribe()`\nmethod.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2117,
"column": 2
},
"end": {
"line": 2119,
"column": 3
}
},
"metadata": {},
"params": [],
"return": {
"type": "void"
}
},
{
"name": "__onNavigationEvent",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2121,
"column": 2
},
"end": {
"line": 2129,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "event"
}
],
"return": {
"type": "void"
}
},
{
"name": "urlForName",
"description": "Generates a URL for the route with the given name, optionally performing\nsubstitution of parameters.\n\nThe route is searched in all the Router instances subscribed to\nnavigation events.\n\n**Note:** For child route names, only array children are considered.\nIt is not possible to generate URLs using a name for routes set with\na children function.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2170,
"column": 2
},
"end": {
"line": 2178,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "name",
"type": "!string",
"description": "the route name or the route’s `component` name."
},
{
"name": "params",
"type": "Params=",
"description": "Optional object with route path parameters.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)."
}
],
"return": {
"type": "string"
}
},
{
"name": "urlForPath",
"description": "Generates a URL for the given route path, optionally performing\nsubstitution of parameters.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2191,
"column": 2
},
"end": {
"line": 2196,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "path",
"type": "!string",
"description": "string route path declared in [express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths\")."
},
{
"name": "params",
"type": "Params=",
"description": "Optional object with route path parameters.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)."
}
],
"return": {
"type": "string"
}
}
],
"staticMethods": [
{
"name": "__createUrl",
"description": "URL constructor polyfill hook. Creates and returns an URL instance.",
"privacy": "private",
"sourceRange": {
"start": {
"line": 1027,
"column": 2
},
"end": {
"line": 1029,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "args",
"rest": true
}
],
"inheritedFrom": "Resolver"
},
{
"name": "setTriggers",
"description": "Configures what triggers Router navigation events:\n - `POPSTATE`: popstate events on the current `window`\n - `CLICK`: click events on `` links leading to the current page\n\nThis method is invoked with the pre-configured values when creating a new Router instance.\nBy default, both `POPSTATE` and `CLICK` are enabled. This setup is expected to cover most of the use cases.\n\nSee the `router-config.js` for the default navigation triggers config. Based on it, you can\ncreate the own one and only import the triggers you need, instead of pulling in all the code,\ne.g. if you want to handle `click` differently.\n\nSee also **Navigation Triggers** section in [Live Examples](#/classes/Router/demos/demo/index.html).",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2147,
"column": 2
},
"end": {
"line": 2149,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "triggers",
"type": "...NavigationTrigger",
"rest": true
}
],
"return": {
"type": "void"
}
},
{
"name": "go",
"description": "Triggers navigation to a new path. Returns a boolean without waiting until\nthe navigation is complete. Returns `true` if at least one `Router`\nhas handled the navigation (was subscribed and had `baseUrl` matching\nthe `path` argument), otherwise returns `false`.",
"privacy": "public",
"sourceRange": {
"start": {
"line": 2209,
"column": 2
},
"end": {
"line": 2214,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "path",
"type": "(!string | !{pathname: !string, search: (string | undefined), hash: (string | undefined)})",
"description": "a new in-app path string, or an URL-like object with `pathname`\n string property, and optional `search` and `hash` string properties."
}
],
"return": {
"type": "boolean"
}
},
{
"name": "__removeDomNodes",
"description": "",
"privacy": "private",
"sourceRange": {
"start": {
"line": 2216,
"column": 2
},
"end": {
"line": 2225,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "nodes"
}
],
"return": {
"type": "void"
}
}
],
"demos": [
{
"url": "demo/index.html",
"description": ""
},
{
"url": "demo/index.html",
"description": ""
}
],
"metadata": {},
"sourceRange": {
"start": {
"line": 1374,
"column": 0
},
"end": {
"line": 2226,
"column": 1
}
},
"privacy": "public",
"superclass": "Resolver",
"name": "Router"
},
{
"description": "This interface describes the lifecycle callbacks supported by Vaadin Router\non view Web Components. It exists only for documentation purposes, i.e.\nyou _do not need_ to extend it in your code—defining a method with a\nmatching name is enough (this class does not exist at the run time).\n\nIf any of the methods described below are defined in a view Web Component,\nVaadin Router calls them at the corresponding points of the view\nlifecycle. Each method can either be synchronous or asynchronous (i.e. return\na Promise). In the latter case Vaadin Router waits until the promise is\nresolved and continues the navigation after that.\n\nCheck the [documentation on the `Router` class](#/classes/Router)\nto learn more.\n\nLifecycle callbacks are executed after the new path is resolved and after all\n`action` callbacks of the routes in the new path are executed.\n\nExample:\n\nFor the following routes definition,\n```\n// router and action declarations are omitted for brevity\nrouter.setRoutes([\n {path: '/a', action: actionA, children: [\n {path: '/b', action: actionB, component: 'component-b'},\n {path: '/c', action: actionC, component: 'component-c'}\n ]}\n]);\n```\nif the router first navigates to `/a/b` path and there was no view rendered\nbefore, the following events happen:\n* actionA\n* actionB\n* onBeforeEnterB (if defined in component-b)\n* outlet contents updated with component-b\n* onAfterEnterB (if defined in component-b)\n\nthen, if the router navigates to `/a/c`, the following events take place:\n* actionA\n* actionC\n* onBeforeLeaveB (if defined in component-b)\n* onBeforeEnterC (if defined in component-c)\n* onAfterLeaveB (if defined in component-b)\n* outlet contents updated with component-c\n* onAfterEnterC (if defined in component-c)\n\nIf a `Promise` is returned by any of the callbacks, it is resolved before\nproceeding further.\n\nAny of the `onBefore...` callbacks have a possibility to prevent\nthe navigation and fall back to the previous navigation result. If there is\nno result and this is the first resolution, an exception is thrown.\n\n`onAfter...` callbacks are considered as non-preventable, and their return\nvalue is ignored.\n\nOther examples can be found in the\n[live demos](#/classes/Router/demos/demo/index.html) and tests.",
"summary": "",
"path": "src/documentation/web-component-interface.js",
"properties": [],
"methods": [
{
"name": "onBeforeLeave",
"description": "Method that gets executed when user navigates away from the component\nthat had defined the method. The user can prevent the navigation\nby returning `commands.prevent()` from the method or same value wrapped\nin `Promise`. This effectively means that the corresponding component\nshould be resolved by the router before the method can be executed.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. In case of navigating to a different path\nbut within the same route object, e.g. the path has parameter or wildcard,\nand this results in rendering the same component instance, the method is called if available.\nThe WebComponent instance on which the callback has been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values:\n\n* if the `commands.prevent()` result is returned (immediately or\nas a Promise), the navigation is aborted and the outlet contents\nis not updated.\n* any other return value is ignored and Vaadin Router proceeds with\nthe navigation.\n\nArguments:",
"privacy": "public",
"sourceRange": {
"start": {
"line": 97,
"column": 2
},
"end": {
"line": 102,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "location",
"description": "the `RouterLocation` object"
},
{
"name": "commands",
"description": "the commands object with the following methods:\n\n| Property | Description\n| -------------------|-------------\n| `commands.prevent()` | function that creates a special object that can be returned to abort the current navigation and fall back to the last one. If there is no existing one, an exception is thrown."
},
{
"name": "router",
"description": "the `Router` instance"
}
]
},
{
"name": "onBeforeEnter",
"description": "Method that gets executed before the outlet contents is updated with\nthe new element. The user can prevent the navigation by returning\n`commands.prevent()` from the method or same value wrapped in `Promise`.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. In case of navigating to a different path\nbut within the same route object, e.g. the path has parameter or wildcard,\nand this results in rendering the same component instance, the method is called if available.\nThe WebComponent instance on which the callback has been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values:\n\n* if the `commands.prevent()` result is returned (immediately or\nas a Promise), the navigation is aborted and the outlet contents\nis not updated.\n* if the `commands.redirect(path)` result is returned (immediately or\nas a Promise), Vaadin Router ends navigation to the current path, and\nstarts a new navigation cycle to the new path.\n* any other return value is ignored and Vaadin Router proceeds with\nthe navigation.\n\nArguments:",
"privacy": "public",
"sourceRange": {
"start": {
"line": 141,
"column": 2
},
"end": {
"line": 146,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "location",
"description": "the `RouterLocation` object"
},
{
"name": "commands",
"description": "the commands object with the following methods:\n\n| Property | Description\n| -------------------------|-------------\n| `commands.redirect(path)` | function that creates a redirect data for the path specified, to use as a return value from the callback.\n| `commands.prevent()` | function that creates a special object that can be returned to abort the current navigation and fall back to the last one. If there is no existing one, an exception is thrown."
},
{
"name": "router",
"description": "the `Router` instance"
}
]
},
{
"name": "onAfterLeave",
"description": "Method that gets executed when user navigates away from the component that\nhad defined the method, just before the element is to be removed\nfrom the DOM. The difference between this method and `onBeforeLeave`\nis that when this method is executed, there is no way to abort\nthe navigation. This effectively means that the corresponding component\nshould be resolved by the router before the method can be executed.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. The WebComponent instance on which the callback\nhas been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values: any return value is ignored and Vaadin Router proceeds with the navigation.\n\nArguments:",
"privacy": "public",
"sourceRange": {
"start": {
"line": 171,
"column": 2
},
"end": {
"line": 174,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "location",
"description": "the `RouterLocation` object"
},
{
"name": "commands",
"description": "empty object"
},
{
"name": "router",
"description": "the `Router` instance"
}
],
"return": {
"type": "void"
}
},
{
"name": "onAfterEnter",
"description": "Method that gets executed after the outlet contents is updated with the new\nelement. If the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. The WebComponent instance on which the callback\nhas been invoked is available inside the callback through\nthe `this` reference.\n\nThis callback is called asynchronously after the native\n[`connectedCallback()`](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions)\ndefined by the Custom Elements spec.\n\nReturn values: any return value is ignored and Vaadin Router proceeds with the navigation.\n\nArguments:",
"privacy": "public",
"sourceRange": {
"start": {
"line": 198,
"column": 2
},
"end": {
"line": 201,
"column": 3
}
},
"metadata": {},
"params": [
{
"name": "location",
"description": "the `RouterLocation` object"
},
{
"name": "commands",
"description": "empty object"
},
{
"name": "router",
"description": "the `Router` instance"
}
],
"return": {
"type": "void"
}
}
],
"staticMethods": [],
"demos": [],
"metadata": {},
"sourceRange": {
"start": {
"line": 61,
"column": 7
},
"end": {
"line": 202,
"column": 1
}
},
"privacy": "public",
"name": "WebComponentInterface"
}
]
}
================================================
FILE: demo/.eslintrc.json
================================================
{
"extends": ["../.eslintrc.json"],
"root": true,
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/unbound-method": "off"
}
}
================================================
FILE: demo/@debug/index.html
================================================
Debug
================================================
FILE: demo/@debug/index.ts
================================================
/* eslint-disable import/no-duplicates, import/default */
import '@helpers/common.js';
import '@helpers/vaadin-presentation.js';
import { html, LitElement, type TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
// An URL of the iframe to debug
// eslint-disable-next-line import/order
import url1 from '../url-generation/d4/iframe.html?url';
declare global {
interface HTMLElementTagNameMap {
'vaadin-demo-debug': DemoDebug;
}
}
@customElement('vaadin-demo-debug')
export default class DemoDebug extends LitElement {
override render(): TemplateResult {
return html``;
}
}
================================================
FILE: demo/@helpers/common.css
================================================
:host {
display: block;
}
================================================
FILE: demo/@helpers/common.ts
================================================
import '@vaadin/vaadin-lumo-styles/badge-global.js';
import '@vaadin/vaadin-lumo-styles/color-global.js';
import '@vaadin/vaadin-lumo-styles/typography-global.js';
================================================
FILE: demo/@helpers/iframe.script.ts
================================================
import './common.css';
import './common.js';
import { Router } from '@vaadin/router';
history.replaceState(null, '', '/');
type MessageData = Readonly<{ url: string }>;
type ParentData = {
source: MessageEventSource | null;
origin: string;
};
let parentData: ParentData | undefined;
function updateParentUrl() {
if (parentData?.source) {
parentData.source.postMessage({ url: location.href }, { targetOrigin: location.origin });
}
}
addEventListener('message', ({ data, origin, source }: MessageEvent) => {
if (data != null) {
Router.go(new URL(data.url, location.origin).href);
} else {
parentData = { source, origin };
}
updateParentUrl();
});
addEventListener('vaadin-router-location-changed', updateParentUrl);
================================================
FILE: demo/@helpers/nested-styles.css
================================================
:host {
display: flex;
flex-direction: column;
background: #ddd;
padding: 5px;
min-height: 100vh;
}
main {
margin-top: 10px;
padding: 5px;
background: #fff;
flex: 1 0 auto;
min-height: 80px;
}
================================================
FILE: demo/@helpers/page.css
================================================
code {
background: var(--code-background);
padding: 0.2em 0.4em;
}
================================================
FILE: demo/@helpers/shared-styles.css
================================================
.note {
background-color: #24c0ea;
padding: 1em;
color: white;
border-radius: 4px;
}
================================================
FILE: demo/@helpers/theme-controller.ts
================================================
import type { ReactiveController, ReactiveControllerHost } from 'lit';
export default class ThemeController implements ReactiveController {
readonly #host: ReactiveControllerHost;
#controller?: AbortController;
constructor(host: ReactiveControllerHost) {
(this.#host = host).addController(this);
}
get value(): string {
return document.documentElement.getAttribute('theme') ?? 'light';
}
hostConnected(): void {
this.#controller = new AbortController();
addEventListener('theme-changed', () => this.#host.requestUpdate(), { signal: this.#controller.signal });
}
hostDisconnected(): void {
this.#controller?.abort();
}
}
================================================
FILE: demo/@helpers/vaadin-demo-code-snippet-file.css
================================================
header {
margin: 1.5em 0 0.5em;
font-size: 0.75rem;
display: flex;
justify-content: space-between;
align-items: baseline;
background: var(--code-file-background);
}
section {
font-size: 0.75rem;
background: var(--code-background);
padding: .5em;
}
section > pre {
white-space: pre-wrap;
}
section > pre > code {
line-break: anywhere;
}
.title {
font-family: monospace;
}
.buttons {
clear: right;
float: right;
display: flex;
margin: -0.25em 0;
}
.buttons > vaadin-button {
margin: 0;
padding: 0;
}
/* ======= Highlight.js styles ======= */
:host([theme~='dark']) {
@nested-import 'highlight.js/styles/atom-one-dark.css';
}
:host([theme~='light']) {
@nested-import 'highlight.js/styles/vs.css';
}
pre {
margin: 0;
padding: 0.5em 1em;
}
================================================
FILE: demo/@helpers/vaadin-demo-code-snippet-file.ts
================================================
/* eslint-disable @typescript-eslint/unbound-method */
import '@vaadin/button/src/vaadin-button';
import '@vaadin/icon/src/vaadin-icon';
import { type TemplateResult, LitElement, html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { map } from 'lit/directives/map.js';
import ThemeController from './theme-controller.js';
import css from './vaadin-demo-code-snippet-file.css?ctr';
export type CodeSnippet = Readonly<{
id?: string;
code: [original: string, full: TemplateResult, ...rest: TemplateResult[]];
title?: string;
}>;
@customElement('vaadin-demo-code-snippet-file')
export default class DemoCodeSnippetFile extends LitElement {
static override styles = [css];
@property({ attribute: false }) accessor file: CodeSnippet | undefined;
@state() accessor #expanded = false;
readonly #theme = new ThemeController(this);
override updated(): void {
this.setAttribute('theme', this.#theme.value);
}
override render(): TemplateResult | typeof nothing {
if (!this.file) {
return nothing;
}
const [_, full, ...snippets] = this.file.code;
return html`
Vaadin Router allows you to animate transitions between routes. In order to add an animation, do the next steps:
update the router config: add the animate property set to true
add @keyframes animations, either in the view Web Component styles or in outside CSS
apply CSS for .leaving and .entering classes to use the animations
The demo below illustrates how to add the transition between all the routes in the same group. You might also
add the transition for the specific routes only, by setting the animate
property on the corresponding route config objects.
To run the animated transition, Vaadin Router performs the actions in the following order:
render the new view component to the outlet content
set the entering CSS class on the new view component
set the leaving CSS class on the old view component, if any
check if some @keyframes animation applies, and wait for it to complete
remove the old view component from the outlet content
continue the remaining navigation steps as usual
Customize CSS Classes
In the basic use case, using single type of the animated transition could be enough to make the web app looking
great, but often we need to configure it depending on the route. Vaadin Router supports this feature by setting
object value to animate property, with the enter and leave string keys.
Their values are used for setting CSS classes to be set on the views.
Note that you can first configure animated transition for the group of routes, and then override it for the
single route. In particular, you can switch back to using default CSS classes, as shown in the demo below.
Vaadin Router allows you to implement your own loading mechanism for bundles using custom
Route Actions. In that case, you can use
dynamic imports.
Note: If the dynamically loaded route has lifecycle callbacks, the action should return a promise that resolves
only when the route component is loaded (like in the example below). Otherwise the lifecycle callbacks on the
dynamically loaded route's web component are not called.
If dynamic imports are used both for parent and child routes, then the example above may possibly slow down
rendering because router would not start importing a child component until its parent is imported.
Splitting and Lazy-Loading the Routes Configuration
Vaadin Router supports splitting the routes configuration object into parts and lazily loading them on-demand,
enabling developers to create non-monolithic app structures. This might be useful for implementing a distributed
sub routes configuration within a big project, so that multiple teams working on different parts of the app no
longer have to merge their changes into the same file.
The children property on the route config object can be set to a function, which returns an array
of the route objects. It may return a Promise, which allows to dynamically import the configuration file, and return the children array exported from it.
See the API documentation for detailed description of the
children callback function.
The Router class is the only thing you need to get started with Vaadin Router. Depending on your
project setup, there are several ways to access it.
In modern browsers that support ES modules the Router class can be imported
directly into a script tag on a page:
In Vite / Webpack / Rollup projects the Router class can be imported from the
@vaadin/router npm package:
Getting Started
Vaadin Router automatically listens to navigation events and asynchronously renders a matching Web Component
into the given DOM node (a.k.a. the router outlet). By default, navigation events are triggered by
popstate events on the window, and by click events on
<a> elements on the page.
The routes config maps URL paths to Web Components. Vaadin Router goes through the routes until it finds the
first match, creates an instance of the route component, and inserts it into the router outlet (replacing any
pre-existing outlet content). For details on the route path syntax see the
Route Parameters demos.
Route components can be any Web Components regardless of how they are built: vanilla JavaScript, Lit, Stencil,
SkateJS, Angular, Vue, etc.
Vaadin Router follows the lifecycle callbacks convention described in
WebComponentInterface: if a route component defines any
of these callback methods, Vaadin Router will call them at the appropriate points in the navigation lifecycle.
See the Lifecycle Callbacks section for more details.
In addition to that Vaadin Router also sets a
location property on every route Web Component so
that you could access the current location details via an instance property (e.g.
this.location.pathname).
Using this.location
For LitElement and TypeScript a declaration in the component is required. Declare the
location property in the class and initialize it from the router Vaadin Router
instance:
This property is automatically updated on navigation.
Fallback Routes (404)
If Vaadin Router does not find a matching route, the promise returned from the render() method gets
rejected, and any content in the router outlet is removed. In order to show a user-friendly 'page not found'
view, a fallback route with a wildcard '(.*)' path can be added to the end of the
routes list.
There can be different fallbacks for different route prefixes, but since the route resolution is based on the
first match, the fallback route should always be after other alternatives.
The path that leads to the fallback route is available to the route component via the
location.pathname property.
the 'vaadin-router-location-changed' / 'vaadin-router-error' events on the
window
Vaadin Router lifecycle callbacks can be defined as methods on the route Web Component
class definition
in a similar way as the native custom element callbacks (like disconnectedCallback()).
onBeforeEnter(location, commands, router)
The component's route has matched the current path, an instance of the component has been created and is about
to be inserted into the DOM. Use this callback to create a route guard (e.g. redirect to the login page if the
user is not logged in).
At this point there is yet
no guarantee that the navigation into this view will actually happen because another route's
callback may interfere.
This callback may return a redirect (return commands.redirect('/new/path')) or a
prevent (return commands.prevent()) router command. If it returns a promise, the router
waits until the promise is resolved before proceeding with the navigation.
Note: Navigating to the same route also triggers this callback, e.g., click on the same link
multiple times will trigger the onBeforeEnter
callback on each click.
onAfterEnter(location, commands, router)
The component's route has matched the current path and an instance of the component has been rendered into the
DOM. At this point it is certain that navigation won't be prevented or redirected. Use this callback to process
route params and initialize the view so that it's ready for user interactions.
NOTE: When navigating between views the onAfterEnter callback on the new view's component is called
before the onAfterLeave callback on the previous view's component (which is being
removed). At some point both the new and the old view components are present in the DOM to allow
animating the transition (you can listen to the
animationend event to detect when it is over).
Any value returned from this callback is ignored. See the
API documentation for more details.
onBeforeLeave(location, commands, router)
The component's route does not match the current path anymore and the component is about to be removed from the
DOM. Use this callback to prevent the navigation if necessary like in the demo below. Note that the user is
still able to copy and open that URL manually in the browser.
Even if this callback does not prevent the navigation, at this point there is yet
no guarantee that the navigation away from this view will actually happen because another
route's callback may also interfere.
This callback may return a prevent (return commands.prevent()) router command. If it
returns a promise, the router waits until the promise is resolved before proceeding with the navigation.
Note: Navigating to the same route also triggers this callback, e.g., click on the same link
multiple times will trigger the onBeforeLeave
callback on each click.
onAfterLeave(location, commands, router)
The component's route does not match the current path anymore and the component's removal from the DOM has been
started (it will be removed after a transition animation, if any). At this point it is certain that navigation
won't be prevented. Use this callback to clean-up and perform any custom actions that leaving a view should
trigger. For example, the demo below auto-saves any unsaved changes when the user navigates away from the view.
NOTE: When navigating between views the onAfterEnter callback on the new view's component is called
before the onAfterLeave callback on the previous view's component (which is being
removed). At some point both the new and the old view components are present in the DOM to allow
animating the transition (you can listen to the
animationend event to detect when it is over).
Any value returned from this callback is ignored. See the
API documentation for more details.
Listen to Global Navigation Events
In order to react to route changes in other parts of the app (outside of route components), you can add an event
listener for the vaadin-router-location-changed events on the window. Vaadin Router
dispatches such events every time after navigating to a new path.
In case if navigation fails for any reason (e.g. if no route matched the given pathname), instead of the
vaadin-router-location-changed event Vaadin Router dispatches vaadin-router-error and
attaches the error object to the event as event.detail.error.
Getting the Current Location
When handling Vaadin Router events, you can access the router instance via
event.detail.router, and the current location via event.detail.location (which is a
shorthand for event.detail.router.location). The
location
object has all details about the current router state. For example,
location.routes is a read-only list of routes that correspond to the last completed navigation,
which may be useful for example when creating a breadcrumbs component to visualize the current in-app location.
The router configuration allows you to add any custom properties to route objects. The example above uses that
to set a custom xBreadcrumb property on the routes that we want to show up in breadcrumbs. That
property is used later when processing the vaadin-router-location-changed events.
TypeScript Interfaces
For using with components defined as TypeScript classes, the following interfaces are available for
implementing:
This feature is for advanced use cases. Please make sure to read the documentation carefully.
There are several events that can trigger in-app navigation with Vaadin Router: popstate events, clicks on the
<a> elements, imperative navigation triggered by JavaScript. In order to make a flexible
solution that can be tweaked to particular app needs and remain efficient, Vaadin Router has a concept of
pluggable Navigation Triggers that listen to specific browser events and convert them into the Vaadin
Router navigation events.
The @vaadin/router package comes with two Navigation Triggers bundled by default:
POPSTATE and CLICK.
Developers can define and use additional Navigation Triggers that are specific to their application. A
Navigation Trigger object must have two methods: activate() to start listening on some UI events,
and inactivate() to stop.
NavigationTrigger.POPSTATE
The default POPSTATE navigation trigger for Vaadin Router listens to popstate events
on the current window and for each of them dispatches a navigation event for Vaadin Router using
the current window.location.pathname as the navigation target. This allows using the browser
Forward and Back buttons for in-app navigation.
In the demo below the popstate events are dispatched at 3 seconds intervals. Before dispatching an
event the global location.pathname is toggled between '/' and '/users'.
Please note that when using the window.history.pushState() or
window.history.replaceState() methods, you need to dispatch the popstate event
manually—these methods do not do that by themselves (see
MDN
for details).
NavigationTrigger.CLICK
The CLICK navigation trigger intercepts clicks on <a> elements on the the page
and converts them into navigation events for Vaadin Router if they refer to a location within the app. That
allows using regular <a> link elements for in-app navigation. You can use
router-ignore
attribute to have the router ignore the link.
Custom Navigation Triggers
The set of default navigation triggers can be changed using the Router.setTriggers() static
method. It accepts zero, one or more NavigationTriggers.
This demo shows how to disable the CLICK navigation trigger. It may be useful when the app has an
own custom element for in-app links instead of using the regular <a> elements for that
purpose. The demo also shows a very basic version of a custom in-app link element based on a list element that
fires popstate events when clicked.
Note: if the default Navigation Triggers are not used by the app, they can be excluded from the app bundle to
avoid sending unnecessary code to the users. See src/router-config.js for details.
Unsubscribe from Navigation Events
Each Vaadin Router instance is automatically subscribed to navigation events upon creation. Sometimes it might
be necessary to cancel this subscription so that the router would re-render only in response to the explicit
render() method calls. Use the unsubscribe() method to cancel this automatic
subscription and the subscribe() method to re-subscribe.
Vaadin Router supports the redirect property on the route
objects, allowing to unconditionally redirect users from one path to
another. The valid values are a path string or a pattern in the same
format as used for the path property.
The original path is not stored as the window.history entry
and cannot be reached by pressing the "Back" browser button. Unconditional
redirects work for routes both with and without parameters.
The original path is available to route Web Components as the
location.redirectFrom string property, and to custom
route actions –
as context.redirectFrom.
Note: If a route has both the redirect and action
properties, action is executed first and if it does not
return a result Vaadin Router proceeds to check the redirect
property. Other route properties (if any) would be ignored. In that case
Vaadin Router would also log a warning to the browser console.
Dynamic Redirects
Vaadin Router allows redirecting to another path dynamically based on
a condition evaluated at the run time. In order to do that,
return commands.redirect('/new/path') from the
onBeforeEnter() lifecycle
callback of the route Web Component.
It is also possible to redirect from a custom route action. The demo below
has an example of that in the /logout route action. See the
Route Actions section for
more details.
Navigation from JavaScript
If you want to send users to another path in response to a user
action (outside of a lifecycle callback), you can do that by using the
static
Router.go('/to/path') method on the Vaadin.Router class.
You can optionally pass search query string and hash to the method, either
as in-app URL string:
... or using an object with named parameters:
NOTE: the same effect can be achieved by dispatching a
vaadin-router-go custom event on the window. The
target path should be provided as event.detail.pathname,
the search and hash strings can be optionally provided
with event.detail.search and event.detail.hash
properties respectively.
`;
}
}
================================================
FILE: demo/redirect/snippets/s1.ts
================================================
import { Router } from '@vaadin/router';
// tag::snippet[]
Router.go('/to/path?paramName=value#sectionName');
// end::snippet[]
================================================
FILE: demo/redirect/snippets/s2.ts
================================================
import { Router } from '@vaadin/router';
// tag::snippet[]
Router.go({
pathname: '/to/path',
// optional
search: '?paramName=value',
// optional
hash: '#sectionName',
});
// end::snippet[]
================================================
FILE: demo/redirect/snippets/s3.ts
================================================
// tag::snippet[]
window.dispatchEvent(
new CustomEvent('vaadin-router-go', {
detail: {
pathname: '/to/path',
// optional search query string
search: '?paramName=value',
// optional hash string
hash: '#sectionName',
},
}),
);
// end::snippet[]
export {};
================================================
FILE: demo/route-actions/d1/iframe.html
================================================
Route Actions
================================================
FILE: demo/route-actions/d1/script.ts
================================================
import '@helpers/iframe.script.js';
import '@helpers/x-home-view.js';
import '@helpers/x-user-list.js';
import '@helpers/x-user-profile.js';
import { Router, type RouteContext } from '@vaadin/router';
// tag::snippet[]
const urlToNumberOfVisits: Record = {};
async function recordUrlVisit(context: RouteContext) {
const visitedPath = context.pathname; // get current path
urlToNumberOfVisits[visitedPath] = (urlToNumberOfVisits[visitedPath] ?? 0) + 1;
document.getElementById('stats')!.textContent =
`Statistics on URL visits: ${JSON.stringify(urlToNumberOfVisits, null, 2)}}`;
return await context.next(); // pass to the next route in the list
}
const router = new Router(document.getElementById('outlet'));
await router.setRoutes([
{ path: '/', component: 'x-home-view' },
{
path: '/users',
action: recordUrlVisit, // will be triggered for all children
children: [
{ path: '/', component: 'x-user-list' },
{ path: '/:user', component: 'x-user-profile' },
],
},
]);
// end::snippet[]
================================================
FILE: demo/route-actions/d2/iframe.html
================================================
Route Actions
================================================
FILE: demo/route-actions/d2/script.ts
================================================
import '@helpers/iframe.script.js';
import '@helpers/x-home-view.js';
import '@helpers/x-user-list.js';
import '@helpers/x-user-profile.js';
import { Router } from '@vaadin/router';
// tag::snippet[]
async function pollBackendForChanges() {
return await new Promise((resolve) => {
// this can be an async backend call
setTimeout(() => resolve(), 1000);
});
}
const router = new Router(document.getElementById('outlet'));
await router.setRoutes([
{ path: '/', component: 'x-home-view' },
{
path: '/users',
action: pollBackendForChanges, // will be triggered for all children
children: [
{ path: '/', component: 'x-user-list' },
{ path: '/:user', component: 'x-user-profile' },
],
},
]);
// end::snippet[]
================================================
FILE: demo/route-actions/d3/iframe.html
================================================
Route Actions
================================================
FILE: demo/route-actions/d3/script.ts
================================================
import '@helpers/iframe.script.js';
import '@helpers/x-home-view.js';
import '@helpers/x-user-profile.js';
import { Router, type Commands, type RouteContext } from '@vaadin/router';
// tag::snippet[]
function redirect(context: RouteContext, commands: Commands) {
(context.params.user as string) += ' (redirected)';
return commands.redirect(`/users/:user`);
}
const router = new Router(document.getElementById('outlet'));
await router.setRoutes([
{ path: '/', component: 'x-home-view' },
// current route parameters are automatically transferred to redirect target
{ path: '/employees/:user', action: redirect },
{ path: '/users/:user', component: 'x-user-profile' },
]);
// end::snippet[]
================================================
FILE: demo/route-actions/d4/iframe.html
================================================
Route Actions
================================================
FILE: demo/route-actions/d4/script.ts
================================================
import '@helpers/iframe.script.js';
import '@helpers/x-home-view.js';
import '@helpers/x-user-profile.js';
import { Router, type Commands, type RouteContext } from '@vaadin/router';
// tag::snippet[]
function render(context: RouteContext, commands: Commands) {
if (context.params.user === 'admin') {
return commands.component('x-user-profile');
}
const stubElement = commands.component('h3');
stubElement.innerHTML = 'Access denied';
return stubElement;
}
const router = new Router(document.getElementById('outlet'));
await router.setRoutes([
{ path: '/', component: 'x-home-view' },
// current route parameters are automatically transferred to rendered element
{ path: '/users/:user', action: render },
]);
// end::snippet[]
================================================
FILE: demo/route-actions/d5/iframe.html
================================================
Route Actions
================================================
FILE: demo/route-actions/d5/script.ts
================================================
import '@helpers/iframe.script.js';
import '@helpers/x-home-view.js';
import '@helpers/x-login-view.js';
import { Router, type Commands, type RouteContext } from '@vaadin/router';
// tag::snippet[]
const router = new Router(document.getElementById('outlet'));
await router.setRoutes([
{
path: '/',
async action(context: RouteContext, commands: Commands) {
// Extract the `?view=` parameter value and decide upon it
const view = new URLSearchParams(context.search).get('view');
if (view === 'login') {
return commands.component('x-login-view');
} else if (view) {
// Redirect home for unkown values
return commands.redirect('/');
}
// Skip to next route if parameter is absent
return await context.next();
},
},
// Same path, only matches if the action above skips
{ path: '/', component: 'x-home-view' },
]);
// end::snippet[]
================================================
FILE: demo/route-actions/index.html
================================================
Route Actions
================================================
FILE: demo/route-actions/index.ts
================================================
/* eslint-disable import/no-duplicates, import/default */
import '@helpers/common.js';
import '@helpers/vaadin-demo-layout.js';
import '@helpers/vaadin-demo-code-snippet.js';
import '@helpers/vaadin-presentation.js';
import { html, LitElement, type TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import htmlCode1 from './d1/iframe.html?snippet';
import tsCode1 from './d1/script.js?snippet';
import htmlCode2 from './d2/iframe.html?snippet';
import tsCode2 from './d2/script.js?snippet';
import htmlCode3 from './d3/iframe.html?snippet';
import tsCode3 from './d3/script.js?snippet';
import htmlCod4 from './d4/iframe.html?snippet';
import tsCode4 from './d4/script.js?snippet';
import htmlCode5 from './d5/iframe.html?snippet';
import tsCode5 from './d5/script.js?snippet';
import css from '@helpers/page.css?ctr';
import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js';
declare global {
interface HTMLElementTagNameMap {
'vaadin-demo-route-actions': DemoRouteActions;
}
}
const files1: readonly CodeSnippet[] = [
{
id: 'html',
code: htmlCode1,
title: 'index.html',
},
{
id: 'ts',
code: tsCode1,
title: 'script.ts',
},
];
const files2: readonly CodeSnippet[] = [
{
id: 'html',
code: htmlCode2,
title: 'index.html',
},
{
id: 'ts',
code: tsCode2,
title: 'script.ts',
},
];
const files3: readonly CodeSnippet[] = [
{
id: 'html',
code: htmlCode3,
title: 'index.html',
},
{
id: 'ts',
code: tsCode3,
title: 'script.ts',
},
];
const files4: readonly CodeSnippet[] = [
{
id: 'html',
code: htmlCod4,
title: 'index.html',
},
{
id: 'ts',
code: tsCode4,
title: 'script.ts',
},
];
const files5: readonly CodeSnippet[] = [
{
id: 'html',
code: htmlCode5,
title: 'index.html',
},
{
id: 'ts',
code: tsCode5,
title: 'script.ts',
},
];
@customElement('vaadin-demo-route-actions')
export default class DemoRouteActions extends LitElement {
static override styles = [css];
override render(): TemplateResult {
return html`
Custom Route Actions
This feature is for advanced use cases. Please make sure to read the documentation carefully.
Route resolution is an async operation started by a navigation event, or by an explicit
render() method call. In that process Vaadin Router goes through the routes config and tries to
resolve each matching route from the root onwards. The default route resolution rule is to create and
return an instance of the route's component (see the API docs for the
setRoutes() method for details on other route properties and how they affect the route resolution).
Vaadin Router provides a flexible API to customize the default route resolution rule. Each route may have an
action functional property that defines how exactly that route is resolved. An
action function can return a result either directly, or within a Promise resolving to
the result. If the action result is an HTMLElement instance, a
commands.component(name) result, a commands.redirect(path) result, or a
context.next() result, the resolution pass, and the returned value is what gets rendered.
Otherwise, the resolution process continues to check the other properties of the route and apply the default
resolution rules, and then further to check other matching routes.
The action(context, commands) function receives a context
parameter with the following properties:
context.pathname [string] the pathname being resolved
context.search [string] the search query string
context.hash [string] the hash string
context.params [object] the route parameters
context.route [object] the route that is currently being rendered
context.next() [function] function for asynchronously getting the next route contents from the
resolution chain (if any)
The commands is a helper object with methods to create return values for the action:
return commands.redirect('/new/path') create and return a redirect command for the specified
path. This command should be returned from the action to perform an actual redirect.
return commands.prevent() create and return a prevent command. This command should be returned
from the action
to instruct router to stop the current navigation and remain at the previous location.
return commands.component('tag-name') create and return a new HTMLElement that will
be rendered into the router outlet. Using the component command ensures that the created
component is initialized as a Vaadin Router view (i.e. the location property is set according to
the current router context.
If an action returns this element, the behavior is the same as for
component route property: the action result will be rendered, if the action is in a child route,
the result will be rendered as light dom child of the component from a parent route.
This demo shows how to use the custom action property to collect visit statistics for each route.
Async Route Resolution
Since route resolution is async, the action() callback may be async as well and return a promise.
One use case for that is to create a custom route action that makes a remote API call to fetch the data
necessary to render the route component, before navigating to a route.
Note: If a route has both the component and action properties, action is
executed first and if it does not return a result Vaadin.Router proceeds to check the
component property.
This demo shows a way to perform async tasks before navigating to any route under /users.
Redirecting from an Action
action() can return a command created using the commands parameter methods to affect
the route resolution result. The first demo had demonstrated the context.next() usage, this demo
demonstrates using commands.redirect(path) to redirect to any other defined route by using its
path. All the parameters in current context will be passed to the redirect target.
Note: If you need only to redirect to another route, defining an action might be an overkill. More convenient
way is described in Redirects section.
Returning Custom Element as an Action Result
Another command available to a custom action() is commands.component('tag-name'). It
is useful to create a custom element with current context. All the parameters in current context will be passed
to the rendered element.
Note: If the only thing your action does is custom element creation, it can be replaced with
component property of the route. See
Getting Started
section for examples.
Routing With Search Query and Hash
Route action function can access context.search and context.hash URL parts, even
though they are not involved in matching route paths.
For example, an action can change the route behavior depending on a search parameter, and optionally render,
skip to next route or redirect.
Route parameters are useful when the same Web Component should be
rendered for a number of paths, where a part of the path is static, and
another part contains a parameter value. E.g. for both /user/1
and /user/42 paths it's the same Web Component that
renders the content, the /user/ part is static, and 1
and 42 are the parameter values.
Route parameters are defined using an express.js-like syntax. The
implementation is based on the path-to-regexp library that is commonly
used in modern front-end libraries and frameworks. All features are
supported:
named parameters: /profile/:user
optional parameters: /:size/:color?
zero-or-more segments: /kb/:path*
one-or-more segments: /kb/:path+
custom parameter patterns: /image-:size(\d+)px
unnamed parameters: /(user[s]?)/:id
Accessing Route Parameters
Route parameters are bound to the location.params property of
the route Web Component
(location
is set on the route components by Vaadin Router).
Named parameters are accessible by a string key, e.g.
location.params.id or location.params['id']
Unnamed parameters are accessible by a numeric index, e.g.
location.params[0]
The example below shows how to access route parameters:
Ambiguous Matches
Route matching rules can be ambiguous, so that several routes would match
the same path. In that case, the order in which the routes are defined is
important. The first route matching the path gets rendered (starting from
the top of the list / root of the tree).
The default route matching is exact, i.e. a
'/user' route (if it does not have children) matches only the
'/user' path, but not '/users' nor
'/user/42'. Trailing slashes are not significant in paths,
but are significant in routes, i.e. a '/user' route matches
both '/user' the '/user/', but a
'/user/' route matches only the '/user/' path.
Prefix matching is used for routes with children, or if
the route explicitly indicates that trailing content is expected (e.g.
a '/users/(*.)' route matches any path starting with
'/users/').
Always place more specific routes before less specific:
{path: '/user/new', ...} - matches only
'/user/new'
{path: '/user/:user', ...} - matches
'/user/42', but not '/user/42/edit'
{path: '/user/(.*)', ...} - matches anything starting
with '/user/'
Typed Route Parameters
The route can be configured so that only specific characters are accepted for a parameter value.
Other characters would not meet the check and the route resolution would continue to other routes.
You only can use unnamed parameters in this case, as it can only be achieved using the custom RegExp.
One possible alternative is to use Route Actions
and check the context.params.
Search Query Parameters
The search query string (?example) URL part is considered
separate from the pathname. Hence, it does not participate in matching
the route path, and location.params does not
contain search query string parameters.
Use location.search to access the raw search query string.
Use location.searchParams to get the URLSearchParams wrapper of the search query string.
Hash String
Likewise with the search query, the hash string (#example)
is separate from the pathname as well. Use location.hash
to access the hash string in the view component.
Vaadin Router supports referring to routes using string names. You can assign a name to a route using the
name property of a route object, then generate URLs for that route using the
router.urlForName(name, parameters)
helper instance method.
Arguments:
name — the route name
parameters — optional object with parameters for substitution in the route path
If the component property is specified on the route object, the name property could be
omitted. In that case, the component name could be used in the router.urlForName().
The router.urlForPath method
router.urlForPath(path, parameters)
is a helper method that generates a URL for the given route path, optionally performing substitution of
parameters.
Arguments:
path — a string route path defined in express.js syntax
parameters — optional object with parameters for path substitution
The location.getUrl method
location.getUrl(params)
is a method that returns a URL corresponding to the location. When given the params argument, it does parameter
substitution in the location’s chain of routes.
Arguments:
params — optional object with parameters to override the location parameters
Base URL in URL generation
When base URL is set, the URL generation helpers return absolute pathnames, including the base.
Generating URLs with search query parameters and hash string
At the moment, Vaadin Router does not provide URL generation APIs for appending search query parameters or hash
strings to the generated URLs. However, you could append those with string concatenation.
For serialising parameters into a query string, use the native
URLSearchParams API.
`;
}
}
================================================
FILE: demo/vite.config.ts
================================================
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';
import { mergeConfig } from 'vite';
import { codeSnippetPlugin } from '../scripts/codeSnippet.js';
import viteConfig from '../vite.config.js';
const root = new URL('./', import.meta.url);
function convertToId(str: string) {
return str.replace(/[-/]./gu, (x) => x[1].toUpperCase());
}
const dirs = Object.fromEntries(
await glob('./**/{index,iframe}.html', { cwd: root }).then((files) =>
files
.filter((file) => !file.startsWith('@') && file !== 'index.html')
.map((name) => [convertToId(dirname(name)), fileURLToPath(new URL(name, root))]),
),
);
export default mergeConfig(viteConfig, {
base: './',
build: {
outDir: fileURLToPath(new URL('../.docs', root)),
rollupOptions: {
input: {
main: fileURLToPath(new URL('./index.html', root)),
...dirs,
},
},
},
resolve: {
alias: {
'@helpers/': fileURLToPath(new URL('./@helpers/', root)),
'@vaadin/router': fileURLToPath(new URL('../src/index.js', root)),
},
},
plugins: [codeSnippetPlugin()],
root: fileURLToPath(root),
});
================================================
FILE: index.html
================================================
================================================
FILE: karma.config.cjs
================================================
const karmaChromeLauncher = require('karma-chrome-launcher');
const karmaCoverage = require('karma-coverage');
const karmaMocha = require('karma-mocha');
const karmaVite = require('karma-vite');
const karmaSpecReporter = require('karma-spec-reporter');
const isCI = !!process.env.CI;
const watch = !!process.argv.find((arg) => arg.includes('watch')) && !isCI;
const coverage = !!process.argv.find((arg) => arg.includes('--coverage'));
module.exports = (config) => {
config.set({
basePath: '',
plugins: [karmaMocha, karmaChromeLauncher, karmaVite, karmaCoverage, karmaSpecReporter],
browsers: ['ChromeHeadlessNoSandbox'],
// you can define custom flags
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-setuid-sandbox', isCI ? undefined : '--disable-dev-shm-usage'].filter(
Boolean,
),
},
},
captureTimeout: 60000, // it was already there
browserDisconnectTimeout: 10000,
browserDisconnectTolerance: 1,
browserNoActivityTimeout: 60000, //by default 10000
frameworks: ['vite', 'mocha'],
files: [
{
pattern: 'test/**/*.spec.ts',
type: 'module',
watched: false,
served: false,
},
],
reporters: ['spec', coverage && 'coverage'].filter(Boolean),
autoWatch: watch,
singleRun: !watch,
coverageReporter: {
dir: '.coverage/',
reporters: [!isCI && { type: 'html', subdir: 'html' }, { type: 'lcovonly', subdir: '.' }].filter(Boolean),
},
vite: {
autoInit: true,
},
});
};
================================================
FILE: package.json
================================================
{
"name": "@vaadin/router",
"version": "2.0.1",
"description": "Small and powerful client-side router for Web Components. Framework-agnostic.",
"main": "dist/index.js",
"module": "dist/index.js",
"type": "module",
"repository": "vaadin/vaadin-router",
"keywords": [
"Vaadin",
"vaadin-router",
"router",
"client-side",
"web-components"
],
"author": "Vaadin Ltd",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/vaadin/vaadin-router/issues"
},
"homepage": "https://vaadin.com",
"files": [
"dist"
],
"scripts": {
"clean:build": "git clean -fx . -e .vite -e node_modules -e .idea -e .vscode",
"install:dependencies": "bower install",
"lint": "concurrently npm:lint:*",
"lint:js": "eslint --ext .html,.js src test demo *.js *.html",
"build": "concurrently npm:build:*",
"build:esbuild": "tsx scripts/build.ts",
"build:copy-dts": "tsx scripts/copy-dts.ts",
"build:tsc": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"start": "npm run docs && cd demo && vite preview",
"test": "karma start karma.config.cjs",
"test:watch": "npm run test -- --watch",
"test:coverage": "npm run test -- --coverage",
"docs": "npm run docs:demo && npm run docs:typedoc",
"docs:typedoc": "typedoc",
"docs:demo": "cd demo && vite build --emptyOutDir",
"demo:dev": "cd demo && vite dev",
"browserslist": "browserslist && browserslist --coverage",
"prepack": "npm run clean:build",
"prepare": "npm run build",
"typecheck": "tsc --noEmit"
},
"browserslist": [
"last 1 Chrome major versions",
"last 1 Firefox major versions",
"Firefox ESR",
"last 1 Edge major versions",
"Safari >= 13",
"iOS >= 13",
"last 1 ChromeAndroid major versions"
],
"dependencies": {
"@vaadin/vaadin-usage-statistics": "^2.1.3",
"path-to-regexp": "^6.3.0",
"type-fest": "^5.2.0"
},
"devDependencies": {
"@esm-bundle/chai": "^4.3.4-fix.0",
"@lit-labs/preact-signals": "^1.0.3",
"@preact/signals-core": "^1.12.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/user-event": "^14.6.1",
"@types/chai-as-promised": "^8.0.2",
"@types/chai-dom": "^1.11.3",
"@types/glob": "^9.0.0",
"@types/karma": "^6.3.9",
"@types/karma-chrome-launcher": "^3.1.4",
"@types/karma-mocha": "^1.3.4",
"@types/mocha": "^10.0.10",
"@types/postcss-import": "^14.0.3",
"@types/sinon": "^17.0.4",
"@types/sinon-chai": "^4.0.0",
"@vaadin/accordion": "^24.9.5",
"@vaadin/app-layout": "^24.9.5",
"@vaadin/button": "^24.9.5",
"@vaadin/dialog": "^24.9.5",
"@vaadin/icon": "^24.9.5",
"@vaadin/icons": "^24.9.5",
"@vaadin/scroller": "^24.9.5",
"@vaadin/side-nav": "^24.9.5",
"@vaadin/tabs": "^24.9.5",
"@vaadin/tabsheet": "^24.9.5",
"@vaadin/text-field": "^24.9.5",
"@vaadin/tooltip": "^24.9.5",
"@vaadin/vaadin-lumo-styles": "^24.9.5",
"bower": "^1.8.14",
"browserslist": "^4.27.0",
"chai-as-promised": "^8.0.2",
"chai-dom": "^1.12.1",
"concurrently": "^9.2.1",
"cssnano": "^7.1.2",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"eslint-config-vaadin": "^1.0.0-beta.3",
"eslint-plugin-html": "^8.1.3",
"eslint-plugin-tsdoc": "^0.4.0",
"fs-extra": "^11.3.2",
"glob": "^11.0.3",
"highlight.js": "^11.11.1",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-iframes": "^1.3.1",
"karma-mocha": "^2.0.1",
"karma-parallel": "^0.3.1",
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"lit": "^3.3.1",
"magic-string": "^0.30.21",
"mocha": "^11.7.5",
"node-html-parser": "^7.0.1",
"postcss": "^8.5.6",
"postcss-import": "^16.1.1",
"postcss-nested-import": "^1.3.0",
"prettier": "^3.6.2",
"regexp.escape": "^2.0.1",
"sinon": "^21.0.0",
"sinon-chai": "^4.0.1",
"tsx": "^4.20.6",
"typedoc": "^0.28.14",
"typedoc-plugin-missing-exports": "^4.1.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "7.2.2",
"yargs": "^18.0.0"
},
"overrides": {
"karma-vite": {
"vite": "$vite"
}
}
}
================================================
FILE: polymer.json
================================================
{
"entrypoint": "bower_components/vaadin-router/index.html",
"shell": "bower_components/iron-component-page/iron-component-page.html",
"sources": [
"bower_components/vaadin-router/index.html",
"bower_components/iron-component-page/iron-component-page.html"
],
"extraDependencies": [
"bower_components/webcomponentsjs/*.js",
"bower_components/webcomponentsjs/*.map",
"bower_components/vaadin-router/analysis.json"
],
"builds": [
{
"preset": "es5-bundled",
"addServiceWorker": false
}
]
}
================================================
FILE: scripts/build.ts
================================================
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
import { glob } from 'glob';
import type { PackageJson } from 'type-fest';
const scriptsDir = new URL('./', import.meta.url);
const root = new URL('../', import.meta.url);
const [packageJson, entryPoints] = await Promise.all([
readFile(new URL('package.json', root), 'utf8').then(JSON.parse) as Promise,
glob('src/**/*.{ts,tsx}', { ignore: ['**/*.t.ts'] }),
]);
await build({
define: {
__NAME__: `'${packageJson.name ?? '@hilla/unknown'}'`,
__VERSION__: `'${packageJson.version ?? '0.0.0'}'`,
},
entryPoints,
format: 'esm',
// Adds a __REGISTER__ function definition everywhere in the built code where
// the call for that function exists.
inject: [fileURLToPath(new URL('./register.js', scriptsDir))],
outdir: 'dist',
sourcemap: 'linked',
sourcesContent: true,
tsconfig: fileURLToPath(new URL('./tsconfig.build.json', root)),
});
================================================
FILE: scripts/codeSnippet.ts
================================================
import 'regexp.escape/auto';
import { extname } from 'node:path';
import hljs from 'highlight.js/lib/core';
import cssLang from 'highlight.js/lib/languages/css';
import javascriptLang from 'highlight.js/lib/languages/javascript';
import typescriptLang from 'highlight.js/lib/languages/typescript';
import xmlLang from 'highlight.js/lib/languages/xml';
import * as prettier from 'prettier';
import type { Plugin } from 'vite';
hljs.registerLanguage('javascript', javascriptLang);
hljs.registerLanguage('css', cssLang);
hljs.registerLanguage('typescript', typescriptLang);
hljs.registerLanguage('xml', xmlLang);
function langFromExt(ext: string) {
switch (ext) {
case 'ts':
return 'typescript';
case 'html':
return 'xml';
default:
return ext;
}
}
type SnippetPatternKey = keyof typeof snippetPattern;
const snippetPattern = {
html: /([\s\S]*?)/gmu,
ts: /\/\/ tag::snippet\[\]([\s\S]*?)\/\/ end::snippet\[\]/gmu,
css: /\/\* tag::snippet\[\] \*\/([\s\S]*?)\/\* end::snippet\[\] \*\//gmu,
} as const;
const patternsToRemove = [/^\s*(?:\/\/|\/\*) eslint-disable.*$/gimu];
function removePatterns(code: string) {
return patternsToRemove.reduce((acc, pattern) => acc.replace(pattern, ''), code);
}
function extractSnippets(code: string, language: SnippetPatternKey) {
const result: string[] = [];
const pattern = snippetPattern[language];
let match;
while ((match = pattern.exec(code)) != null) {
result.push(match[1].trim());
}
if (result.length === 0) {
result.push(code);
}
return result;
}
function escapeString(str: string) {
return str.replaceAll(/(`|\$|\{\})/gu, '\\$1');
}
export function codeSnippetPlugin(): Plugin {
return {
name: 'code-snippet',
enforce: 'pre',
async resolveId(id, importer) {
if (id.includes('?')) {
const resolved = await this.resolve(id, importer);
if (resolved) {
const search = resolved.id.substring(resolved.id.indexOf('?'));
const purePath = resolved.id.substring(0, resolved.id.length - search.length);
const params = new URLSearchParams(search);
if (params.has('snippet') && purePath.endsWith('.css')) {
params.append('raw', '');
return {
...resolved,
id: `${purePath}?${params.toString()}`,
};
}
return resolved;
}
}
return null;
},
async transform(code, id) {
if (id.includes('?')) {
const search = id.substring(id.indexOf('?'));
const params = new URLSearchParams(search);
if (params.has('snippet')) {
const purePath = id.substring(0, id.length - search.length);
const ext = extname(purePath).substring(1);
if (ext === 'ts' || ext === 'html' || ext === 'css') {
let snippets = extractSnippets(removePatterns(code), ext);
snippets = [code, ...snippets];
snippets = await Promise.all(
snippets.map(
async (snippet) =>
await prettier.format(snippet, {
parser: ext === 'ts' ? 'typescript' : ext,
linewidth: 80,
singleQuote: true,
trailingComma: 'all',
}),
),
);
snippets = snippets.map((snippet) => hljs.highlight(snippet, { language: langFromExt(ext) }).value);
return {
code: `import { html } from 'lit'; export default [\`${escapeString(code)}\`,${snippets
.map((snippet) => `html\`${escapeString(snippet)}\``)
.join(',')}];`,
map: null,
};
}
}
}
return null;
},
};
}
================================================
FILE: scripts/constructCss.ts
================================================
import { readFile } from 'node:fs/promises';
import cssnanoPlugin from 'cssnano';
import postcss from 'postcss';
import nestedImport from 'postcss-nested-import';
import type { Plugin } from 'vite';
const cssTransformer = postcss([
nestedImport(),
cssnanoPlugin({
preset: 'default',
}),
]);
export default function constructCss(): Plugin {
const styles = new Map();
return {
enforce: 'post',
name: 'vite-construct-css',
async load(id) {
if (id.endsWith('.css?ctr')) {
const content = await readFile(id.substring(0, id.length - 4), 'utf8');
styles.set(id, content);
return {
code: '',
};
}
return undefined;
},
async transform(_, id) {
if (styles.has(id)) {
const css = styles.get(id);
const { content } = await cssTransformer.process(css, {
from: id,
});
return {
code: `const css = new CSSStyleSheet(); css.replaceSync(${JSON.stringify(content)}); export default css;`,
};
}
return undefined;
},
};
}
================================================
FILE: scripts/copy-dts.ts
================================================
import { constants, copyFile, mkdir } from 'node:fs/promises';
import { glob } from 'glob';
const root = new URL('../', import.meta.url);
const src = new URL('./src/', root);
const dist = new URL('./dist/', root);
await mkdir(dist, { recursive: true });
const files = await glob(['**/*.d.ts'], { cwd: src });
await Promise.all(
files.map(async (f) => {
const file = new URL(f, dist);
await mkdir(new URL('./', file), { recursive: true });
return await copyFile(new URL(f, src), file, constants.COPYFILE_FICLONE);
}),
);
================================================
FILE: scripts/loadRegisterJs.ts
================================================
import { readFile } from 'node:fs/promises';
import MagicString from 'magic-string';
import type { Plugin } from 'vite';
const scripts = new URL('./', import.meta.url);
// This plugin adds "__REGISTER__()" function definition everywhere where it finds
// the call for that function. It is necessary for a correct code for tests.
export default function loadRegisterJs(): Plugin {
return {
enforce: 'pre',
name: 'vite-hilla-register',
async transform(code) {
if (code.includes('__REGISTER__()') && !code.includes('function __REGISTER__')) {
const registerCode = await readFile(new URL('register.js', scripts), 'utf8').then((c) =>
c.replace('export', ''),
);
const _code: MagicString = new MagicString(code);
_code.prepend(registerCode);
return {
code: _code.toString(),
map: _code.generateMap(),
};
}
return null;
},
};
}
================================================
FILE: scripts/register.js
================================================
export function __REGISTER__(feature, vaadinObj = (window.Vaadin ??= {})) {
vaadinObj.registrations ??= [];
vaadinObj.registrations.push({
is: feature ? `${__NAME__}/${feature}` : __NAME__,
version: __VERSION__,
});
}
================================================
FILE: scripts/resolveHTMLImports.ts
================================================
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { Plugin } from 'vite';
const searchParamsPattern = /\?.*$/iu;
export function resolveHTMLImports(): Plugin {
return {
enforce: 'pre',
name: 'resolve-html-imports',
resolveId(id, importer) {
const _importer = importer ? pathToFileURL(importer.replace(searchParamsPattern, '')) : null;
if (
_importer?.pathname.endsWith('.html') &&
!_importer.pathname.endsWith('index.html') &&
!_importer.pathname.includes('node_modules') &&
id.endsWith('.js')
) {
return fileURLToPath(new URL(id.replace('.js', '.ts'), _importer as URL));
}
return null;
},
};
}
================================================
FILE: scripts/types.d.ts
================================================
/* eslint-disable import/unambiguous */
declare module 'regexp.escape/auto';
interface RegExpConstructor {
escape(str: string): string;
}
declare module 'postcss-nested-import' {
import type { Processor } from 'postcss';
function plugin(): Processor;
export = plugin;
}
================================================
FILE: src/index.ts
================================================
export * from './router.js';
export { DEFAULT_TRIGGERS } from './triggers/navigation.js';
export type * from './types.t.js';
export type * from './v1-compat.t.js';
export {
processNewChildren,
amend,
maybeCall,
renderElement,
createRedirect,
createLocation,
getMatchedPath,
getPathnameForRouter,
copyContextWithoutNext,
} from './utils.js';
================================================
FILE: src/mod.t.ts
================================================
// eslint-disable-next-line import/unambiguous
declare module '@vaadin/vaadin-usage-statistics/vaadin-usage-statistics.js' {
// eslint-disable-next-line import/prefer-default-export
export function usageStatistics(): void;
}
================================================
FILE: src/resolver/LICENSE.txt
================================================
The MIT License
Copyright (c) 2015-present Kriasoft.
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: src/resolver/generateUrls.ts
================================================
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import { parse, type ParseOptions, type Token, tokensToFunction, type TokensToFunctionOptions } from 'path-to-regexp';
import type { EmptyObject, Writable } from 'type-fest';
import Resolver from './resolver.js';
import type { ChildrenCallback, IndexedParams, Params, Route } from './types.t.js';
import { getRoutePath, isString } from './utils.js';
export type UrlParams = Readonly | number | string>>;
function cacheRoutes(
routesByName: Map>>,
route: Route,
routes?: ReadonlyArray> | ChildrenCallback,
cacheKeyProvider?: (route: Route) => string | undefined,
): void {
const name = route.name ?? cacheKeyProvider?.(route);
if (name) {
if (routesByName.has(name)) {
routesByName.get(name)?.push(route);
} else {
routesByName.set(name, [route]);
}
}
if (Array.isArray(routes)) {
for (const childRoute of routes) {
childRoute.parent = route;
cacheRoutes(routesByName, childRoute, childRoute.__children ?? childRoute.children, cacheKeyProvider);
}
}
}
function getRouteByName(
routesByName: Map>>,
routeName: string,
): Route | undefined {
const routes = routesByName.get(routeName);
if (routes) {
if (routes.length > 1) {
throw new Error(`Duplicate route with name "${routeName}".` + ` Try seting unique 'name' route properties.`);
}
return routes[0];
}
return undefined;
}
export type StringifyQueryParams = (params: UrlParams) => string;
export type GenerateUrlOptions = ParseOptions &
Readonly<{
/**
* Add a query string to generated url based on unknown route params.
*/
stringifyQueryParams?: StringifyQueryParams;
/**
* Generates a unique route name based on all parent routes with the specified separator.
*/
uniqueRouteNameSep?: string;
cacheKeyProvider?(route: Route): string | undefined;
}> &
TokensToFunctionOptions;
type RouteCacheRecord = Readonly<{
keys: Record;
tokens: Token[];
}>;
export type UrlGenerator = (routeName: string, params?: Params) => string;
function generateUrls(
resolver: Resolver,
options: GenerateUrlOptions = {},
): UrlGenerator {
if (!(resolver instanceof Resolver)) {
throw new TypeError('An instance of Resolver is expected');
}
const cache = new Map();
const routesByName = new Map>>();
return (routeName, params) => {
let route = getRouteByName(routesByName, routeName);
if (!route) {
routesByName.clear(); // clear cache
cacheRoutes(routesByName, resolver.root, resolver.root.__children, options.cacheKeyProvider);
route = getRouteByName(routesByName, routeName);
if (!route) {
throw new Error(`Route "${routeName}" not found`);
}
}
let cached: RouteCacheRecord | undefined = route.fullPath ? cache.get(route.fullPath) : undefined;
if (!cached) {
let fullPath = getRoutePath(route);
let rt = route.parent;
while (rt) {
const path = getRoutePath(rt);
if (path) {
fullPath = `${path.replace(/\/$/u, '')}/${fullPath.replace(/^\//u, '')}`;
}
rt = rt.parent;
}
const tokens = parse(fullPath);
const keys: Record = Object.create(null);
for (const item of tokens) {
if (!isString(item)) {
keys[item.name] = true;
}
}
cached = { keys, tokens };
cache.set(fullPath, cached);
route.fullPath = fullPath;
}
const toPath = tokensToFunction(cached.tokens, { encode: encodeURIComponent, ...options });
let url = toPath(params) || '/';
if (options.stringifyQueryParams && params) {
const queryParams: Writable = {};
for (const [key, value] of Object.entries(params)) {
if (!(key in cached.keys) && value) {
queryParams[key] = value;
}
}
const query = options.stringifyQueryParams(queryParams as UrlParams);
if (query) {
url += query.startsWith('?') ? query : `?${query}`;
}
}
return url;
};
}
export default generateUrls;
================================================
FILE: src/resolver/matchPath.ts
================================================
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import { type Key, pathToRegexp } from 'path-to-regexp';
import type { Writable } from 'type-fest';
import type { IndexedParams } from '../types.t.js';
import { resolvePath } from './utils.js';
export interface RegExpExecOptArray extends ReadonlyArray {
0: string;
index: number;
input: string;
}
type Matcher = Readonly<{
keys: readonly Key[];
pattern: RegExp;
}>;
export type Match = Readonly<{
keys: readonly Key[];
params: IndexedParams;
path: string;
}>;
const cache = new Map();
// see https://github.com/pillarjs/path-to-regexp/issues/148
cache.set('|false', {
keys: [],
pattern: /(?:)/u,
});
function decodeParam(val: string): string {
try {
return decodeURIComponent(val);
} catch {
return val;
}
}
// eslint-disable-next-line @typescript-eslint/max-params
function matchPath(
routePath: string,
path?: string[] | string,
exact: boolean = false,
parentKeys: readonly Key[] = [],
parentParams?: IndexedParams,
): Match | null {
const cacheKey = `${routePath}|${String(exact)}`;
const _path = resolvePath(path);
let regexp = cache.get(cacheKey);
if (!regexp) {
const keys: Key[] = [];
regexp = {
keys,
pattern: pathToRegexp(routePath, keys, {
end: exact,
strict: routePath === '',
}),
};
cache.set(cacheKey, regexp);
}
const m: RegExpExecOptArray | null = regexp.pattern.exec(_path);
if (!m) {
return null;
}
const params: Writable = { ...parentParams };
for (let i = 1; i < m.length; i++) {
const key = regexp.keys[i - 1];
const prop = key.name;
const value = m[i];
if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) {
if (key.modifier === '+' || key.modifier === '*') {
// by default, as of path-to-regexp 6.0.0, the default delimiters
// are `/`, `#` and `?`.
params[prop] = value ? value.split(/[/?#]/u).map(decodeParam) : [];
} else {
params[prop] = value ? decodeParam(value) : value!;
}
}
}
return {
keys: [...parentKeys, ...regexp.keys],
params,
path: m[0],
};
}
export default matchPath;
================================================
FILE: src/resolver/matchRoute.ts
================================================
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import type { Key } from 'path-to-regexp';
import matchPath, { type Match } from './matchPath.js';
import type { IndexedParams, Route } from './types.t.js';
import { getRoutePath, unwrapChildren } from './utils.js';
export type MatchWithRoute = Match &
Readonly<{
route: Route;
}>;
type RouteMatchIterator = Iterator<
MatchWithRoute,
undefined,
Route | undefined
>;
/**
* Traverses the routes tree and matches its nodes to the given pathname from
* the root down to the leaves. Each match consumes a part of the pathname and
* the matching process continues for as long as there is a matching child
* route for the remaining part of the pathname.
*
* The returned value is a lazily evaluated iterator.
*
* The leading "/" in a route path matters only for the root of the routes
* tree (or if all parent routes are ""). In all other cases a leading "/" in
* a child route path has no significance.
*
* The trailing "/" in a _route path_ matters only for the leaves of the
* routes tree. A leaf route with a trailing "/" matches only a pathname that
* also has a trailing "/".
*
* The trailing "/" in a route path does not affect matching of child routes
* in any way.
*
* The trailing "/" in a _pathname_ generally does not matter (except for
* the case of leaf nodes described above).
*
* The "" and "/" routes have special treatment:
* 1. as a single route
* the "" and "/" routes match only the "" and "/" pathnames respectively
* 2. as a parent in the routes tree
* the "" route matches any pathname without consuming any part of it
* the "/" route matches any absolute pathname consuming its leading "/"
* 3. as a leaf in the routes tree
* the "" and "/" routes match only if the entire pathname is consumed by
* the parent routes chain. In this case "" and "/" are equivalent.
* 4. several directly nested "" or "/" routes
* - directly nested "" or "/" routes are 'squashed' (i.e. nesting two
* "/" routes does not require a double "/" in the pathname to match)
* - if there are only "" in the parent routes chain, no part of the
* pathname is consumed, and the leading "/" in the child routes' paths
* remains significant
*
* Side effect:
* - the routes tree `{ path: '' }` matches only the '' pathname
* - the routes tree `{ path: '', children: [ { path: '' } ] }` matches any
* pathname (for the tree root)
*
* Prefix matching can be enabled also by `children: true`.
*/
// eslint-disable-next-line @typescript-eslint/max-params
function matchRoute(
route: Route,
pathname: string,
ignoreLeadingSlash?: boolean,
parentKeys?: readonly Key[],
parentParams?: IndexedParams,
): Iterator, undefined, Route | undefined> {
let match: Match | null;
let childMatches: RouteMatchIterator | null;
let childIndex = 0;
let routepath = getRoutePath(route);
if (routepath.startsWith('/')) {
if (ignoreLeadingSlash) {
routepath = routepath.substring(1);
}
// eslint-disable-next-line no-param-reassign
ignoreLeadingSlash = true;
}
return {
next(routeToSkip?: Route): IteratorResult, undefined> {
if (route === routeToSkip) {
return { done: true, value: undefined };
}
route.__children ??= unwrapChildren(route.children);
const children = route.__children ?? [];
const exact = !route.__children && !route.children;
if (!match) {
match = matchPath(routepath, pathname, exact, parentKeys, parentParams);
if (match) {
return {
value: {
keys: match.keys,
params: match.params,
path: match.path,
route,
},
};
}
}
if (match && children.length > 0) {
while (childIndex < children.length) {
if (!childMatches) {
const childRoute = children[childIndex];
childRoute.parent = route;
let matchedLength = match.path.length;
if (matchedLength > 0 && pathname.charAt(matchedLength) === '/') {
matchedLength += 1;
}
childMatches = matchRoute(
childRoute,
pathname.substring(matchedLength),
ignoreLeadingSlash,
match.keys,
match.params,
);
}
const childMatch = childMatches.next(routeToSkip);
if (!childMatch.done) {
return {
done: false,
value: childMatch.value,
};
}
childMatches = null;
childIndex += 1;
}
}
return { done: true, value: undefined };
},
};
}
export default matchRoute;
================================================
FILE: src/resolver/resolveRoute.ts
================================================
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import type { ActionResult, MaybePromise, RouteContext } from './types.t.js';
import { isFunction } from './utils.js';
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
export default function resolveRoute(
context: RouteContext,
): MaybePromise>> {
if (isFunction(context.route.action)) {
// @ts-expect-error: ignore "never" type here
return context.route.action(context);
}
return undefined;
}
================================================
FILE: src/resolver/resolver.ts
================================================
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import type { EmptyObject } from 'type-fest';
import matchRoute, { type MatchWithRoute } from './matchRoute.js';
import defaultResolveRoute from './resolveRoute.js';
import type { ActionResult, Route, Match, MaybePromise, ResolveContext, RouteContext } from './types.t.js';
import { getNotFoundError, getRoutePath, isString, NotFoundError, notFoundResult, toArray } from './utils.js';
function isDescendantRoute(
route?: Route,
maybeParent?: Route,
) {
let _route = route;
while (_route) {
_route = _route.parent;
if (_route === maybeParent) {
return true;
}
}
return false;
}
function isRouteContext(value: unknown): value is RouteContext {
return (
!!value &&
typeof value === 'object' &&
'next' in value &&
'params' in value &&
'result' in value &&
'route' in value
);
}
export interface ResolutionErrorOptions {
code?: number;
cause?: unknown;
}
/**
* An error that is thrown when a route resolution fails.
*/
export class ResolutionError extends Error {
/**
* The resolution error cause, possibly an error thrown from the action callback.
*/
override readonly cause?: unknown;
/**
* A HTTP status code associated with the error.
*/
readonly code?: number;
/**
* The context object associated with the route that was not found.
*/
readonly context: RouteContext;
constructor(context: RouteContext, options?: ResolutionErrorOptions) {
let errorMessage = `Path '${context.pathname}' is not properly resolved due to an error.`;
const routePath = getRoutePath(context.route);
if (routePath) {
errorMessage += ` Resolution had failed on route: '${routePath}'`;
}
super(errorMessage);
this.cause = options?.cause;
this.code = options?.code;
this.context = context;
}
/**
* Logs the error message to the console as a warning.
*/
warn(): void {
console.warn(this.message);
}
}
function updateChainForRoute(
context: RouteContext,
match: Match,
) {
const { path, route } = match;
if (route && !route.__synthetic) {
const item = { path, route };
if (route.parent && context.chain) {
for (let i = context.chain.length - 1; i >= 0; i--) {
if (context.chain[i].route === route.parent) {
break;
}
context.chain.pop();
}
}
context.chain?.push(item);
}
}
/**
* A callback function that handles errors during route resolution.
*/
export type ErrorHandlerCallback = (error: unknown) => T;
/**
* A callback function that resolves a route. It is used as a fallback in case
* the route is not correctly resolved.
*/
export type ResolveRouteCallback = (
context: RouteContext,
) => MaybePromise>>;
/**
* Options for the constructor of the `Resolver` class.
*
* @interface
*/
export type ResolverOptions = Readonly<{
baseUrl?: string;
context?: RouteContext;
errorHandler?: ErrorHandlerCallback;
resolveRoute?: ResolveRouteCallback;
}>;
class Resolver {
/**
* The base URL for all routes in the router instance. By default,
* if the base element exists in the ``, vaadin-router
* takes the `` attribute value, resolved against the current
* `document.URL`.
*/
readonly baseUrl: string;
#context: RouteContext;
readonly errorHandler?: ErrorHandlerCallback;
readonly resolveRoute: ResolveRouteCallback;
readonly #root: Route;
constructor(routes: ReadonlyArray> | Route, options?: ResolverOptions);
constructor(
routes: ReadonlyArray> | Route,
{ baseUrl = '', context, errorHandler, resolveRoute = defaultResolveRoute }: ResolverOptions = {},
) {
if (Object(routes) !== routes) {
throw new TypeError('Invalid routes');
}
this.baseUrl = baseUrl;
this.errorHandler = errorHandler;
this.resolveRoute = resolveRoute;
if (Array.isArray(routes)) {
// @FIXME: We should have a route array instead of a single route object
// to avoid type clash because of a missing `R` part of a route.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.#root = {
__children: routes,
__synthetic: true,
action: () => undefined,
path: '',
} as Route;
} else {
this.#root = { ...routes, parent: undefined };
}
this.#context = {
...context!,
hash: '',
// eslint-disable-next-line @typescript-eslint/require-await
async next() {
return notFoundResult;
},
params: {},
pathname: '',
resolver: this,
route: this.#root,
search: '',
chain: [],
};
}
/**
* The root route.
*/
get root(): Route {
return this.#root;
}
/**
* The current route context.
*/
get context(): RouteContext {
return this.#context;
}
/**
* If the baseUrl property is set, transforms the baseUrl and returns the full
* actual `base` string for using in the `new URL(path, base);` and for
* prepernding the paths with. The returned base ends with a trailing slash.
*
* Otherwise, returns empty string.
*/
protected get __effectiveBaseUrl(): string {
return this.baseUrl ? new URL(this.baseUrl, document.baseURI || document.URL).href.replace(/[^/]*$/u, '') : '';
}
/**
* Returns the current list of routes (as a shallow copy). Adding / removing
* routes to / from the returned array does not affect the routing config,
* but modifying the route objects does.
*
* @public
*/
getRoutes(): ReadonlyArray> {
return [...(this.#root.__children ?? [])];
}
/**
* Removes all existing routes from the routing config.
*
* @public
*/
removeRoutes(): void {
this.#root.__children = [];
}
/**
* Asynchronously resolves the given pathname, i.e. finds all routes matching
* the pathname and tries resolving them one after another in the order they
* are listed in the routes config until the first non-null result.
*
* Returns a promise that is fulfilled with the return value of an object that consists of the first
* route handler result that returns something other than `null` or `undefined` and context used to get this result.
*
* If no route handlers return a non-null result, or if no route matches the
* given pathname the returned promise is rejected with a 'page not found'
* `Error`.
*
* @param pathnameOrContext - the pathname to
* resolve or a context object with a `pathname` property and other
* properties to pass to the route resolver functions.
*/
async resolve(pathnameOrContext: ResolveContext | string): Promise>> {
const self = this;
const context: RouteContext = {
...this.#context,
...(isString(pathnameOrContext) ? { pathname: pathnameOrContext } : pathnameOrContext),
// eslint-disable-next-line @typescript-eslint/no-use-before-define
next,
};
const match = matchRoute(
this.#root,
this.__normalizePathname(context.pathname) ?? context.pathname,
!!this.baseUrl,
);
const resolve = this.resolveRoute;
let matches: IteratorResult, undefined> | null = null;
let nextMatches: IteratorResult, undefined> | null = null;
let currentContext = context;
async function next(
resume: boolean = false,
parent: Route | undefined = matches?.value?.route,
prevResult?: ActionResult>,
): Promise>> {
const routeToSkip = prevResult === null ? matches?.value?.route : undefined;
matches = nextMatches ?? match.next(routeToSkip);
nextMatches = null;
if (!resume) {
if (!!matches.done || !isDescendantRoute(matches.value.route, parent)) {
nextMatches = matches;
return notFoundResult;
}
}
if (matches.done) {
throw getNotFoundError(context);
}
currentContext = {
...context,
params: matches.value.params,
route: matches.value.route,
chain: currentContext.chain?.slice(),
};
updateChainForRoute(currentContext, matches.value);
const resolution = await resolve(currentContext);
if (resolution !== null && resolution !== undefined && resolution !== notFoundResult) {
currentContext.result = isRouteContext(resolution) ? resolution.result : resolution;
self.#context = currentContext;
return currentContext;
}
return await next(resume, parent, resolution);
}
try {
return await next(true, this.#root);
} catch (error: unknown) {
const _error =
error instanceof NotFoundError
? error
: new ResolutionError(currentContext as RouteContext, { code: 500, cause: error });
if (this.errorHandler) {
currentContext.result = this.errorHandler(_error);
return currentContext;
}
throw error;
}
}
/**
* Sets the routing config (replacing the existing one).
*
* @param routes - a single route or an array of those
* (the array is shallow copied)
*/
setRoutes(routes: ReadonlyArray> | Route): object {
this.#root.__children = [...toArray(routes)];
return {};
}
/**
* If the baseUrl is set, matches the pathname with the router’s baseUrl,
* and returns the local pathname with the baseUrl stripped out.
*
* If the pathname does not match the baseUrl, returns undefined.
*
* If the `baseUrl` is not set, returns the unmodified pathname argument.
*/
protected __normalizePathname(pathname: string): string | undefined {
if (!this.baseUrl) {
// No base URL, no need to transform the pathname.
return pathname;
}
const base = this.__effectiveBaseUrl;
// Convert pathname to a valid URL constructor argument
const url = pathname.startsWith('/') ? new URL(base).origin + pathname : `./${pathname}`;
const normalizedUrl = new URL(url, base).href;
if (normalizedUrl.startsWith(base)) {
return normalizedUrl.slice(base.length);
}
return undefined;
}
/**
* Appends one or several routes to the routing config and returns the
* effective routing config after the operation.
*
* @param routes - a single route or an array of those
* (the array is shallow copied)
*/
protected addRoutes(routes: ReadonlyArray> | Route): ReadonlyArray> {
this.#root.__children = [...(this.#root.__children ?? []), ...toArray(routes)];
return this.getRoutes();
}
}
export default Resolver;
================================================
FILE: src/resolver/types.t.ts
================================================
import type { EmptyObject } from 'type-fest';
import type Resolver from './resolver.js';
import type { NotFoundResult } from './utils.js';
/* ========================
* Common Types
* ======================== */
/**
* Represents either a value or a promise of a value.
*
* @typeParam T - The type of the value.
*/
export type MaybePromise = Promise | T;
/**
* A result of a {@link Route.action}.
*
* @typeParam T - The type of the result.
*/
export type ActionResult = T | NotFoundResult | null | undefined | void;
/* ========================
* Resolver-Specific Types
* ======================== */
/**
* A function that dynamically creates children of a route.
*
* @typeParam T - The type of the result produced by the route.
* @typeParam R - The type of additional route-specific data. Defaults to an
* empty object.
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*
* @param context - The context of the current route.
*
* @deprecated The route children callback is deprecated and will be removed in
* the next major version.
*
* @interface
*/
export type ChildrenCallback = (
context: RouteChildrenContext,
) => MaybePromise | ReadonlyArray> | void>;
/**
* Defines a single route.
*
* A route represents a single or multiple sections in the URL. It defines the
* behavior of a page in response to URL updates. A route can act as a content
* producer or as middleware for child routes.
*
* @typeParam T - The type of the result produced by the route.
* @typeParam R - The type of additional route-specific data. Defaults to an
* empty object.
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*
* @interface
*/
export type Route = Readonly<{
/**
* The name of the route.
*/
name?: string;
/**
* The path pattern that the route matches.
*/
path: string;
/**
* An action that is executed when the route is resolved.
*
* Actions are executed recursively from the root route to the child route and
* can either produce content or perform actions before or after the child's
* action.
*
* @param context - The context of the current route.
*
* @returns The result of the route resolution. It could be either a value
* produced by the action or a new context to continue the resolution process.
*
* @internal
*/
action?(
this: Route,
context: RouteContext,
commands: never,
): MaybePromise>>;
}> & {
/** @internal */
__children?: ReadonlyArray>;
/** @internal */
__synthetic?: true;
children?: ReadonlyArray> | ChildrenCallback;
parent?: Route;
fullPath?: string;
} & R;
/**
* A matched route with its associated path.
*
* @typeParam T - The type of the result produced by the route.
* @typeParam R - The type of additional route-specific data. Defaults to an
* empty object.
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*
* @internal
*/
export type Match = Readonly<{
/** The path of the matched route. */
path: string;
/** The route object associated with the matched path. */
route?: Route;
}>;
/**
* An item of the resolved route sequence.
*
* @typeParam T - The type of the result produced by the route.
* @typeParam R - The type of additional route-specific data. Defaults to an
* empty object.
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*
* @interface
*/
export type ChainItem = {
/** A DOM element associated with the route. */
element?: Element;
/** The path of the route. */
path: string;
/** The route object containing route-specific information. */
route: Route;
};
/**
* The context for a `resolve` operation that can be extended with
* the user-defined properties.
*
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*
* @interface
*/
export type ResolveContext = Readonly<{
/** The current location. */
pathname: string;
}> &
C;
/**
* The context for a {@link Route.action} that could be used to access the
* route-specific data during the resolution process.
*
* @typeParam T - The type of the result produced by the route.
* @typeParam R - The type of additional route-specific data. Defaults to
* `EmptyObject`.
* @typeParam C - The type of additional context-specific data. Defaults to
* `EmptyObject`.
*
* @interface
*/
export type RouteContext = Readonly<{
/**
* The {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/hash | hash}
* fragment of the URL.
*/
hash?: string;
/**
* The {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/search | search}
* query string of the URL.
*/
search?: string;
/**
* The sequence of the resolved route items, so said the path from the root
* route to the current route.
*/
chain?: Array>;
/**
* The parameters resolved from the current URL.
*/
params: IndexedParams;
/**
* The resolver instance.
*/
resolver?: Resolver;
/**
* The URL from which a redirect occurred.
*/
redirectFrom?: string;
/**
* The current route.
*/
route: Route;
/**
* Proceed to the next route in the chain, down the route tree.
*/
next(
resume?: boolean,
parent?: Route,
prevResult?: ActionResult>,
): Promise>>;
}> & {
/** @internal */
__divergedChainIndex?: number;
/** @internal */
__redirectCount?: number;
/** @internal */
__renderId: number;
/** @internal */
__skipAttach?: boolean;
/**
* The result of the route resolution. It could be either a value produced by
* the {@link Route.action} or a new context to continue the resolution
* process.
*/
result?: T | RouteContext;
} & ResolveContext;
/**
* Represents the context that is accessible from the route children callback.
* It is the a {@link RouteContext} without the 'next' property.
*
* @typeParam T - The type of the route parameters.
* @typeParam R - The type of the route's resolved data. Defaults to `EmptyObject`.
* @typeParam C - The type of the route's context. Defaults to `EmptyObject`.
*
* @deprecated The route children callback is deprecated and will be removed in
* the next major version.
*
* @interface
*/
export type RouteChildrenContext = Omit<
RouteContext,
'next'
>;
export type PrimitiveParamValue = string | number | null;
/**
* The value of a parameter resolved from the URL.
*/
export type ParamValue = PrimitiveParamValue | readonly PrimitiveParamValue[];
/**
* The collection of parameters resolved from the URL.
*
* @remarks
* Parameters are the parts of the URL represented by the placeholders in the
* route path. Placeholders are often named, and these names become the keys of
* the `IndexedParams` object.
*
* If a placeholder is unnamed, its index in the path becomes the key.
*/
export type IndexedParams = Readonly>;
export type Params = IndexedParams | ParamValue[];
================================================
FILE: src/resolver/utils.ts
================================================
import type { ChildrenCallback, Route, RouteContext } from './types.t.js';
/**
* {@inheritDoc "".NotFoundError}
*/
export const notFoundResult = Symbol('NotFoundResult');
/**
* A special result to be returned from a route action to indicate that the
* route was not found.
*/
export type NotFoundResult = typeof notFoundResult;
/**
* An error to be thrown when a route is not found.
*/
export class NotFoundError extends Error {
/**
* The HTTP status code to be used when the route is not found.
*/
readonly code: number;
/**
* The context object associated with the route that was not found.
*/
readonly context: RouteContext;
constructor(context: RouteContext) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
super(log(`Page not found (${context.pathname})`));
this.context = context;
this.code = 404;
}
}
/** @internal */
export function isObject(o: unknown): o is object {
// guard against null passing the typeof check
return typeof o === 'object' && !!o;
}
/** @internal */
export function isFunction unknown>(f: unknown): f is F {
return typeof f === 'function';
}
/** @internal */
export function isString(s: unknown): s is string {
return typeof s === 'string';
}
/** @internal */
export function toArray(value: T | readonly T[] = []): readonly T[] {
return Array.isArray(value) ? value : [value];
}
/** @internal */
export function log(msg: string): string {
return `[Vaadin.Router] ${msg}`;
}
/** @internal */
export function getNotFoundError(
context: RouteContext,
): NotFoundError {
return new NotFoundError(context);
}
/** @internal */
export function resolvePath(path?: string | readonly string[]): string {
return (Array.isArray(path) ? path[0] : path) ?? '';
}
/** @internal */
export function getRoutePath(route: Route | undefined): string {
return resolvePath(route?.path);
}
/** @internal */
export function unwrapChildren(
children: ChildrenCallback | ReadonlyArray> | undefined,
): ReadonlyArray> | undefined {
return Array.isArray>>(children) && children.length > 0 ? children : undefined;
}
================================================
FILE: src/router-config.ts
================================================
import './router-meta.js';
================================================
FILE: src/router-meta.ts
================================================
import { usageStatistics } from '@vaadin/vaadin-usage-statistics/vaadin-usage-statistics.js';
// @ts-expect-error: Generated function
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
__REGISTER__();
usageStatistics();
================================================
FILE: src/router.ts
================================================
/* eslint-disable @typescript-eslint/consistent-return */
import { compile } from 'path-to-regexp';
import type { EmptyObject, Writable } from 'type-fest';
import generateUrls from './resolver/generateUrls.js';
import Resolver from './resolver/resolver.js';
import './router-config.js';
import { getNotFoundError, isFunction, isObject, isString, log, notFoundResult } from './resolver/utils.js';
import animate from './transitions/animate.js';
import { DEFAULT_TRIGGERS, setNavigationTriggers } from './triggers/navigation.js';
import type {
ActionResult,
Commands,
ChainItem,
ContextExtension,
EmptyCommands,
NavigationTrigger,
Params,
PreventAndRedirectCommands,
PreventCommands,
PreventResult,
RedirectContextInfo,
RedirectResult,
ResolveContext,
Route,
RouteContext,
RouteExtension,
RouterLocation,
WebComponentInterface,
RouterOptions,
ActionValue,
NextResult,
} from './types.t.js';
import {
amend,
copyContextWithoutNext,
createLocation,
createRedirect,
ensureRoutes,
fireRouterEvent,
getMatchedPath,
getPathnameForRouter,
logValue,
maybeCall,
processNewChildren,
renderElement,
} from './utils.js';
const MAX_REDIRECT_COUNT = 256;
function prevent(): PreventResult {
return { cancel: true };
}
const rootContext: RouteContext = {
__renderId: -1,
params: {},
route: {
__synthetic: true,
children: [],
path: '',
action() {
return undefined;
},
},
pathname: '',
// eslint-disable-next-line @typescript-eslint/require-await
async next() {
return notFoundResult;
},
};
/**
* A simple client-side router for single-page applications. It uses
* express-style middleware and has a first-class support for Web Components and
* lazy-loading. Works great in Polymer and non-Polymer apps.
*
* Use `new Router(outlet, options)` to create a new Router instance.
*
* * The `outlet` parameter is a reference to the DOM node to render
* the content into.
*
* * The `options` parameter is an optional object with options. The following
* keys are supported:
* * `baseUrl` — the initial value for [
* the `baseUrl` property
* ](#/classes/Router#property-baseUrl)
*
* The Router instance is automatically subscribed to navigation events
* on `window`.
*
* See [Live Examples](#/classes/Router/demos/demo/index.html) for the detailed usage demo and code snippets.
*
* See also detailed API docs for the following methods, for the advanced usage:
*
* * [setOutlet](#/classes/Router#method-setOutlet) – should be used to configure the outlet.
* * [setTriggers](#/classes/Router#method-setTriggers) – should be used to configure the navigation events.
* * [setRoutes](#/classes/Router#method-setRoutes) – should be used to configure the routes.
*
* Only `setRoutes` has to be called manually, others are automatically invoked when creating a new instance.
*
* @deprecated This library is no longer actively maintained. Vaadin uses React Router as a primary client-side routing tool.
*
* Consider [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) standard Web API or a library based on it, such as [@lit-labs/router](https://www.npmjs.com/package/@lit-labs/router)
*/
export class Router extends Resolver<
ActionValue,
RouteExtension,
ContextExtension
> {
/**
* Contains read-only information about the current router location:
* pathname, active routes, parameters. See the
* [Location type declaration](#/classes/RouterLocation)
* for more details.
*/
location = createLocation({ resolver: this });
/**
* A promise that is settled after the current render cycle completes. If
* there is no render cycle in progress the promise is immediately settled
* with the last render cycle result.
*/
ready: Promise> = Promise.resolve(this.location);
readonly #addedByRouter = new WeakSet();
readonly #createdByRouter = new WeakSet();
readonly #navigationEventHandler = this.#onNavigationEvent.bind(this);
#lastStartedRenderId = 0;
#outlet: Element | DocumentFragment | null | undefined;
/** @internal */
private __previousContext?: RouteContext;
#urlForName?: ReturnType;
#appearingContent: Element[] | null = null;
#disappearingContent: Element[] | null = null;
/**
* Creates a new Router instance with a given outlet, and
* automatically subscribes it to navigation events on the `window`.
* Using a constructor argument or a setter for outlet is equivalent:
*
* ```
* const router = new Router();
* router.setOutlet(outlet);
* ```
* @param outlet - a container to render the resolved route
* @param options - an optional object with options
*/
constructor(outlet?: Element | DocumentFragment | null, options?: RouterOptions) {
const baseElement = document.head.querySelector('base');
const baseHref = baseElement?.getAttribute('href');
super([], {
baseUrl: baseHref ? new URL(baseHref, document.URL).href.replace(/[^/]*$/u, '') : undefined,
...options,
resolveRoute: async (context) => await this.#resolveRoute(context),
});
setNavigationTriggers(Object.values(DEFAULT_TRIGGERS));
this.setOutlet(outlet);
this.subscribe();
}
async #resolveRoute(context: RouteContext): Promise> {
const { route } = context;
if (isFunction(route.children)) {
let children = await route.children(copyContextWithoutNext(context));
// The route.children() callback might have re-written the
// route.children property instead of returning a value
if (!isFunction(route.children)) {
// eslint-disable-next-line no-param-reassign
({ children } = route);
}
processNewChildren(children, route);
}
const commands: Commands = {
component: (component: string) => {
const element = document.createElement(component);
this.#createdByRouter.add(element);
return element;
},
prevent,
redirect: (path) => createRedirect(context, path),
};
return await Promise.resolve()
.then(async () => {
if (this.#isLatestRender(context)) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return await maybeCall(route.action, route, context, commands);
}
})
.then((result) => {
if (result != null && (typeof result === 'object' || typeof result === 'symbol')) {
// Actions like `() => import('my-view.js')` are not expected to
// end the resolution, despite the result is not empty. Checking
// the result with a whitelist of values that end the resolution.
if (
result instanceof HTMLElement ||
result === notFoundResult ||
(isObject(result) && 'redirect' in result)
) {
return result;
}
}
if (isString(route.redirect)) {
return commands.redirect(route.redirect);
}
})
.then((result) => {
if (result != null) {
return result;
}
if (isString(route.component)) {
return commands.component(route.component);
}
});
}
/**
* Sets the router outlet (the DOM node where the content for the current
* route is inserted). Any content pre-existing in the router outlet is
* removed at the end of each render pass.
*
* @remarks
* This method is automatically invoked first time when creating a new Router
* instance.
*
* @param outlet - the DOM node where the content for the current route is
* inserted.
*/
setOutlet(outlet?: Element | DocumentFragment | null): void {
if (outlet) {
this.#ensureOutlet(outlet);
}
this.#outlet = outlet;
}
/**
* Returns the current router outlet. The initial value is `undefined`.
*
* @returns the current router outlet (or `undefined`)
*/
getOutlet(): Element | DocumentFragment | null | undefined {
return this.#outlet;
}
/**
* Sets the routing config (replacing the existing one) and triggers a
* navigation event so that the router outlet is refreshed according to the
* current `window.location` and the new routing config.
*
* Each route object may have the following properties, listed here in the processing order:
* * `path` – the route path (relative to the parent route if any) in the
* [express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths).
*
* * `children` – an array of nested routes or a function that provides this
* array at the render time. The function can be synchronous or asynchronous:
* in the latter case the render is delayed until the returned promise is
* resolved. The `children` function is executed every time when this route is
* being rendered. This allows for dynamic route structures (e.g. backend-defined),
* but it might have a performance impact as well. In order to avoid calling
* the function on subsequent renders, you can override the `children` property
* of the route object and save the calculated array there
* (via `context.route.children = [ route1, route2, ...];`).
* Parent routes are fully resolved before resolving the children. Children
* 'path' values are relative to the parent ones.
*
* * `action` – the action that is executed before the route is resolved.
* The value for this property should be a function, accepting `context`
* and `commands` parameters described below. If present, this function is
* always invoked first, disregarding of the other properties' presence.
* The action can return a result directly or within a `Promise`, which
* resolves to the result. If the action result is an `HTMLElement` instance,
* a `commands.component(name)` result, a `commands.redirect(path)` result,
* or a `context.next()` result, the current route resolution is finished,
* and other route config properties are ignored.
* See also **Route Actions** section in [Live Examples](#/classes/Router/demos/demo/index.html).
*
* * `redirect` – other route's path to redirect to. Passes all route parameters to the redirect target.
* The target route should also be defined.
* See also **Redirects** section in [Live Examples](#/classes/Router/demos/demo/index.html).
*
* * `component` – the tag name of the Web Component to resolve the route to.
* The property is ignored when either an `action` returns the result or `redirect` property is present.
* If route contains the `component` property (or an action that return a component)
* and its child route also contains the `component` property, child route's component
* will be rendered as a light dom child of a parent component.
*
* * `name` – the string name of the route to use in the
* [`router.urlForName(name, params)`](#/classes/Router#method-urlForName)
* navigation helper method.
*
* For any route function (`action`, `children`) defined, the corresponding `route` object is available inside the
* callback through the `this` reference. If you need to access it, make sure you define the callback as a non-arrow
* function because arrow functions do not have their own `this` reference.
*
* `context` object that is passed to `action` function holds the following properties:
* * `context.pathname` – string with the pathname being resolved
*
* * `context.search` – search query string
*
* * `context.hash` – hash string
*
* * `context.params` – object with route parameters
*
* * `context.route` – object that holds the route that is currently being rendered.
*
* * `context.next()` – function for asynchronously getting the next route
* contents from the resolution chain (if any)
*
* `commands` object that is passed to `action` function has
* the following methods:
*
* * `commands.redirect(path)` – function that creates a redirect data
* for the path specified.
*
* * `commands.component(component)` – function that creates a new HTMLElement
* with current context. Note: the component created by this function is reused if visiting the same path twice in
* row.
*
* @param routes - a single route or an array of those
* @param skipRender - configure the router but skip rendering the
* route corresponding to the current `window.location` values
*/
override async setRoutes(
routes: Route | ReadonlyArray>,
skipRender = false,
): Promise> {
this.__previousContext = undefined;
this.#urlForName = undefined;
ensureRoutes(routes);
super.setRoutes(routes);
if (!skipRender) {
this.#onNavigationEvent();
}
return await this.ready;
}
protected override addRoutes(routes: Route | ReadonlyArray>): ReadonlyArray> {
ensureRoutes(routes);
return super.addRoutes(routes);
}
/**
* Asynchronously resolves the given pathname and renders the resolved route
* component into the router outlet. If no router outlet is set at the time of
* calling this method, or at the time when the route resolution is completed,
* a `TypeError` is thrown.
*
* Returns a promise that is fulfilled with the router outlet DOM Element | DocumentFragment after
* the route component is created and inserted into the router outlet, or
* rejected if no route matches the given path.
*
* If another render pass is started before the previous one is completed, the
* result of the previous render pass is ignored.
*
* @param pathnameOrContext - the pathname to render or a context object with
* a `pathname` property, optional `search` and `hash` properties, and other
* properties to pass to the resolver.
* @param shouldUpdateHistory - update browser history with the rendered
* location
*/
async render(
pathnameOrContext: string | ResolveContext,
shouldUpdateHistory: boolean = false,
): Promise> {
this.#lastStartedRenderId += 1;
const renderId = this.#lastStartedRenderId;
const context = {
...(rootContext as RouteContext),
...(isString(pathnameOrContext) ? { hash: '', search: '', pathname: pathnameOrContext } : pathnameOrContext),
__renderId: renderId,
} satisfies RouteContext;
this.ready = this.#doRender(context, shouldUpdateHistory);
return await this.ready;
}
async #doRender(context: RouteContext, shouldUpdateHistory: boolean) {
const { __renderId: renderId } = context;
try {
// Find the first route that resolves to a non-empty result
const ctx = await this.resolve(context);
// Process the result of this.resolve() and handle all special commands:
// (redirect / prevent / component). If the result is a 'component',
// then go deeper and build the entire chain of nested components matching
// the pathname. Also call all 'on before' callbacks along the way.
const contextWithChain = await this.#fullyResolveChain(ctx);
if (!this.#isLatestRender(contextWithChain)) {
return this.location;
}
const previousContext = this.__previousContext;
// Check if the render was prevented and make an early return in that case
if (contextWithChain === previousContext) {
// Replace the history with the previous context
// to make sure the URL stays the same.
this.#updateBrowserHistory(previousContext, true);
return this.location;
}
this.location = createLocation(contextWithChain);
if (shouldUpdateHistory) {
// Replace only if first render redirects, so that we don’t leave
// the redirecting record in the history
this.#updateBrowserHistory(contextWithChain, renderId === 1);
}
fireRouterEvent('location-changed', {
router: this,
location: this.location,
});
// Skip detaching/re-attaching there are no render changes
if (contextWithChain.__skipAttach) {
this.#copyUnchangedElements(contextWithChain, previousContext);
this.__previousContext = contextWithChain;
return this.location;
}
this.#addAppearingContent(contextWithChain, previousContext);
const animationDone = this.#animateIfNeeded(contextWithChain);
this.#runOnAfterEnterCallbacks(contextWithChain);
this.#runOnAfterLeaveCallbacks(contextWithChain, previousContext);
await animationDone;
if (this.#isLatestRender(contextWithChain)) {
// If there is another render pass started after this one,
// the 'disappearing content' would be removed when the other
// render pass calls `this.__addAppearingContent()`
this.#removeDisappearingContent();
this.__previousContext = contextWithChain;
return this.location;
}
} catch (error: unknown) {
if (renderId === this.#lastStartedRenderId) {
if (shouldUpdateHistory) {
this.#updateBrowserHistory(this.context);
}
for (const child of this.#outlet?.children ?? []) {
child.remove();
}
this.location = createLocation(Object.assign(context, { resolver: this }));
fireRouterEvent('error', {
router: this,
error,
...context,
});
throw error;
}
}
return this.location;
}
// `topOfTheChainContextBeforeRedirects` is a context coming from Resolver.resolve().
// It would contain a 'redirect' route or the first 'component' route that
// matched the pathname. There might be more child 'component' routes to be
// resolved and added into the chain. This method would find and add them.
// `contextBeforeRedirects` is the context containing such a child component
// route. It's only necessary when this method is called recursively (otherwise
// it's the same as the 'top of the chain' context).
//
// Apart from building the chain of child components, this method would also
// handle 'redirect' routes, call 'onBefore' callbacks and handle 'prevent'
// and 'redirect' callback results.
async #fullyResolveChain(
topOfTheChainContextBeforeRedirects: RouteContext,
contextBeforeRedirects: RouteContext = topOfTheChainContextBeforeRedirects,
): Promise> {
const contextAfterRedirects = await this.#findComponentContextAfterAllRedirects(contextBeforeRedirects);
const redirectsHappened = contextAfterRedirects !== contextBeforeRedirects;
const topOfTheChainContextAfterRedirects = redirectsHappened
? contextAfterRedirects
: topOfTheChainContextBeforeRedirects;
const matchedPath = getPathnameForRouter(getMatchedPath(contextAfterRedirects.chain ?? []), this);
const isFound = matchedPath === contextAfterRedirects.pathname;
// Recursive method to try matching more child and sibling routes
const findNextContextIfAny = async (
context: RouteContext,
parent: Route | undefined = context.route,
prevResult?: NextResult | null,
): Promise> => {
const nextContext = await context.next(false, parent, prevResult);
if (nextContext === null || nextContext === notFoundResult) {
// Next context is not found in children, ...
if (isFound) {
// ...but original context is already fully matching - use it
return context;
} else if (parent.parent != null) {
// ...and there is no full match yet - step up to check siblings
return await findNextContextIfAny(context, parent.parent, nextContext);
}
return nextContext;
}
return nextContext;
};
const nextContext = await findNextContextIfAny(contextAfterRedirects);
if (nextContext == null || nextContext === notFoundResult) {
throw getNotFoundError, ContextExtension>(
topOfTheChainContextAfterRedirects,
);
}
return nextContext !== contextAfterRedirects
? await this.#fullyResolveChain(topOfTheChainContextAfterRedirects, nextContext)
: await this.#amendWithOnBeforeCallbacks(contextAfterRedirects);
}
async #findComponentContextAfterAllRedirects(context: RouteContext): Promise> {
const { result } = context;
if (result instanceof HTMLElement) {
renderElement(context, result as WebComponentInterface);
return context;
} else if (result && 'redirect' in result) {
const ctx = await this.#redirect(result.redirect, context.__redirectCount, context.__renderId);
return await this.#findComponentContextAfterAllRedirects(ctx);
}
throw result instanceof Error
? result
: new Error(
log(
`Invalid route resolution result for path "${context.pathname}". ` +
`Expected redirect object or HTML element, but got: "${logValue(result)}". ` +
`Double check the action return value for the route.`,
),
);
}
async #amendWithOnBeforeCallbacks(contextWithFullChain: RouteContext): Promise> {
return await this.#runOnBeforeCallbacks(contextWithFullChain).then(async (amendedContext) => {
if (amendedContext === this.__previousContext || amendedContext === contextWithFullChain) {
return amendedContext;
}
return await this.#fullyResolveChain(amendedContext);
});
}
async #runOnBeforeCallbacks(newContext: RouteContext): Promise> {
const previousContext = (this.__previousContext ?? {}) as Partial>;
const previousChain = previousContext.chain ?? [];
const newChain = newContext.chain ?? [];
let callbacks: Promise = Promise.resolve(undefined);
const redirect = (pathname: string) => createRedirect(newContext, pathname) as unknown as RedirectResult;
newContext.__divergedChainIndex = 0;
newContext.__skipAttach = false;
if (previousChain.length) {
for (let i = 0; i < Math.min(previousChain.length, newChain.length); newContext.__divergedChainIndex++, i++) {
if (
previousChain[i].route !== newChain[i].route ||
(previousChain[i].path !== newChain[i].path && previousChain[i].element !== newChain[i].element) ||
!this.#isReusableElement(
previousChain[i].element as HTMLElement | undefined,
newChain[i].element as HTMLElement | undefined,
)
) {
break;
}
}
// Skip re-attaching and notifications if element and chain do not change
newContext.__skipAttach =
// Same route chain
newChain.length === previousChain.length &&
newContext.__divergedChainIndex === newChain.length &&
// Same element
this.#isReusableElement(newContext.result, previousContext.result);
if (newContext.__skipAttach) {
// execute onBeforeLeave for changed segment element when skipping attach
for (let i = newChain.length - 1; i >= 0; i--) {
callbacks = this.#runOnBeforeLeaveCallbacks(callbacks, newContext, { prevent }, previousChain[i]);
}
// execute onBeforeEnter for changed segment element when skipping attach
for (let i = 0; i < newChain.length; i++) {
callbacks = this.#runOnBeforeEnterCallbacks(
callbacks,
newContext,
{
prevent,
redirect,
},
newChain[i],
);
previousChain[i].element!.location = createLocation(newContext, previousChain[i].route);
}
} else {
// execute onBeforeLeave when NOT skipping attach
for (let i = previousChain.length - 1; i >= newContext.__divergedChainIndex; i--) {
callbacks = this.#runOnBeforeLeaveCallbacks(callbacks, newContext, { prevent }, previousChain[i]);
}
}
}
// execute onBeforeEnter when NOT skipping attach
if (!newContext.__skipAttach) {
for (let i = 0; i < newChain.length; i++) {
if (i < newContext.__divergedChainIndex) {
if (i < previousChain.length && previousChain[i].element) {
previousChain[i].element!.location = createLocation(newContext, previousChain[i].route);
}
} else {
callbacks = this.#runOnBeforeEnterCallbacks(
callbacks,
newContext,
{
prevent,
redirect,
},
newChain[i],
);
if (newChain[i].element) {
newChain[i].element!.location = createLocation(newContext, newChain[i].route);
}
}
}
}
return await callbacks.then(async (amendmentResult: ActionResult) => {
if (amendmentResult && isObject(amendmentResult)) {
if ('cancel' in amendmentResult && this.__previousContext) {
this.__previousContext.__renderId = newContext.__renderId;
return this.__previousContext;
}
if ('redirect' in amendmentResult) {
return await this.#redirect(amendmentResult.redirect, newContext.__redirectCount, newContext.__renderId);
}
}
return newContext;
});
}
async #runOnBeforeLeaveCallbacks(
callbacks: Promise,
newContext: RouteContext,
commands: PreventCommands,
chainElement: ChainItem,
): Promise {
const location = createLocation(newContext);
let result: ActionResult = await callbacks;
if (this.#isLatestRender(newContext)) {
const beforeLeaveFunction = amend('onBeforeLeave', chainElement.element, location, commands, this);
result = beforeLeaveFunction(result);
}
if (!(isObject(result) && 'redirect' in result)) {
return result as ActionResult;
}
}
async #runOnBeforeEnterCallbacks(
callbacks: Promise,
newContext: RouteContext,
commands: PreventAndRedirectCommands,
chainElement: ChainItem,
): Promise {
const location = createLocation(newContext, chainElement.route);
const result = await callbacks;
if (this.#isLatestRender(newContext)) {
const beforeEnterFunction = amend('onBeforeEnter', chainElement.element, location, commands, this);
return beforeEnterFunction(result);
}
}
#isReusableElement(element?: unknown, otherElement?: unknown): boolean {
if (element instanceof Element && otherElement instanceof Element) {
return this.#createdByRouter.has(element) && this.#createdByRouter.has(otherElement)
? element.localName === otherElement.localName
: element === otherElement;
}
return false;
}
#isLatestRender(context: Partial>): boolean {
return context.__renderId === this.#lastStartedRenderId;
}
declare ['resolve']: (
contextOrPathname: RouteContext | string,
) => Promise & RedirectContextInfo>;
async #redirect(
redirectData: RedirectContextInfo,
counter: number = 0,
renderId: number = 0,
): Promise & RedirectContextInfo> {
if (counter > MAX_REDIRECT_COUNT) {
throw new Error(log(`Too many redirects when rendering ${redirectData.from}`));
}
return await this.resolve({
...(rootContext as RouteContext),
pathname: this.urlForPath(redirectData.pathname, redirectData.params),
redirectFrom: redirectData.from,
__redirectCount: counter + 1,
__renderId: renderId,
});
}
#ensureOutlet(outlet: Element | DocumentFragment | undefined | null = this.#outlet): void {
if (!(outlet instanceof Element || outlet instanceof DocumentFragment)) {
throw new TypeError(
log(`Expected router outlet to be a valid DOM Element | DocumentFragment (but got ${outlet})`),
);
}
}
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
#updateBrowserHistory({ pathname, search = '', hash = '' }: ResolveContext, replace?: boolean): void {
if (window.location.pathname !== pathname || window.location.search !== search || window.location.hash !== hash) {
const changeState = replace ? 'replaceState' : 'pushState';
window.history[changeState](null, document.title, pathname + search + hash);
window.dispatchEvent(new PopStateEvent('popstate', { state: 'vaadin-router-ignore' }));
}
}
#copyUnchangedElements(
context: RouteContext,
previousContext?: RouteContext,
): Element | DocumentFragment | null | undefined {
// Find the deepest common parent between the last and the new component
// chains. Update references for the unchanged elements in the new chain
let deepestCommonParent = this.#outlet;
for (let i = 0; i < (context.__divergedChainIndex ?? 0); i++) {
const unchangedElement = previousContext?.chain?.[i].element;
if (unchangedElement) {
if (unchangedElement.parentNode === deepestCommonParent) {
(context.chain![i] as Writable>).element = unchangedElement;
deepestCommonParent = unchangedElement;
} else {
break;
}
}
}
return deepestCommonParent;
}
#addAppearingContent(context: RouteContext, previousContext?: RouteContext): void {
this.#ensureOutlet();
// If the previous 'entering' animation has not completed yet,
// stop it and remove that content from the DOM before adding new one.
this.#removeAppearingContent();
// Copy reusable elements from the previousContext to current
const deepestCommonParent = this.#copyUnchangedElements(context, previousContext);
// Keep two lists of DOM elements:
// - those that should be removed once the transition animation is over
// - and those that should remain
this.#appearingContent = [];
this.#disappearingContent = Array.from(deepestCommonParent?.children ?? []).filter(
// Only remove layout content that was added by router
(e) =>
this.#addedByRouter.has(e) &&
// Do not remove the result element to avoid flickering
e !== context.result,
);
// Add new elements (starting after the deepest common parent) to the DOM.
// That way only the components that are actually different between the two
// locations are added to the DOM (and those that are common remain in the
// DOM without first removing and then adding them again).
let parentElement = deepestCommonParent;
for (let i = context.__divergedChainIndex ?? 0; i < (context.chain?.length ?? 0); i++) {
const elementToAdd = context.chain![i].element;
if (elementToAdd) {
parentElement?.appendChild(elementToAdd);
this.#addedByRouter.add(elementToAdd);
if (parentElement === deepestCommonParent) {
this.#appearingContent.push(elementToAdd);
}
parentElement = elementToAdd;
}
}
}
#removeDisappearingContent(): void {
if (this.#disappearingContent) {
for (const element of this.#disappearingContent) {
element.remove();
}
}
this.#disappearingContent = null;
this.#appearingContent = null;
}
#removeAppearingContent(): void {
if (this.#disappearingContent && this.#appearingContent) {
for (const element of this.#appearingContent) {
element.remove();
}
this.#disappearingContent = null;
this.#appearingContent = null;
}
}
#runOnAfterLeaveCallbacks(currentContext: RouteContext, targetContext?: RouteContext): void {
if (!targetContext?.chain || currentContext.__divergedChainIndex == null) {
return;
}
// REVERSE iteration: from Z to A
for (let i = targetContext.chain.length - 1; i >= currentContext.__divergedChainIndex; i--) {
if (!this.#isLatestRender(currentContext)) {
break;
}
const currentComponent = targetContext.chain[i].element;
if (!currentComponent) {
continue;
}
try {
const location = createLocation(currentContext);
// eslint-disable-next-line @typescript-eslint/unbound-method
maybeCall(currentComponent.onAfterLeave, currentComponent, location, {} as EmptyCommands, this);
} finally {
if (this.#disappearingContent?.includes(currentComponent)) {
for (const child of currentComponent.children) {
child.remove();
}
}
}
}
}
#runOnAfterEnterCallbacks(currentContext: RouteContext): void {
if (!currentContext.chain || currentContext.__divergedChainIndex == null) {
return;
}
// forward iteration: from A to Z
for (let i = currentContext.__divergedChainIndex; i < currentContext.chain.length; i++) {
if (!this.#isLatestRender(currentContext)) {
break;
}
const currentComponent = currentContext.chain[i].element;
if (currentComponent) {
const location = createLocation(currentContext, currentContext.chain[i].route);
// eslint-disable-next-line @typescript-eslint/unbound-method
maybeCall(currentComponent.onAfterEnter, currentComponent, location, {}, this);
}
}
}
async #animateIfNeeded(context: RouteContext): Promise> {
const from = this.#disappearingContent?.[0];
const to = this.#appearingContent?.[0];
const promises = [];
const { chain = [] } = context;
let config;
for (let i = chain.length - 1; i >= 0; i--) {
if (chain[i].route.animate) {
config = chain[i].route.animate;
break;
}
}
if (from && to && config) {
const leave = isObject(config) && config.leave ? config.leave : 'leaving';
const enter = isObject(config) && config.enter ? config.enter : 'entering';
promises.push(animate(from, leave));
promises.push(animate(to, enter));
}
await Promise.all(promises);
return context;
}
/**
* Subscribes this instance to navigation events on the `window`.
*
* NOTE: beware of resource leaks. For as long as a router instance is
* subscribed to navigation events, it won't be garbage collected.
*/
subscribe(): void {
window.addEventListener('vaadin-router-go', this.#navigationEventHandler);
}
/**
* Removes the subscription to navigation events created in the `subscribe()`
* method.
*/
unsubscribe(): void {
window.removeEventListener('vaadin-router-go', this.#navigationEventHandler);
}
#onNavigationEvent(event?: Event): void {
const { pathname, search, hash } =
event instanceof CustomEvent ? (event.detail as ResolveContext) : window.location;
if (isString(this.__normalizePathname(pathname))) {
if (event?.preventDefault) {
event.preventDefault();
}
// eslint-disable-next-line no-void
void this.render({ pathname, search, hash }, true);
}
}
/**
* Configures what triggers Router navigation events:
* - `POPSTATE`: popstate events on the current `window`
* - `CLICK`: click events on `` links leading to the current page
*
* This method is invoked with the pre-configured values when creating a new Router instance.
* By default, both `POPSTATE` and `CLICK` are enabled. This setup is expected to cover most of the use cases.
*
* See the `router-config.js` for the default navigation triggers config. Based on it, you can
* create the own one and only import the triggers you need, instead of pulling in all the code,
* e.g. if you want to handle `click` differently.
*
* See also **Navigation Triggers** section in [Live Examples](#/classes/Router/demos/demo/index.html).
*
* @param triggers - navigation triggers
*/
static setTriggers(...triggers: readonly NavigationTrigger[]): void {
setNavigationTriggers(triggers);
}
/**
* Generates a URL for the route with the given name, optionally performing
* substitution of parameters.
*
* The route is searched in all the Router instances subscribed to
* navigation events.
*
* **Note:** For child route names, only array children are considered.
* It is not possible to generate URLs using a name for routes set with
* a children function.
*
* @param name - The route name or the route’s `component` name.
* @param params - Optional object with route path parameters.
* Named parameters are passed by name (`params[name] = value`), unnamed
* parameters are passed by index (`params[index] = value`).
*/
urlForName(name: string, params?: Params | null): string {
if (!this.#urlForName) {
this.#urlForName = generateUrls(this, {
cacheKeyProvider(route): string | undefined {
return 'component' in route && typeof route.component === 'string'
? (route as Readonly<{ component: string }>).component
: undefined;
},
});
}
return getPathnameForRouter(this.#urlForName(name, params ?? undefined), this);
}
/**
* Generates a URL for the given route path, optionally performing
* substitution of parameters.
*
* @param path - String route path declared in [express.js
* syntax](https://expressjs.com/en/guide/routing.html#route-paths).
* @param params - Optional object with route path parameters.
* Named parameters are passed by name (`params[name] = value`), unnamed
* parameters are passed by index (`params[index] = value`).
*/
urlForPath(path: string, params?: Params | null): string {
return getPathnameForRouter(
compile(path)((params as Partial> | null) ?? undefined),
this,
);
}
/**
* Triggers navigation to a new path. Returns a boolean without waiting until
* the navigation is complete. Returns `true` if at least one `Router`
* has handled the navigation (was subscribed and had `baseUrl` matching
* the `path` argument), otherwise returns `false`.
*
* @param path - A new in-app path string, or an URL-like object with
* `pathname` string property, and optional `search` and `hash` string
* properties.
*/
static go(path: string | ResolveContext): boolean {
const { pathname, search, hash } = isString(path)
? new URL(path, 'http://a') // some base to omit origin
: path;
return fireRouterEvent('go', { pathname, search, hash });
}
}
================================================
FILE: src/transitions/animate.ts
================================================
const willAnimate = (elem: Element) => {
const name = getComputedStyle(elem).getPropertyValue('animation-name');
return name && name !== 'none';
};
const waitForAnimation = (elem: Element, cb: () => void) => {
const listener = () => {
elem.removeEventListener('animationend', listener);
cb();
};
elem.addEventListener('animationend', listener);
};
async function animate(elem: Element, className: string): Promise {
elem.classList.add(className);
return await new Promise((resolve: () => void) => {
if (willAnimate(elem)) {
const rect = elem.getBoundingClientRect();
const size = `height: ${rect.bottom - rect.top}px; width: ${rect.right - rect.left}px`;
elem.setAttribute('style', `position: absolute; ${size}`);
waitForAnimation(elem, () => {
elem.classList.remove(className);
elem.removeAttribute('style');
resolve();
});
} else {
elem.classList.remove(className);
resolve();
}
});
}
export default animate;
================================================
FILE: src/triggers/click.ts
================================================
import type { NavigationTrigger } from '../types.t.js';
import { fireRouterEvent } from '../utils.js';
/* istanbul ignore next: coverage is calculated in Chrome, this code is for IE */
function getAnchorOrigin(anchor: HTMLAnchorElement) {
// IE11: on HTTP and HTTPS the default port is not included into
// window.location.origin, so won't include it here either.
const { port, protocol } = anchor;
const defaultHttp = protocol === 'http:' && port === '80';
const defaultHttps = protocol === 'https:' && port === '443';
const host =
defaultHttp || defaultHttps
? anchor.hostname // does not include the port number (e.g. www.example.org)
: anchor.host; // does include the port number (e.g. www.example.org:80)
return `${protocol}//${host}`;
}
function getNormalizedNodeName(e: EventTarget): string | undefined {
if (!(e instanceof Element)) {
return undefined;
}
return e.nodeName.toLowerCase();
}
// TODO: Name correctly when the type purpose is known
type __Pathable = Readonly<{
path?: readonly EventTarget[];
}>;
// The list of checks is not complete:
// - SVG support is missing
// - the 'rel' attribute is not considered
function vaadinRouterGlobalClickHandler(event: MouseEvent & __Pathable) {
// ignore the click if the default action is prevented
if (event.defaultPrevented) {
return;
}
// ignore the click if not with the primary mouse button
if (event.button !== 0) {
return;
}
// ignore the click if a modifier key is pressed
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
return;
}
// find the element that the click is at (or within)
let anchorCandidate = event.target;
const path = event instanceof MouseEvent ? event.composedPath() : ((event as __Pathable).path ?? []);
// FIXME(web-padawan): `Symbol.iterator` used by webcomponentsjs is broken for arrays
// example to check: `for...of` loop here throws the "Not yet implemented" error
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < path.length; i++) {
const target = path[i];
if ('nodeName' in target && (target as Element).nodeName.toLowerCase() === 'a') {
anchorCandidate = target;
break;
}
}
while (anchorCandidate && anchorCandidate instanceof Node && getNormalizedNodeName(anchorCandidate) !== 'a') {
anchorCandidate = anchorCandidate.parentNode;
}
// ignore the click if not at an element
if (!anchorCandidate || getNormalizedNodeName(anchorCandidate) !== 'a') {
return;
}
const anchor = anchorCandidate as HTMLAnchorElement;
// ignore the click if the element has a non-default target
if (anchor.target && anchor.target.toLowerCase() !== '_self') {
return;
}
// ignore the click if the element has the 'download' attribute
if (anchor.hasAttribute('download')) {
return;
}
// ignore the click if the element has the 'router-ignore' attribute
if (anchor.hasAttribute('router-ignore')) {
return;
}
// ignore the click if the target URL is a fragment on the current page
if (anchor.pathname === window.location.pathname && anchor.hash !== '') {
return;
}
// ignore the click if the target is external to the app
// In IE11 HTMLAnchorElement does not have the `origin` property
const origin = anchor.origin || getAnchorOrigin(anchor);
if (origin !== window.location.origin) {
return;
}
// if none of the above, convert the click into a navigation event
const { hash, pathname, search } = anchor;
if (fireRouterEvent('go', { hash, pathname, search }) && event instanceof MouseEvent) {
event.preventDefault();
// for a click event, the scroll is reset to the top position.
// FIXME: undefined here?
if (event.type === 'click') {
window.scrollTo(0, 0);
}
}
}
/**
* A navigation trigger for Vaadin Router that translated clicks on `` links
* into Vaadin Router navigation events.
*
* Only regular clicks on in-app links are translated (primary mouse button, no
* modifier keys, the target href is within the app's URL space).
*/
const CLICK: NavigationTrigger = {
activate() {
window.document.addEventListener('click', vaadinRouterGlobalClickHandler);
},
inactivate() {
window.document.removeEventListener('click', vaadinRouterGlobalClickHandler);
},
};
export default CLICK;
================================================
FILE: src/triggers/navigation.ts
================================================
import type { NavigationTrigger } from '../types.t.js';
import CLICK from './click.js';
import POPSTATE from './popstate.js';
let triggers: readonly NavigationTrigger[] = [];
const DEFAULT_TRIGGERS = {
CLICK,
POPSTATE,
} as const;
export { DEFAULT_TRIGGERS };
export function setNavigationTriggers(newTriggers: readonly NavigationTrigger[] = []): void {
triggers.forEach((trigger) => trigger.inactivate());
newTriggers.forEach((trigger) => trigger.activate());
triggers = newTriggers;
}
================================================
FILE: src/triggers/popstate.ts
================================================
import type { NavigationTrigger } from '../types.t.js';
import { fireRouterEvent } from '../utils.js';
function vaadinRouterGlobalPopstateHandler(event: PopStateEvent) {
if (event.state === 'vaadin-router-ignore') {
return;
}
const { hash, pathname, search } = window.location;
fireRouterEvent('go', { hash, pathname, search });
}
/**
* A navigation trigger for Vaadin Router that translates popstate events into
* Vaadin Router navigation events.
*/
const POPSTATE: NavigationTrigger = {
activate() {
window.addEventListener('popstate', vaadinRouterGlobalPopstateHandler);
},
inactivate() {
window.removeEventListener('popstate', vaadinRouterGlobalPopstateHandler);
},
};
export default POPSTATE;
================================================
FILE: src/types.t.ts
================================================
import type { EmptyObject, RequireAtLeastOne } from 'type-fest';
import type { ResolutionError, ResolverOptions } from './resolver/resolver.js';
import type {
ActionResult as _ActionResult,
ChildrenCallback as _ChildrenCallback,
ChainItem as _ChainItem,
RouteChildrenContext as _RouteChildrenContext,
IndexedParams,
MaybePromise,
Params,
ParamValue,
PrimitiveParamValue,
Route as _Route,
RouteContext as _RouteContext,
} from './resolver/types.t.js';
import type { Router } from './router.js';
export type { ResolutionError, IndexedParams, Params, ParamValue, PrimitiveParamValue };
/**
* A custom event that is triggered when the location changes.
*/
export type VaadinRouterLocationChangedEvent<
R extends object = EmptyObject,
C extends object = EmptyObject,
> = CustomEvent<
Readonly<{
/** The new location after the change */
location: RouterLocation;
/** The router instance that triggered the event */
router: Router;
}>
>;
/**
* A custom event triggered by an error occurred during route resolution.
*
* @typeParam R - The type of additional route-specific data. Defaults to an
* empty object.
* @typeParam C - The type of user-defined context-specific data. Defaults to an
* empty object.
*/
export type VaadinRouterErrorEvent = CustomEvent<
Readonly<{
/** The error object. */
error: ResolutionError;
/** The router instance that triggered the error event. */
router: Router;
}> &
RouteContext
>;
/**
* A custom event triggered when the user navigates to a new location.
*/
export type VaadinRouterGoEvent = CustomEvent;
declare global {
interface WindowEventMap {
'vaadin-router-go': VaadinRouterGoEvent;
'vaadin-router-location-changed': VaadinRouterLocationChangedEvent;
'vaadin-router-error': VaadinRouterErrorEvent;
}
interface ArrayConstructor {
isArray(arg: unknown): arg is T;
}
}
/**
* A context information for a redirect operation.
*/
export type RedirectContextInfo = Readonly<{
/** The original path from which the redirect is happening. */
from: string;
/** An object containing URL parameters related to the redirect. */
params: IndexedParams;
/** The pathname of the new URL to which the redirect is directed. */
pathname: string;
}>;
/**
* A result that can be returned from a route action to request a redirect to
* a different location.
*/
export interface RedirectResult {
/** The path info to redirect to. */
readonly redirect: RedirectContextInfo;
}
/**
* A result that can be returned from a route action to prevent the navigation.
*/
export interface PreventResult {
/** A flag indicating that the navigation should be prevented. */
readonly cancel: true;
}
/**
* A controller to set up and tear down navigation event listeners.
*/
export interface NavigationTrigger {
/** Sets up navigation listeners. */
activate(): void;
/** Tears down navigation listeners. */
inactivate(): void;
}
/**
* A value of a result that can be returned from the router action.
*/
export type ActionValue = HTMLElement | PreventResult | RedirectResult;
/**
* A result of the {@link RouteContext.next} function.
*/
export type NextResult = _ActionResult>;
/**
* A result of the {@link RouteExtension.action | Route.action}.
*/
export type ActionResult = _ActionResult;
/**
* {@inheritDoc "".ChainItem}
*/
export type ChainItem = _ChainItem<
ActionValue,
RouteExtension,
ContextExtension
> &
Readonly<{
element?: WebComponentInterface;
}>;
/**
* A specialized extension for the internal Resolver's
* {@link "".Context | Context} object that redefines some types to
* make it compatible with the {@link Router}.
*
* @internal
*/
export type ContextExtension = Readonly<{
resolver?: Router;
chain?: Array>;
}> &
C;
/**
* {@inheritDoc "".ChildrenCallback}
*/
export type ChildrenCallback = _ChildrenCallback<
ActionValue,
RouteExtension,
ContextExtension
>;
/**
* An specialized extension for the internal Resolver's {@link "".Route | Route}
* object that redefines some types to make it compatible with the
* {@link Router}.
*
* @internal
*/
export type RouteExtension = RequireAtLeastOne<{
children?: ChildrenCallback | ReadonlyArray>;
component?: string;
redirect?: string;
/**
* An action that is executed when the route is resolved.
*
* Actions are executed recursively from the root route to the child route and
* can either produce content or perform actions before or after the child's
* action.
*
* @param context - The context of the current route.
*
* @returns The result of the route resolution. It could be either a value
* produced by the action or a new context to continue the resolution process.
*/
action?(
this: Route,
context: RouteContext,
commands: Commands,
): MaybePromise>;
}> & {
animate?: AnimateCustomClasses | boolean;
} & R;
/**
* {@inheritDoc "".RouteContext}
* @interface
*/
export type RouteContext = _RouteContext<
ActionValue,
RouteExtension,
ContextExtension
>;
/**
* {@inheritDoc "