├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── pr.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cypress.config.mjs ├── cypress ├── .gitignore ├── e2e │ ├── child-outlets.cy.js │ ├── error-handler.cy.js │ ├── guards-resolve-route.cy.js │ ├── guards.cy.js │ ├── hash-error-handler.cy.js │ ├── hash-guards-resolve-route.cy.js │ ├── hash-guards.cy.js │ ├── hash-redirects.cy.js │ ├── hash-url-params.cy.js │ ├── hash.cy.js │ ├── lazy-views.cy.js │ ├── login.cy.js │ ├── redirects.cy.js │ ├── routed-app.cy.js │ ├── sticky-outlets.cy.js │ └── url-params.cy.js ├── fixtures │ └── example.json ├── plugins │ └── index.cjs └── support │ ├── commands.js │ └── e2e.js ├── dist ├── router.mjs └── router.mjs.map ├── favicon.ico ├── img ├── ficus-icon-512x512.png ├── ficus-icon-optimised.svg └── ficus-icon.svg ├── package-lock.json ├── package.json ├── src ├── router.mjs └── util │ ├── add-matcher-to-route.mjs │ ├── element-empty.mjs │ ├── element-from-string.mjs │ ├── emit.mjs │ ├── flatten-routes.mjs │ ├── is-element.mjs │ ├── is-promise.mjs │ ├── object-has-key.mjs │ ├── render-outlet.mjs │ ├── slashes.mjs │ ├── url-search-params.mjs │ └── wait-for.mjs ├── test └── e2e │ ├── css │ └── styles.css │ ├── index.html │ ├── lib │ ├── index.mjs │ └── lit-html.mjs │ ├── routed-app-child-outlets │ ├── app.js │ ├── contents │ │ ├── bar.js │ │ ├── baz.js │ │ └── foo.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-error-handler │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-guards-resolve-route │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ └── page-one.js │ ├── routed-app-guards │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ └── page-one.js │ ├── routed-app-hash-error-handler │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-hash-guards-resolve-route │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ └── page-one.js │ ├── routed-app-hash-guards │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ └── page-one.js │ ├── routed-app-hash-login │ ├── app.js │ ├── components │ │ └── nav.js │ ├── index.html │ ├── main.js │ ├── router │ │ ├── router.js │ │ └── routes.js │ ├── store │ │ └── store.js │ └── views │ │ ├── home-page.js │ │ ├── loggedin-page.js │ │ ├── login-page.js │ │ ├── page-error.js │ │ ├── page-not-found.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-hash-params │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ ├── page-two.js │ │ └── user-page.js │ ├── routed-app-hash-redirects │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ └── page-two.js │ ├── routed-app-hash-url-params │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ ├── page-two.js │ │ └── user-page.js │ ├── routed-app-hash │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-lazy-views │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.esm.js │ │ └── page-two.js │ ├── routed-app-login │ ├── app.js │ ├── components │ │ └── nav.js │ ├── index.html │ ├── main.js │ ├── router.js │ ├── store.js │ └── views │ │ ├── home-page.js │ │ ├── index.js │ │ ├── loggedin-page.js │ │ ├── login-page.js │ │ ├── page-not-found.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-no-auto-start-path │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-no-auto-start │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-redirects │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ └── page-two.js │ ├── routed-app-sticky-outlets │ ├── app.js │ ├── contents │ │ ├── bar.js │ │ ├── baz.js │ │ └── foo.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ ├── routed-app-url-params │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ ├── page-two.js │ │ └── user-page.js │ ├── routed-app │ ├── app.js │ ├── index.html │ ├── main.js │ ├── router.js │ └── views │ │ ├── home-page.js │ │ ├── page-one.js │ │ └── page-two.js │ └── util │ ├── component.js │ ├── detect.js │ ├── emit.js │ ├── find-parent.js │ ├── methods.js │ └── wait-for.js └── types └── router.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | trim_trailing_whitespace = true 7 | indent_size = 2 8 | indent_style = space 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ficusjs] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "npm" 9 | directory: "/site" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [opened, synchronize, edited] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.x 18 | - name: Install dependencies 19 | run: npm install 20 | - name: Lint 21 | run: npm run lint 22 | - name: Run tests 23 | run: npm test 24 | - name: Build 25 | run: npm run build 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/snapshots 3 | .DS_Store 4 | .idea 5 | .vscode 6 | *.log 7 | tmp -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | site 3 | test 4 | examples 5 | cypress 6 | img 7 | .editorconfig 8 | .idea 9 | .vscode 10 | cypress.json 11 | favicon.ico -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.4.2] - 2024-10-10 9 | 10 | ### Updates 11 | - Uplift devDependencies 12 | 13 | ## [3.4.1] - 2023-07-31 14 | 15 | ### Updates 16 | - Uplift devDependencies 17 | 18 | ## [3.4.0] - 2022-10-25 19 | 20 | ### Updates 21 | - Generate sourcemaps on build 22 | 23 | ## [3.3.2] - 2022-10-25 24 | 25 | ### Updates 26 | - Uplift devDependencies 27 | 28 | ## [3.3.1] - 2022-10-18 29 | 30 | ### Fixes 31 | - Fix `addMatcherToRoute` issue with slashes 32 | 33 | ## [3.3.0] - 2022-10-18 34 | 35 | ### New 36 | - Export `addMatcherToRoute` function for adding a matcher to a route 37 | 38 | ## [3.2.0] - 2022-10-02 39 | 40 | ### New 41 | - Emit `outlet-change` event when an outlet is populated 42 | 43 | ### Updates 44 | - Uplift devDependencies 45 | - Convert e2e tests to Cypress v10.x 46 | 47 | ## [3.1.1] - 2021-11-16 48 | 49 | ### Fixes 50 | - Fix types for `createRouter` function 51 | 52 | ## [3.1.0] - 2021-10-20 53 | 54 | ### Updates 55 | - Add `sideEffects` to package.json 56 | - Uplift devDependencies 57 | 58 | ## [3.0.0] - 2021-06-10 59 | 60 | ### Breaking 61 | - Make package `type: module` 62 | - Rename build file to `router.mjs` 63 | 64 | ### New 65 | - Allow routes to be cancelled 66 | 67 | ### Updates 68 | - Add guards to documentation 69 | - Uplift NPM dependencies 70 | 71 | ## [2.2.0] - 2021-05-07 72 | 73 | ### New 74 | - Allow `sticky` attribute for persistent named outlets 75 | 76 | ## [2.1.0] - 2021-04-29 77 | 78 | ### Updates 79 | - Uplift NPM dev dependencies 80 | 81 | ## [2.0.2] - 2021-03-04 82 | 83 | ### Fixes 84 | - Missing package.json properties 85 | 86 | ## [2.0.1] - 2021-03-04 87 | 88 | ### Fixes 89 | - Add exports map to package.json 90 | 91 | ### Updates 92 | - Uplift dev dependencies 93 | 94 | ## [2.0.0] - 2021-02-06 95 | 96 | ### Changed 97 | - Rename package to @ficusjs/router 98 | 99 | ## [1.1.2] - 2020-10-06 100 | 101 | ### Fixed 102 | - Wait for child outlets before continuing render 103 | 104 | ## [1.1.1] - 2020-10-06 105 | 106 | ### Fixed 107 | - Ensure an outlet is clear before rendering contents 108 | 109 | ## [1.1.0] - 2020-09-30 110 | 111 | ### Fixed 112 | - Rename private properties 113 | 114 | ### Added 115 | - Cache outlets to avoid re-rendering the same content 116 | 117 | ## [1.0.0] - 2020-09-29 118 | 119 | - Initial release 120 | 121 | ## [0.1.0] - 2020-09-29 122 | 123 | - Beta release 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Commits 9 | 10 | The Conventional Commits specification must be followed for commit messages. 11 | 12 | Conventional commits provide an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages. 13 | 14 | Please review [https://www.conventionalcommits.org/en/v1.0.0/](https://www.conventionalcommits.org/en/v1.0.0/) before making your first commit. 15 | 16 | ## Pull Request Process 17 | 18 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 19 | build. 20 | 2. Update the README.md with details of changes to the interface, this includes new environment 21 | variables, exposed ports, useful file locations and container parameters. 22 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 23 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 24 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 25 | do not have permission to do that, you may request the second reviewer to merge it for you. 26 | 27 | ## Code of Conduct 28 | 29 | ### Our Pledge 30 | 31 | In the interest of fostering an open and welcoming environment, we as 32 | contributors and maintainers pledge to making participation in our project and 33 | our community a harassment-free experience for everyone, regardless of age, body 34 | size, disability, ethnicity, gender identity and expression, level of experience, 35 | nationality, personal appearance, race, religion, or sexual identity and 36 | orientation. 37 | 38 | ### Our Standards 39 | 40 | Examples of behavior that contributes to creating a positive environment 41 | include: 42 | 43 | * Using welcoming and inclusive language 44 | * Being respectful of differing viewpoints and experiences 45 | * Gracefully accepting constructive criticism 46 | * Focusing on what is best for the community 47 | * Showing empathy towards other community members 48 | 49 | Examples of unacceptable behavior by participants include: 50 | 51 | * The use of sexualized language or imagery and unwelcome sexual attention or 52 | advances 53 | * Trolling, insulting/derogatory comments, and personal or political attacks 54 | * Public or private harassment 55 | * Publishing others' private information, such as a physical or electronic 56 | address, without explicit permission 57 | * Other conduct which could reasonably be considered inappropriate in a 58 | professional setting 59 | 60 | ### Our Responsibilities 61 | 62 | Project maintainers are responsible for clarifying the standards of acceptable 63 | behavior and are expected to take appropriate and fair corrective action in 64 | response to any instances of unacceptable behavior. 65 | 66 | Project maintainers have the right and responsibility to remove, edit, or 67 | reject comments, commits, code, wiki edits, issues, and other contributions 68 | that are not aligned to this Code of Conduct, or to ban temporarily or 69 | permanently any contributor for other behaviors that they deem inappropriate, 70 | threatening, offensive, or harmful. 71 | 72 | ### Scope 73 | 74 | This Code of Conduct applies both within project spaces and in public spaces 75 | when an individual is representing the project or its community. Examples of 76 | representing a project or community include using an official project e-mail 77 | address, posting via an official social media account, or acting as an appointed 78 | representative at an online or offline event. Representation of a project may be 79 | further defined and clarified by project maintainers. 80 | 81 | ### Enforcement 82 | 83 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 84 | reported by contacting the project team at [dev@ficusjs.org](dev@ficusjs.org). All 85 | complaints will be reviewed and investigated and will result in a response that 86 | is deemed necessary and appropriate to the circumstances. The project team is 87 | obligated to maintain confidentiality with regard to the reporter of an incident. 88 | Further details of specific enforcement policies may be posted separately. 89 | 90 | Project maintainers who do not follow or enforce the Code of Conduct in good 91 | faith may face temporary or permanent repercussions as determined by other 92 | members of the project's leadership. 93 | 94 | ### Attribution 95 | 96 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 97 | available at [http://contributor-covenant.org/version/1/4][version] 98 | 99 | [homepage]: http://contributor-covenant.org 100 | [version]: http://contributor-covenant.org/version/1/4/ 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matt Levy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FicusJS router 2 | 3 | FicusJS 4 | 5 | Lightweight standalone client-side router that supports history and hash routing. 6 | 7 | ## Features 8 | 9 | - Declarative route set-up 10 | - Two modes of routing - history and hash 11 | - Routes render web components or HTML strings 12 | - Routes can be sync or async 13 | - Provide context to routes 14 | - Handle redirects from routes 15 | - Named URL parameters 16 | - Guard navigations by redirecting or cancelling 17 | - Outlets act as placeholders for rendering content per route 18 | - Control navigation with methods 19 | - Lazy load views based on route 20 | - Start router programmatically 21 | 22 | ## Documentation 23 | 24 | See the [full documentation](https://docs.ficusjs.org/router). 25 | 26 | ## Getting started 27 | 28 | The easiest way to try out FicusJS router is using a simple example. 29 | 30 | Create an `index.html` file and copy the following between the `` tags. 31 | 32 | ```html 33 | 34 |
35 | 36 | 86 | ``` 87 | 88 | > Alternatively, fork this Codepen to see it in action - [https://codepen.io/ducksoupdev/pen/PoNvGwK](https://codepen.io/ducksoupdev/pen/PoNvGwK) 89 | 90 | The example creates a set of page components, a page navigation component and a new router using hash mode. 91 | 92 | ## Installation 93 | 94 | FicusJS router can be installed in a number of ways. 95 | 96 | ### CDN 97 | 98 | We recommend using native ES modules in the browser. 99 | 100 | ```html 101 | 104 | ``` 105 | 106 | FicusJS router is available on [Skypack](https://www.skypack.dev/view/@ficusjs/router). 107 | 108 | ### NPM 109 | 110 | FicusJS router works nicely with build tools such as Webpack or Rollup. If you are using a NodeJS tool, you can install the NPM package. 111 | 112 | ```bash 113 | npm install @ficusjs/router 114 | ``` 115 | 116 | ### Available builds 117 | 118 | FicusJS router is only available as an ES module. For legacy browsers or alternative modules such as CommonJS, it is recommended to use a build tool to transpile the code. 119 | 120 | ## Development 121 | 122 | How to set-up FicusJS router for local development. 123 | 124 | 1. Clone the repository: 125 | 126 | ```bash 127 | git clone https://github.com/ficusjs/ficusjs-router.git 128 | ``` 129 | 130 | 2. Change the working directory 131 | 132 | ```bash 133 | cd ficusjs-router 134 | ``` 135 | 136 | 3. Install dependencies 137 | 138 | ```bash 139 | npm install # or, yarn install 140 | ``` 141 | 142 | 4. Run the local development server 143 | 144 | ```bash 145 | npm run dev # or, yarn dev 146 | ``` 147 | 148 | That's it! Now open http://localhost:8080 to see a local app. 149 | 150 | ## License 151 | 152 | This project is licensed under the MIT License - see the [`LICENSE`](LICENSE) file for details. 153 | 154 | ## Contributing to FicusJS router 155 | 156 | Any kind of positive contribution is welcome! Please help us to grow by contributing to the project. 157 | 158 | If you wish to contribute, you can work on any features you think would enhance the library. After adding your code, please send us a Pull Request. 159 | 160 | > Please read [CONTRIBUTING](CONTRIBUTING.md) for details on our [CODE OF CONDUCT](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us. 161 | 162 | ## Support 163 | 164 | We all need support and motivation. FicusJS is not an exception. Please give this project a ⭐️ to encourage and show that you liked it. Don't forget to leave a star ⭐️ before you move away. 165 | 166 | If you found the library helpful, please consider [sponsoring us](https://github.com/sponsors/ficusjs). 167 | -------------------------------------------------------------------------------- /cypress.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | reporter: 'junit', 5 | reporterOptions: { 6 | mochaFile: 'cypress/reports/junit/ci-test-output-[hash].xml', 7 | jenkinsMode: true, 8 | rootSuiteTitle: 'FicusJS Router Integration Tests', 9 | testsuitesTitle: 'Cypress Tests' 10 | }, 11 | video: false, 12 | e2e: { 13 | testIsolation: false 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /cypress/.gitignore: -------------------------------------------------------------------------------- 1 | videos 2 | screenshots 3 | reports -------------------------------------------------------------------------------- /cypress/e2e/child-outlets.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Child outlets', () => { 3 | before(() => { 4 | cy.visit('routed-app-child-outlets') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | 32 | it('should render foo contents in the view outlet', () => { 33 | cy.get('foo-contents') 34 | .should('contain.text', 'Foo contents') 35 | }) 36 | 37 | it('should render text in the title outlet', () => { 38 | cy.get('#title') 39 | .should('contain.text', 'Page one title outlet') 40 | }) 41 | }) 42 | 43 | describe('route to page two', () => { 44 | before(() => { 45 | cy.get('button:nth-of-type(3)').click() 46 | }) 47 | 48 | it('has rendered page two', () => { 49 | cy.get('#router-outlet') 50 | .should('contain.text', 'Welcome to page two!') 51 | }) 52 | 53 | it('page two navigation is active', () => { 54 | cy.get('button.active') 55 | .should('have.text', 'Page two') 56 | }) 57 | 58 | it('should render bar contents in the view outlet', () => { 59 | cy.get('bar-contents') 60 | .should('contain.text', 'Bar contents with param Matt is one') 61 | }) 62 | 63 | it('should render baz contents in the view outlet', () => { 64 | cy.get('baz-contents') 65 | .should('contain.text', 'Baz contents with param Matt is one') 66 | }) 67 | 68 | it('should render text in the title outlet', () => { 69 | cy.get('#title') 70 | .should('contain.text', 'Page two title outlet') 71 | }) 72 | }) 73 | 74 | describe('route to home from page two content', () => { 75 | before(() => { 76 | cy.get('nav button:nth-of-type(1)').click() 77 | }) 78 | 79 | it('has rendered the home page', () => { 80 | cy.get('#router-outlet') 81 | .should('contain.text', 'Welcome to the home page!') 82 | }) 83 | 84 | it('home page navigation is active', () => { 85 | cy.get('button.active') 86 | .should('have.text', 'Home') 87 | }) 88 | 89 | it('should clear the text in the title outlet', () => { 90 | cy.get('#title') 91 | .should('be.empty') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /cypress/e2e/error-handler.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Routed errors', () => { 3 | before(() => { 4 | cy.visit('routed-app-error-handler') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to fake page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered an error', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Page not found: /test/e2e/routed-app-error-handler/fake') 73 | }) 74 | }) 75 | 76 | describe('route to error page', () => { 77 | before(() => { 78 | cy.get('button:nth-of-type(5)').click() 79 | }) 80 | 81 | it('has rendered an error', () => { 82 | cy.get('#router-outlet') 83 | .should('contain.text', 'An error occurred: Action error') 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /cypress/e2e/guards-resolve-route.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Guards - resolveRoute', () => { 3 | before(() => { 4 | cy.visit('routed-app-guards-resolve-route') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to cancelled page', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has remained on page one', () => { 39 | cy.location().should((loc) => { 40 | expect(loc.pathname).to.eq('/test/e2e/routed-app-guards-resolve-route/one') 41 | }) 42 | }) 43 | }) 44 | 45 | describe('route to error page', () => { 46 | before(() => { 47 | cy.get('button:nth-of-type(4)').click() 48 | }) 49 | 50 | it('has rendered an error', () => { 51 | cy.get('#router-outlet') 52 | .should('contain.text', 'An error occurred: Action error') 53 | }) 54 | }) 55 | 56 | describe('route to page two', () => { 57 | before(() => { 58 | cy.get('button:nth-of-type(5)').click() 59 | }) 60 | 61 | it('has redirected to page one', () => { 62 | cy.location().should((loc) => { 63 | expect(loc.pathname).to.eq('/test/e2e/routed-app-guards-resolve-route/one') 64 | }) 65 | }) 66 | 67 | it('has rendered page one', () => { 68 | cy.get('#router-outlet') 69 | .should('contain.text', 'Welcome to page one!') 70 | }) 71 | 72 | it('page one navigation is active', () => { 73 | cy.get('button.active') 74 | .should('have.text', 'Page one') 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /cypress/e2e/guards.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Guards', () => { 3 | before(() => { 4 | cy.visit('routed-app-guards') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to cancelled page', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has remained on page one', () => { 39 | cy.location().should((loc) => { 40 | expect(loc.pathname).to.eq('/test/e2e/routed-app-guards/one') 41 | }) 42 | }) 43 | }) 44 | 45 | describe('route to error page', () => { 46 | before(() => { 47 | cy.get('button:nth-of-type(4)').click() 48 | }) 49 | 50 | it('has rendered an error', () => { 51 | cy.get('#router-outlet') 52 | .should('contain.text', 'An error occurred: Action error') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /cypress/e2e/hash-error-handler.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash routed errors', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash-error-handler') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to fake page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered an error', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Page not found: /fake') 73 | }) 74 | }) 75 | 76 | describe('route to error page', () => { 77 | before(() => { 78 | cy.get('button:nth-of-type(5)').click() 79 | }) 80 | 81 | it('has rendered an error', () => { 82 | cy.get('#router-outlet') 83 | .should('contain.text', 'An error occurred: Action error') 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /cypress/e2e/hash-guards-resolve-route.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash guards - resolveRoute', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash-guards-resolve-route') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to cancelled page', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has remained on page one', () => { 39 | cy.location().should((loc) => { 40 | expect(loc.hash).to.eq('#/one') 41 | }) 42 | }) 43 | }) 44 | 45 | describe('route to error page', () => { 46 | before(() => { 47 | cy.get('button:nth-of-type(4)').click() 48 | }) 49 | 50 | it('has rendered an error', () => { 51 | cy.get('#router-outlet') 52 | .should('contain.text', 'An error occurred: Action error') 53 | }) 54 | }) 55 | 56 | describe('route to page two', () => { 57 | before(() => { 58 | cy.get('button:nth-of-type(5)').click() 59 | }) 60 | 61 | it('has redirected to page one', () => { 62 | cy.location().should((loc) => { 63 | expect(loc.hash).to.eq('#/one') 64 | }) 65 | }) 66 | 67 | it('has rendered page one', () => { 68 | cy.get('#router-outlet') 69 | .should('contain.text', 'Welcome to page one!') 70 | }) 71 | 72 | it('page one navigation is active', () => { 73 | cy.get('button.active') 74 | .should('have.text', 'Page one') 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /cypress/e2e/hash-guards.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash Ggards', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash-guards') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to cancelled page', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has remained on page one', () => { 39 | cy.location().should((loc) => { 40 | expect(loc.hash).to.eq('#/one') 41 | }) 42 | }) 43 | }) 44 | 45 | describe('route to error page', () => { 46 | before(() => { 47 | cy.get('button:nth-of-type(4)').click() 48 | }) 49 | 50 | it('has rendered an error', () => { 51 | cy.get('#router-outlet') 52 | .should('contain.text', 'An error occurred: Action error') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /cypress/e2e/hash-redirects.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash redirects', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash-redirects') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page two', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page two!') 25 | }) 26 | 27 | it('page two navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page two') 30 | }) 31 | 32 | it('page two location is active', () => { 33 | cy.location('pathname') 34 | .should('equal', '/test/e2e/routed-app-hash-redirects/') 35 | cy.location('hash') 36 | .should('equal', '#/two') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /cypress/e2e/hash-url-params.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash URL params', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash-url-params') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to user page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered the user page', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Welcome matt to the user page!') 73 | }) 74 | }) 75 | 76 | describe('route to task page', () => { 77 | before(() => { 78 | cy.get('button:nth-of-type(5)').click() 79 | }) 80 | 81 | it('has rendered the user page with task information', () => { 82 | cy.get('#router-outlet') 83 | .should('contain.text', 'Task 12345') 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /cypress/e2e/hash.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Hash routed app', () => { 3 | before(() => { 4 | cy.visit('routed-app-hash') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to fake page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered an error', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Router error: not_found') 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /cypress/e2e/lazy-views.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Lazy views', () => { 3 | before(() => { 4 | cy.visit('routed-app-lazy-views') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /cypress/e2e/login.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Login', () => { 3 | before(() => { 4 | cy.visit('routed-app-login') 5 | }) 6 | 7 | it('has rendered the login page', () => { 8 | cy.get('button[type="submit"]') 9 | .should('have.text', 'Login') 10 | cy.get('input[type="email"]') 11 | .should('exist') 12 | cy.get('input[type="password"]') 13 | .should('exist') 14 | }) 15 | 16 | it('does not have a logout button', () => { 17 | cy.get('nav button:last') 18 | .should('contain.text', 'Fake page') 19 | }) 20 | 21 | describe('logging in', () => { 22 | before(() => { 23 | cy.get('input[type="email"]').type('test@test.com') 24 | cy.get('input[type="password"]').type('test') 25 | cy.get('button[type="submit"]').click() 26 | }) 27 | 28 | it('has rendered the home page', () => { 29 | cy.get('#router-outlet') 30 | .should('contain.text', 'You are logged in test@test.com') 31 | }) 32 | 33 | it('home page navigation is active', () => { 34 | cy.get('button.active') 35 | .should('have.text', 'Home') 36 | }) 37 | 38 | it('has a logout button', () => { 39 | cy.get('nav button:last') 40 | .should('have.text', 'Logout') 41 | }) 42 | 43 | describe('route to page one', () => { 44 | before(() => { 45 | cy.get('button:nth-of-type(2)').click() 46 | }) 47 | 48 | it('has rendered page one', () => { 49 | cy.get('#router-outlet') 50 | .should('contain.text', 'Welcome to page one!') 51 | }) 52 | 53 | it('page one navigation is active', () => { 54 | cy.get('button.active') 55 | .should('have.text', 'Page one') 56 | }) 57 | }) 58 | 59 | describe('route to page two', () => { 60 | before(() => { 61 | cy.get('button:nth-of-type(3)').click() 62 | }) 63 | 64 | it('has rendered page two', () => { 65 | cy.get('#router-outlet') 66 | .should('contain.text', 'Welcome to page two!') 67 | }) 68 | 69 | it('page two navigation is active', () => { 70 | cy.get('button.active') 71 | .should('have.text', 'Page two') 72 | }) 73 | }) 74 | 75 | describe('logout', () => { 76 | before(() => { 77 | cy.get('nav button:last').click() 78 | }) 79 | 80 | it('has rendered the login page', () => { 81 | cy.get('button[type="submit"]') 82 | .should('have.text', 'Login') 83 | cy.get('input[type="email"]') 84 | .should('exist') 85 | cy.get('input[type="password"]') 86 | .should('exist') 87 | }) 88 | 89 | it('does not have a logout button', () => { 90 | cy.get('nav button:last') 91 | .should('have.text', 'Fake page') 92 | }) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /cypress/e2e/redirects.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Redirects', () => { 3 | before(() => { 4 | cy.visit('routed-app-redirects') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page two', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page two!') 25 | }) 26 | 27 | it('page two navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page two') 30 | }) 31 | 32 | it('page two location is active', () => { 33 | cy.location('pathname') 34 | .should('equal', '/test/e2e/routed-app-redirects/two') 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /cypress/e2e/routed-app.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Routed app', () => { 3 | before(() => { 4 | cy.visit('routed-app') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to fake page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered an error', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Router error: not_found') 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /cypress/e2e/sticky-outlets.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('Sticky outlets', () => { 3 | before(() => { 4 | cy.visit('routed-app-sticky-outlets') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | 32 | it('should render foo contents in the view outlet', () => { 33 | cy.get('foo-contents') 34 | .should('contain.text', 'Foo contents') 35 | }) 36 | 37 | it('should keep sticky text in the title outlet', () => { 38 | cy.get('#title') 39 | .should('contain.text', 'Home title outlet') 40 | }) 41 | }) 42 | 43 | describe('route to page two', () => { 44 | before(() => { 45 | cy.get('button:nth-of-type(3)').click() 46 | }) 47 | 48 | it('has rendered page two', () => { 49 | cy.get('#router-outlet') 50 | .should('contain.text', 'Welcome to page two!') 51 | }) 52 | 53 | it('page two navigation is active', () => { 54 | cy.get('button.active') 55 | .should('have.text', 'Page two') 56 | }) 57 | 58 | it('should render bar contents in the view outlet', () => { 59 | cy.get('bar-contents') 60 | .should('contain.text', 'Bar contents with param Matt is one') 61 | }) 62 | 63 | it('should render baz contents in the view outlet', () => { 64 | cy.get('baz-contents') 65 | .should('contain.text', 'Baz contents with param Matt is one') 66 | }) 67 | 68 | it('should render text in the title outlet', () => { 69 | cy.get('#title') 70 | .should('contain.text', 'Page two title outlet') 71 | }) 72 | }) 73 | 74 | describe('route to home from page two content', () => { 75 | before(() => { 76 | cy.get('nav button:nth-of-type(1)').click() 77 | }) 78 | 79 | it('has rendered the home page', () => { 80 | cy.get('#router-outlet') 81 | .should('contain.text', 'Welcome to the home page!') 82 | }) 83 | 84 | it('home page navigation is active', () => { 85 | cy.get('button.active') 86 | .should('have.text', 'Home') 87 | }) 88 | 89 | it('should render text in the title outlet', () => { 90 | cy.get('#title') 91 | .should('contain.text', 'Home title outlet') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /cypress/e2e/url-params.cy.js: -------------------------------------------------------------------------------- 1 | /* global describe cy before it */ 2 | describe('URL params', () => { 3 | before(() => { 4 | cy.visit('routed-app-url-params') 5 | }) 6 | 7 | it('has rendered the home page', () => { 8 | cy.get('#router-outlet') 9 | .should('contain.text', 'Welcome to the home page!') 10 | }) 11 | 12 | it('home page navigation is active', () => { 13 | cy.get('button.active') 14 | .should('have.text', 'Home') 15 | }) 16 | 17 | describe('route to page one', () => { 18 | before(() => { 19 | cy.get('button:nth-of-type(2)').click() 20 | }) 21 | 22 | it('has rendered page one', () => { 23 | cy.get('#router-outlet') 24 | .should('contain.text', 'Welcome to page one!') 25 | }) 26 | 27 | it('page one navigation is active', () => { 28 | cy.get('button.active') 29 | .should('have.text', 'Page one') 30 | }) 31 | }) 32 | 33 | describe('route to page two', () => { 34 | before(() => { 35 | cy.get('button:nth-of-type(3)').click() 36 | }) 37 | 38 | it('has rendered page two', () => { 39 | cy.get('#router-outlet') 40 | .should('contain.text', 'Welcome to page two!') 41 | }) 42 | 43 | it('page two navigation is active', () => { 44 | cy.get('button.active') 45 | .should('have.text', 'Page two') 46 | }) 47 | }) 48 | 49 | describe('route to home from page two content', () => { 50 | before(() => { 51 | cy.get('#router-outlet button').click() 52 | }) 53 | 54 | it('has rendered the home page', () => { 55 | cy.get('#router-outlet') 56 | .should('contain.text', 'Welcome to the home page!') 57 | }) 58 | 59 | it('home page navigation is active', () => { 60 | cy.get('button.active') 61 | .should('have.text', 'Home') 62 | }) 63 | }) 64 | 65 | describe('route to user page', () => { 66 | before(() => { 67 | cy.get('button:nth-of-type(4)').click() 68 | }) 69 | 70 | it('has rendered the user page', () => { 71 | cy.get('#router-outlet') 72 | .should('contain.text', 'Welcome matt to the user page!') 73 | }) 74 | }) 75 | 76 | describe('route to task page', () => { 77 | before(() => { 78 | cy.get('button:nth-of-type(5)').click() 79 | }) 80 | 81 | it('has rendered the user page with task information', () => { 82 | cy.get('#router-outlet') 83 | .should('contain.text', 'Task 12345') 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/plugins/index.cjs: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | Cypress.Commands.overwrite('visit', (originalFn, url, options) => originalFn(`http://localhost:8080/test/e2e/${url}/`, options)) -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /dist/router.mjs: -------------------------------------------------------------------------------- 1 | const waitFor=(t=(()=>!0),e=1e4)=>new Promise(((r,o)=>{const check=()=>{const s=t();s?r(s):(e-=100)<0?o(new Error("Timed out waiting!")):setTimeout(check,100)};setTimeout(check,100)}));function isPromise(t){return("object"==typeof t||"function"==typeof t)&&"function"==typeof t.then}function hasKey(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function elementEmpty(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function renderOutlet(t,e){const r="string"==typeof e?document.querySelector(e):e;if(!r)return void console.warn(`Unable to find outlet: ${e}`);let o;if(function(t){return t instanceof Element||t instanceof HTMLElement}(t))o=t;else{o=window.customElements.get(t)?document.createElement(t):function(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstChild}(t)}elementEmpty(r),r.appendChild(o);const s="data-router-outlet";r.hasAttribute(s)||r.setAttribute(s,"")}let t=class extends Array{peek(){return this[this.length-1]}};function flatten(t,e,r,o=!0){r.forEach((r=>{if(r.children)!function(t,e,r){let o,s=[...r.children];if(r.action||r.component)o={...r},delete o.children;else{const t=s.find((t=>""===t.path));t&&(o={...t},o.path=r.path,delete o.children,s=s.filter((t=>""!==t.path)))}const n=copyRouteAndAdjustPath(o,e);t.push(n),e.push(n),flatten(t,e,s,!1)}(t,e,r);else{const o=copyRouteAndAdjustPath(r,e);t.push(o)}o&&e.splice(0,e.length)}))}function copyRouteAndAdjustPath(t,e){const r={...t},o=e.peek();return o&&(r.path=`${o.path}${r.path}`),r}function stripTrailingSlash(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t}function addMatcherToRoute(t){const e={...t,matcher:e=>stripTrailingSlash(t.path)===stripTrailingSlash(e)?e:void 0};if(t.component&&!t.action&&(e.action=()=>t.component),/:[^/]+/.test(t.path)){const r=t.path.match(/(:[^/]+)/gm);r&&r.length>0&&(e.urlParamKeys=r.map((t=>t.substring(1))));const o=stripTrailingSlash(t.path.replace(/:[^/]+/g,"([^/]+)"));e.pathRegex=new RegExp(`^${o}$`),e.pathRegexCapture=new RegExp(o,"gm"),e.matcher=t=>{if(e.pathRegex.test(t)){const r={};let o;for(;null!==(o=e.pathRegexCapture.exec(t));)if(o&&o.length===e.urlParamKeys.length+1){const t=o.slice(1);for(let o=0;oe[0].toLowerCase()?1:t[0].toLowerCase()/g,">").replace(/&/g,"&")}_unesc(t){return String(t).replace(/</g,"<").replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/&/g,"&")}}class r{constructor(){this.items=[]}push(t){this.items.push(t)}pop(){this.items.pop()}peek(){return this.items[this.items.length-1]}isEmpty(){return 0===this.items.length}clear(){this.items=[]}size(){return this.items.length}}class o{constructor(t,e,o){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router;this._rootOutletSelector=e,this.stack=new r,this._routes=this._processRoutes(t),this._routerOptions=this._processOptions(o),this._outletCache=new WeakMap,"undefined"!=typeof window&&(window.addEventListener("popstate",(()=>{this._findAndRenderRoute(this.location).then((()=>this.stack.pop())).catch((t=>{throw this._renderError(this.location,t),t}))})),window.__ficusjs__=window.__ficusjs__||{},window.__ficusjs__.router=window.__ficusjs__.router||this)}get location(){let t;return"undefined"!=typeof window&&(t={host:window.location.host,protocol:window.location.protocol,pathname:window.location.pathname,hash:window.location.hash,href:window.location.href,search:window.location.search,state:window.history.state},"hash"===this._routerOptions.mode&&(t.pathname=this._getHashPathname(),t.hash="")),t}get options(){return this._routerOptions}setOptions(t={}){this._routerOptions=this._processOptions(t)}addRoutes(t){this._routes=[...this._routes,...this._processRoutes(t)]}hasRoute(t){return!!this._findRoute(t)}_getQueryStringParams(t){return Object.fromEntries(new e(t.href).entries())}_getUrlParams(t,e){const r=e.matcher(t.pathname);return"string"==typeof r?void 0:r}_findRoute(t){return this._routes.find((e=>void 0!==e.matcher(t)))}_getHashPathname(){let t=window.location.hash.substring(1);return""===t&&(t="/"),t}_processRoutes(e){return function(e){const r=new t;return flatten(r,new t,e),r}(e).map((t=>addMatcherToRoute(t)))}_processOptions(t={}){const e=this,r={mode:"history",autoStart:!0,changeHistoryState:!0,warnOnMissingOutlets:!1,...t};if(!r.resolveRoute)return{...r,resolveRoute(t,r){if(t.route&&"function"==typeof t.route.action)return{template:new Promise(((e,o)=>{e(t.route.action(t,r))})).catch((r=>{throw e._renderError(t.location,r),r})),outlets:t.route.outlets}}};if(r.resolveRoute){const t=r.resolveRoute;return{...r,resolveRoute:(e,r)=>t(e,r)}}return r}_render(t,e,r,o={}){return isPromise(r)?r.then((r=>this._performRender(t,e,r,o))):this._performRender(t,e,r,o)}_performRender(t,e,r,o){return"object"==typeof r&&r.redirect?Promise.resolve(r):r instanceof Error?(this._renderError(t,r),Promise.resolve(!0)):!1===r?Promise.resolve(!1):waitFor((()=>document.querySelector(this._rootOutletSelector))).then((()=>{const t=document.querySelector(this._rootOutletSelector),s=Object.keys(o),n=document.querySelectorAll("[data-router-outlet]:not([sticky])");return n.length&&[...n].filter((e=>e!==t)).forEach((t=>elementEmpty(t))),this._renderIntoOutlet(r,t),s.length?Promise.all(s.map((t=>waitFor((()=>{const e=document.querySelectorAll(t);return e.length>0&&e})).then((r=>{const s=o[t](e,e&&e.params);isPromise(s)?s.then((t=>this._renderIntoAllOutlets(t,r))):this._renderIntoAllOutlets(s,r)})).catch((t=>this._routerOptions.warnOnMissingOutlets&&console.warn(t)))))).then((()=>!0)):Promise.resolve(!0)}))}_renderIntoOutlet(t,e){t&&(this._isSameOutletContent(t,e)||(renderOutlet(t,e),this._outletCache.set(e,t),this._emitRouterOutletChangeEvent(e)))}_renderIntoAllOutlets(t,e){for(let r=0;r{if("object"==typeof t&&t.redirect){const e={from:o.href};return this.push(t.redirect,e)}return t&&this._routerOptions.changeHistoryState&&this._setState(o.href,e,r),Promise.resolve(t)})).catch((t=>{throw this._renderError(o,t),t}))}_normalizeLocation(t){const e={host:void 0,protocol:void 0,href:void 0,pathname:void 0,search:void 0,hash:void 0};if("object"==typeof t){if("history"===this._routerOptions.mode&&!t.pathname)throw new Error(`Unable to navigate to: ${t}`);let r=t.href||this.location.href;e.pathname=t.pathname||this.location.pathname,t.search&&""!==t.search&&!r.includes(t.search)&&(r=`${r}${t.search}`,e.search=t.search),t.hash&&""!==t.hash&&!r.includes(t.hash)&&(r=`${r}${t.hash}`,e.hash=t.hash),e.hash=t.hash||this.location.hash,e.search=t.search||this.location.search,e.host=t.host||this.location.host,e.protocol=t.protocol||this.location.protocol,e.href=r}else if("string"==typeof t){const r=new URL(/^https?:\/\//.test(t)?t:`${window.location.protocol}//${window.location.host}${t}`);e.host=r.host,e.protocol=r.protocol,e.href=r.href,e.pathname=r.pathname,e.search=r.search,e.hash=r.hash,t.includes("?")?(e.pathname=t.substring(0,t.indexOf("?")),e.search=t.substring(t.indexOf("?"))):t.includes("#")&&(e.pathname=t.substring(0,t.indexOf("#")))}return"hash"===this._routerOptions.mode&&(e.href=`${window.location.pathname}${e.search||""}#${e.pathname}`),e}_renderActionResult(t,e,r){if(hasKey(r,"template")&&hasKey(r,"outlets")){const{template:o,outlets:s}=r;return this._render(t,e,o,s)}return this._render(t,e,r,{})}_getRouteContext(t,e){let r={...this._getQueryStringParams(e)};const o={context:this._routerOptions.context,router:this,route:t,location:e,params:r};return t&&(r={...r,...this._getUrlParams(e,t)},o.params=r),o}_resolveRoute(t){const e=this._findRoute(t.pathname),r=this._getRouteContext(e,t);try{const t=this._routerOptions.resolveRoute(r,r.params);return null!=t?{actionResult:t,context:r}:e?{actionResult:e.action(r,r.params),context:r}:{actionResult:void 0,context:r}}catch(e){throw this._renderError(t,e),e}}_findAndRenderRoute(t){const{actionResult:e,context:r}=this._resolveRoute(t);if(!e)throw new Error("not_found");return this._renderActionResult(t,r,e)}_setState(t,e,r=!1){r?window.history.replaceState(e,null,t):(this.stack.push(t),window.history.pushState(e,null,t))}_renderError(t,e){if(console.error(`A router error occurred for location '${t.href}'`,e),this._routerOptions.errorHandler){const r={message:e.message,status:"not_found"===e.message?404:500},o=this._getRouteContext(this._findRoute(t.pathname),t),s=this._routerOptions.errorHandler(r,o);isPromise(s)?s.then((e=>this._render(t,o,e))).catch((r=>this._render(t,o,`
Router error from errorHandler: ${r.message}, original error: ${e.message}
`))):this._render(t,o,s)}else this._render(t,null,`
Router error: ${e.message}
`)}go(t){window.history.go(t)}goBack(){this.go(-1)}goForward(){this.go(1)}start(t=this.location){/complete|interactive|loaded/.test(document.readyState)?this.replace(t):document.addEventListener("DOMContentLoaded",(()=>this.replace(t)))}}function createRouter(t,e,r={}){const s=new o(t,e,r);return s.options.autoStart&&s.start(),s}function getRouter(){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router}export{addMatcherToRoute,createRouter,getRouter}; 2 | //# sourceMappingURL=router.mjs.map 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ficusjs/ficusjs-router/0738df1dd6776cb10a4d2b54f606646a69b8e0a0/favicon.ico -------------------------------------------------------------------------------- /img/ficus-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ficusjs/ficusjs-router/0738df1dd6776cb10a4d2b54f606646a69b8e0a0/img/ficus-icon-512x512.png -------------------------------------------------------------------------------- /img/ficus-icon-optimised.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/ficus-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 53 | 55 | 63 | 67 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ficusjs/router", 3 | "version": "3.4.2", 4 | "description": "Lightweight standalone client-side router that supports history and hash routing", 5 | "type": "module", 6 | "main": "dist/router.mjs", 7 | "module": "dist/router.mjs", 8 | "types": "types/router.d.ts", 9 | "sideEffects": false, 10 | "scripts": { 11 | "build": "rollup -i src/router.mjs -o dist/router.mjs -f es -m -p \"terser={mangle: { keep_fnames: true, toplevel: true, module: true }}\" -p \"filesize={}\"", 12 | "build:dev": "rollup -i src/router.mjs -o tmp/router.mjs -f es", 13 | "cy:open": "cypress open", 14 | "cy:run": "cypress run", 15 | "dev": "run-p serve open", 16 | "lint": "standard \"./src/**/*.mjs\"", 17 | "open": "open-cli http://localhost:8080/test/e2e", 18 | "test": "start-server-and-test serve:silent http://localhost:8080 cy:run", 19 | "serve": "http-server", 20 | "serve:silent": "http-server -s" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/ficusjs/ficusjs-router.git" 25 | }, 26 | "keywords": [ 27 | "ficusjs", 28 | "frontend", 29 | "web-components", 30 | "router" 31 | ], 32 | "author": "Matt Levy", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@rollup/plugin-terser": "^0.4.4", 36 | "cypress": "13.15.0", 37 | "http-server": "14.1.1", 38 | "npm-run-all": "4.1.5", 39 | "open-cli": "8.0.0", 40 | "rollup": "4.24.0", 41 | "rollup-plugin-filesize": "10.0.0", 42 | "standard": "17.1.2", 43 | "start-server-and-test": "2.0.8" 44 | }, 45 | "exports": { 46 | ".": { 47 | "import": "./dist/router.mjs" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/util/add-matcher-to-route.mjs: -------------------------------------------------------------------------------- 1 | import { stripTrailingSlash } from './slashes.mjs' 2 | 3 | export function addMatcherToRoute (route) { 4 | const newRoute = { 5 | ...route, 6 | matcher: (path) => { 7 | return stripTrailingSlash(route.path) === stripTrailingSlash(path) ? path : undefined 8 | } 9 | } 10 | if (route.component && !route.action) newRoute.action = () => route.component 11 | if (/:[^/]+/.test(route.path)) { 12 | const keys = route.path.match(/(:[^/]+)/gm) 13 | if (keys && keys.length > 0) { 14 | newRoute.urlParamKeys = keys.map(k => k.substring(1)) 15 | } 16 | const pathRegexStr = stripTrailingSlash(route.path.replace(/:[^/]+/g, '([^/]+)')) 17 | newRoute.pathRegex = new RegExp(`^${pathRegexStr}$`) 18 | newRoute.pathRegexCapture = new RegExp(pathRegexStr, 'gm') 19 | newRoute.matcher = (path) => { 20 | if (newRoute.pathRegex.test(path)) { 21 | const params = {} 22 | let v 23 | while ((v = newRoute.pathRegexCapture.exec(path)) !== null) { 24 | if (v && v.length === newRoute.urlParamKeys.length + 1) { 25 | const nv = v.slice(1) 26 | for (let i = 0; i < nv.length; i++) { 27 | params[newRoute.urlParamKeys[i]] = nv[i] 28 | } 29 | } 30 | } 31 | return params 32 | } 33 | return undefined 34 | } 35 | } 36 | return newRoute 37 | } 38 | -------------------------------------------------------------------------------- /src/util/element-empty.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to empty an element 3 | * @param {Element} el 4 | */ 5 | export function elementEmpty (el) { 6 | while (el.firstChild) el.removeChild(el.firstChild) 7 | } 8 | -------------------------------------------------------------------------------- /src/util/element-from-string.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to create an element from an HTML string 3 | * @param {string} html The HTML string 4 | * @returns {HTMLElement} The created element 5 | * @example 6 | * elementFromString(`
7 | *

A 3rd level header

8 | *

A paragraph

9 | *
`) 10 | */ 11 | export function elementFromString (html) { 12 | const div = document.createElement('div') 13 | div.innerHTML = html.trim() 14 | return div.firstChild 15 | } 16 | -------------------------------------------------------------------------------- /src/util/emit.mjs: -------------------------------------------------------------------------------- 1 | /* global CustomEvent */ 2 | /** 3 | * Function to emit an event on an element 4 | * @param {Node} elem The element that emits the event 5 | * @param {string} name The name of the event 6 | * @param {object} opts Options to pass to the event 7 | * @returns {boolean} The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault(). Otherwise it returns true. 8 | */ 9 | export function emit (elem, name, opts = {}) { 10 | const defs = { 11 | bubbles: true, 12 | cancelable: true, 13 | composed: false 14 | } 15 | const eventOptions = Object.assign({}, defs, opts) 16 | const e = new CustomEvent(name, eventOptions) 17 | return elem.dispatchEvent(e) 18 | } 19 | -------------------------------------------------------------------------------- /src/util/flatten-routes.mjs: -------------------------------------------------------------------------------- 1 | class Stack extends Array { 2 | peek () { 3 | return this[this.length - 1] 4 | } 5 | } 6 | 7 | function flatten (stack, parentRouteStack, routes, clearParentRouteStack = true) { 8 | routes.forEach(route => { 9 | if (route.children) { 10 | flattenChildren(stack, parentRouteStack, route) 11 | } else { 12 | const nr = copyRouteAndAdjustPath(route, parentRouteStack) 13 | stack.push(nr) 14 | } 15 | if (clearParentRouteStack) { 16 | parentRouteStack.splice(0, parentRouteStack.length) 17 | } 18 | }) 19 | } 20 | 21 | function flattenChildren (stack, parentRouteStack, route) { 22 | let children = [...route.children] 23 | let newRoute 24 | if (route.action || route.component) { 25 | newRoute = { ...route } 26 | delete newRoute.children 27 | } else { 28 | const emptyRoute = children.find(r => r.path === '') 29 | if (emptyRoute) { 30 | newRoute = { ...emptyRoute } 31 | newRoute.path = route.path 32 | delete newRoute.children 33 | children = children.filter(r => r.path !== '') 34 | } 35 | } 36 | const nr = copyRouteAndAdjustPath(newRoute, parentRouteStack) 37 | stack.push(nr) 38 | parentRouteStack.push(nr) 39 | flatten(stack, parentRouteStack, children, false) 40 | } 41 | 42 | function copyRouteAndAdjustPath (route, parentRouteStack) { 43 | const nr = { ...route } 44 | const last = parentRouteStack.peek() 45 | if (last) { 46 | nr.path = `${last.path}${nr.path}` 47 | } 48 | return nr 49 | } 50 | 51 | export function flattenRoutes (routes) { 52 | const stack = new Stack() 53 | const parentRouteStack = new Stack() 54 | flatten(stack, parentRouteStack, routes) 55 | return stack 56 | } 57 | -------------------------------------------------------------------------------- /src/util/is-element.mjs: -------------------------------------------------------------------------------- 1 | /* global Element HTMLElement */ 2 | 3 | /** 4 | * Function to assert for a DOM element 5 | * @param {Element|HTMLElement} element 6 | * @returns {boolean} 7 | */ 8 | export function isElement (element) { 9 | return element instanceof Element || element instanceof HTMLElement 10 | } 11 | -------------------------------------------------------------------------------- /src/util/is-promise.mjs: -------------------------------------------------------------------------------- 1 | export function isPromise (obj) { 2 | return (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' 3 | } 4 | -------------------------------------------------------------------------------- /src/util/object-has-key.mjs: -------------------------------------------------------------------------------- 1 | export function hasKey (obj, key) { 2 | return Object.prototype.hasOwnProperty.call(obj, key) 3 | } 4 | -------------------------------------------------------------------------------- /src/util/render-outlet.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to render a router outlet 3 | * @param {string|HTMLElement} what A component tag, HTML snippet or DOM element 4 | * @param {HTMLElement|string} where An element to render into 5 | */ 6 | import { elementFromString } from './element-from-string.mjs' 7 | import { isElement } from './is-element.mjs' 8 | import { elementEmpty } from './element-empty.mjs' 9 | 10 | export function renderOutlet (what, where) { 11 | // check for an outlet 12 | const outlet = typeof where === 'string' ? document.querySelector(where) : where 13 | if (!outlet) { 14 | console.warn(`Unable to find outlet: ${where}`) 15 | return 16 | } 17 | 18 | // decide what to render into the outlet 19 | let element 20 | if (isElement(what)) { 21 | element = what 22 | } else { 23 | const isComponent = window.customElements.get(what) 24 | if (isComponent) { 25 | element = document.createElement(what) 26 | } else { 27 | element = elementFromString(what) 28 | } 29 | } 30 | 31 | elementEmpty(outlet) 32 | outlet.appendChild(element) 33 | 34 | // set the data attribute so we can clear the outlet later 35 | const dro = 'data-router-outlet' 36 | if (!outlet.hasAttribute(dro)) outlet.setAttribute(dro, '') 37 | } 38 | -------------------------------------------------------------------------------- /src/util/slashes.mjs: -------------------------------------------------------------------------------- 1 | export function stripTrailingSlash (path) { 2 | return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path 3 | } 4 | -------------------------------------------------------------------------------- /src/util/url-search-params.mjs: -------------------------------------------------------------------------------- 1 | export class UrlSearchOrHashParams { 2 | constructor (url) { 3 | this.urlParamMap = {} 4 | let paramUrl 5 | let urlHash 6 | let paramPairs = [] 7 | 8 | if (url.includes('?')) { 9 | paramUrl = url.substring(url.indexOf('?') + 1) 10 | if (paramUrl.includes('#')) { 11 | paramUrl = paramUrl.substring(0, paramUrl.indexOf('#')) 12 | urlHash = paramUrl.substring(paramUrl.indexOf('#') + 1) 13 | } 14 | } else if (url.includes('#')) { 15 | paramUrl = '' 16 | urlHash = url.substring(url.indexOf('#') + 1, url.length) 17 | } 18 | 19 | paramPairs = paramUrl ? paramUrl.split('&') : [] 20 | 21 | if (urlHash && urlHash.includes('&') && urlHash.includes('=')) { 22 | paramPairs = [...paramPairs, ...urlHash.split('&')] 23 | } 24 | 25 | for (let i = 0; i < paramPairs.length; i++) { 26 | const paramComponents = paramPairs[i].split('=') 27 | if (paramComponents.length === 2) { 28 | this.urlParamMap[paramComponents[0]] = this._esc(decodeURIComponent(paramComponents[1])) 29 | } 30 | } 31 | } 32 | 33 | entries () { 34 | let entries = [] 35 | for (const key in this.urlParamMap) { 36 | if (this.has(key)) { 37 | entries.push([key, this.urlParamMap[key]]) 38 | } 39 | } 40 | 41 | entries = entries.sort(function (entryA, entryB) { 42 | if (entryA[0].toLowerCase() > entryB[0].toLowerCase()) { 43 | return 1 44 | } 45 | if (entryA[0].toLowerCase() < entryB[0].toLowerCase()) { 46 | return -1 47 | } 48 | return 0 49 | }) 50 | 51 | return entries 52 | } 53 | 54 | remove (key) { 55 | delete this.urlParamMap[key] 56 | } 57 | 58 | get (key) { 59 | return this.urlParamMap[key] 60 | } 61 | 62 | has (key) { 63 | // eslint-disable-next-line no-prototype-builtins 64 | return this.urlParamMap.hasOwnProperty(key) 65 | } 66 | 67 | keys () { 68 | const keys = [] 69 | for (const key in this.urlParamMap) { 70 | if (this.has(key)) { 71 | keys.push(key) 72 | } 73 | } 74 | return keys 75 | } 76 | 77 | set (key, val) { 78 | this.urlParamMap[key] = val 79 | } 80 | 81 | toString () { 82 | let paramsString = '' 83 | const entries = this.entries() 84 | for (let i = 0; i < entries.length; i++) { 85 | const entry = entries[i] 86 | if (this.has(entry[0])) { 87 | paramsString += '&' + entry[0] + '=' + encodeURIComponent(this._unesc(entry[1])) 88 | } 89 | } 90 | return paramsString.substring(1) 91 | } 92 | 93 | values () { 94 | const values = [] 95 | for (const key in this.urlParamMap) { 96 | if (this.has(key)) { 97 | values.push(this.urlParamMap[key]) 98 | } 99 | } 100 | return values 101 | } 102 | 103 | _esc (s) { 104 | return String(s) 105 | .replace(//g, '>') 109 | .replace(/&/g, '&') 110 | } 111 | 112 | _unesc (s) { 113 | return String(s) 114 | .replace(/</g, '<') 115 | .replace(/'/g, '\'') 116 | .replace(/"/g, '"') 117 | .replace(/>/g, '>') 118 | .replace(/&/g, '&') 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/util/wait-for.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * A promise based function to wait for a condition to pass or timeout - whichever is the soonest 3 | * @param {Function} test 4 | * @param {number} timeoutInMilliseconds 5 | * @returns {Promise} 6 | */ 7 | export const waitFor = (test = () => true, timeoutInMilliseconds = 10000) => new Promise((resolve, reject) => { 8 | const check = () => { 9 | const result = test() 10 | if (result) { 11 | resolve(result) 12 | } else if ((timeoutInMilliseconds -= 100) < 0) { 13 | reject(new Error('Timed out waiting!')) 14 | } else { 15 | setTimeout(check, 100) 16 | } 17 | } 18 | setTimeout(check, 100) 19 | }) 20 | -------------------------------------------------------------------------------- /test/e2e/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-weight: 200; 4 | } 5 | 6 | li { 7 | line-height: 1.5; 8 | } 9 | 10 | li.title { 11 | font-size: 1.5rem; 12 | font-weight: 700; 13 | } -------------------------------------------------------------------------------- /test/e2e/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FicusJS router - E2E apps 6 | 7 | 8 | 9 | 33 | 34 | -------------------------------------------------------------------------------- /test/e2e/lib/lit-html.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2017 Google LLC 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | */ 6 | var t, e; 7 | const i = globalThis.trustedTypes, s = i ? i.createPolicy("lit-html", {createHTML: (t2) => t2}) : void 0, n = `lit$${(Math.random() + "").slice(9)}$`, o = "?" + n, l = `<${o}>`, h = document, r = (t2 = "") => h.createComment(t2), d = (t2) => t2 === null || typeof t2 != "object" && typeof t2 != "function", $ = Array.isArray, a = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, u = /-->/g, c = />/g, _ = />|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g, v = /'/g, p = /"/g, g = /^(?:script|style|textarea)$/i, m = ((t2) => (e2, ...i2) => ({_$litType$: t2, strings: e2, values: i2}))(1), f = Symbol.for("lit-noChange"), y = Symbol.for("lit-nothing"), H = new WeakMap(), A = (t2, e2, i2) => { 8 | var s2, n2; 9 | const o2 = (s2 = i2 == null ? void 0 : i2.renderBefore) !== null && s2 !== void 0 ? s2 : e2; 10 | let l2 = o2._$litPart$; 11 | if (l2 === void 0) { 12 | const t3 = (n2 = i2 == null ? void 0 : i2.renderBefore) !== null && n2 !== void 0 ? n2 : null; 13 | o2._$litPart$ = l2 = new T(e2.insertBefore(r(), t3), t3, void 0, i2 != null ? i2 : {}); 14 | } 15 | return l2._$AI(t2), l2; 16 | }, x = h.createTreeWalker(h, 129, null, false), P = (t2, e2) => { 17 | const i2 = t2.length - 1, o2 = []; 18 | let h2, $2 = e2 === 2 ? "" : "", m2 = a; 19 | for (let e3 = 0; e3 < i2; e3++) { 20 | const i3 = t2[e3]; 21 | let s2, f3, y2 = -1, H2 = 0; 22 | for (; H2 < i3.length && (m2.lastIndex = H2, f3 = m2.exec(i3), f3 !== null); ) 23 | H2 = m2.lastIndex, m2 === a ? f3[1] === "!--" ? m2 = u : f3[1] !== void 0 ? m2 = c : f3[2] !== void 0 ? (g.test(f3[2]) && (h2 = RegExp("" ? (m2 = h2 != null ? h2 : a, y2 = -1) : f3[1] === void 0 ? y2 = -2 : (y2 = m2.lastIndex - f3[2].length, s2 = f3[1], m2 = f3[3] === void 0 ? _ : f3[3] === '"' ? p : v) : m2 === p || m2 === v ? m2 = _ : m2 === u || m2 === c ? m2 = a : (m2 = _, h2 = void 0); 24 | const x2 = m2 === _ && t2[e3 + 1].startsWith("/>") ? " " : ""; 25 | $2 += m2 === a ? i3 + l : y2 >= 0 ? (o2.push(s2), i3.slice(0, y2) + "$lit$" + i3.slice(y2) + n + x2) : i3 + n + (y2 === -2 ? (o2.push(void 0), e3) : x2); 26 | } 27 | const f2 = $2 + (t2[i2] || "") + (e2 === 2 ? "" : ""); 28 | return [s !== void 0 ? s.createHTML(f2) : f2, o2]; 29 | }; 30 | class N { 31 | constructor({strings: t2, _$litType$: e2}, s2) { 32 | let l2; 33 | this.parts = []; 34 | let h2 = 0, $2 = 0; 35 | const a2 = t2.length - 1, u2 = this.parts, [c2, _2] = P(t2, e2); 36 | if (this.el = N.createElement(c2, s2), x.currentNode = this.el.content, e2 === 2) { 37 | const t3 = this.el.content, e3 = t3.firstChild; 38 | e3.remove(), t3.append(...e3.childNodes); 39 | } 40 | for (; (l2 = x.nextNode()) !== null && u2.length < a2; ) { 41 | if (l2.nodeType === 1) { 42 | if (l2.hasAttributes()) { 43 | const t3 = []; 44 | for (const e3 of l2.getAttributeNames()) 45 | if (e3.endsWith("$lit$") || e3.startsWith(n)) { 46 | const i2 = _2[$2++]; 47 | if (t3.push(e3), i2 !== void 0) { 48 | const t4 = l2.getAttribute(i2.toLowerCase() + "$lit$").split(n), e4 = /([.?@])?(.*)/.exec(i2); 49 | u2.push({type: 1, index: h2, name: e4[2], strings: t4, ctor: e4[1] === "." ? M : e4[1] === "?" ? S : e4[1] === "@" ? w : C}); 50 | } else 51 | u2.push({type: 6, index: h2}); 52 | } 53 | for (const e3 of t3) 54 | l2.removeAttribute(e3); 55 | } 56 | if (g.test(l2.tagName)) { 57 | const t3 = l2.textContent.split(n), e3 = t3.length - 1; 58 | if (e3 > 0) { 59 | l2.textContent = i ? i.emptyScript : ""; 60 | for (let i2 = 0; i2 < e3; i2++) 61 | l2.append(t3[i2], r()), x.nextNode(), u2.push({type: 2, index: ++h2}); 62 | l2.append(t3[e3], r()); 63 | } 64 | } 65 | } else if (l2.nodeType === 8) 66 | if (l2.data === o) 67 | u2.push({type: 2, index: h2}); 68 | else { 69 | let t3 = -1; 70 | for (; (t3 = l2.data.indexOf(n, t3 + 1)) !== -1; ) 71 | u2.push({type: 7, index: h2}), t3 += n.length - 1; 72 | } 73 | h2++; 74 | } 75 | } 76 | static createElement(t2, e2) { 77 | const i2 = h.createElement("template"); 78 | return i2.innerHTML = t2, i2; 79 | } 80 | } 81 | function E(t2, e2, i2 = t2, s2) { 82 | var n2, o2, l2, h2; 83 | if (e2 === f) 84 | return e2; 85 | let $2 = s2 !== void 0 ? (n2 = i2._$Cl) === null || n2 === void 0 ? void 0 : n2[s2] : i2._$Cu; 86 | const a2 = d(e2) ? void 0 : e2._$litDirective$; 87 | return ($2 == null ? void 0 : $2.constructor) !== a2 && ((o2 = $2 == null ? void 0 : $2._$AO) === null || o2 === void 0 || o2.call($2, false), a2 === void 0 ? $2 = void 0 : ($2 = new a2(t2), $2._$AT(t2, i2, s2)), s2 !== void 0 ? ((l2 = (h2 = i2)._$Cl) !== null && l2 !== void 0 ? l2 : h2._$Cl = [])[s2] = $2 : i2._$Cu = $2), $2 !== void 0 && (e2 = E(t2, $2._$AS(t2, e2.values), $2, s2)), e2; 88 | } 89 | class b { 90 | constructor(t2, e2) { 91 | this.v = [], this._$AN = void 0, this._$AD = t2, this._$AM = e2; 92 | } 93 | get parentNode() { 94 | return this._$AM.parentNode; 95 | } 96 | get _$AU() { 97 | return this._$AM._$AU; 98 | } 99 | p(t2) { 100 | var e2; 101 | const {el: {content: i2}, parts: s2} = this._$AD, n2 = ((e2 = t2 == null ? void 0 : t2.creationScope) !== null && e2 !== void 0 ? e2 : h).importNode(i2, true); 102 | x.currentNode = n2; 103 | let o2 = x.nextNode(), l2 = 0, $2 = 0, a2 = s2[0]; 104 | for (; a2 !== void 0; ) { 105 | if (l2 === a2.index) { 106 | let e3; 107 | a2.type === 2 ? e3 = new T(o2, o2.nextSibling, this, t2) : a2.type === 1 ? e3 = new a2.ctor(o2, a2.name, a2.strings, this, t2) : a2.type === 6 && (e3 = new I(o2, this, t2)), this.v.push(e3), a2 = s2[++$2]; 108 | } 109 | l2 !== (a2 == null ? void 0 : a2.index) && (o2 = x.nextNode(), l2++); 110 | } 111 | return n2; 112 | } 113 | m(t2) { 114 | let e2 = 0; 115 | for (const i2 of this.v) 116 | i2 !== void 0 && (i2.strings !== void 0 ? (i2._$AI(t2, i2, e2), e2 += i2.strings.length - 2) : i2._$AI(t2[e2])), e2++; 117 | } 118 | } 119 | class T { 120 | constructor(t2, e2, i2, s2) { 121 | var n2; 122 | this.type = 2, this._$AH = y, this._$AN = void 0, this._$AA = t2, this._$AB = e2, this._$AM = i2, this.options = s2, this._$Cg = (n2 = s2 == null ? void 0 : s2.isConnected) === null || n2 === void 0 || n2; 123 | } 124 | get _$AU() { 125 | var t2, e2; 126 | return (e2 = (t2 = this._$AM) === null || t2 === void 0 ? void 0 : t2._$AU) !== null && e2 !== void 0 ? e2 : this._$Cg; 127 | } 128 | get parentNode() { 129 | let t2 = this._$AA.parentNode; 130 | const e2 = this._$AM; 131 | return e2 !== void 0 && t2.nodeType === 11 && (t2 = e2.parentNode), t2; 132 | } 133 | get startNode() { 134 | return this._$AA; 135 | } 136 | get endNode() { 137 | return this._$AB; 138 | } 139 | _$AI(t2, e2 = this) { 140 | t2 = E(this, t2, e2), d(t2) ? t2 === y || t2 == null || t2 === "" ? (this._$AH !== y && this._$AR(), this._$AH = y) : t2 !== this._$AH && t2 !== f && this.$(t2) : t2._$litType$ !== void 0 ? this.T(t2) : t2.nodeType !== void 0 ? this.S(t2) : ((t3) => { 141 | var e3; 142 | return $(t3) || typeof ((e3 = t3) === null || e3 === void 0 ? void 0 : e3[Symbol.iterator]) == "function"; 143 | })(t2) ? this.M(t2) : this.$(t2); 144 | } 145 | A(t2, e2 = this._$AB) { 146 | return this._$AA.parentNode.insertBefore(t2, e2); 147 | } 148 | S(t2) { 149 | this._$AH !== t2 && (this._$AR(), this._$AH = this.A(t2)); 150 | } 151 | $(t2) { 152 | this._$AH !== y && d(this._$AH) ? this._$AA.nextSibling.data = t2 : this.S(h.createTextNode(t2)), this._$AH = t2; 153 | } 154 | T(t2) { 155 | var e2; 156 | const {values: i2, _$litType$: s2} = t2, n2 = typeof s2 == "number" ? this._$AC(t2) : (s2.el === void 0 && (s2.el = N.createElement(s2.h, this.options)), s2); 157 | if (((e2 = this._$AH) === null || e2 === void 0 ? void 0 : e2._$AD) === n2) 158 | this._$AH.m(i2); 159 | else { 160 | const t3 = new b(n2, this), e3 = t3.p(this.options); 161 | t3.m(i2), this.S(e3), this._$AH = t3; 162 | } 163 | } 164 | _$AC(t2) { 165 | let e2 = H.get(t2.strings); 166 | return e2 === void 0 && H.set(t2.strings, e2 = new N(t2)), e2; 167 | } 168 | M(t2) { 169 | $(this._$AH) || (this._$AH = [], this._$AR()); 170 | const e2 = this._$AH; 171 | let i2, s2 = 0; 172 | for (const n2 of t2) 173 | s2 === e2.length ? e2.push(i2 = new T(this.A(r()), this.A(r()), this, this.options)) : i2 = e2[s2], i2._$AI(n2), s2++; 174 | s2 < e2.length && (this._$AR(i2 && i2._$AB.nextSibling, s2), e2.length = s2); 175 | } 176 | _$AR(t2 = this._$AA.nextSibling, e2) { 177 | var i2; 178 | for ((i2 = this._$AP) === null || i2 === void 0 || i2.call(this, false, true, e2); t2 && t2 !== this._$AB; ) { 179 | const e3 = t2.nextSibling; 180 | t2.remove(), t2 = e3; 181 | } 182 | } 183 | setConnected(t2) { 184 | var e2; 185 | this._$AM === void 0 && (this._$Cg = t2, (e2 = this._$AP) === null || e2 === void 0 || e2.call(this, t2)); 186 | } 187 | } 188 | class C { 189 | constructor(t2, e2, i2, s2, n2) { 190 | this.type = 1, this._$AH = y, this._$AN = void 0, this.element = t2, this.name = e2, this._$AM = s2, this.options = n2, i2.length > 2 || i2[0] !== "" || i2[1] !== "" ? (this._$AH = Array(i2.length - 1).fill(new String()), this.strings = i2) : this._$AH = y; 191 | } 192 | get tagName() { 193 | return this.element.tagName; 194 | } 195 | get _$AU() { 196 | return this._$AM._$AU; 197 | } 198 | _$AI(t2, e2 = this, i2, s2) { 199 | const n2 = this.strings; 200 | let o2 = false; 201 | if (n2 === void 0) 202 | t2 = E(this, t2, e2, 0), o2 = !d(t2) || t2 !== this._$AH && t2 !== f, o2 && (this._$AH = t2); 203 | else { 204 | const s3 = t2; 205 | let l2, h2; 206 | for (t2 = n2[0], l2 = 0; l2 < n2.length - 1; l2++) 207 | h2 = E(this, s3[i2 + l2], e2, l2), h2 === f && (h2 = this._$AH[l2]), o2 || (o2 = !d(h2) || h2 !== this._$AH[l2]), h2 === y ? t2 = y : t2 !== y && (t2 += (h2 != null ? h2 : "") + n2[l2 + 1]), this._$AH[l2] = h2; 208 | } 209 | o2 && !s2 && this.k(t2); 210 | } 211 | k(t2) { 212 | t2 === y ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 != null ? t2 : ""); 213 | } 214 | } 215 | class M extends C { 216 | constructor() { 217 | super(...arguments), this.type = 3; 218 | } 219 | k(t2) { 220 | this.element[this.name] = t2 === y ? void 0 : t2; 221 | } 222 | } 223 | class S extends C { 224 | constructor() { 225 | super(...arguments), this.type = 4; 226 | } 227 | k(t2) { 228 | t2 && t2 !== y ? this.element.setAttribute(this.name, "") : this.element.removeAttribute(this.name); 229 | } 230 | } 231 | class w extends C { 232 | constructor(t2, e2, i2, s2, n2) { 233 | super(t2, e2, i2, s2, n2), this.type = 5; 234 | } 235 | _$AI(t2, e2 = this) { 236 | var i2; 237 | if ((t2 = (i2 = E(this, t2, e2, 0)) !== null && i2 !== void 0 ? i2 : y) === f) 238 | return; 239 | const s2 = this._$AH, n2 = t2 === y && s2 !== y || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, o2 = t2 !== y && (s2 === y || n2); 240 | n2 && this.element.removeEventListener(this.name, this, s2), o2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2; 241 | } 242 | handleEvent(t2) { 243 | var e2, i2; 244 | typeof this._$AH == "function" ? this._$AH.call((i2 = (e2 = this.options) === null || e2 === void 0 ? void 0 : e2.host) !== null && i2 !== void 0 ? i2 : this.element, t2) : this._$AH.handleEvent(t2); 245 | } 246 | } 247 | class I { 248 | constructor(t2, e2, i2) { 249 | this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = e2, this.options = i2; 250 | } 251 | get _$AU() { 252 | return this._$AM._$AU; 253 | } 254 | _$AI(t2) { 255 | E(this, t2); 256 | } 257 | } 258 | (t = globalThis.litHtmlPolyfillSupport) === null || t === void 0 || t.call(globalThis, N, T), ((e = globalThis.litHtmlVersions) !== null && e !== void 0 ? e : globalThis.litHtmlVersions = []).push("2.0.0"); 259 | export {m as html, A as renderer}; 260 | export default null; 261 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app', { 9 | render () { 10 | return html`
11 | 16 |
17 |
` 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/contents/bar.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('bar-contents', { 4 | props: { 5 | dummy: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Bar contents with param ${this.props.dummy}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/contents/baz.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('baz-contents', { 4 | props: { 5 | dummy: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Baz contents with param ${this.props.dummy}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/contents/foo.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('foo-contents', { 4 | render () { 5 | return html`
Foo contents
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Child outlets 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Child outlets

13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | import './contents/foo.js' 4 | 5 | export const router = createRouter([ 6 | { 7 | path: '/test/e2e/routed-app-child-outlets', 8 | component: 'home-page' 9 | }, 10 | { 11 | path: '/test/e2e/routed-app-child-outlets/one', 12 | component: 'page-one', 13 | outlets: { 14 | '#foo': () => 'foo-contents', 15 | '#title': () => 'Page one title outlet' 16 | } 17 | }, 18 | { 19 | path: '/test/e2e/routed-app-child-outlets/two', 20 | component: 'page-two', 21 | outlets: { 22 | '#bar': (context, { dummy }) => import('./contents/bar.js').then(() => ``), 23 | '.baz': (context) => import('./contents/baz.js').then(() => ``), 24 | '#title': () => 'Page two title outlet' 25 | } 26 | } 27 | ], '#router-outlet') 28 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
8 |
` 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/routed-app-child-outlets/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |
9 |
10 |

Go back to home page

11 |
12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | import './views/page-error.js' 8 | import './views/page-not-found.js' 9 | 10 | createComponent('mock-routed-app', { 11 | render () { 12 | return html`
13 | 20 |
21 |
` 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Route errors 6 | 7 | 8 | 9 | 10 |
11 |

Route errors

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-error-handler', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-error-handler/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-error-handler/two', component: 'page-two' }, 7 | { path: '/test/e2e/routed-app-error-handler/error', action: () => Promise.reject(new Error('Action error')) } 8 | ], '#router-outlet', { 9 | errorHandler: (error, context) => { 10 | return error.status === 404 ? `` : `` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
An error occurred: ${this.props.message}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Page not found: ${this.props.path}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
Welcome to page one!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-error-handler/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-error.js' 7 | import './views/page-not-found.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 19 |
20 |
` 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Guards 6 | 7 | 8 | 9 | 10 |
11 |

Guards

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-guards-resolve-route', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-guards-resolve-route/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-guards-resolve-route/cancelled', component: 'dummy-page' }, 7 | { path: '/test/e2e/routed-app-guards-resolve-route/error', component: 'dummy-page' }, 8 | { path: '/test/e2e/routed-app-guards-resolve-route/two', component: 'dummy-page' } 9 | ], '#router-outlet', { 10 | resolveRoute (context) { 11 | if (context.location.pathname === '/test/e2e/routed-app-guards-resolve-route/cancelled') { 12 | return false 13 | } 14 | if (context.location.pathname === '/test/e2e/routed-app-guards-resolve-route/error') { 15 | return new Error('Action error') 16 | } 17 | if (context.location.pathname === '/test/e2e/routed-app-guards-resolve-route/two') { 18 | return { redirect: '/test/e2e/routed-app-guards-resolve-route/one' } 19 | } 20 | }, 21 | errorHandler: (error, context) => { 22 | return error.status === 404 ? `` : `` 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
An error occurred: ${this.props.message}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Page not found: ${this.props.path}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards-resolve-route/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
Welcome to page one!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-error.js' 7 | import './views/page-not-found.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 18 |
19 |
` 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Guards 6 | 7 | 8 | 9 | 10 |
11 |

Guards

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-guards', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-guards/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-guards/cancelled', action: () => false }, 7 | { path: '/test/e2e/routed-app-guards/error', action: () => new Error('Action error') } 8 | ], '#router-outlet', { 9 | errorHandler: (error, context) => { 10 | return error.status === 404 ? `` : `` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
An error occurred: ${this.props.message}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Page not found: ${this.props.path}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-guards/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
Welcome to page one!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | import './views/page-error.js' 8 | import './views/page-not-found.js' 9 | 10 | createComponent('mock-routed-app', { 11 | render () { 12 | return html`
13 | 20 |
21 |
` 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Route errors 6 | 7 | 8 | 9 | 10 |
11 |

Route errors

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { path: '/one', component: 'page-one' }, 6 | { path: '/two', component: 'page-two' }, 7 | { path: '/error', action: () => Promise.reject(new Error('Action error')) } 8 | ], '#router-outlet', { 9 | mode: 'hash', 10 | errorHandler: (error, context) => { 11 | return error.status === 404 ? `` : `` 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

An error occurred: ${this.props.message}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

Page not found: ${this.props.path}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-error-handler/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-error.js' 7 | import './views/page-not-found.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 19 |
20 |
` 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Guards 6 | 7 | 8 | 9 | 10 |
11 |

Guards

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { path: '/one', component: 'page-one' }, 6 | { path: '/cancelled', component: 'dummy-page' }, 7 | { path: '/error', component: 'dummy-page' }, 8 | { path: '/two', component: 'dummy-page' } 9 | ], '#router-outlet', { 10 | mode: 'hash', 11 | resolveRoute (context) { 12 | if (context.location.pathname === '/cancelled') { 13 | return false 14 | } 15 | if (context.location.pathname === '/error') { 16 | return new Error('Action error') 17 | } 18 | if (context.location.pathname === '/two') { 19 | return { redirect: '/one' } 20 | } 21 | }, 22 | errorHandler: (error, context) => { 23 | return error.status === 404 ? `` : `` 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
An error occurred: ${this.props.message}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Page not found: ${this.props.path}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards-resolve-route/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
Welcome to page one!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-error.js' 7 | import './views/page-not-found.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 18 |
19 |
` 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hash guards 6 | 7 | 8 | 9 | 10 |
11 |

Hash guards

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { path: '/one', component: 'page-one' }, 6 | { path: '/cancelled', action: () => false }, 7 | { path: '/error', action: () => new Error('Action error') } 8 | ], '#router-outlet', { 9 | mode: 'hash', 10 | errorHandler: (error, context) => { 11 | return error.status === 404 ? `` : `` 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

An error occurred: ${this.props.message}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

Page not found: ${this.props.path}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-guards/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/app.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../util/component.js' 2 | import { store } from './store/store.js' 3 | import { router } from './router/router.js' 4 | 5 | import './components/nav.js' 6 | 7 | // add the custom login handler to the router 8 | router.setOptions({ 9 | ...router.options, 10 | resolveRoute (context, params) { 11 | if (context.location.pathname !== '/login' && !store.state.userLoggedIn) { 12 | return { redirect: '/login' } 13 | } 14 | 15 | if (context.location.pathname === '/' && store.state.userLoggedIn) { 16 | return { redirect: '/home' } 17 | } 18 | 19 | if (context.location.pathname === '/logout') { 20 | store.dispatch('setUserLoggedIn', false) 21 | return { redirect: '/' } 22 | } 23 | }, 24 | errorHandler (error, context) { 25 | return error.status === 404 26 | ? import('./views/page-not-found.js').then(() => html``) 27 | : import('./views/page-error.js').then(() => html``) 28 | } 29 | }) 30 | 31 | createComponent('mock-routed-app-login', { 32 | store, 33 | render () { 34 | return html`
35 | 44 | 45 |
46 |
` 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/components/nav.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { store } from '../store/store.js' 3 | 4 | createComponent('top-nav', { 5 | store, 6 | render () { 7 | if (this.store.state.userLoggedIn) { 8 | return html`` 15 | } else { 16 | return html`` 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hash router login 6 | 7 | 8 | 9 | 10 |
11 |

Login app

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/router/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../../src/router.mjs' 2 | import { routes } from './routes.js' 3 | 4 | export const router = createRouter(routes, '#router-outlet', { mode: 'hash' }) 5 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/router/routes.js: -------------------------------------------------------------------------------- 1 | export const routes = [ 2 | { 3 | path: '', 4 | action () { 5 | return import('../views/home-page.js').then(() => ``) 6 | } 7 | }, 8 | { 9 | path: '/home', 10 | action () { 11 | return import('../views/loggedin-page.js').then(() => ``) 12 | } 13 | }, 14 | { 15 | path: '/login', 16 | action () { 17 | return import('../views/login-page.js').then(() => ``) 18 | } 19 | }, 20 | { 21 | path: '/one', 22 | action () { 23 | return import('../views/page-one.js').then(() => ``) 24 | } 25 | }, 26 | { 27 | path: '/two', 28 | action () { 29 | return import('../views/page-two.js').then(() => ``) 30 | } 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from '../../util/component.js' 2 | import { router } from '../router/router.js' 3 | 4 | export const store = createStore('routed-app-hash-login', { 5 | initialState: { 6 | userLoggedIn: false, 7 | user: null 8 | }, 9 | actions: { 10 | setUserLoggedIn (context, payload) { 11 | context.commit('setUserLoggedIn', !!payload) 12 | context.commit('setUser', payload) 13 | router.push('/', { user: 'test' }) 14 | } 15 | }, 16 | mutations: { 17 | setUserLoggedIn (state, payload) { 18 | state.userLoggedIn = payload 19 | return state 20 | }, 21 | setUser (state, payload) { 22 | state.user = payload 23 | return state 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/loggedin-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { store } from '../store/store.js' 3 | 4 | createComponent('loggedin-page', { 5 | store, 6 | render () { 7 | return html`
You are logged in ${this.store.state.user.email}
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/login-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { store } from '../store/store.js' 3 | 4 | createComponent('login-page', { 5 | store, 6 | login (e) { 7 | e.preventDefault() 8 | const fd = new FormData(e.target) 9 | const data = {} 10 | for (const pair of fd.entries()) { 11 | data[pair[0]] = pair[1] 12 | } 13 | this.store.dispatch('setUserLoggedIn', e.detail.data) 14 | }, 15 | render () { 16 | return html`
17 |
18 | 19 | 20 | 21 | 22 | 23 |
24 |
` 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/page-error.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-error', { 4 | props: { 5 | message: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

Page error: ${this.props.message}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

Page not found: ${this.props.path}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | goBack () { 5 | this.router.goBack() 6 | }, 7 | render () { 8 | return html`
9 | Welcome to page one! 10 | 11 |
` 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-login/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router/router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | import './views/user-page.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 20 |
21 |
` 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | URL params 6 | 7 | 8 | 9 | 10 |
11 |

URL params

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-hash-params', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-hash-params/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-hash-params/two', component: 'page-two' }, 7 | { path: '/test/e2e/routed-app-hash-params/user/:personName', action: (context) => `` }, 8 | { 9 | path: '/test/e2e/routed-app-hash-params/user/:personName/task', 10 | // eslint-disable-next-line camelcase 11 | action (context, { personName, access_token, code }) { 12 | // eslint-disable-next-line camelcase 13 | return `` 14 | } 15 | } 16 | ], '#router-outlet') 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-params/views/user-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html, nothing } from '../../util/component.js' 2 | 3 | createComponent('user-page', { 4 | props: { 5 | personName: { 6 | type: String 7 | }, 8 | task: { 9 | type: String 10 | } 11 | }, 12 | render () { 13 | return html`
14 |

Welcome ${this.props.personName ? this.props.personName : nothing} to the user page!

15 | ${this.props.task ? html`

Task ${this.props.task}

` : nothing} 16 |
` 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-two.js' 6 | 7 | createComponent('mock-routed-app', { 8 | render () { 9 | return html`
10 | 15 |
16 |
` 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redirects 6 | 7 | 8 | 9 | 10 |
11 |

Redirects

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { 6 | path: '/one', 7 | action () { 8 | return { redirect: '/two' } 9 | } 10 | }, 11 | { path: '/two', component: 'page-two' } 12 | ], '#router-outlet', { mode: 'hash' }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-redirects/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | import './views/user-page.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 19 |
20 |
` 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | URL params 6 | 7 | 8 | 9 | 10 |
11 |

Hash URL params

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { path: '/one', component: 'page-one' }, 6 | { path: '/two', component: 'page-two' }, 7 | { path: '/user/:personName', action: (context) => `` }, 8 | { path: '/user/:personName/task/:task', action: (context, { personName, task }) => `` } 9 | ], '#router-outlet', { mode: 'hash' }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash-url-params/views/user-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html, nothing } from '../../util/component.js' 2 | 3 | createComponent('user-page', { 4 | props: { 5 | personName: { 6 | type: String 7 | }, 8 | task: { 9 | type: String 10 | } 11 | }, 12 | render () { 13 | return html`
14 |

Welcome ${this.props.personName ? this.props.personName : nothing} to the user page!

15 |

${this.props.task ? html`Task ${this.props.task}` : nothing}

16 |
` 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app', { 9 | render () { 10 | return html`
11 | 20 | 26 |
27 |
` 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hash routed app 6 | 7 | 8 | 9 | 10 |
11 |

Hash routed app

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '', component: 'home-page' }, 5 | { path: '/one', component: 'page-one' }, 6 | { path: '/two', action: (context, { test }) => `` } 7 | ], '#router-outlet', { mode: 'hash' }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-hash/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | props: { 6 | test: { 7 | type: String 8 | } 9 | }, 10 | render () { 11 | return html`
12 |

Welcome to page two!

13 |

Test: ${this.props.test}

14 |

15 |
` 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | createComponent('mock-routed-app', { 5 | render () { 6 | return html`
7 | 12 |
13 |
` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lazy views 6 | 7 | 8 | 9 | 10 |
11 |

Lazy views

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { 5 | path: '/test/e2e/routed-app-lazy-views', 6 | action (context) { 7 | return import('./views/home-page.js') 8 | .then(() => `home-page`) 9 | } 10 | }, 11 | { path: '/test/e2e/routed-app-lazy-views/one', action: () => import('./views/page-one.esm.js').then(() => 'page-one') }, 12 | { path: '/test/e2e/routed-app-lazy-views/two', action: () => import('./views/page-two.js').then(() => 'page-two') } 13 | ], '#router-outlet') 14 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/views/page-one.esm.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-lazy-views/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponentWithStore } from '../util/component.js' 2 | import { getRouter } from './router.js' 3 | import { getStore } from './store.js' 4 | import { createTopNavComponent } from './components/nav.js' 5 | import './views/index.js' 6 | 7 | function isLoginPath (path) { 8 | return path === '/test/e2e/routed-app-login/login' || path === '/test/e2e/routed-app-login/login/' 9 | } 10 | 11 | function isRootPath (path) { 12 | return path === '/test/e2e/routed-app-login' || path === '/test/e2e/routed-app-login/' 13 | } 14 | 15 | const store = getStore() 16 | const router = getRouter(resolveRoute, errorHandler) 17 | 18 | function resolveRoute (context, params) { 19 | if (!isLoginPath(context.location.pathname) && !store.state.userLoggedIn) { 20 | return { redirect: '/test/e2e/routed-app-login/login' } 21 | } 22 | 23 | if (isRootPath(context.location.pathname) && store.state.userLoggedIn) { 24 | return { redirect: '/test/e2e/routed-app-login/home' } 25 | } 26 | 27 | if (context.location.pathname === '/test/e2e/routed-app-login/logout') { 28 | store.dispatch('setUserLoggedIn', false) 29 | return { redirect: '/test/e2e/routed-app-login' } 30 | } 31 | } 32 | 33 | function errorHandler (error, context) { 34 | return error.status === 404 ? `` : `` 35 | } 36 | 37 | createTopNavComponent(router) 38 | 39 | createComponentWithStore( 40 | 'mock-routed-app-login', 41 | { 42 | render () { 43 | return html`
44 | 53 | 54 |
55 |
` 56 | } 57 | }, 58 | store 59 | ) 60 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/components/nav.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithStore, html } from '../../util/component.js' 2 | import { getStore } from '../store.js' 3 | 4 | export function createTopNavComponent (router) { 5 | createComponentWithStore( 6 | 'top-nav', 7 | { 8 | render () { 9 | if (this.store.state.userLoggedIn) { 10 | return html`` 17 | } else { 18 | return html`` 24 | } 25 | } 26 | }, 27 | getStore() 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login 6 | 7 | 8 | 9 | 10 |
11 |

Login app

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | let router = null 4 | 5 | export function getRouter (resolveRoute, errorHandler) { 6 | if (!router && resolveRoute && errorHandler) { 7 | router = createRouter([ 8 | { path: '/test/e2e/routed-app-login', component: 'home-page' }, 9 | { path: '/test/e2e/routed-app-login/home', component: 'loggedin-page' }, 10 | { path: '/test/e2e/routed-app-login/login', component: 'login-page' }, 11 | { path: '/test/e2e/routed-app-login/one', component: 'page-one' }, 12 | { path: '/test/e2e/routed-app-login/two', component: 'page-two' } 13 | ], '#router-outlet', { resolveRoute, errorHandler }) 14 | } 15 | return router 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from '../util/component.js' 2 | import { getRouter } from '../../../src/router.mjs' 3 | 4 | let store = null 5 | 6 | export function getStore () { 7 | if (!store) { 8 | store = createStore('routed-app-login', { 9 | initialState: { 10 | userLoggedIn: false, 11 | user: null 12 | }, 13 | actions: { 14 | setUserLoggedIn (context, payload) { 15 | context.commit('setUserLoggedIn', !!payload) 16 | context.commit('setUser', payload) 17 | getRouter().push('/test/e2e/routed-app-login') 18 | } 19 | }, 20 | mutations: { 21 | setUserLoggedIn (state, payload) { 22 | state.userLoggedIn = payload 23 | return state 24 | }, 25 | setUser (state, payload) { 26 | state.user = payload 27 | return state 28 | } 29 | } 30 | }) 31 | } 32 | return store 33 | } 34 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/index.js: -------------------------------------------------------------------------------- 1 | import './home-page.js' 2 | import './login-page.js' 3 | import './loggedin-page.js' 4 | import './page-not-found.js' 5 | import './page-one.js' 6 | import './page-two.js' 7 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/loggedin-page.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithStore, html } from '../../util/component.js' 2 | import { getStore } from '../store.js' 3 | 4 | createComponentWithStore( 5 | 'loggedin-page', 6 | { 7 | render () { 8 | return html`
You are logged in ${this.store.state.user.email}
` 9 | } 10 | }, 11 | getStore() 12 | ) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/login-page.js: -------------------------------------------------------------------------------- 1 | import { createComponentWithStore, html } from '../../util/component.js' 2 | import { getStore } from '../store.js' 3 | 4 | createComponentWithStore( 5 | 'login-page', 6 | { 7 | login (e) { 8 | e.preventDefault() 9 | const fd = new FormData(e.target) 10 | const data = {} 11 | for (const pair of fd.entries()) { 12 | data[pair[0]] = pair[1] 13 | } 14 | this.store.dispatch('setUserLoggedIn', data) 15 | }, 16 | render () { 17 | return html`
18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
` 26 | } 27 | }, 28 | getStore() 29 | ) 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/page-not-found.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-not-found', { 4 | props: { 5 | path: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
11 |

Page not found: ${this.props.path}

12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { getRouter } from '../router.js' 3 | 4 | createComponent('page-one', { 5 | goBack () { 6 | getRouter().goBack() 7 | }, 8 | render () { 9 | return html`
10 | Welcome to page one! 11 | 12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-login/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { getRouter } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app-no-auto-start-path', { 9 | render () { 10 | return html`
11 | 20 | 26 |
27 |
` 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | No auto-start (start with path) 6 | 7 | 8 | 9 | 10 |
11 |

No auto-start (start with path)

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/main.js: -------------------------------------------------------------------------------- 1 | import { router } from './router.js' 2 | 3 | import('./app.js').then(() => router.start('/test/e2e/routed-app-no-auto-start-path/two')) 4 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-no-auto-start-path', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-no-auto-start-path/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-no-auto-start-path/two', component: 'page-two' } 7 | ], '#router-outlet', { autoStart: false }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start-path/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app-no-auto-start', { 9 | render () { 10 | return html`
11 | 20 | 26 |
27 |
` 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | No auto-start 6 | 7 | 8 | 9 | 10 |
11 |

No auto-start

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/main.js: -------------------------------------------------------------------------------- 1 | import { router } from './router.js' 2 | 3 | import('./app.js').then(() => router.start()) 4 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-no-auto-start', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-no-auto-start/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-no-auto-start/two', component: 'page-two' } 7 | ], '#router-outlet', { autoStart: false }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-no-auto-start/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-two.js' 6 | 7 | createComponent('mock-routed-app', { 8 | render () { 9 | return html`
10 | 15 |
16 |
` 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redirects 6 | 7 | 8 | 9 | 10 |
11 |

Redirects

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-redirects', component: 'home-page' }, 5 | { 6 | path: '/test/e2e/routed-app-redirects/one', 7 | action () { 8 | return { redirect: '/test/e2e/routed-app-redirects/two' } 9 | } 10 | }, 11 | { path: '/test/e2e/routed-app-redirects/two', component: 'page-two' } 12 | ], '#router-outlet') 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-redirects/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app', { 9 | render () { 10 | return html`
11 | 16 |
17 |
` 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/contents/bar.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('bar-contents', { 4 | props: { 5 | dummy: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Bar contents with param ${this.props.dummy}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/contents/baz.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('baz-contents', { 4 | props: { 5 | dummy: { 6 | type: String 7 | } 8 | }, 9 | render () { 10 | return html`
Baz contents with param ${this.props.dummy}
` 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/contents/foo.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('foo-contents', { 4 | render () { 5 | return html`
Foo contents
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sticky outlets 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Sticky outlets

13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | import './contents/foo.js' 4 | 5 | export const router = createRouter([ 6 | { 7 | path: '/test/e2e/routed-app-sticky-outlets', 8 | component: 'home-page', 9 | outlets: { 10 | '#title': () => 'Home title outlet' 11 | } 12 | }, 13 | { 14 | path: '/test/e2e/routed-app-sticky-outlets/one', 15 | component: 'page-one', 16 | outlets: { 17 | '#foo': () => 'foo-contents' 18 | } 19 | }, 20 | { 21 | path: '/test/e2e/routed-app-sticky-outlets/two', 22 | component: 'page-two', 23 | outlets: { 24 | '#bar': (context, { dummy }) => import('./contents/bar.js').then(() => ``), 25 | '.baz': (context) => import('./contents/baz.js').then(() => ``), 26 | '#title': () => 'Page two title outlet' 27 | } 28 | } 29 | ], '#router-outlet') 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
8 |
` 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/routed-app-sticky-outlets/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |
9 |
10 |

Go back to home page

11 |
12 |
` 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | import './views/user-page.js' 8 | 9 | createComponent('mock-routed-app', { 10 | render () { 11 | return html`
12 | 19 |
20 |
` 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | URL params 6 | 7 | 8 | 9 | 10 |
11 |

URL params

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app-url-params', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app-url-params/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app-url-params/two', component: 'page-two' }, 7 | { path: '/test/e2e/routed-app-url-params/user/:personName', action: (context) => `` }, 8 | { path: '/test/e2e/routed-app-url-params/user/:personName/task/:task', action: (context, { personName, task }) => `` } 9 | ], '#router-outlet') 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
6 | Welcome to page one! 7 |
` 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/routed-app-url-params/views/user-page.js: -------------------------------------------------------------------------------- 1 | import { createComponent, html, nothing } from '../../util/component.js' 2 | 3 | createComponent('user-page', { 4 | props: { 5 | personName: { 6 | type: String 7 | }, 8 | task: { 9 | type: String 10 | } 11 | }, 12 | render () { 13 | return html`
14 |

Welcome ${this.props.personName ? this.props.personName : nothing} to the user page!

15 |

${this.props.task ? html`Task ${this.props.task}` : nothing}

16 |
` 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/e2e/routed-app/app.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../util/component.js' 2 | import { router } from './router.js' 3 | 4 | import './views/home-page.js' 5 | import './views/page-one.js' 6 | import './views/page-two.js' 7 | 8 | createComponent('mock-routed-app', { 9 | render () { 10 | return html`
11 | 20 | 26 |
27 |
` 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/e2e/routed-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Routed app 6 | 7 | 8 | 9 | 10 |
11 |

Routed app

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/routed-app/main.js: -------------------------------------------------------------------------------- 1 | import './app.js' 2 | -------------------------------------------------------------------------------- /test/e2e/routed-app/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../../src/router.mjs' 2 | 3 | export const router = createRouter([ 4 | { path: '/test/e2e/routed-app', component: 'home-page' }, 5 | { path: '/test/e2e/routed-app/one', component: 'page-one' }, 6 | { path: '/test/e2e/routed-app/two', component: 'page-two' } 7 | ], '#router-outlet') 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app/views/home-page.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('home-page', { 4 | render () { 5 | return html`
Welcome to the home page!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app/views/page-one.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | 3 | createComponent('page-one', { 4 | render () { 5 | return html`
Welcome to page one!
` 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/routed-app/views/page-two.js: -------------------------------------------------------------------------------- 1 | import { html, createComponent } from '../../util/component.js' 2 | import { router } from '../router.js' 3 | 4 | createComponent('page-two', { 5 | render () { 6 | return html`
7 |

Welcome to page two!

8 |

9 |
` 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/util/component.js: -------------------------------------------------------------------------------- 1 | import { renderer, html } from 'https://cdn.skypack.dev/@ficusjs/renderers@5/lit-html' 2 | import { createComponent as componentCreator, createStore, withStore } from 'https://cdn.skypack.dev/ficusjs@5' 3 | import { navigateTo } from './methods.js' 4 | 5 | function createComponent (tagName, options) { 6 | componentCreator(tagName, { ...options, renderer, navigateTo }) 7 | } 8 | 9 | function createComponentWithStore (tagName, options, store) { 10 | componentCreator(tagName, withStore(store, { ...options, renderer, navigateTo })) 11 | } 12 | 13 | const nothing = '' 14 | 15 | export { createComponent, createComponentWithStore, createStore, html, nothing } 16 | -------------------------------------------------------------------------------- /test/e2e/util/detect.js: -------------------------------------------------------------------------------- 1 | export function detect () { 2 | return window.URLSearchParams && Object.fromEntries 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/util/emit.js: -------------------------------------------------------------------------------- 1 | /* global Event */ 2 | export function emit (eventName) { 3 | console.log(`emitting event on document: ${eventName}`) 4 | const event = new Event(eventName) 5 | document.dispatchEvent(event) 6 | } 7 | -------------------------------------------------------------------------------- /test/e2e/util/find-parent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to find the nearest parent element 3 | * @param {Node} el The starting element 4 | * @param {Function} tester The function to execute for each element 5 | * @returns {Node|null} The parent element or null if not found 6 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Node} for Node reference 7 | */ 8 | export function findParent (el, tester) { 9 | if (tester(el)) { 10 | return el 11 | } 12 | 13 | while (el.parentNode) { 14 | el = el.parentNode 15 | if (tester(el)) { 16 | return el 17 | } 18 | } 19 | return null 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/util/methods.js: -------------------------------------------------------------------------------- 1 | export function navigateTo (e, router) { 2 | let target = e.target 3 | const path = target.dataset.href 4 | router.push(path) 5 | .then(() => { 6 | const currentPath = router.location.pathname + router.location.search 7 | const navItems = [...document.querySelectorAll('nav button')] 8 | navItems.forEach(b => { 9 | b.classList.remove('active') 10 | if (currentPath === b.dataset.href) b.classList.add('active') 11 | }) 12 | }) 13 | .catch(e => console.error(`navigateTo router error`, e)) 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/util/wait-for.js: -------------------------------------------------------------------------------- 1 | export const waitFor = (test = () => true, timeoutInMilliseconds = 10000) => new Promise((resolve, reject) => { 2 | const check = () => { 3 | if (test()) { 4 | resolve() 5 | } else if ((timeoutInMilliseconds -= 100) < 0) { 6 | reject(new Error('Timed out waiting!')) 7 | } else { 8 | setTimeout(check, 100) 9 | } 10 | } 11 | setTimeout(check, 100) 12 | }) 13 | -------------------------------------------------------------------------------- /types/router.d.ts: -------------------------------------------------------------------------------- 1 | export type RouteOrOutletHTMLResult = string | HTMLElement 2 | 3 | export interface Outlets { 4 | [outletName: string]: () => RouteOrOutletHTMLResult | Promise 5 | } 6 | 7 | export interface QueryParams { 8 | [paramName: string]: string | string[] 9 | } 10 | 11 | export interface RouteLocation { 12 | host: string 13 | protocol: string 14 | href: string 15 | pathname: string 16 | search: string 17 | hash: string 18 | } 19 | 20 | export interface RouteContext { 21 | context?: RouterOptionsContext 22 | router: Router 23 | route: Route 24 | path: string 25 | params: QueryParams 26 | } 27 | 28 | type RedirectObject = { redirect: string } 29 | 30 | export type ActionResult = boolean | Error | RedirectObject | RouteOrOutletHTMLResult | Promise | { template: RouteOrOutletHTMLResult | Promise, outlets?: Outlets } 31 | 32 | export type ResolveRoute = (context: RouteContext, params: QueryParams) => ActionResult 33 | 34 | export type ErrorHandler = (error: Error & { status?: number }, context: RouteContext) => RouteOrOutletHTMLResult 35 | 36 | export type RouterOptionsContext = object 37 | 38 | export interface RouterOptions { 39 | mode: 'history' | 'hash' 40 | autoStart?: boolean 41 | changeHistoryState?: boolean 42 | warnOnMissingOutlets?: boolean 43 | context?: RouterOptionsContext 44 | resolveRoute?: ResolveRoute 45 | errorHandler?: ErrorHandler 46 | } 47 | 48 | export type Route = { 49 | path: string 50 | component: string 51 | outlets?: Outlets 52 | children?: Array 53 | } | { 54 | path: string 55 | action: (context: RouteContext, params: QueryParams) => ActionResult 56 | outlets?: Outlets 57 | children?: Array 58 | matcher?: (path: string) => QueryParams | string | undefined 59 | } 60 | 61 | export interface RouterLocation { 62 | host: string | undefined 63 | protocol: string | undefined 64 | pathname: string | undefined 65 | hash: string | undefined 66 | href: string | undefined 67 | search: string | undefined 68 | state: any 69 | } 70 | 71 | type Routes = Array 72 | 73 | declare class Router { 74 | constructor(routes: Routes, rootOutletSelector: string, options?: RouterOptions) 75 | push (location: string): Promise 76 | replace (location: string): Promise 77 | go (n: number): void 78 | goBack (): void 79 | goForward (): void 80 | start (location?: string | object): void 81 | setOptions(options: RouterOptions): void 82 | addRoutes(routes: Routes): void 83 | hasRoute(pathname: string): boolean 84 | get options(): RouterOptions 85 | get location(): RouterLocation 86 | } 87 | 88 | type GetRouterFunction = () => Router 89 | 90 | export declare function createRouter (routes: Routes, rootOutletSelector: string, options?: RouterOptions): Router 91 | 92 | export declare function getRouter (): Router 93 | 94 | export declare function addMatcherToRoute (route: Route): Route 95 | --------------------------------------------------------------------------------