├── .eslintrc.js ├── .github ├── stale.yml └── workflows │ ├── ci.yml │ └── update_dependencies.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .yarnclean ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DOCS.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── artifacts.json ├── bower.json ├── downstream_projects.json ├── jest.config.js ├── karma.conf.js ├── package.json ├── rollup.config.js ├── scripts ├── bower_release.js └── npm_angular_ui_router_release.js ├── src ├── angular.ts ├── directives │ ├── stateDirectives.ts │ └── viewDirective.ts ├── index.ts ├── injectables.ts ├── interface.ts ├── legacy │ ├── core-adapter.js │ ├── resolveService.ts │ └── stateEvents.ts ├── locationServices.ts ├── services.ts ├── stateFilters.ts ├── stateProvider.ts ├── statebuilders │ ├── onEnterExitRetain.ts │ └── views.ts ├── templateFactory.ts ├── urlRouterProvider.ts └── viewScroll.ts ├── test ├── angular │ ├── 1.2 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── 1.3 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── 1.4 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── 1.5 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── 1.6 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── 1.7 │ │ ├── angular-animate.js │ │ ├── angular-mocks.js │ │ └── angular.js │ ├── jest-angular.js │ └── update_all.sh ├── jest.init.ts ├── ng1StateBuilderSpec.ts ├── resolveSpec.ts ├── servicesSpec.ts ├── stateDirectivesSpec.ts ├── stateEventsSpec.ts ├── stateFiltersSpec.ts ├── stateSpec.ts ├── templateFactorySpec.ts ├── tsconfig.json ├── typescript │ ├── 3.9 │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── mktest.sh │ └── template │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json ├── urlMatcherFactorySpec.ts ├── urlRouterSpec.ts ├── util │ └── testUtilsNg1.ts ├── viewDirectiveSpec.ts ├── viewHookSpec.ts ├── viewScrollSpec.ts └── viewSpec.ts ├── tsconfig.docgen.json ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | env: { browser: true }, 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'prettier/@typescript-eslint'], 7 | rules: { 8 | '@typescript-eslint/no-explicit-any': 'off', 9 | '@typescript-eslint/explicit-module-boundary-types': 'off', 10 | 11 | '@typescript-eslint/ban-types': [ 12 | 'error', 13 | { 14 | types: { 15 | Function: false, 16 | }, 17 | }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 180 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - notstale 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: | 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. 15 | 16 | This does not mean that the issue is invalid. Valid issues 17 | may be reopened. 18 | 19 | Thank you for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | needs: [test] 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: true 15 | 16 | test: 17 | name: yarn ${{ matrix.yarncmd }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | yarncmd: ['test', 'test:downstream', 'docs'] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Configure Environment 25 | run: | 26 | git config --global user.email uirouter@github.actions 27 | git config --global user.name uirouter_github_actions 28 | - name: Install Dependencies 29 | run: yarn install --pure-lockfile 30 | - name: Check Peer Dependencies 31 | run: npx check-peer-dependencies 32 | - name: Run yarn ${{ matrix.yarncmd }} 33 | run: yarn ${{ matrix.yarncmd }} 34 | -------------------------------------------------------------------------------- /.github/workflows/update_dependencies.yml: -------------------------------------------------------------------------------- 1 | # This workflow requires a personal access token for uirouterbot 2 | name: Weekly Dependency Bumps 3 | on: 4 | repository_dispatch: 5 | types: [update_dependencies] 6 | schedule: 7 | - cron: '0 20 * * 0' 8 | 9 | jobs: 10 | upgrade-dependencies: 11 | runs-on: ubuntu-latest 12 | name: Update dependencies 13 | strategy: 14 | matrix: 15 | excludes: [''] 16 | deptype: ['dependencies', 'devDependencies'] 17 | latest: [false] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - run: | 21 | git config user.name uirouterbot 22 | git config user.password ${{ secrets.UIROUTERBOT_PAT }} 23 | git remote set-url origin $(git remote get-url origin | sed -e 's/ui-router/uirouterbot/') 24 | git fetch --unshallow -p origin 25 | - name: Update dependencies 26 | id: upgrade 27 | uses: ui-router/publish-scripts/actions/upgrade@actions-upgrade-v1.0.3 28 | with: 29 | excludes: ${{ matrix.excludes }} 30 | deptype: ${{ matrix.deptype }} 31 | latest: ${{ matrix.latest }} 32 | - name: Create Pull Request 33 | id: cpr 34 | if: ${{ steps.upgrade.outputs.upgrades != '' }} 35 | # the following hash is from https://github.com/peter-evans/create-pull-request/releases/tag/v2.7.0 36 | uses: peter-evans/create-pull-request@340e629d2f63059fb3e3f15437e92cfbc7acd85b 37 | with: 38 | token: ${{ secrets.UIROUTERBOT_PAT }} 39 | request-to-parent: true 40 | branch-suffix: 'random' 41 | commit-message: 'chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }}' 42 | title: 'chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }}' 43 | body: | 44 | chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }} 45 | 46 | ``` 47 | ${{ steps.upgrade.outputs.upgrades }} 48 | ``` 49 | 50 | Auto-generated by [create-pull-request][1] 51 | 52 | [1]: https://github.com/peter-evans/create-pull-request 53 | - name: Apply Merge Label 54 | if: ${{ steps.cpr.outputs.pr_number != '' }} 55 | uses: actions/github-script@0.9.0 56 | with: 57 | github-token: ${{ secrets.UIROUTERBOT_PAT }} 58 | script: | 59 | await github.issues.addLabels({ 60 | owner: context.repo.owner, 61 | repo: context.repo.repo, 62 | issue_number: ${{ steps.cpr.outputs.pr_number }}, 63 | labels: ['ready to squash and merge'] 64 | }); 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # common 2 | node_modules 3 | bower_components 4 | yarn-error.log 5 | 6 | .DS_Store 7 | *~ 8 | **/.* 9 | 10 | # webstorm files 11 | idea-out 12 | *.iml 13 | *.ipr 14 | *.iws 15 | 16 | # generated files 17 | _doc 18 | lib 19 | lib-esm 20 | release 21 | 22 | # do not ignore github 23 | !.github 24 | !.npmrc 25 | !.npmignore 26 | !.prettier* 27 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Auto Squash and Merge 3 | conditions: 4 | - base=master 5 | - status-success=ci 6 | - 'label=ready to squash and merge' 7 | actions: 8 | delete_head_branch: {} 9 | merge: 10 | method: squash 11 | strict: smart 12 | - name: Auto Rebase and Merge 13 | conditions: 14 | - base=master 15 | - status-success=ci 16 | - 'label=ready to rebase and merge' 17 | actions: 18 | delete_head_branch: {} 19 | merge: 20 | method: rebase 21 | strict: smart 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Any hidden files 2 | **/.* 3 | 4 | node_modules 5 | scripts 6 | src 7 | test 8 | _docs 9 | 10 | bower.json 11 | karma.conf.js 12 | tsconfig.json 13 | tsconfig.**.json 14 | webpack.config.js 15 | 16 | *.iml 17 | *.ipr 18 | *.iws 19 | idea-out 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=auto 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | bower.json 3 | CHANGELOG.md 4 | test/**/angular.js 5 | test/**/angular-mocks.js 6 | test/**/angular-animate.js 7 | release/** 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | #__tests__ 3 | #test 4 | #tests 5 | #powered-test 6 | 7 | # asset directories 8 | #docs 9 | #doc 10 | #website 11 | #images 12 | #assets 13 | 14 | # examples 15 | #example 16 | #examples 17 | 18 | # code coverage directories 19 | #coverage 20 | #.nyc_output 21 | 22 | # build scripts 23 | #Makefile 24 | #Gulpfile.js 25 | #Gruntfile.js 26 | 27 | # configs 28 | #.tern-project 29 | #.gitattributes 30 | #.editorconfig 31 | #.*ignore 32 | #.eslintrc 33 | #.jshintrc 34 | #.flowconfig 35 | #.documentup.json 36 | #.yarn-metadata.json 37 | #.*.yml 38 | #*.yml 39 | 40 | # misc 41 | #*.gz 42 | #*.md 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Report an Issue 3 | 4 | Help us make UI-Router better! If you think you might have found a bug, or some other weirdness, start by making sure 5 | it hasn't already been reported. You can [search through existing issues](https://github.com/angular-ui/ui-router/search?q=wat%3F&type=Issues) 6 | to see if someone's reported one similar to yours. 7 | 8 | If not, then [create a plunkr](http://bit.ly/UIR-Plunk) that demonstrates the problem (try to use as little code 9 | as possible: the more minimalist, the faster we can debug it). 10 | 11 | Next, [create a new issue](https://github.com/angular-ui/ui-router/issues/new) that briefly explains the problem, 12 | and provides a bit of background as to the circumstances that triggered it. Don't forget to include the link to 13 | that plunkr you created! 14 | 15 | **Note**: If you're unsure how a feature is used, or are encountering some unexpected behavior that you aren't sure 16 | is a bug, it's best to talk it out on 17 | [StackOverflow](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router) before reporting it. This 18 | keeps development streamlined, and helps us focus on building great software. 19 | 20 | 21 | Issues only! | 22 | -------------| 23 | Please keep in mind that the issue tracker is for *issues*. Please do *not* post an issue if you need help or support. Instead, see one of the above-mentioned forums or [IRC](irc://irc.freenode.net/#angularjs). | 24 | 25 | #### Purple Labels 26 | A purple label means that **you** need to take some further action. 27 | - ![Not Actionable - Need Info](https://angular-ui.github.io/ui-router/ngdoc_assets/incomplete.png): Your issue is not specific enough, or there is no clear action that we can take. Please clarify and refine your issue. 28 | - ![Plunkr Please](https://angular-ui.github.io/ui-router/ngdoc_assets/example.png): Please [create a plunkr](http://bit.ly/UIR-Plunk) 29 | - ![StackOverflow](https://angular-ui.github.io/ui-router/ngdoc_assets/so.png): We suspect your issue is really a help request, or could be answered by the community. Please ask your question on [StackOverflow](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router). If you determine that is an actual issue, please explain why. 30 | 31 | If your issue gets labeled with purple label, no further action will be taken until you respond to the label appropriately. 32 | 33 | # Contribute 34 | 35 | **(1)** See the **[Developing](#developing)** section below, to get the development version of UI-Router up and running on your local machine. 36 | 37 | **(2)** Check out the [roadmap](https://github.com/angular-ui/ui-router/milestones) to see where the project is headed, and if your feature idea fits with where we're headed. 38 | 39 | **(3)** If you're not sure, [open an RFC](https://github.com/angular-ui/ui-router/issues/new?title=RFC:%20My%20idea) to get some feedback on your idea. 40 | 41 | **(4)** Finally, commit some code and open a pull request. Code & commits should abide by the following rules: 42 | 43 | - *Always* have test coverage for new features (or regression tests for bug fixes), and *never* break existing tests 44 | - Commits should represent one logical change each; if a feature goes through multiple iterations, squash your commits down to one 45 | - Make sure to follow the [Angular commit message format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) so your change will appear in the changelog of the next release. 46 | - Changes should always respect the coding style of the project 47 | 48 | 49 | 50 | # Developing 51 | 52 | UI-Router uses npm and Rollup. 53 | 54 | ## Fetch the source code 55 | 56 | The code for Angular UI-Router is split into two source repositories: 57 | 58 | * [UI-Router Core](https://github.com/ui-router/core) (`@uirouter/core` on npm) 59 | * [UI-Router for Angular 1](https://github.com/angular-ui/ui-router) (`@ui-router/angularjs` on npm) 60 | 61 | Clone both repositories into directories next to each other. 62 | 63 | ``` 64 | mkdir uirouter 65 | cd uirouter 66 | git clone https://github.com/angular-ui/ui-router.git angularjs 67 | git clone https://github.com/ui-router/core.git core 68 | ``` 69 | 70 | ## Install dependencies 71 | 72 | Use `npm` to install the development dependencies for each repository. 73 | 74 | ``` 75 | cd core 76 | npm install 77 | cd ../angularjs 78 | npm install 79 | cd .. 80 | ``` 81 | 82 | ## Link the directories 83 | 84 | This step is necessary if you need to modify any code in `@uirouter/core`. 85 | Using `npm`, link `@uirouter/core` into `@uirouter/angularjs` 86 | 87 | ``` 88 | cd core 89 | npm link 90 | cd ../angularjs 91 | npm link '@uirouter/core' 92 | ``` 93 | 94 | After executing these steps, `@uirouter/angularjs` will be depend on your local copy of `@uirouter/core` instead of the version listed in `package.json`. 95 | 96 | ## Develop 97 | 98 | These scripts may be run in the `angularjs` directory: 99 | 100 | * `npm run build`: Compiles TypeScript source 101 | * `npm run package`: Compiles TypeScript source and creates the Rollup bundles. 102 | * `npm test`: Runs the test suite (against Angular 1.2 through 1.5). 103 | * `npm run watch`: Continuously compiles the source and runs the test suite (when either source or tests change). 104 | 105 | Scripts of the same name (in the `core` directory) can be used. 106 | 107 | * `npm run build`: Compiles `@uirouter/core` TypeScript source 108 | * `npm test`: Runs the `@uirouter/core` test suite 109 | * `npm run watch`: Continuously compiles the source and runs the `@uirouter/core` test suite (when core source or tests change). 110 | 111 | If you've followed the [linking instructions](#link-the-directories), it's useful to run both 112 | `npm run watch` tasks (each task from `@uirouter/core` *and* `@uirouter/angularjs`). 113 | This ensures that changes to either `@uirouter/core` and `@uirouter/angularjs` compile successfully and are run against their test suites. 114 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # [UI Router for Angular 1](https://ui-router.github.io/ng1/docs/latest) 2 | 3 | #### The de-facto solution to flexible routing in angular 1 4 | 5 |
6 | 7 | 8 | 9 | [![Build Status](https://travis-ci.org/angular-ui/ui-router.svg?branch=master)](https://travis-ci.org/angular-ui/ui-router) 10 | 11 |
12 | 13 | 14 | Angular UI-Router is a client-side [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) 15 | routing framework for [AngularJS](http://angularjs.org). 16 | 17 | **[View on Github](http://github.com/angular-ui/ui-router) |** 18 | **[Tutorials](https://ui-router.github.io/ng1/tutorials/)** | 19 | **[Guides](https://ui-router.github.io/guide) |** 20 | **[Sample App](http://ui-router.github.io/resources/sampleapp/) |** 21 | **[Wiki](https://github.com/angular-ui/ui-router/wiki) |** 22 | **[FAQ](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions)** 23 | 24 | #### API Documentation Organization 25 | 26 | The documentation is arranged according to API concern, such as `url`, `resolve`, and `core`. 27 | For a list of services and objects that can be injectable, see the [`injectables` section](./injectables.html). 28 | 29 | By default, only the public UI-Router API is shown. 30 | To view both public API and the internal APIs, check the "Internal UI-Router API" checkbox. 31 | 32 | #### Typescript 33 | 34 | UI-Router is written in Typescript. 35 | The API documentation is generated using [TypeDoc](https://github.com/TypeStrong/typedoc). 36 | The documentation reflects the Typescript classes, interfaces, and parameter types information embedded in the source code. 37 | 38 | #### Contributing 39 | 40 | Angular UI-Router depends on the framework agnostic `@uirouter/core`. 41 | To contribute to the documentation, please submit a PR to either 42 | [@uirouter/angularjs](http://github.com/angular-ui/ui-router) 43 | or 44 | [@uirouter/core](http://github.com/ui-router/core). 45 | To find where a specific piece of documentation is written, follow the links that say: 46 | > _Defined in ui-router/somedir/somefile.ts_ 47 | 48 | 49 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This issue tracker is for Bug Reports and Feature Requests only. 2 | 3 | Please direct requests for help to StackOverflow. 4 | See http://bit.ly/UIR-SOF for details. 5 | 6 | This is a (check one box): 7 | 8 | - [ ] Bug Report 9 | - [ ] Feature Request 10 | - [ ] General Query 11 | 12 | My version of UI-Router is: (type version) 13 | 14 | 15 | 16 | # Bug Report 17 | 18 | #### Current Behavior: 19 | 20 | (type current behavior here) 21 | 22 | #### Expected Behavior: 23 | 24 | (type expected behavior here) 25 | 26 | #### Link to Plunker or stackblitz that reproduces the issue: 27 | 28 | ( if you want a response to your issue, provide a way to reproduce it ) 29 | ( http://bit.ly/UIR-Plunk1 ) 30 | ( https://stackblitz.com/edit/ui-router-angularjs ) 31 | 32 | 33 | 34 | 35 | # Feature Request 36 | 37 | (type feature request here) 38 | 39 | 40 | 41 | 42 | # General Query 43 | 44 | Please direct general implementation questions to StackOverflow: 45 | http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router 46 | 47 | Please review the Sample Application which highlights common approaches: 48 | https://github.com/ui-router/sample-app-angularjs 49 | 50 | - [ ] I have already asked my question on StackOverflow and nobody could answer the question 51 | 52 | - [ ] I have already reviewed the sample application for examples of common approaches 53 | 54 | - [ ] I believe my question can only be answered by the UI-Router maintainers 55 | 56 | 57 | (type general query here) 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2018 The AngularUI Team, Karsten Sperling 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularUI Router  [![Build Status](https://github.com/angular-ui/ui-router/workflows/CI:%20UIRouter%20for%20AngularJS/badge.svg)](https://github.com/angular-ui/ui-router/actions?query=workflow%3A%22CI%3A+UIRouter+for+AngularJS%22) 2 | 3 | **Note: this is the Angular 1.x source for UI-Router version 1.x. If you are looking for the source for UI-Router 4 | version 0.x, it can be found [here](https://github.com/angular-ui/ui-router/tree/legacy)** 5 | 6 | --- 7 | 8 | 9 | #### The de-facto solution to flexible routing in angular 10 | --- 11 | **[Tutorials](https://ui-router.github.io/tutorials/)** | 12 | **[API Docs](https://ui-router.github.io/docs/latest/)** | 13 | **[Download stable](http://unpkg.com/@uirouter/angularjs@latest/release/angular-ui-router.js)** (or **[Minified](http://unpkg.com/@uirouter/angularjs@latest/release/angular-ui-router.min.js)**) **|** 14 | **[Guide](https://ui-router.github.io/guide/) |** 15 | **[Sample App](http://ui-router.github.io/resources/sampleapp/) |** 16 | **[FAQ](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions) |** 17 | **[Report an Issue](https://github.com/angular-ui/ui-router/blob/master/CONTRIBUTING.md#report-an-issue) |** 18 | **[Contribute](https://github.com/angular-ui/ui-router/blob/master/CONTRIBUTING.md#contribute) |** 19 | **[Help!](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router) |** 20 | 21 | --- 22 | 23 | Angular UI-Router is a client-side [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) 24 | routing framework for [AngularJS](http://angularjs.org). 25 | 26 | Routing frameworks for SPAs update the browser's URL as the user navigates through the app. Conversely, this allows 27 | changes to the browser's URL to drive navigation through the app, thus allowing the user to create a bookmark to a 28 | location deep within the SPA. 29 | 30 | UI-Router applications are modeled as a hierarchical tree of states. UI-Router provides a 31 | [*state machine*](https://en.wikipedia.org/wiki/Finite-state_machine) to manage the transitions between those 32 | application states in a transaction-like manner. 33 | 34 | ## Get Started 35 | 36 | 37 | - [UI-Router for Angular 1](https://ui-router.github.io/ng1) 38 | - [UI-Router for Angular 2](https://ui-router.github.io/ng2) 39 | - [UI-Router for React](https://ui-router.github.io/react) 40 | 41 | ## Resources 42 | 43 | * [In-Depth Guide](https://github.com/angular-ui/ui-router/wiki) 44 | * [Slides comparing ngRoute to ui-router](http://slid.es/timkindberg/ui-router#/) 45 | * [UI-Router Extras / Addons for legacy (0.x)](http://christopherthielen.github.io/ui-router-extras/#/home) (@christopherthielen) 46 | 47 | ### Videos 48 | 49 | * [Introduction Video](https://egghead.io/lessons/angularjs-introduction-ui-router) (egghead.io) 50 | * [Tim Kindberg on Angular UI-Router](https://www.youtube.com/watch?v=lBqiZSemrqg) 51 | * [Activating States](https://egghead.io/lessons/angularjs-ui-router-activating-states) (egghead.io) 52 | * [Learn Angular.js using UI-Router](http://youtu.be/QETUuZ27N0w) (LearnCode.academy) 53 | 54 | ## Reporting issues and Contributing 55 | 56 | Please read our [Contributor guidelines](CONTRIBUTING.md) before reporting an issue or creating a pull request. 57 | -------------------------------------------------------------------------------- /artifacts.json: -------------------------------------------------------------------------------- 1 | { 2 | "ARTIFACTS": ["lib", "lib-esm", "release", "package.json"] 3 | } 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router", 3 | "description": "State-based routing for AngularJS", 4 | "license": "MIT", 5 | "main": "./release/angular-ui-router.js", 6 | "dependencies": { 7 | "angular": ">= 1.2.0" 8 | }, 9 | "ignore": [ 10 | "**/.*", 11 | "**/tsconfig.json", 12 | "**/tsconfig.typedoc.json", 13 | "**/webpack.config.js", 14 | "**/node_modules", 15 | "package.json", 16 | "scripts", 17 | "test", 18 | "src" 19 | ], 20 | "version": "1.0.30" 21 | } -------------------------------------------------------------------------------- /downstream_projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": { 3 | "typescript3.9": "./test/typescript/3.9" 4 | }, 5 | "sample-app-angularjs": "https://github.com/ui-router/sample-app-angularjs.git" 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const NG = process.env.NG || '1.7'; 2 | 3 | console.log(`Testing with AngularJS ${NG}`); 4 | 5 | module.exports = { 6 | preset: 'ts-jest', 7 | testEnvironment: 'jsdom', 8 | roots: ['src', 'test'], 9 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)', '**/?*Spec.[jt]s'], 10 | setupFilesAfterEnv: ['./test/jest.init.ts'], 11 | moduleNameMapper: { 12 | '^angular$': '/test/angular/jest-angular.js', 13 | '^jest-angular-import$': `/test/angular/${NG}/angular.js`, 14 | '^angular-animate$': `/test/angular/${NG}/angular-animate.js`, 15 | '^angular-mocks$': `/test/angular/${NG}/angular-mocks.js`, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file 2 | var karma = require('karma'); 3 | var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | var DEFAULT_NG_VERSION = '1.6'; 5 | 6 | /** 7 | * This returns a Karma 'files configuration'. 8 | * http://karma-runner.github.io/0.8/config/files.html 9 | * 10 | * Specifies which files can be served by the Karma web server 11 | * 12 | * included: true -- files that are always served to the browser (like 13 | * ``` 14 | * and also make sure you depend on the `ui.router.state.events` angular module, e.g., 15 | * ``` 16 | * angular.module("myApplication", ['ui.router', 'ui.router.state.events'] 17 | * ``` 18 | * 19 | * @publicapi @module ng1_state_events 20 | */ /** */ 21 | import { ng as angular } from '../angular'; 22 | import { IScope, IAngularEvent, IServiceProviderFactory } from 'angular'; 23 | import { 24 | Obj, 25 | TargetState, 26 | StateService, 27 | Transition, 28 | TransitionService, 29 | UrlRouter, 30 | HookResult, 31 | UIInjector, 32 | } from '@uirouter/core'; 33 | import { StateProvider } from '../stateProvider'; 34 | 35 | /** 36 | * An event broadcast on `$rootScope` when the state transition **begins**. 37 | * 38 | * ### Deprecation warning: use [[TransitionService.onStart]] instead 39 | * 40 | * You can use `event.preventDefault()` 41 | * to prevent the transition from happening and then the transition promise will be 42 | * rejected with a `'transition prevented'` value. 43 | * 44 | * Additional arguments to the event handler are provided: 45 | * - `toState`: the Transition Target state 46 | * - `toParams`: the Transition Target Params 47 | * - `fromState`: the state the transition is coming from 48 | * - `fromParams`: the parameters from the state the transition is coming from 49 | * - `options`: any Transition Options 50 | * - `$transition$`: the [[Transition]] 51 | * 52 | * #### Example: 53 | * ```js 54 | * $rootScope.$on('$stateChangeStart', function(event, transition) { 55 | * event.preventDefault(); 56 | * // transitionTo() promise will be rejected with 57 | * // a 'transition prevented' error 58 | * }) 59 | * ``` 60 | * 61 | * @event $stateChangeStart 62 | * @deprecated 63 | */ 64 | export let $stateChangeStart: IAngularEvent; 65 | 66 | /** 67 | * An event broadcast on `$rootScope` if a transition is **cancelled**. 68 | * 69 | * ### Deprecation warning: use [[TransitionService.onStart]] instead 70 | * 71 | * Additional arguments to the event handler are provided: 72 | * - `toState`: the Transition Target state 73 | * - `toParams`: the Transition Target Params 74 | * - `fromState`: the state the transition is coming from 75 | * - `fromParams`: the parameters from the state the transition is coming from 76 | * - `options`: any Transition Options 77 | * - `$transition$`: the [[Transition]] that was cancelled 78 | * 79 | * @event $stateChangeCancel 80 | * @deprecated 81 | */ 82 | export let $stateChangeCancel: IAngularEvent; 83 | 84 | /** 85 | * An event broadcast on `$rootScope` once the state transition is **complete**. 86 | * 87 | * ### Deprecation warning: use [[TransitionService.onStart]] and [[Transition.promise]], or [[Transition.onSuccess]] 88 | * 89 | * Additional arguments to the event handler are provided: 90 | * - `toState`: the Transition Target state 91 | * - `toParams`: the Transition Target Params 92 | * - `fromState`: the state the transition is coming from 93 | * - `fromParams`: the parameters from the state the transition is coming from 94 | * - `options`: any Transition Options 95 | * - `$transition$`: the [[Transition]] that just succeeded 96 | * 97 | * @event $stateChangeSuccess 98 | * @deprecated 99 | */ 100 | export let $stateChangeSuccess: IAngularEvent; 101 | 102 | /** 103 | * An event broadcast on `$rootScope` when an **error occurs** during transition. 104 | * 105 | * ### Deprecation warning: use [[TransitionService.onStart]] and [[Transition.promise]], or [[Transition.onError]] 106 | * 107 | * It's important to note that if you 108 | * have any errors in your resolve functions (javascript errors, non-existent services, etc) 109 | * they will not throw traditionally. You must listen for this $stateChangeError event to 110 | * catch **ALL** errors. 111 | * 112 | * Additional arguments to the event handler are provided: 113 | * - `toState`: the Transition Target state 114 | * - `toParams`: the Transition Target Params 115 | * - `fromState`: the state the transition is coming from 116 | * - `fromParams`: the parameters from the state the transition is coming from 117 | * - `error`: The reason the transition errored. 118 | * - `options`: any Transition Options 119 | * - `$transition$`: the [[Transition]] that errored 120 | * 121 | * @event $stateChangeError 122 | * @deprecated 123 | */ 124 | export let $stateChangeError: IAngularEvent; 125 | 126 | /** 127 | * An event broadcast on `$rootScope` when a requested state **cannot be found** using the provided state name. 128 | * 129 | * ### Deprecation warning: use [[StateService.onInvalid]] instead 130 | * 131 | * The event is broadcast allowing any handlers a single chance to deal with the error (usually by 132 | * lazy-loading the unfound state). A `TargetState` object is passed to the listener handler, 133 | * you can see its properties in the example. You can use `event.preventDefault()` to abort the 134 | * transition and the promise returned from `transitionTo()` will be rejected with a 135 | * `'transition aborted'` error. 136 | * 137 | * Additional arguments to the event handler are provided: 138 | * - `unfoundState` Unfound State information. Contains: `to, toParams, options` properties. 139 | * - `fromState`: the state the transition is coming from 140 | * - `fromParams`: the parameters from the state the transition is coming from 141 | * - `options`: any Transition Options 142 | * 143 | * #### Example: 144 | * ```js 145 | * // somewhere, assume lazy.state has not been defined 146 | * $state.go("lazy.state", { a: 1, b: 2 }, { inherit: false }); 147 | * 148 | * // somewhere else 149 | * $scope.$on('$stateNotFound', function(event, transition) { 150 | * function(event, unfoundState, fromState, fromParams){ 151 | * console.log(unfoundState.to); // "lazy.state" 152 | * console.log(unfoundState.toParams); // {a:1, b:2} 153 | * console.log(unfoundState.options); // {inherit:false} + default options 154 | * }); 155 | * ``` 156 | * 157 | * @event $stateNotFound 158 | * @deprecated 159 | */ 160 | export let $stateNotFound: IAngularEvent; 161 | 162 | (function () { 163 | const { isFunction, isString } = angular; 164 | 165 | function applyPairs(memo: Obj, keyValTuple: any[]) { 166 | let key: string, value: any; 167 | if (Array.isArray(keyValTuple)) [key, value] = keyValTuple; 168 | if (!isString(key)) throw new Error('invalid parameters to applyPairs'); 169 | memo[key] = value; 170 | return memo; 171 | } 172 | 173 | function stateChangeStartHandler($transition$: Transition) { 174 | if (!$transition$.options().notify || !$transition$.valid() || $transition$.ignored()) return; 175 | 176 | const $injector = $transition$.injector(); 177 | const $stateEvents = $injector.get('$stateEvents'); 178 | const $rootScope = $injector.get('$rootScope'); 179 | const $state = $injector.get('$state'); 180 | const $urlRouter = $injector.get('$urlRouter'); 181 | 182 | const enabledEvents = $stateEvents.provider.enabled(); 183 | 184 | const toParams = $transition$.params('to'); 185 | const fromParams = $transition$.params('from'); 186 | 187 | if (enabledEvents.$stateChangeSuccess) { 188 | const startEvent = $rootScope.$broadcast( 189 | '$stateChangeStart', 190 | $transition$.to(), 191 | toParams, 192 | $transition$.from(), 193 | fromParams, 194 | $transition$.options(), 195 | $transition$ 196 | ); 197 | 198 | if (startEvent.defaultPrevented) { 199 | if (enabledEvents.$stateChangeCancel) { 200 | $rootScope.$broadcast( 201 | '$stateChangeCancel', 202 | $transition$.to(), 203 | toParams, 204 | $transition$.from(), 205 | fromParams, 206 | $transition$.options(), 207 | $transition$ 208 | ); 209 | } 210 | // Don't update and resync url if there's been a new transition started. see issue #2238, #600 211 | if ($state.transition == null) $urlRouter.update(); 212 | return false; 213 | } 214 | 215 | // right after global state is updated 216 | const successOpts = { priority: 9999 }; 217 | $transition$.onSuccess( 218 | {}, 219 | function () { 220 | $rootScope.$broadcast( 221 | '$stateChangeSuccess', 222 | $transition$.to(), 223 | toParams, 224 | $transition$.from(), 225 | fromParams, 226 | $transition$.options(), 227 | $transition$ 228 | ); 229 | }, 230 | successOpts 231 | ); 232 | } 233 | 234 | if (enabledEvents.$stateChangeError) { 235 | $transition$.promise['catch'](function (error) { 236 | if (error && (error.type === 2 /* RejectType.SUPERSEDED */ || error.type === 3) /* RejectType.ABORTED */) 237 | return; 238 | 239 | const evt = $rootScope.$broadcast( 240 | '$stateChangeError', 241 | $transition$.to(), 242 | toParams, 243 | $transition$.from(), 244 | fromParams, 245 | error, 246 | $transition$.options(), 247 | $transition$ 248 | ); 249 | 250 | if (!evt.defaultPrevented) { 251 | $urlRouter.update(); 252 | } 253 | }); 254 | } 255 | } 256 | 257 | stateNotFoundHandler.$inject = ['$to$', '$from$', '$state', '$rootScope', '$urlRouter']; 258 | function stateNotFoundHandler($to$: TargetState, $from$: TargetState, injector: UIInjector): HookResult { 259 | const $state: StateService = injector.get('$state'); 260 | const $rootScope: IScope = injector.get('$rootScope'); 261 | const $urlRouter: UrlRouter = injector.get('$urlRouter'); 262 | 263 | interface StateNotFoundEvent extends IAngularEvent { 264 | retry: Promise; 265 | } 266 | 267 | const redirect = { to: $to$.identifier(), toParams: $to$.params(), options: $to$.options() }; 268 | const e = $rootScope.$broadcast('$stateNotFound', redirect, $from$.state(), $from$.params()); 269 | 270 | if (e.defaultPrevented || e.retry) $urlRouter.update(); 271 | 272 | function redirectFn(): TargetState { 273 | return $state.target(redirect.to, redirect.toParams, redirect.options); 274 | } 275 | 276 | if (e.defaultPrevented) { 277 | return false; 278 | } else if (e.retry || !!$state.get(redirect.to)) { 279 | return e.retry && isFunction(e.retry.then) ? e.retry.then(redirectFn) : redirectFn(); 280 | } 281 | } 282 | 283 | $StateEventsProvider.$inject = ['$stateProvider']; 284 | function $StateEventsProvider($stateProvider: StateProvider) { 285 | $StateEventsProvider.prototype.instance = this; 286 | 287 | interface IEventsToggle { 288 | [key: string]: boolean; 289 | $stateChangeStart: boolean; 290 | $stateNotFound: boolean; 291 | $stateChangeSuccess: boolean; 292 | $stateChangeError: boolean; 293 | } 294 | 295 | let runtime = false; 296 | const allEvents = ['$stateChangeStart', '$stateNotFound', '$stateChangeSuccess', '$stateChangeError']; 297 | const enabledStateEvents = allEvents.map((e) => [e, true]).reduce(applyPairs, {}); 298 | 299 | function assertNotRuntime() { 300 | if (runtime) throw new Error('Cannot enable events at runtime (use $stateEventsProvider'); 301 | } 302 | 303 | /** 304 | * Enables the deprecated UI-Router 0.2.x State Events 305 | * [ '$stateChangeStart', '$stateNotFound', '$stateChangeSuccess', '$stateChangeError' ] 306 | */ 307 | this.enable = function (...events: string[]) { 308 | assertNotRuntime(); 309 | if (!events || !events.length) events = allEvents; 310 | events.forEach((event) => (enabledStateEvents[event] = true)); 311 | }; 312 | 313 | /** 314 | * Disables the deprecated UI-Router 0.2.x State Events 315 | * [ '$stateChangeStart', '$stateNotFound', '$stateChangeSuccess', '$stateChangeError' ] 316 | */ 317 | this.disable = function (...events: string[]) { 318 | assertNotRuntime(); 319 | if (!events || !events.length) events = allEvents; 320 | events.forEach((event) => delete enabledStateEvents[event]); 321 | }; 322 | 323 | this.enabled = () => enabledStateEvents; 324 | 325 | this.$get = $get; 326 | $get.$inject = ['$transitions']; 327 | function $get($transitions: TransitionService) { 328 | runtime = true; 329 | 330 | if (enabledStateEvents['$stateNotFound']) $stateProvider.onInvalid(stateNotFoundHandler); 331 | if (enabledStateEvents.$stateChangeStart) $transitions.onBefore({}, stateChangeStartHandler, { priority: 1000 }); 332 | 333 | return { 334 | provider: $StateEventsProvider.prototype.instance, 335 | }; 336 | } 337 | } 338 | 339 | angular 340 | .module('ui.router.state.events', ['ui.router.state']) 341 | .provider('$stateEvents', ($StateEventsProvider as any) as IServiceProviderFactory) 342 | .run([ 343 | '$stateEvents', 344 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 345 | function ($stateEvents: any) { 346 | /* Invokes $get() */ 347 | }, 348 | ]); 349 | })(); 350 | -------------------------------------------------------------------------------- /src/locationServices.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | import { LocationConfig, LocationServices, UIRouter, ParamType, isDefined } from '@uirouter/core'; 3 | import { val, createProxyFunctions, removeFrom, isObject } from '@uirouter/core'; 4 | import { ILocationService, ILocationProvider, IWindowService } from 'angular'; 5 | 6 | /** 7 | * Implements UI-Router LocationServices and LocationConfig using Angular 1's $location service 8 | * @internalapi 9 | */ 10 | export class Ng1LocationServices implements LocationConfig, LocationServices { 11 | private $locationProvider: ILocationProvider; 12 | private $location: ILocationService; 13 | private $sniffer: any; 14 | private $browser: any; 15 | private $window: IWindowService; 16 | 17 | path; 18 | search; 19 | hash; 20 | hashPrefix; 21 | port; 22 | protocol; 23 | host; 24 | 25 | private _baseHref: string; 26 | 27 | // .onChange() registry 28 | private _urlListeners: Function[] = []; 29 | 30 | /** 31 | * Applys ng1-specific path parameter encoding 32 | * 33 | * The Angular 1 `$location` service is a bit weird. 34 | * It doesn't allow slashes to be encoded/decoded bi-directionally. 35 | * 36 | * See the writeup at https://github.com/angular-ui/ui-router/issues/2598 37 | * 38 | * This code patches the `path` parameter type so it encoded/decodes slashes as ~2F 39 | * 40 | * @param router 41 | */ 42 | static monkeyPatchPathParameterType(router: UIRouter) { 43 | const pathType: ParamType = router.urlMatcherFactory.type('path'); 44 | 45 | pathType.encode = (x: any) => 46 | x != null ? x.toString().replace(/(~|\/)/g, (m) => ({ '~': '~~', '/': '~2F' }[m])) : x; 47 | 48 | pathType.decode = (x: string) => 49 | x != null ? x.toString().replace(/(~~|~2F)/g, (m) => ({ '~~': '~', '~2F': '/' }[m])) : x; 50 | } 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-empty-function 53 | dispose() {} 54 | 55 | constructor($locationProvider: ILocationProvider) { 56 | this.$locationProvider = $locationProvider; 57 | const _lp = val($locationProvider); 58 | createProxyFunctions(_lp, this, _lp, ['hashPrefix']); 59 | } 60 | 61 | onChange(callback: Function) { 62 | this._urlListeners.push(callback); 63 | return () => removeFrom(this._urlListeners)(callback); 64 | } 65 | 66 | html5Mode() { 67 | let html5Mode: any = this.$locationProvider.html5Mode(); 68 | html5Mode = isObject(html5Mode) ? html5Mode.enabled : html5Mode; 69 | return html5Mode && this.$sniffer.history; 70 | } 71 | 72 | baseHref() { 73 | return this._baseHref || (this._baseHref = this.$browser.baseHref() || this.$window.location.pathname); 74 | } 75 | 76 | url(newUrl?: string, replace = false, state?) { 77 | if (isDefined(newUrl)) this.$location.url(newUrl); 78 | if (replace) this.$location.replace(); 79 | if (state) this.$location.state(state); 80 | return this.$location.url(); 81 | } 82 | 83 | _runtimeServices($rootScope, $location: ILocationService, $sniffer, $browser, $window: IWindowService) { 84 | this.$location = $location; 85 | this.$sniffer = $sniffer; 86 | this.$browser = $browser; 87 | this.$window = $window; 88 | 89 | // Bind $locationChangeSuccess to the listeners registered in LocationService.onChange 90 | $rootScope.$on('$locationChangeSuccess', (evt) => this._urlListeners.forEach((fn) => fn(evt))); 91 | const _loc = val($location); 92 | 93 | // Bind these LocationService functions to $location 94 | createProxyFunctions(_loc, this, _loc, ['replace', 'path', 'search', 'hash']); 95 | // Bind these LocationConfig functions to $location 96 | createProxyFunctions(_loc, this, _loc, ['port', 'protocol', 'host']); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/services.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /** 4 | * # Angular 1 types 5 | * 6 | * UI-Router core provides various Typescript types which you can use for code completion and validating parameter values, etc. 7 | * The customizations to the core types for Angular UI-Router are documented here. 8 | * 9 | * The optional [[$resolve]] service is also documented here. 10 | * 11 | * @preferred @publicapi @module ng1 12 | */ /** */ 13 | import { ng as angular } from './angular'; 14 | import { 15 | IRootScopeService, 16 | IQService, 17 | ILocationService, 18 | ILocationProvider, 19 | IHttpService, 20 | ITemplateCacheService, 21 | } from 'angular'; 22 | import { 23 | services, 24 | applyPairs, 25 | isString, 26 | trace, 27 | extend, 28 | UIRouter, 29 | StateService, 30 | UrlRouter, 31 | UrlMatcherFactory, 32 | ResolveContext, 33 | unnestR, 34 | TypedMap, 35 | } from '@uirouter/core'; 36 | import { ng1ViewsBuilder, getNg1ViewConfigFactory } from './statebuilders/views'; 37 | import { TemplateFactory } from './templateFactory'; 38 | import { StateProvider } from './stateProvider'; 39 | import { getStateHookBuilder } from './statebuilders/onEnterExitRetain'; 40 | import { Ng1LocationServices } from './locationServices'; 41 | import { UrlRouterProvider } from './urlRouterProvider'; 42 | import IInjectorService = angular.auto.IInjectorService; 43 | 44 | angular.module('ui.router.angular1', []); 45 | const mod_init = angular.module('ui.router.init', ['ng']); 46 | const mod_util = angular.module('ui.router.util', ['ui.router.init']); 47 | const mod_rtr = angular.module('ui.router.router', ['ui.router.util']); 48 | const mod_state = angular.module('ui.router.state', ['ui.router.router', 'ui.router.util', 'ui.router.angular1']); 49 | const mod_main = angular.module('ui.router', ['ui.router.init', 'ui.router.state', 'ui.router.angular1']); 50 | const mod_cmpt = angular.module('ui.router.compat', ['ui.router']); 51 | 52 | declare module '@uirouter/core/lib/router' { 53 | interface UIRouter { 54 | /** @hidden */ 55 | stateProvider: StateProvider; 56 | /** @hidden */ 57 | urlRouterProvider: UrlRouterProvider; 58 | } 59 | } 60 | 61 | let router: UIRouter = null; 62 | 63 | $uiRouterProvider.$inject = ['$locationProvider']; 64 | /** This angular 1 provider instantiates a Router and exposes its services via the angular injector */ 65 | function $uiRouterProvider($locationProvider: ILocationProvider) { 66 | // Create a new instance of the Router when the $uiRouterProvider is initialized 67 | router = this.router = new UIRouter(); 68 | router.stateProvider = new StateProvider(router.stateRegistry, router.stateService); 69 | 70 | // Apply ng1 specific StateBuilder code for `views`, `resolve`, and `onExit/Retain/Enter` properties 71 | router.stateRegistry.decorator('views', ng1ViewsBuilder); 72 | router.stateRegistry.decorator('onExit', getStateHookBuilder('onExit')); 73 | router.stateRegistry.decorator('onRetain', getStateHookBuilder('onRetain')); 74 | router.stateRegistry.decorator('onEnter', getStateHookBuilder('onEnter')); 75 | 76 | router.viewService._pluginapi._viewConfigFactory('ng1', getNg1ViewConfigFactory()); 77 | 78 | // Disable decoding of params by UrlMatcherFactory because $location already handles this 79 | router.urlService.config._decodeParams = false; 80 | 81 | const ng1LocationService = (router.locationService = router.locationConfig = new Ng1LocationServices( 82 | $locationProvider 83 | )); 84 | 85 | Ng1LocationServices.monkeyPatchPathParameterType(router); 86 | 87 | // backwards compat: also expose router instance as $uiRouterProvider.router 88 | router['router'] = router; 89 | router['$get'] = $get; 90 | $get.$inject = ['$location', '$browser', '$window', '$sniffer', '$rootScope', '$http', '$templateCache']; 91 | function $get( 92 | $location: ILocationService, 93 | $browser: any, 94 | $window: any, 95 | $sniffer: any, 96 | $rootScope: ng.IScope, 97 | $http: IHttpService, 98 | $templateCache: ITemplateCacheService 99 | ) { 100 | ng1LocationService._runtimeServices($rootScope, $location, $sniffer, $browser, $window); 101 | delete router['router']; 102 | delete router['$get']; 103 | return router; 104 | } 105 | return router; 106 | } 107 | 108 | const getProviderFor = (serviceName) => [ 109 | '$uiRouterProvider', 110 | ($urp) => { 111 | const service = $urp.router[serviceName]; 112 | service['$get'] = () => service; 113 | return service; 114 | }, 115 | ]; 116 | 117 | // This effectively calls $get() on `$uiRouterProvider` to trigger init (when ng enters runtime) 118 | runBlock.$inject = ['$injector', '$q', '$uiRouter']; 119 | function runBlock($injector: IInjectorService, $q: IQService, $uiRouter: UIRouter) { 120 | services.$injector = $injector; 121 | services.$q = $q; 122 | 123 | // https://github.com/angular-ui/ui-router/issues/3678 124 | if (!Object.prototype.hasOwnProperty.call($injector, 'strictDi')) { 125 | try { 126 | $injector.invoke(function (checkStrictDi) {}); 127 | } catch (error) { 128 | $injector.strictDi = !!/strict mode/.exec(error && error.toString()); 129 | } 130 | } 131 | 132 | // The $injector is now available. 133 | // Find any resolvables that had dependency annotation deferred 134 | $uiRouter.stateRegistry 135 | .get() 136 | .map((x) => x.$$state().resolvables) 137 | .reduce(unnestR, []) 138 | .filter((x) => x.deps === 'deferred') 139 | .forEach((resolvable) => (resolvable.deps = $injector.annotate(resolvable.resolveFn, $injector.strictDi))); 140 | } 141 | 142 | // $urlRouter service and $urlRouterProvider 143 | const getUrlRouterProvider = (uiRouter: UIRouter) => (uiRouter.urlRouterProvider = new UrlRouterProvider(uiRouter)); 144 | 145 | // $state service and $stateProvider 146 | // $urlRouter service and $urlRouterProvider 147 | const getStateProvider = () => extend(router.stateProvider, { $get: () => router.stateService }); 148 | 149 | watchDigests.$inject = ['$rootScope']; 150 | export function watchDigests($rootScope: IRootScopeService) { 151 | $rootScope.$watch(function () { 152 | trace.approximateDigests++; 153 | }); 154 | } 155 | 156 | mod_init.provider('$uiRouter', $uiRouterProvider); 157 | mod_rtr.provider('$urlRouter', ['$uiRouterProvider', getUrlRouterProvider]); 158 | mod_util.provider('$urlService', getProviderFor('urlService')); 159 | mod_util.provider('$urlMatcherFactory', ['$uiRouterProvider', () => router.urlMatcherFactory]); 160 | mod_util.provider('$templateFactory', () => new TemplateFactory()); 161 | mod_state.provider('$stateRegistry', getProviderFor('stateRegistry')); 162 | mod_state.provider('$uiRouterGlobals', getProviderFor('globals')); 163 | mod_state.provider('$transitions', getProviderFor('transitionService')); 164 | mod_state.provider('$state', ['$uiRouterProvider', getStateProvider]); 165 | 166 | mod_state.factory('$stateParams', ['$uiRouter', ($uiRouter: UIRouter) => $uiRouter.globals.params]); 167 | mod_main.factory('$view', () => router.viewService); 168 | mod_main.service('$trace', () => trace); 169 | 170 | mod_main.run(watchDigests); 171 | mod_util.run(['$urlMatcherFactory', function ($urlMatcherFactory: UrlMatcherFactory) {}]); 172 | mod_state.run(['$state', function ($state: StateService) {}]); 173 | mod_rtr.run(['$urlRouter', function ($urlRouter: UrlRouter) {}]); 174 | mod_init.run(runBlock); 175 | 176 | /** @hidden TODO: find a place to move this */ 177 | export const getLocals = (ctx: ResolveContext): TypedMap => { 178 | const tokens = ctx.getTokens().filter(isString); 179 | 180 | const tuples = tokens.map((key) => { 181 | const resolvable = ctx.getResolvable(key); 182 | const waitPolicy = ctx.getPolicy(resolvable).async; 183 | return [key, waitPolicy === 'NOWAIT' ? resolvable.promise : resolvable.data]; 184 | }); 185 | 186 | return tuples.reduce(applyPairs, {}); 187 | }; 188 | -------------------------------------------------------------------------------- /src/stateFilters.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | 3 | import { ng as angular } from './angular'; 4 | import { Obj, StateService, StateOrName } from '@uirouter/core'; 5 | 6 | /** 7 | * `isState` Filter: truthy if the current state is the parameter 8 | * 9 | * Translates to [[StateService.is]] `$state.is("stateName")`. 10 | * 11 | * #### Example: 12 | * ```html 13 | *
show if state is 'stateName'
14 | * ``` 15 | */ 16 | $IsStateFilter.$inject = ['$state']; 17 | function $IsStateFilter($state: StateService) { 18 | const isFilter: any = function (state: StateOrName, params: Obj, options?: { relative?: StateOrName }) { 19 | return $state.is(state, params, options); 20 | }; 21 | isFilter.$stateful = true; 22 | return isFilter; 23 | } 24 | 25 | /** 26 | * `includedByState` Filter: truthy if the current state includes the parameter 27 | * 28 | * Translates to [[StateService.includes]]` $state.is("fullOrPartialStateName")`. 29 | * 30 | * #### Example: 31 | * ```html 32 | *
show if state includes 'fullOrPartialStateName'
33 | * ``` 34 | */ 35 | $IncludedByStateFilter.$inject = ['$state']; 36 | function $IncludedByStateFilter($state: StateService) { 37 | const includesFilter: any = function (state: StateOrName, params: Obj, options: { relative?: StateOrName }) { 38 | return $state.includes(state, params, options); 39 | }; 40 | includesFilter.$stateful = true; 41 | return includesFilter; 42 | } 43 | 44 | angular.module('ui.router.state').filter('isState', $IsStateFilter).filter('includedByState', $IncludedByStateFilter); 45 | 46 | export { $IsStateFilter, $IncludedByStateFilter }; 47 | -------------------------------------------------------------------------------- /src/stateProvider.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | import { 3 | val, 4 | isObject, 5 | createProxyFunctions, 6 | BuilderFunction, 7 | StateRegistry, 8 | StateService, 9 | OnInvalidCallback, 10 | } from '@uirouter/core'; 11 | import { Ng1StateDeclaration } from './interface'; 12 | 13 | /** 14 | * The Angular 1 `StateProvider` 15 | * 16 | * The `$stateProvider` works similar to Angular's v1 router, but it focuses purely 17 | * on state. 18 | * 19 | * A state corresponds to a "place" in the application in terms of the overall UI and 20 | * navigation. A state describes (via the controller / template / view properties) what 21 | * the UI looks like and does at that place. 22 | * 23 | * States often have things in common, and the primary way of factoring out these 24 | * commonalities in this model is via the state hierarchy, i.e. parent/child states aka 25 | * nested states. 26 | * 27 | * The `$stateProvider` provides interfaces to declare these states for your app. 28 | */ 29 | export class StateProvider { 30 | constructor(private stateRegistry: StateRegistry, private stateService: StateService) { 31 | createProxyFunctions(val(StateProvider.prototype), this, val(this)); 32 | } 33 | 34 | /** 35 | * Decorates states when they are registered 36 | * 37 | * Allows you to extend (carefully) or override (at your own peril) the 38 | * `stateBuilder` object used internally by [[StateRegistry]]. 39 | * This can be used to add custom functionality to ui-router, 40 | * for example inferring templateUrl based on the state name. 41 | * 42 | * When passing only a name, it returns the current (original or decorated) builder 43 | * function that matches `name`. 44 | * 45 | * The builder functions that can be decorated are listed below. Though not all 46 | * necessarily have a good use case for decoration, that is up to you to decide. 47 | * 48 | * In addition, users can attach custom decorators, which will generate new 49 | * properties within the state's internal definition. There is currently no clear 50 | * use-case for this beyond accessing internal states (i.e. $state.$current), 51 | * however, expect this to become increasingly relevant as we introduce additional 52 | * meta-programming features. 53 | * 54 | * **Warning**: Decorators should not be interdependent because the order of 55 | * execution of the builder functions in non-deterministic. Builder functions 56 | * should only be dependent on the state definition object and super function. 57 | * 58 | * 59 | * Existing builder functions and current return values: 60 | * 61 | * - **parent** `{object}` - returns the parent state object. 62 | * - **data** `{object}` - returns state data, including any inherited data that is not 63 | * overridden by own values (if any). 64 | * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} 65 | * or `null`. 66 | * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is 67 | * navigable). 68 | * - **params** `{object}` - returns an array of state params that are ensured to 69 | * be a super-set of parent's params. 70 | * - **views** `{object}` - returns a views object where each key is an absolute view 71 | * name (i.e. "viewName@stateName") and each value is the config object 72 | * (template, controller) for the view. Even when you don't use the views object 73 | * explicitly on a state config, one is still created for you internally. 74 | * So by decorating this builder function you have access to decorating template 75 | * and controller properties. 76 | * - **ownParams** `{object}` - returns an array of params that belong to the state, 77 | * not including any params defined by ancestor states. 78 | * - **path** `{string}` - returns the full path from the root down to this state. 79 | * Needed for state activation. 80 | * - **includes** `{object}` - returns an object that includes every state that 81 | * would pass a `$state.includes()` test. 82 | * 83 | * #### Example: 84 | * Override the internal 'views' builder with a function that takes the state 85 | * definition, and a reference to the internal function being overridden: 86 | * ```js 87 | * $stateProvider.decorator('views', function (state, parent) { 88 | * let result = {}, 89 | * views = parent(state); 90 | * 91 | * angular.forEach(views, function (config, name) { 92 | * let autoName = (state.name + '.' + name).replace('.', '/'); 93 | * config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html'; 94 | * result[name] = config; 95 | * }); 96 | * return result; 97 | * }); 98 | * 99 | * $stateProvider.state('home', { 100 | * views: { 101 | * 'contact.list': { controller: 'ListController' }, 102 | * 'contact.item': { controller: 'ItemController' } 103 | * } 104 | * }); 105 | * ``` 106 | * 107 | * 108 | * ```js 109 | * // Auto-populates list and item views with /partials/home/contact/list.html, 110 | * // and /partials/home/contact/item.html, respectively. 111 | * $state.go('home'); 112 | * ``` 113 | * 114 | * @param {string} name The name of the builder function to decorate. 115 | * @param {object} func A function that is responsible for decorating the original 116 | * builder function. The function receives two parameters: 117 | * 118 | * - `{object}` - state - The state config object. 119 | * - `{object}` - super - The original builder function. 120 | * 121 | * @return {object} $stateProvider - $stateProvider instance 122 | */ 123 | decorator(name: string, func: BuilderFunction) { 124 | return this.stateRegistry.decorator(name, func) || this; 125 | } 126 | 127 | /** 128 | * Registers a state 129 | * 130 | * ### This is a passthrough to [[StateRegistry.register]]. 131 | * 132 | * Registers a state configuration under a given state name. 133 | * The stateConfig object has the following acceptable properties. 134 | * 135 | * 136 | * 137 | * - **`template`** - {string|function=} - html template as a string or a function that returns 138 | * an html template as a string which should be used by the uiView directives. This property 139 | * takes precedence over templateUrl. 140 | * 141 | * If `template` is a function, it will be called with the following parameters: 142 | * 143 | * - {array.<object>} - state parameters extracted from the current $location.path() by 144 | * applying the current state 145 | * 146 | * 147 | * 148 | * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html 149 | * template that should be used by uiView. 150 | * 151 | * If `templateUrl` is a function, it will be called with the following parameters: 152 | * 153 | * - {array.<object>} - state parameters extracted from the current $location.path() by 154 | * applying the current state 155 | * 156 | * 157 | * 158 | * - **`templateProvider`** - {function=} - Provider function that returns HTML content 159 | * string. 160 | * 161 | * 162 | * 163 | * - **`controller`** - {string|function=} - Controller fn that should be associated with newly 164 | * related scope or the name of a registered controller if passed as a string. 165 | * 166 | * 167 | * 168 | * - **`controllerProvider`** - {function=} - Injectable provider function that returns 169 | * the actual controller or string. 170 | * 171 | * 172 | * 173 | * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be 174 | * published to scope under the controllerAs name. 175 | * 176 | * 177 | * 178 | * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which 179 | * should be injected into the controller. If any of these dependencies are promises, 180 | * the router will wait for them all to be resolved or one to be rejected before the 181 | * controller is instantiated. If all the promises are resolved successfully, the values 182 | * of the resolved promises are injected and $stateChangeSuccess event is fired. If any 183 | * of the promises are rejected the $stateChangeError event is fired. The map object is: 184 | * 185 | * - key - {string}: name of dependency to be injected into controller 186 | * - factory - {string|function}: If string then it is alias for service. Otherwise if function, 187 | * it is injected and return value it treated as dependency. If result is a promise, it is 188 | * resolved before its value is injected into controller. 189 | * 190 | * 191 | * 192 | * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or 193 | * transitioned to, the `$stateParams` service will be populated with any 194 | * parameters that were passed. 195 | * 196 | * 197 | * 198 | * - **`params`** - {object=} - An array of parameter names or regular expressions. Only 199 | * use this within a state if you are not using url. Otherwise you can specify your 200 | * parameters within the url. When a state is navigated or transitioned to, the 201 | * $stateParams service will be populated with any parameters that were passed. 202 | * 203 | * 204 | * 205 | * - **`views`** - {object=} - Use the views property to set up multiple views or to target views 206 | * manually/explicitly. 207 | * 208 | * 209 | * 210 | * - **`abstract`** - {boolean=} - An abstract state will never be directly activated, 211 | * but can provide inherited properties to its common children states. 212 | * 213 | * 214 | * 215 | * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way 216 | * to trigger an action or dispatch an event, such as opening a dialog. 217 | * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. 218 | * 219 | * 220 | * 221 | * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to 222 | * trigger an action or dispatch an event, such as opening a dialog. 223 | * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. 224 | * 225 | * 226 | * 227 | * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state 228 | * just because a search/query parameter has changed (via $location.search() or $location.hash()). 229 | * Useful for when you'd like to modify $location.search() without triggering a reload. 230 | * 231 | * 232 | * 233 | * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration. 234 | * 235 | * #### Example: 236 | * Some state name examples 237 | * ```js 238 | * // stateName can be a single top-level name (must be unique). 239 | * $stateProvider.state("home", {}); 240 | * 241 | * // Or it can be a nested state name. This state is a child of the 242 | * // above "home" state. 243 | * $stateProvider.state("home.newest", {}); 244 | * 245 | * // Nest states as deeply as needed. 246 | * $stateProvider.state("home.newest.abc.xyz.inception", {}); 247 | * 248 | * // state() returns $stateProvider, so you can chain state declarations. 249 | * $stateProvider 250 | * .state("home", {}) 251 | * .state("about", {}) 252 | * .state("contacts", {}); 253 | * ``` 254 | * 255 | * @param {string} name A unique state name, e.g. "home", "about", "contacts". 256 | * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". 257 | * @param {object} definition State configuration object. 258 | */ 259 | state(name: string, definition: Ng1StateDeclaration): StateProvider; 260 | state(definition: Ng1StateDeclaration): StateProvider; 261 | state(name: any, definition?: any) { 262 | if (isObject(name)) { 263 | definition = name; 264 | } else { 265 | definition.name = name; 266 | } 267 | this.stateRegistry.register(definition); 268 | return this; 269 | } 270 | 271 | /** 272 | * Registers an invalid state handler 273 | * 274 | * This is a passthrough to [[StateService.onInvalid]] for ng1. 275 | */ 276 | 277 | onInvalid(callback: OnInvalidCallback): Function { 278 | return this.stateService.onInvalid(callback); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/statebuilders/onEnterExitRetain.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | import { 3 | StateObject, 4 | TransitionStateHookFn, 5 | HookResult, 6 | Transition, 7 | services, 8 | ResolveContext, 9 | extend, 10 | } from '@uirouter/core'; 11 | import { getLocals } from '../services'; 12 | import { Ng1StateDeclaration } from '../interface'; 13 | 14 | /** 15 | * This is a [[StateBuilder.builder]] function for angular1 `onEnter`, `onExit`, 16 | * `onRetain` callback hooks on a [[Ng1StateDeclaration]]. 17 | * 18 | * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder 19 | * ensures that those hooks are injectable for @uirouter/angularjs (ng1). 20 | * 21 | * @internalapi 22 | */ 23 | export const getStateHookBuilder = (hookName: 'onEnter' | 'onExit' | 'onRetain') => 24 | function stateHookBuilder(stateObject: StateObject): TransitionStateHookFn { 25 | const hook = stateObject[hookName]; 26 | const pathname = hookName === 'onExit' ? 'from' : 'to'; 27 | 28 | function decoratedNg1Hook(trans: Transition, state: Ng1StateDeclaration): HookResult { 29 | const resolveContext = new ResolveContext(trans.treeChanges(pathname)); 30 | const subContext = resolveContext.subContext(state.$$state()); 31 | const locals = extend(getLocals(subContext), { $state$: state, $transition$: trans }); 32 | return services.$injector.invoke(hook, this, locals); 33 | } 34 | 35 | return hook ? decoratedNg1Hook : undefined; 36 | }; 37 | -------------------------------------------------------------------------------- /src/statebuilders/views.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | import { 3 | StateObject, 4 | pick, 5 | forEach, 6 | tail, 7 | extend, 8 | isArray, 9 | isInjectable, 10 | isDefined, 11 | isString, 12 | services, 13 | trace, 14 | ViewConfig, 15 | ViewService, 16 | ViewConfigFactory, 17 | PathNode, 18 | ResolveContext, 19 | Resolvable, 20 | IInjectable, 21 | } from '@uirouter/core'; 22 | import { Ng1ViewDeclaration } from '../interface'; 23 | import { TemplateFactory } from '../templateFactory'; 24 | 25 | /** @internalapi */ 26 | export function getNg1ViewConfigFactory(): ViewConfigFactory { 27 | let templateFactory: TemplateFactory = null; 28 | return (path, view) => { 29 | templateFactory = templateFactory || services.$injector.get('$templateFactory'); 30 | return [new Ng1ViewConfig(path, view, templateFactory)]; 31 | }; 32 | } 33 | 34 | /** @internalapi */ 35 | const hasAnyKey = (keys, obj) => keys.reduce((acc, key) => acc || isDefined(obj[key]), false); 36 | 37 | /** 38 | * This is a [[StateBuilder.builder]] function for angular1 `views`. 39 | * 40 | * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder 41 | * handles the `views` property with logic specific to @uirouter/angularjs (ng1). 42 | * 43 | * If no `views: {}` property exists on the [[StateDeclaration]], then it creates the `views` object 44 | * and applies the state-level configuration to a view named `$default`. 45 | * 46 | * @internalapi 47 | */ 48 | export function ng1ViewsBuilder(state: StateObject) { 49 | // Do not process root state 50 | if (!state.parent) return {}; 51 | 52 | const tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'], 53 | ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'], 54 | compKeys = ['component', 'bindings', 'componentProvider'], 55 | nonCompKeys = tplKeys.concat(ctrlKeys), 56 | allViewKeys = compKeys.concat(nonCompKeys); 57 | 58 | // Do not allow a state to have both state-level props and also a `views: {}` property. 59 | // A state without a `views: {}` property can declare properties for the `$default` view as properties of the state. 60 | // However, the `$default` approach should not be mixed with a separate `views: ` block. 61 | if (isDefined(state.views) && hasAnyKey(allViewKeys, state)) { 62 | throw new Error( 63 | `State '${state.name}' has a 'views' object. ` + 64 | `It cannot also have "view properties" at the state level. ` + 65 | `Move the following properties into a view (in the 'views' object): ` + 66 | ` ${allViewKeys.filter((key) => isDefined(state[key])).join(', ')}` 67 | ); 68 | } 69 | 70 | const views: { [key: string]: Ng1ViewDeclaration } = {}, 71 | viewsObject = state.views || { $default: pick(state, allViewKeys) }; 72 | 73 | forEach(viewsObject, function (config: Ng1ViewDeclaration, name: string) { 74 | // Account for views: { "": { template... } } 75 | name = name || '$default'; 76 | // Account for views: { header: "headerComponent" } 77 | if (isString(config)) config = { component: config }; 78 | 79 | // Make a shallow copy of the config object 80 | config = extend({}, config); 81 | 82 | // Do not allow a view to mix props for component-style view with props for template/controller-style view 83 | if (hasAnyKey(compKeys, config) && hasAnyKey(nonCompKeys, config)) { 84 | throw new Error( 85 | `Cannot combine: ${compKeys.join('|')} with: ${nonCompKeys.join('|')} in stateview: '${name}@${state.name}'` 86 | ); 87 | } 88 | 89 | config.resolveAs = config.resolveAs || '$resolve'; 90 | config.$type = 'ng1'; 91 | config.$context = state; 92 | config.$name = name; 93 | 94 | const normalized = ViewService.normalizeUIViewTarget(config.$context, config.$name); 95 | config.$uiViewName = normalized.uiViewName; 96 | config.$uiViewContextAnchor = normalized.uiViewContextAnchor; 97 | 98 | views[name] = config; 99 | }); 100 | return views; 101 | } 102 | 103 | /** @hidden */ 104 | let id = 0; 105 | 106 | /** @internalapi */ 107 | export class Ng1ViewConfig implements ViewConfig { 108 | $id = id++; 109 | loaded = false; 110 | controller: Function; // actually IInjectable|string 111 | template: string; 112 | component: string; 113 | locals: any; // TODO: delete me 114 | 115 | constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration, public factory: TemplateFactory) {} 116 | 117 | load() { 118 | const $q = services.$q; 119 | const context = new ResolveContext(this.path); 120 | const params = this.path.reduce((acc, node) => extend(acc, node.paramValues), {}); 121 | 122 | const promises: any = { 123 | template: $q.when(this.factory.fromConfig(this.viewDecl, params, context)), 124 | controller: $q.when(this.getController(context)), 125 | }; 126 | 127 | return $q.all(promises).then((results) => { 128 | trace.traceViewServiceEvent('Loaded', this); 129 | this.controller = results.controller; 130 | extend(this, results.template); // Either { template: "tpl" } or { component: "cmpName" } 131 | return this; 132 | }); 133 | } 134 | 135 | getTemplate = (uiView, context: ResolveContext) => 136 | this.component 137 | ? this.factory.makeComponentTemplate(uiView, context, this.component, this.viewDecl.bindings) 138 | : this.template; 139 | 140 | /** 141 | * Gets the controller for a view configuration. 142 | * 143 | * @returns {Function|Promise.} Returns a controller, or a promise that resolves to a controller. 144 | */ 145 | getController(context: ResolveContext): IInjectable | string | Promise { 146 | const provider = this.viewDecl.controllerProvider; 147 | if (!isInjectable(provider)) return this.viewDecl.controller; 148 | const deps = services.$injector.annotate(provider); 149 | const providerFn = isArray(provider) ? tail(provider) : provider; 150 | const resolvable = new Resolvable('', providerFn, deps); 151 | return resolvable.get(context); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/templateFactory.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module view */ /** */ 2 | import { ng as angular } from './angular'; 3 | import { IAugmentedJQuery } from 'angular'; 4 | import { 5 | isArray, 6 | isDefined, 7 | isFunction, 8 | isObject, 9 | services, 10 | Obj, 11 | IInjectable, 12 | tail, 13 | kebobString, 14 | unnestR, 15 | ResolveContext, 16 | Resolvable, 17 | RawParams, 18 | } from '@uirouter/core'; 19 | import { Ng1ViewDeclaration, TemplateFactoryProvider } from './interface'; 20 | 21 | /** 22 | * Service which manages loading of templates from a ViewConfig. 23 | */ 24 | export class TemplateFactory implements TemplateFactoryProvider { 25 | /** @hidden */ private _useHttp = angular.version.minor < 3; 26 | /** @hidden */ private $templateRequest; 27 | /** @hidden */ private $templateCache; 28 | /** @hidden */ private $http; 29 | 30 | /** @hidden */ $get = [ 31 | '$http', 32 | '$templateCache', 33 | '$injector', 34 | ($http, $templateCache, $injector) => { 35 | this.$templateRequest = $injector.has && $injector.has('$templateRequest') && $injector.get('$templateRequest'); 36 | this.$http = $http; 37 | this.$templateCache = $templateCache; 38 | return this; 39 | }, 40 | ]; 41 | 42 | /** @hidden */ 43 | useHttpService(value: boolean) { 44 | this._useHttp = value; 45 | } 46 | 47 | /** 48 | * Creates a template from a configuration object. 49 | * 50 | * @param config Configuration object for which to load a template. 51 | * The following properties are search in the specified order, and the first one 52 | * that is defined is used to create the template: 53 | * 54 | * @param params Parameters to pass to the template function. 55 | * @param context The resolve context associated with the template's view 56 | * 57 | * @return {string|object} The template html as a string, or a promise for 58 | * that string,or `null` if no template is configured. 59 | */ 60 | fromConfig( 61 | config: Ng1ViewDeclaration, 62 | params: any, 63 | context: ResolveContext 64 | ): Promise<{ template?: string; component?: string }> { 65 | const defaultTemplate = ''; 66 | 67 | const asTemplate = (result) => services.$q.when(result).then((str) => ({ template: str })); 68 | const asComponent = (result) => services.$q.when(result).then((str) => ({ component: str })); 69 | 70 | return isDefined(config.template) 71 | ? asTemplate(this.fromString(config.template, params)) 72 | : isDefined(config.templateUrl) 73 | ? asTemplate(this.fromUrl(config.templateUrl, params)) 74 | : isDefined(config.templateProvider) 75 | ? asTemplate(this.fromProvider(config.templateProvider, params, context)) 76 | : isDefined(config.component) 77 | ? asComponent(config.component) 78 | : isDefined(config.componentProvider) 79 | ? asComponent(this.fromComponentProvider(config.componentProvider, params, context)) 80 | : asTemplate(defaultTemplate); 81 | } 82 | 83 | /** 84 | * Creates a template from a string or a function returning a string. 85 | * 86 | * @param template html template as a string or function that returns an html template as a string. 87 | * @param params Parameters to pass to the template function. 88 | * 89 | * @return {string|object} The template html as a string, or a promise for that 90 | * string. 91 | */ 92 | fromString(template: string | Function, params?: RawParams) { 93 | return isFunction(template) ? (template)(params) : template; 94 | } 95 | 96 | /** 97 | * Loads a template from the a URL via `$http` and `$templateCache`. 98 | * 99 | * @param {string|Function} url url of the template to load, or a function 100 | * that returns a url. 101 | * @param {Object} params Parameters to pass to the url function. 102 | * @return {string|Promise.} The template html as a string, or a promise 103 | * for that string. 104 | */ 105 | fromUrl(url: string | Function, params: any) { 106 | if (isFunction(url)) url = (url)(params); 107 | if (url == null) return null; 108 | 109 | if (this._useHttp) { 110 | return this.$http 111 | .get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' } }) 112 | .then(function (response) { 113 | return response.data; 114 | }); 115 | } 116 | 117 | return this.$templateRequest(url); 118 | } 119 | 120 | /** 121 | * Creates a template by invoking an injectable provider function. 122 | * 123 | * @param provider Function to invoke via `locals` 124 | * @param {Function} injectFn a function used to invoke the template provider 125 | * @return {string|Promise.} The template html as a string, or a promise 126 | * for that string. 127 | */ 128 | fromProvider(provider: IInjectable, params: any, context: ResolveContext) { 129 | const deps = services.$injector.annotate(provider); 130 | const providerFn = isArray(provider) ? tail(provider) : provider; 131 | const resolvable = new Resolvable('', providerFn, deps); 132 | return resolvable.get(context); 133 | } 134 | 135 | /** 136 | * Creates a component's template by invoking an injectable provider function. 137 | * 138 | * @param provider Function to invoke via `locals` 139 | * @param {Function} injectFn a function used to invoke the template provider 140 | * @return {string} The template html as a string: "". 141 | */ 142 | fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) { 143 | const deps = services.$injector.annotate(provider); 144 | const providerFn = isArray(provider) ? tail(provider) : provider; 145 | const resolvable = new Resolvable('', providerFn, deps); 146 | return resolvable.get(context); 147 | } 148 | 149 | /** 150 | * Creates a template from a component's name 151 | * 152 | * This implements route-to-component. 153 | * It works by retrieving the component (directive) metadata from the injector. 154 | * It analyses the component's bindings, then constructs a template that instantiates the component. 155 | * The template wires input and output bindings to resolves or from the parent component. 156 | * 157 | * @param uiView {object} The parent ui-view (for binding outputs to callbacks) 158 | * @param context The ResolveContext (for binding outputs to callbacks returned from resolves) 159 | * @param component {string} Component's name in camel case. 160 | * @param bindings An object defining the component's bindings: {foo: '<'} 161 | * @return {string} The template as a string: "". 162 | */ 163 | makeComponentTemplate(uiView: IAugmentedJQuery, context: ResolveContext, component: string, bindings?: any) { 164 | bindings = bindings || {}; 165 | 166 | // Bind once prefix 167 | const prefix = angular.version.minor >= 3 ? '::' : ''; 168 | // Convert to kebob name. Add x- prefix if the string starts with `x-` or `data-` 169 | const kebob = (camelCase: string) => { 170 | const kebobed = kebobString(camelCase); 171 | return /^(x|data)-/.exec(kebobed) ? `x-${kebobed}` : kebobed; 172 | }; 173 | 174 | const attributeTpl = (input: BindingTuple) => { 175 | const { name, type } = input; 176 | const attrName = kebob(name); 177 | // If the ui-view has an attribute which matches a binding on the routed component 178 | // then pass that attribute through to the routed component template. 179 | // Prefer ui-view wired mappings to resolve data, unless the resolve was explicitly bound using `bindings:` 180 | if (uiView.attr(attrName) && !bindings[name]) return `${attrName}='${uiView.attr(attrName)}'`; 181 | 182 | const resolveName = bindings[name] || name; 183 | // Pre-evaluate the expression for "@" bindings by enclosing in {{ }} 184 | // some-attr="{{ ::$resolve.someResolveName }}" 185 | if (type === '@') return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`; 186 | 187 | // Wire "&" callbacks to resolves that return a callback function 188 | // Get the result of the resolve (should be a function) and annotate it to get its arguments. 189 | // some-attr="$resolve.someResolveResultName(foo, bar)" 190 | if (type === '&') { 191 | const res = context.getResolvable(resolveName); 192 | const fn = res && res.data; 193 | const args = (fn && services.$injector.annotate(fn)) || []; 194 | // account for array style injection, i.e., ['foo', function(foo) {}] 195 | const arrayIdxStr = isArray(fn) ? `[${fn.length - 1}]` : ''; 196 | return `${attrName}='$resolve.${resolveName}${arrayIdxStr}(${args.join(',')})'`; 197 | } 198 | 199 | // some-attr="::$resolve.someResolveName" 200 | return `${attrName}='${prefix}$resolve.${resolveName}'`; 201 | }; 202 | 203 | const attrs = getComponentBindings(component).map(attributeTpl).join(' '); 204 | const kebobName = kebob(component); 205 | return `<${kebobName} ${attrs}>`; 206 | } 207 | } 208 | 209 | // Gets all the directive(s)' inputs ('@', '=', and '<') and outputs ('&') 210 | function getComponentBindings(name: string) { 211 | const cmpDefs = services.$injector.get(name + 'Directive'); // could be multiple 212 | if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`); 213 | return cmpDefs.map(getBindings).reduce(unnestR, []); 214 | } 215 | 216 | // Given a directive definition, find its object input attributes 217 | // Use different properties, depending on the type of directive (component, bindToController, normal) 218 | const getBindings = (def: any) => { 219 | if (isObject(def.bindToController)) return scopeBindings(def.bindToController); 220 | return scopeBindings(def.scope); 221 | }; 222 | 223 | interface BindingTuple { 224 | name: string; 225 | type: string; 226 | } 227 | 228 | // for ng 1.2 style, process the scope: { input: "=foo" } 229 | // for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object 230 | const scopeBindings = (bindingsObj: Obj) => 231 | Object.keys(bindingsObj || {}) 232 | // [ 'input', [ '=foo', '=', 'foo' ] ] 233 | .map((key) => [key, /^([=<@&])[?]?(.*)/.exec(bindingsObj[key])]) 234 | // skip malformed values 235 | .filter((tuple) => isDefined(tuple) && isArray(tuple[1])) 236 | // { name: ('foo' || 'input'), type: '=' } 237 | .map((tuple) => ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] } as BindingTuple)); 238 | -------------------------------------------------------------------------------- /src/urlRouterProvider.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module url */ /** */ 2 | import { 3 | UIRouter, 4 | LocationServices, 5 | $InjectorLike, 6 | BaseUrlRule, 7 | UrlRuleHandlerFn, 8 | UrlMatcher, 9 | IInjectable, 10 | UrlRouter, 11 | } from '@uirouter/core'; 12 | import { services, isString, isFunction, isArray, identity } from '@uirouter/core'; 13 | 14 | export interface RawNg1RuleFunction { 15 | ($injector: $InjectorLike, $location: LocationServices): string | void; 16 | } 17 | 18 | /** 19 | * Manages rules for client-side URL 20 | * 21 | * ### Deprecation warning: 22 | * This class is now considered to be an internal API 23 | * Use the [[UrlService]] instead. 24 | * For configuring URL rules, use the [[UrlRulesApi]] which can be found as [[UrlService.rules]]. 25 | * 26 | * This class manages the router rules for what to do when the URL changes. 27 | * 28 | * This provider remains for backwards compatibility. 29 | * 30 | * @internalapi 31 | * @deprecated 32 | */ 33 | export class UrlRouterProvider { 34 | static injectableHandler(router: UIRouter, handler: IInjectable): UrlRuleHandlerFn { 35 | return (match) => services.$injector.invoke(handler, null, { $match: match, $stateParams: router.globals.params }); 36 | } 37 | 38 | /** @hidden */ 39 | constructor(/** @hidden */ private router: UIRouter) {} 40 | 41 | /** @hidden */ 42 | $get(): UrlRouter { 43 | const urlService = this.router.urlService; 44 | this.router.urlRouter.update(true); 45 | if (!urlService.interceptDeferred) urlService.listen(); 46 | return this.router.urlRouter; 47 | } 48 | 49 | /** 50 | * Registers a url handler function. 51 | * 52 | * Registers a low level url handler (a `rule`). 53 | * A rule detects specific URL patterns and returns a redirect, or performs some action. 54 | * 55 | * If a rule returns a string, the URL is replaced with the string, and all rules are fired again. 56 | * 57 | * #### Example: 58 | * ```js 59 | * var app = angular.module('app', ['ui.router.router']); 60 | * 61 | * app.config(function ($urlRouterProvider) { 62 | * // Here's an example of how you might allow case insensitive urls 63 | * $urlRouterProvider.rule(function ($injector, $location) { 64 | * var path = $location.path(), 65 | * normalized = path.toLowerCase(); 66 | * 67 | * if (path !== normalized) { 68 | * return normalized; 69 | * } 70 | * }); 71 | * }); 72 | * ``` 73 | * 74 | * @param ruleFn 75 | * Handler function that takes `$injector` and `$location` services as arguments. 76 | * You can use them to detect a url and return a different url as a string. 77 | * 78 | * @return [[UrlRouterProvider]] (`this`) 79 | */ 80 | rule(ruleFn: RawNg1RuleFunction): UrlRouterProvider { 81 | if (!isFunction(ruleFn)) throw new Error("'rule' must be a function"); 82 | 83 | const match = () => ruleFn(services.$injector, this.router.locationService); 84 | 85 | const rule = new BaseUrlRule(match, identity); 86 | this.router.urlService.rules.rule(rule); 87 | return this; 88 | } 89 | 90 | /** 91 | * Defines the path or behavior to use when no url can be matched. 92 | * 93 | * #### Example: 94 | * ```js 95 | * var app = angular.module('app', ['ui.router.router']); 96 | * 97 | * app.config(function ($urlRouterProvider) { 98 | * // if the path doesn't match any of the urls you configured 99 | * // otherwise will take care of routing the user to the 100 | * // specified url 101 | * $urlRouterProvider.otherwise('/index'); 102 | * 103 | * // Example of using function rule as param 104 | * $urlRouterProvider.otherwise(function ($injector, $location) { 105 | * return '/a/valid/url'; 106 | * }); 107 | * }); 108 | * ``` 109 | * 110 | * @param rule 111 | * The url path you want to redirect to or a function rule that returns the url path or performs a `$state.go()`. 112 | * The function version is passed two params: `$injector` and `$location` services, and should return a url string. 113 | * 114 | * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance 115 | */ 116 | otherwise(rule: string | RawNg1RuleFunction): UrlRouterProvider { 117 | const urlRules = this.router.urlService.rules; 118 | if (isString(rule)) { 119 | urlRules.otherwise(rule); 120 | } else if (isFunction(rule)) { 121 | urlRules.otherwise(() => rule(services.$injector, this.router.locationService)); 122 | } else { 123 | throw new Error("'rule' must be a string or function"); 124 | } 125 | 126 | return this; 127 | } 128 | 129 | /** 130 | * Registers a handler for a given url matching. 131 | * 132 | * If the handler is a string, it is 133 | * treated as a redirect, and is interpolated according to the syntax of match 134 | * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). 135 | * 136 | * If the handler is a function, it is injectable. 137 | * It gets invoked if `$location` matches. 138 | * You have the option of inject the match object as `$match`. 139 | * 140 | * The handler can return 141 | * 142 | * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` 143 | * will continue trying to find another one that matches. 144 | * - **string** which is treated as a redirect and passed to `$location.url()` 145 | * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. 146 | * 147 | * #### Example: 148 | * ```js 149 | * var app = angular.module('app', ['ui.router.router']); 150 | * 151 | * app.config(function ($urlRouterProvider) { 152 | * $urlRouterProvider.when($state.url, function ($match, $stateParams) { 153 | * if ($state.$current.navigable !== state || 154 | * !equalForKeys($match, $stateParams) { 155 | * $state.transitionTo(state, $match, false); 156 | * } 157 | * }); 158 | * }); 159 | * ``` 160 | * 161 | * @param what A pattern string to match, compiled as a [[UrlMatcher]]. 162 | * @param handler The path (or function that returns a path) that you want to redirect your user to. 163 | * @param ruleCallback [optional] A callback that receives the `rule` registered with [[UrlMatcher.rule]] 164 | * 165 | * Note: the handler may also invoke arbitrary code, such as `$state.go()` 166 | */ 167 | when(what: RegExp | UrlMatcher | string, handler: string | IInjectable): this { 168 | if (isArray(handler) || isFunction(handler)) { 169 | handler = UrlRouterProvider.injectableHandler(this.router, handler); 170 | } 171 | 172 | this.router.urlService.rules.when(what, handler as any); 173 | return this; 174 | } 175 | 176 | /** 177 | * Disables monitoring of the URL. 178 | * 179 | * Call this method before UI-Router has bootstrapped. 180 | * It will stop UI-Router from performing the initial url sync. 181 | * 182 | * This can be useful to perform some asynchronous initialization before the router starts. 183 | * Once the initialization is complete, call [[listen]] to tell UI-Router to start watching and synchronizing the URL. 184 | * 185 | * #### Example: 186 | * ```js 187 | * var app = angular.module('app', ['ui.router']); 188 | * 189 | * app.config(function ($urlRouterProvider) { 190 | * // Prevent $urlRouter from automatically intercepting URL changes; 191 | * $urlRouterProvider.deferIntercept(); 192 | * }) 193 | * 194 | * app.run(function (MyService, $urlRouter, $http) { 195 | * $http.get("/stuff").then(function(resp) { 196 | * MyService.doStuff(resp.data); 197 | * $urlRouter.listen(); 198 | * $urlRouter.sync(); 199 | * }); 200 | * }); 201 | * ``` 202 | * 203 | * @param defer Indicates whether to defer location change interception. 204 | * Passing no parameter is equivalent to `true`. 205 | */ 206 | deferIntercept(defer?: boolean): void { 207 | this.router.urlService.deferIntercept(defer); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/viewScroll.ts: -------------------------------------------------------------------------------- 1 | /** @publicapi @module ng1 */ /** */ 2 | import { ng as angular } from './angular'; 3 | import { IServiceProviderFactory } from 'angular'; 4 | import IAnchorScrollService = angular.IAnchorScrollService; 5 | import ITimeoutService = angular.ITimeoutService; 6 | 7 | export interface UIViewScrollProvider { 8 | /** 9 | * Uses standard anchorScroll behavior 10 | * 11 | * Reverts [[$uiViewScroll]] back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) 12 | * service for scrolling based on the url anchor. 13 | */ 14 | useAnchorScroll(): void; 15 | } 16 | 17 | /** @hidden */ 18 | function $ViewScrollProvider() { 19 | let useAnchorScroll = false; 20 | 21 | this.useAnchorScroll = function () { 22 | useAnchorScroll = true; 23 | }; 24 | 25 | this.$get = [ 26 | '$anchorScroll', 27 | '$timeout', 28 | function ($anchorScroll: IAnchorScrollService, $timeout: ITimeoutService): Function { 29 | if (useAnchorScroll) { 30 | return $anchorScroll; 31 | } 32 | 33 | return function ($element: JQuery) { 34 | return $timeout( 35 | function () { 36 | $element[0].scrollIntoView(); 37 | }, 38 | 0, 39 | false 40 | ); 41 | }; 42 | }, 43 | ]; 44 | } 45 | 46 | angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); 47 | -------------------------------------------------------------------------------- /test/angular/jest-angular.js: -------------------------------------------------------------------------------- 1 | // This setup is used to inject specific versions of angularjs to test against 2 | 3 | // Jest is configured to alias `import * as angular from 'angular'` to this file in jest.config.js. 4 | // This file then imports angularjs bundle via the 'jest-angular-import' module alias which maps to 5 | // a specific version of the angularjs bundle. 6 | // It then exports the window.angular for use in tests that import from 'angular' 7 | 8 | require('jest-angular-import'); 9 | module.exports = window.angular; 10 | -------------------------------------------------------------------------------- /test/angular/update_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for NGVER in 1.* ; do 4 | pushd $NGVER 5 | for PKG in angular angular-animate angular-mocks ; do 6 | curl -O -L https://unpkg.com/${PKG}@${NGVER}/$PKG.js 7 | done 8 | popd 9 | done 10 | -------------------------------------------------------------------------------- /test/jest.init.ts: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | Object.defineProperty(window, 'scrollTo', { value: noop, writable: true }); 3 | 4 | import 'angular'; 5 | import 'angular-mocks'; 6 | import 'angular-animate'; 7 | import './util/testUtilsNg1'; 8 | 9 | require('../src/index'); 10 | -------------------------------------------------------------------------------- /test/ng1StateBuilderSpec.ts: -------------------------------------------------------------------------------- 1 | import { StateBuilder, StateMatcher, ng1ViewsBuilder, extend } from '../src/index'; 2 | import { Resolvable } from '@uirouter/core'; 3 | declare var inject; 4 | 5 | describe('Ng1 StateBuilder', function () { 6 | const parent = { name: '' }; 7 | let builder, 8 | matcher, 9 | urlMatcherFactoryProvider: any = { 10 | compile: function () {}, 11 | isMatcher: function () {}, 12 | }; 13 | 14 | beforeEach(function () { 15 | matcher = new StateMatcher({}); 16 | builder = new StateBuilder(matcher, urlMatcherFactoryProvider); 17 | builder.builder('views', ng1ViewsBuilder); 18 | }); 19 | 20 | it('should use the state object to build a default view, when no `views` property is found', function () { 21 | const config = { url: '/foo', templateUrl: '/foo.html', controller: 'FooController', parent: parent }; 22 | const built = builder.builder('views')(config); 23 | 24 | expect(built.$default).not.toEqual(config); 25 | expect(built.$default).toEqual( 26 | expect.objectContaining({ 27 | templateUrl: '/foo.html', 28 | controller: 'FooController', 29 | resolveAs: '$resolve', 30 | }) 31 | ); 32 | }); 33 | 34 | it('It should use the views object to build views, when defined', function () { 35 | const config = { a: { foo: 'bar', controller: 'FooController' } }; 36 | const builtViews = builder.builder('views')({ parent: parent, views: config }); 37 | expect(builtViews.a.foo).toEqual(config.a.foo); 38 | expect(builtViews.a.controller).toEqual(config.a.controller); 39 | }); 40 | 41 | it('should not allow a view config with both component and template keys', inject(function ($injector) { 42 | const config = { 43 | name: 'foo', 44 | url: '/foo', 45 | template: '

hey

', 46 | controller: 'FooController', 47 | parent: parent, 48 | }; 49 | expect(() => builder.builder('views')(config)).not.toThrow(); 50 | expect(() => builder.builder('views')(extend({ component: 'fooComponent' }, config))).toThrow(); 51 | expect(() => builder.builder('views')(extend({ componentProvider: () => 'fooComponent' }, config))).toThrow(); 52 | expect(() => builder.builder('views')(extend({ bindings: {} }, config))).toThrow(); 53 | })); 54 | 55 | it('should replace a resolve: string value with a function that injects the service of the same name', inject(function ( 56 | $injector 57 | ) { 58 | const config = { resolve: { foo: 'bar' } }; 59 | expect(builder.builder('resolvables')).toBeDefined(); 60 | const built: Resolvable[] = builder.builder('resolvables')(config); 61 | expect(built[0].deps).toEqual(['bar']); 62 | })); 63 | }); 64 | -------------------------------------------------------------------------------- /test/servicesSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { UIRouter, trace } from '@uirouter/core'; 3 | 4 | declare var inject; 5 | 6 | const module = angular['mock'].module; 7 | 8 | describe('UI-Router services', () => { 9 | let $uiRouterProvider: UIRouter, $uiRouter: UIRouter; 10 | let providers; 11 | let services; 12 | 13 | beforeEach( 14 | module('ui.router', function ( 15 | _$uiRouterProvider_, 16 | $urlMatcherFactoryProvider, 17 | $urlRouterProvider, 18 | $stateRegistryProvider, 19 | $uiRouterGlobalsProvider, 20 | $transitionsProvider, 21 | $stateProvider 22 | ) { 23 | $uiRouterProvider = _$uiRouterProvider_; 24 | 25 | expect($uiRouterProvider['router']).toBe($uiRouterProvider); 26 | 27 | providers = { 28 | $uiRouterProvider, 29 | $urlMatcherFactoryProvider, 30 | $urlRouterProvider, 31 | $stateRegistryProvider, 32 | $uiRouterGlobalsProvider, 33 | $transitionsProvider, 34 | $stateProvider, 35 | }; 36 | }) 37 | ); 38 | 39 | beforeEach(inject(function ( 40 | _$uiRouter_, 41 | $urlMatcherFactory, 42 | $urlRouter, 43 | $stateRegistry, 44 | $uiRouterGlobals, 45 | $transitions, 46 | $state, 47 | $stateParams, 48 | $templateFactory, 49 | $view, 50 | $trace 51 | ) { 52 | $uiRouter = _$uiRouter_; 53 | 54 | services = { 55 | $urlMatcherFactory, 56 | $urlRouter, 57 | $stateRegistry, 58 | $uiRouterGlobals, 59 | $transitions, 60 | $state, 61 | $stateParams, 62 | $templateFactory, 63 | $view, 64 | $trace, 65 | }; 66 | })); 67 | 68 | it('Should expose ui-router providers from the UIRouter instance', () => { 69 | expect(providers.$urlMatcherFactoryProvider).toBe($uiRouterProvider.urlMatcherFactory); 70 | expect(providers.$urlRouterProvider).toBe($uiRouterProvider.urlRouterProvider); 71 | expect(providers.$stateRegistryProvider).toBe($uiRouterProvider.stateRegistry); 72 | expect(providers.$uiRouterGlobalsProvider).toBe($uiRouterProvider.globals); 73 | expect(providers.$transitionsProvider).toBe($uiRouterProvider.transitionService); 74 | expect(providers.$stateProvider).toBe($uiRouterProvider.stateProvider); 75 | }); 76 | 77 | it('Should expose ui-router services from the UIRouter instance', () => { 78 | expect($uiRouter).toBe($uiRouterProvider); 79 | expect(services.$urlMatcherFactory).toBe($uiRouter.urlMatcherFactory); 80 | expect(services.$urlRouter).toBe($uiRouter.urlRouter); 81 | expect(services.$stateRegistry).toBe($uiRouter.stateRegistry); 82 | expect(services.$uiRouterGlobals).toBe($uiRouter.globals); 83 | expect(services.$transitions).toBe($uiRouter.transitionService); 84 | expect(services.$state).toBe($uiRouter.stateService); 85 | expect(services.$stateParams).toBe($uiRouter.globals.params); 86 | expect(services.$templateFactory.constructor.name).toBe('TemplateFactory'); 87 | expect(services.$view).toBe($uiRouter.viewService); 88 | expect(services.$trace).toBe(trace); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/stateEventsSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { StateDeclaration } from '@uirouter/core'; 3 | import { resolvedError, obj, decorateExceptionHandler } from './util/testUtilsNg1'; 4 | import '../src/legacy/stateEvents'; 5 | 6 | declare var inject; 7 | 8 | const module = angular['mock'].module; 9 | 10 | describe('UI-Router v0.2.x $state events', function () { 11 | let $injector, stateProvider; 12 | 13 | beforeEach( 14 | module('ui.router.state.events', function ($stateEventsProvider, $exceptionHandlerProvider) { 15 | $stateEventsProvider.enable(); 16 | decorateExceptionHandler($exceptionHandlerProvider); 17 | }) 18 | ); 19 | 20 | let log, logEvents, logEnterExit; 21 | function eventLogger(event, to, toParams, from, fromParams) { 22 | if (logEvents && angular.isFunction(to.to)) { 23 | const transition = to; 24 | log += event.name + '(' + transition.to().name + ',' + transition.from().name + ');'; 25 | } else if (logEvents) { 26 | log += 27 | event.name + 28 | '(' + 29 | (angular.isString(to.name) ? to.name : to) + 30 | ',' + 31 | (angular.isString(from.name) ? from.name : from) + 32 | ');'; 33 | } 34 | } 35 | function callbackLogger(what) { 36 | return function () { 37 | if (logEnterExit) log += this.name + '.' + what + ';'; 38 | }; 39 | } 40 | 41 | const A: StateDeclaration = { data: {} }, 42 | B: StateDeclaration = {}, 43 | C: StateDeclaration = {}, 44 | D: StateDeclaration = { params: { x: {}, y: {} } }, 45 | DD: StateDeclaration = { parent: D, params: { z: {} } }, 46 | E: StateDeclaration = { params: { i: {} } }, 47 | F: StateDeclaration = { 48 | resolve: { 49 | delay: function ($timeout) { 50 | return $timeout(angular.noop, 50); 51 | }, 52 | }, 53 | }; 54 | 55 | beforeEach( 56 | module(function ($stateProvider, $provide) { 57 | angular.forEach([A, B, C, D, DD], function (state) { 58 | state.onEnter = callbackLogger('onEnter'); 59 | state.onExit = callbackLogger('onExit'); 60 | }); 61 | stateProvider = $stateProvider; 62 | 63 | $stateProvider 64 | .state('A', A) 65 | .state('B', B) 66 | .state('C', C) 67 | .state('D', D) 68 | .state('DD', DD) 69 | .state('E', E) 70 | .state('F', F); 71 | }) 72 | ); 73 | 74 | beforeEach(inject(function ($rootScope, _$injector_) { 75 | $injector = _$injector_; 76 | log = ''; 77 | logEvents = logEnterExit = false; 78 | $rootScope.$on('$stateChangeStart', eventLogger); 79 | $rootScope.$on('$stateChangeSuccess', eventLogger); 80 | $rootScope.$on('$stateChangeError', eventLogger); 81 | $rootScope.$on('$stateNotFound', eventLogger); 82 | })); 83 | 84 | function $get(what) { 85 | return $injector.get(what); 86 | } 87 | 88 | function initStateTo(state, optionalParams?, optionalOptions?) { 89 | const $state = $get('$state'), 90 | $q = $get('$q'); 91 | $state.transitionTo(state, optionalParams || {}, optionalOptions || {}); 92 | $q.flush(); 93 | expect($state.current).toBe(state); 94 | } 95 | 96 | describe('.transitionTo()', function () { 97 | it('triggers $stateChangeStart', inject(function ($state, $q, $rootScope) { 98 | initStateTo(E, { i: 'iii' }, { anOption: true }); 99 | let called; 100 | $rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams, options) { 101 | expect(from).toBe(E); 102 | 103 | expect(obj(fromParams)).toEqual({ i: 'iii' }); 104 | 105 | expect(to).toBe(D); 106 | 107 | expect(obj(toParams)).toEqual({ x: '1', y: '2' }); 108 | 109 | expect(options.custom.anOption).toBe(false); 110 | 111 | expect($state.current).toBe(from); // $state not updated yet 112 | expect(obj($state.params)).toEqual(obj(fromParams)); 113 | called = true; 114 | }); 115 | $state.transitionTo(D, { x: '1', y: '2' }, { custom: { anOption: false } }); 116 | $q.flush(); 117 | expect(called).toBeTruthy(); 118 | expect($state.current).toBe(D); 119 | })); 120 | 121 | it('can be cancelled by preventDefault() in $stateChangeStart', inject(function ($state, $q, $rootScope) { 122 | $state.defaultErrorHandler(function () {}); 123 | initStateTo(A); 124 | let called; 125 | $rootScope.$on('$stateChangeStart', function (ev) { 126 | ev.preventDefault(); 127 | called = true; 128 | }); 129 | const promise = $state.transitionTo(B, {}); 130 | $q.flush(); 131 | expect(called).toBeTruthy(); 132 | expect($state.current).toBe(A); 133 | expect(resolvedError(promise)).toBeTruthy(); 134 | })); 135 | 136 | it('triggers $stateNotFound', inject(function ($state, $q, $rootScope) { 137 | initStateTo(E, { i: 'iii' }); 138 | let called; 139 | $rootScope.$on('$stateNotFound', function (ev, unfoundState, fromState, fromParams) { 140 | expect(fromState).toBe(E); 141 | expect(obj(fromParams)).toEqual({ i: 'iii' }); 142 | expect(unfoundState.to).toEqual('never_defined'); 143 | expect(unfoundState.toParams).toEqual({ x: '1', y: '2' }); 144 | 145 | expect($state.current).toBe(E); // $state not updated yet 146 | expect(obj($state.params)).toEqual({ i: 'iii' }); 147 | called = true; 148 | }); 149 | let message; 150 | $state.transitionTo('never_defined', { x: '1', y: '2' }).catch(function (e) { 151 | message = e.detail; 152 | }); 153 | $q.flush(); 154 | expect(message).toEqual("No such state 'never_defined'"); 155 | expect(called).toBeTruthy(); 156 | expect($state.current).toBe(E); 157 | })); 158 | 159 | it('throws Error on failed relative state resolution', inject(function ($state, $q) { 160 | $state.transitionTo(DD); 161 | $q.flush(); 162 | let error, 163 | promise = $state.transitionTo('^.Z', null, { relative: $state.$current }); 164 | promise.catch(function (e) { 165 | error = e.detail; 166 | }); 167 | $q.flush(); 168 | 169 | const err = "Could not resolve '^.Z' from state 'DD'"; 170 | expect(error).toBe(err); 171 | })); 172 | 173 | it('sends $stateChangeError for exceptions in onEnter', inject(function ( 174 | $state, 175 | $q, 176 | $rootScope, 177 | $exceptionHandler 178 | ) { 179 | $exceptionHandler.disabled = true; 180 | $state.defaultErrorHandler(function () {}); 181 | 182 | stateProvider.state('onEnterFail', { 183 | onEnter: function () { 184 | throw new Error('negative onEnter'); 185 | }, 186 | }); 187 | 188 | let called; 189 | $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, options) { 190 | called = true; 191 | }); 192 | 193 | initStateTo(A); 194 | $state.transitionTo('onEnterFail'); 195 | $q.flush(); 196 | 197 | expect(called).toBeTruthy(); 198 | expect($state.current.name).toEqual(A.name); 199 | })); 200 | 201 | it('can be cancelled by preventDefault() in $stateNotFound', inject(function ($state, $q, $rootScope) { 202 | initStateTo(A); 203 | let called; 204 | $rootScope.$on('$stateNotFound', function (ev) { 205 | ev.preventDefault(); 206 | called = true; 207 | }); 208 | const promise = $state.transitionTo('never_defined', {}); 209 | $q.flush(); 210 | expect(called).toBeTruthy(); 211 | expect($state.current).toBe(A); 212 | expect(resolvedError(promise)).toBeTruthy(); 213 | })); 214 | 215 | it('can be redirected in $stateNotFound', inject(function ($state, $q, $rootScope) { 216 | initStateTo(A); 217 | let called; 218 | $rootScope.$on('$stateNotFound', function (ev, redirect) { 219 | redirect.to = D; 220 | redirect.toParams = { x: '1', y: '2' }; 221 | called = true; 222 | }); 223 | const promise = $state.transitionTo('never_defined', { z: 3 }); 224 | $q.flush(); 225 | expect(called).toBeTruthy(); 226 | expect($state.current).toBe(D); 227 | expect(obj($state.params)).toEqual({ x: '1', y: '2' }); 228 | })); 229 | 230 | it('can lazy-define a state in $stateNotFound', inject(function ($state, $q, $rootScope) { 231 | initStateTo(DD, { x: 1, y: 2, z: 3 }); 232 | let called; 233 | $rootScope.$on('$stateNotFound', function (ev, redirect) { 234 | stateProvider.state(redirect.to, { parent: DD, params: { w: {} } }); 235 | ev.retry = called = true; 236 | }); 237 | const promise = $state.go('DDD', { w: 4 }); 238 | $q.flush(); 239 | expect(called).toBeTruthy(); 240 | expect($state.current.name).toEqual('DDD'); 241 | expect(obj($state.params)).toEqual({ x: 1, y: 2, z: 3, w: 4 }); 242 | })); 243 | 244 | it('can defer a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) { 245 | initStateTo(A); 246 | let called; 247 | const deferred = $q.defer(); 248 | $rootScope.$on('$stateNotFound', function (ev, redirect) { 249 | ev.retry = deferred.promise; 250 | called = true; 251 | }); 252 | const promise = $state.go('AA', { a: 1 }); 253 | stateProvider.state('AA', { parent: A, params: { a: {} } }); 254 | deferred.resolve(); 255 | $q.flush(); 256 | expect(called).toBeTruthy(); 257 | expect($state.current.name).toEqual('AA'); 258 | expect(obj($state.params)).toEqual({ a: 1 }); 259 | })); 260 | 261 | it('can defer and supersede a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) { 262 | initStateTo(A); 263 | let called; 264 | const deferred = $q.defer(); 265 | $rootScope.$on('$stateNotFound', function (ev, redirect) { 266 | ev.retry = deferred.promise; 267 | called = true; 268 | }); 269 | const promise = $state.go('AA', { a: 1 }); 270 | $state.go(B); 271 | stateProvider.state('AA', { parent: A, params: { a: {} } }); 272 | deferred.resolve(); 273 | $q.flush(); 274 | expect(called).toBeTruthy(); 275 | expect($state.current).toEqual(B); 276 | expect(obj($state.params)).toEqual({}); 277 | })); 278 | 279 | it('triggers $stateChangeSuccess', inject(function ($state, $q, $rootScope) { 280 | initStateTo(E, { i: 'iii' }); 281 | let called; 282 | $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) { 283 | expect(from).toBe(E); 284 | expect(obj(fromParams)).toEqual({ i: 'iii' }); 285 | expect(to).toBe(D); 286 | expect(obj(toParams)).toEqual({ x: '1', y: '2' }); 287 | 288 | expect($state.current).toBe(to); // $state has been updated 289 | expect(obj($state.params)).toEqual(obj(toParams)); 290 | called = true; 291 | }); 292 | $state.transitionTo(D, { x: '1', y: '2' }); 293 | $q.flush(); 294 | expect(called).toBeTruthy(); 295 | expect($state.current).toBe(D); 296 | })); 297 | 298 | it('does not trigger $stateChangeSuccess when suppressed, but changes state', inject(function ( 299 | $state, 300 | $q, 301 | $rootScope 302 | ) { 303 | initStateTo(E, { i: 'iii' }); 304 | let called; 305 | 306 | $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) { 307 | called = true; 308 | }); 309 | 310 | $state.transitionTo(D, { x: '1', y: '2' }, { notify: false }); 311 | $q.flush(); 312 | 313 | expect(called).toBeFalsy(); 314 | expect($state.current).toBe(D); 315 | })); 316 | 317 | it('does not trigger $stateChangeSuccess when suppressed, but updates params', inject(function ( 318 | $state, 319 | $q, 320 | $rootScope 321 | ) { 322 | initStateTo(E, { x: 'iii' }); 323 | let called; 324 | 325 | $rootScope.$on('$stateChangeSuccess', function (ev, transition) { 326 | called = true; 327 | }); 328 | $state.transitionTo(E, { i: '1', y: '2' }, { notify: false }); 329 | $q.flush(); 330 | 331 | expect(called).toBeFalsy(); 332 | expect($state.params.i).toBe('1'); 333 | expect($state.current).toBe(E); 334 | })); 335 | 336 | it('aborts pending transitions even when going back to the current state', inject(function ($state, $q, $timeout) { 337 | initStateTo(A); 338 | logEvents = true; 339 | $state.defaultErrorHandler(function () {}); 340 | 341 | const superseded = $state.transitionTo(F, {}); 342 | $q.flush(); 343 | expect($state.current).toBe(A); 344 | 345 | $state.transitionTo(A, {}); 346 | $q.flush(); 347 | $timeout.flush(); 348 | expect($state.current).toBe(A); 349 | expect(resolvedError(superseded)).toBeTruthy(); 350 | expect(log).toBe('$stateChangeStart(F,A);'); 351 | })); 352 | 353 | it('aborts pending transitions (last call wins)', inject(function ($state, $q, $timeout) { 354 | initStateTo(A); 355 | logEvents = true; 356 | $state.defaultErrorHandler(function () {}); 357 | 358 | const superseded = $state.transitionTo(F, {}); 359 | $q.flush(); 360 | 361 | $state.transitionTo(C, {}); 362 | $q.flush(); 363 | $timeout.flush(); 364 | expect($state.current).toBe(C); 365 | expect(resolvedError(superseded)).toBeTruthy(); 366 | expect(log).toBe('$stateChangeStart(F,A);' + '$stateChangeStart(C,A);' + '$stateChangeSuccess(C,A);'); 367 | })); 368 | }); 369 | }); 370 | -------------------------------------------------------------------------------- /test/stateFiltersSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | declare var inject; 3 | 4 | const module = angular['mock'].module; 5 | 6 | describe('isState filter', function () { 7 | beforeEach(module('ui.router')); 8 | beforeEach( 9 | module(function ($stateProvider) { 10 | $stateProvider.state('a', { url: '/' }).state('a.b', { url: '/b' }).state('with-param', { url: '/with/:param' }); 11 | }) 12 | ); 13 | 14 | it('should return true if the current state exactly matches the input state', inject(function ( 15 | $parse, 16 | $state, 17 | $q, 18 | $rootScope 19 | ) { 20 | $state.go('a'); 21 | $q.flush(); 22 | expect($parse('"a" | isState')($rootScope)).toBe(true); 23 | })); 24 | 25 | it('should return false if the current state does not exactly match the input state', inject(function ( 26 | $parse, 27 | $q, 28 | $state, 29 | $rootScope 30 | ) { 31 | $state.go('a.b'); 32 | $q.flush(); 33 | expect($parse('"a" | isState')($rootScope)).toBe(false); 34 | })); 35 | 36 | it('should return true if the current state and param matches the input state', inject(function ( 37 | $parse, 38 | $state, 39 | $q, 40 | $rootScope 41 | ) { 42 | $state.go('with-param', { param: 'a' }); 43 | $q.flush(); 44 | expect($parse('"with-param" | isState: {param: "a"}')($rootScope)).toBe(true); 45 | })); 46 | 47 | it('should return false if the current state and param does not match the input state', inject(function ( 48 | $parse, 49 | $state, 50 | $q, 51 | $rootScope 52 | ) { 53 | $state.go('with-param', { param: 'b' }); 54 | $q.flush(); 55 | expect($parse('"with-param" | isState: {param: "a"}')($rootScope)).toBe(false); 56 | })); 57 | }); 58 | 59 | describe('includedByState filter', function () { 60 | beforeEach(module('ui.router')); 61 | beforeEach( 62 | module(function ($stateProvider) { 63 | $stateProvider 64 | .state('a', { url: '/' }) 65 | .state('a.b', { url: '/b' }) 66 | .state('c', { url: '/c' }) 67 | .state('d', { url: '/d/:id' }); 68 | }) 69 | ); 70 | 71 | it('should return true if the current state exactly matches the input state', inject(function ( 72 | $parse, 73 | $state, 74 | $q, 75 | $rootScope 76 | ) { 77 | $state.go('a'); 78 | $q.flush(); 79 | expect($parse('"a" | includedByState')($rootScope)).toBe(true); 80 | })); 81 | 82 | it('should return true if the current state includes the input state', inject(function ( 83 | $parse, 84 | $state, 85 | $q, 86 | $rootScope 87 | ) { 88 | $state.go('a.b'); 89 | $q.flush(); 90 | expect($parse('"a" | includedByState')($rootScope)).toBe(true); 91 | })); 92 | 93 | it('should return false if the current state does not include input state', inject(function ( 94 | $parse, 95 | $state, 96 | $q, 97 | $rootScope 98 | ) { 99 | $state.go('c'); 100 | $q.flush(); 101 | expect($parse('"a" | includedByState')($rootScope)).toBe(false); 102 | })); 103 | 104 | it('should return true if the current state include input state and params', inject(function ( 105 | $parse, 106 | $state, 107 | $q, 108 | $rootScope 109 | ) { 110 | $state.go('d', { id: 123 }); 111 | $q.flush(); 112 | expect($parse('"d" | includedByState:{ id: 123 }')($rootScope)).toBe(true); 113 | })); 114 | 115 | it('should return false if the current state does not include input state and params', inject(function ( 116 | $parse, 117 | $state, 118 | $q, 119 | $rootScope 120 | ) { 121 | $state.go('d', { id: 2377 }); 122 | $q.flush(); 123 | expect($parse('"d" | includedByState:{ id: 123 }')($rootScope)).toBe(false); 124 | })); 125 | }); 126 | -------------------------------------------------------------------------------- /test/templateFactorySpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { UIRouter } from '@uirouter/core'; 3 | 4 | declare let inject; 5 | 6 | const module = angular['mock'].module; 7 | 8 | describe('templateFactory', function () { 9 | beforeEach(module('ui.router')); 10 | 11 | it('exists', inject(function ($templateFactory) { 12 | expect($templateFactory).toBeDefined(); 13 | })); 14 | 15 | if (angular.version.minor >= 3) { 16 | // Post 1.2, there is a $templateRequest and a $sce service 17 | describe('should follow $sce policy and', function () { 18 | it('accepts relative URLs', inject(function ($templateFactory, $httpBackend, $sce) { 19 | $httpBackend.expectGET('views/view.html').respond(200, 'template!'); 20 | $templateFactory.fromUrl('views/view.html'); 21 | $httpBackend.flush(); 22 | })); 23 | 24 | it('rejects untrusted URLs', inject(function ($templateFactory, $httpBackend, $sce) { 25 | let error = 'No error thrown'; 26 | try { 27 | $templateFactory.fromUrl('http://evil.com/views/view.html'); 28 | } catch (e) { 29 | error = e.message; 30 | } 31 | expect(error).toMatch(/sce:insecurl/); 32 | })); 33 | 34 | it('accepts explicitly trusted URLs', inject(function ($templateFactory, $httpBackend, $sce) { 35 | $httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!'); 36 | $templateFactory.fromUrl($sce.trustAsResourceUrl('http://evil.com/views/view.html')); 37 | $httpBackend.flush(); 38 | })); 39 | }); 40 | } 41 | 42 | if (angular.version.minor <= 2) { 43 | // 1.2 and before will use directly $http 44 | it('does not restrict URL loading', inject(function ($templateFactory, $httpBackend) { 45 | $httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!'); 46 | $templateFactory.fromUrl('http://evil.com/views/view.html'); 47 | $httpBackend.flush(); 48 | 49 | $httpBackend.expectGET('data:text/html,foo').respond(200, 'template!'); 50 | $templateFactory.fromUrl('data:text/html,foo'); 51 | $httpBackend.flush(); 52 | })); 53 | 54 | // Behavior not kept in >1.2 with $templateRequest 55 | it('should request templates as text/html', inject(function ($templateFactory, $httpBackend) { 56 | $httpBackend 57 | .expectGET('views/view.html', function (headers) { 58 | return headers.Accept === 'text/html'; 59 | }) 60 | .respond(200); 61 | $templateFactory.fromUrl('views/view.html'); 62 | $httpBackend.flush(); 63 | })); 64 | } 65 | 66 | describe('templateFactory with forced use of $http service', function () { 67 | beforeEach(function () { 68 | angular.module('forceHttpInTemplateFactory', []).config(function ($templateFactoryProvider) { 69 | $templateFactoryProvider.useHttpService(true); 70 | }); 71 | module('ui.router'); 72 | module('forceHttpInTemplateFactory'); 73 | }); 74 | 75 | it('does not restrict URL loading', inject(function ($templateFactory, $httpBackend) { 76 | $httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!'); 77 | $templateFactory.fromUrl('http://evil.com/views/view.html'); 78 | $httpBackend.flush(); 79 | 80 | $httpBackend.expectGET('data:text/html,foo').respond(200, 'template!'); 81 | $templateFactory.fromUrl('data:text/html,foo'); 82 | $httpBackend.flush(); 83 | })); 84 | }); 85 | 86 | if (angular.version.minor >= 5) { 87 | describe('component template builder', () => { 88 | let router: UIRouter, el, rootScope; 89 | const cmp = { template: 'hi' }; 90 | 91 | beforeEach(() => { 92 | const mod = angular.module('foo', []); 93 | mod.component('myComponent', cmp); 94 | mod.component('dataComponent', cmp); 95 | mod.component('xComponent', cmp); 96 | }); 97 | beforeEach(module('foo')); 98 | 99 | beforeEach(inject(($uiRouter, $compile, $rootScope) => { 100 | router = $uiRouter; 101 | rootScope = $rootScope; 102 | el = $compile(angular.element('
'))($rootScope.$new()); 103 | })); 104 | 105 | it('should not prefix the components dom element with anything', () => { 106 | router.stateRegistry.register({ name: 'cmp', component: 'myComponent' }); 107 | router.stateService.go('cmp'); 108 | rootScope.$digest(); 109 | expect(el.html()).toMatch(/\ { 113 | router.stateRegistry.register({ name: 'cmp', component: 'dataComponent' }); 114 | router.stateService.go('cmp'); 115 | rootScope.$digest(); 116 | expect(el.html()).toMatch(/\ { 120 | router.stateRegistry.register({ name: 'cmp', component: 'xComponent' }); 121 | router.stateService.go('cmp'); 122 | rootScope.$digest(); 123 | expect(el.html()).toMatch(/\ $1/package.json 16 | jq ".typescript += { \"typescript$1\": \"./test/typescript/$1\" }" < ../../downstream_projects.json > temp.json 17 | mv temp.json ../../downstream_projects.json 18 | -------------------------------------------------------------------------------- /test/typescript/template/index.ts: -------------------------------------------------------------------------------- 1 | import { UIRouter } from '@uirouter/angularjs'; 2 | console.log(UIRouter); 3 | -------------------------------------------------------------------------------- /test/typescript/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Test against Typescript VERSION", 3 | "scripts": { 4 | "test": "tsc" 5 | }, 6 | "dependencies": { 7 | "@types/angular": "latest", 8 | "@uirouter/angularjs": "latest", 9 | "typescript": "VERSION" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/typescript/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "lib": ["es6", "dom"], 6 | "noImplicitAny": true, 7 | "noEmit": true, 8 | "target": "es5", 9 | "typeRoots": ["node_modules/@types"] 10 | }, 11 | "files": ["index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /test/urlRouterSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { ILocationService, ILocationProvider } from 'angular'; 3 | import { html5Compat } from './util/testUtilsNg1'; 4 | import { UrlRule, UrlMatcher, UrlMatcherFactory, UrlRouter, StateService, UIRouter } from '../src/index'; 5 | import { UrlRouterProvider } from '../src/urlRouterProvider'; 6 | 7 | declare var inject; 8 | 9 | const module = angular['mock'].module; 10 | 11 | describe('UrlRouter', function () { 12 | let router: UIRouter; 13 | let $urp: UrlRouterProvider, 14 | $lp: ILocationProvider, 15 | $umf: UrlMatcherFactory, 16 | $s: StateService, 17 | $ur: UrlRouter, 18 | location: ILocationService, 19 | match, 20 | scope; 21 | 22 | describe('provider', function () { 23 | beforeEach(function () { 24 | angular.module('ui.router.router.test', []).config(function ($uiRouterProvider) { 25 | router = $uiRouterProvider; 26 | $umf = router.urlMatcherFactory; 27 | $urp = router.urlRouterProvider; 28 | $urp.deferIntercept(); 29 | }); 30 | 31 | module('ui.router.router', 'ui.router.router.test'); 32 | 33 | inject(function ($rootScope, $location) { 34 | scope = $rootScope.$new(); 35 | location = $location; 36 | }); 37 | }); 38 | 39 | it('should throw on non-function rules', function () { 40 | expect(function () { 41 | $urp.rule(null); 42 | }).toThrowError("'rule' must be a function"); 43 | expect(function () { 44 | $urp.otherwise(null); 45 | }).toThrowError("'rule' must be a string or function"); 46 | }); 47 | 48 | it('should allow location changes to be deferred', inject(function ($urlRouter, $location, $rootScope) { 49 | const log = []; 50 | 51 | $urp.rule(function ($injector, $location) { 52 | log.push($location.path()); 53 | return null; 54 | }); 55 | 56 | $location.path('/foo'); 57 | $rootScope.$broadcast('$locationChangeSuccess'); 58 | 59 | expect(log).toEqual([]); 60 | 61 | $urlRouter.listen(); 62 | $rootScope.$broadcast('$locationChangeSuccess'); 63 | 64 | expect(log).toEqual(['/foo']); 65 | })); 66 | }); 67 | 68 | describe('service', function () { 69 | beforeEach(function () { 70 | angular.module('ui.router.router.test', []).config(function ($uiRouterProvider, $locationProvider) { 71 | router = $uiRouterProvider; 72 | $umf = router.urlMatcherFactory; 73 | $urp = router.urlRouterProvider; 74 | $lp = $locationProvider; 75 | $locationProvider.hashPrefix(''); 76 | 77 | $urp 78 | .rule(function ($injector, $location) { 79 | const path = $location.path(); 80 | if (!/baz/.test(path)) return; 81 | return path.replace('baz', 'b4z'); 82 | }) 83 | .when('/foo/:param', function ($match) { 84 | match = ['/foo/:param', $match]; 85 | }) 86 | .when('/bar', function ($match) { 87 | match = ['/bar', $match]; 88 | }); 89 | }); 90 | 91 | module('ui.router.router', 'ui.router.router.test'); 92 | 93 | inject(function ($rootScope, $location, $injector) { 94 | scope = $rootScope.$new(); 95 | location = $location; 96 | $ur = $injector.invoke($urp['$get'], $urp); 97 | $s = $injector.get('$sniffer'); 98 | $s['history'] = true; 99 | }); 100 | }); 101 | 102 | it('should execute rewrite rules', function () { 103 | location.path('/foo'); 104 | scope.$emit('$locationChangeSuccess'); 105 | expect(location.path()).toBe('/foo'); 106 | 107 | location.path('/baz'); 108 | scope.$emit('$locationChangeSuccess'); 109 | expect(location.path()).toBe('/b4z'); 110 | }); 111 | 112 | it('should keep otherwise last', function () { 113 | $urp.otherwise('/otherwise'); 114 | 115 | location.path('/lastrule'); 116 | scope.$emit('$locationChangeSuccess'); 117 | expect(location.path()).toBe('/otherwise'); 118 | 119 | $urp.when('/lastrule', function ($match) { 120 | match = ['/lastrule', $match]; 121 | }); 122 | 123 | location.path('/lastrule'); 124 | scope.$emit('$locationChangeSuccess'); 125 | expect(location.path()).toBe('/lastrule'); 126 | }); 127 | 128 | it('can be cancelled by preventDefault() in $locationChangeSuccess', inject(function () { 129 | let called; 130 | location.path('/baz'); 131 | scope.$on('$locationChangeSuccess', function (ev) { 132 | ev.preventDefault(); 133 | called = true; 134 | }); 135 | scope.$emit('$locationChangeSuccess'); 136 | expect(called).toBeTruthy(); 137 | expect(location.path()).toBe('/baz'); 138 | })); 139 | 140 | it('can be deferred and updated in $locationChangeSuccess', inject(function ($urlRouter, $timeout) { 141 | let called; 142 | location.path('/baz'); 143 | scope.$on('$locationChangeSuccess', function (ev) { 144 | ev.preventDefault(); 145 | called = true; 146 | $timeout(() => $urlRouter.sync(), 2000); 147 | }); 148 | scope.$emit('$locationChangeSuccess'); 149 | $timeout.flush(); 150 | expect(called).toBeTruthy(); 151 | expect(location.path()).toBe('/b4z'); 152 | })); 153 | 154 | it('rule should return a deregistration function', function () { 155 | let count = 0; 156 | let rule: UrlRule = { 157 | match: () => count++, 158 | handler: (match) => match, 159 | matchPriority: () => 0, 160 | $id: 0, 161 | priority: 0, 162 | _group: 0, 163 | type: 'RAW', 164 | }; 165 | const dereg = $ur.rule(rule as any); 166 | 167 | $ur.sync(); 168 | expect(count).toBe(1); 169 | $ur.sync(); 170 | expect(count).toBe(2); 171 | 172 | dereg(); 173 | $ur.sync(); 174 | expect(count).toBe(2); 175 | }); 176 | 177 | it('removeRule should remove a previously registered rule', function () { 178 | let count = 0; 179 | let rule: UrlRule = { 180 | match: () => count++, 181 | handler: (match) => match, 182 | matchPriority: () => 0, 183 | $id: 0, 184 | priority: 0, 185 | _group: 0, 186 | type: 'RAW', 187 | }; 188 | $ur.rule(rule as any); 189 | 190 | $ur.sync(); 191 | expect(count).toBe(1); 192 | $ur.sync(); 193 | expect(count).toBe(2); 194 | 195 | $ur.removeRule(rule); 196 | $ur.sync(); 197 | expect(count).toBe(2); 198 | }); 199 | 200 | describe('location updates', function () { 201 | it('can push location changes', inject(function ($urlRouter) { 202 | const spy = spyOn(router.locationService, 'url'); 203 | $urlRouter.push($umf.compile('/hello/:name'), { name: 'world' }); 204 | expect(spy).toHaveBeenCalled(); 205 | expect(spy.calls.mostRecent().args[0]).toBe('/hello/world'); 206 | })); 207 | 208 | it('can push a replacement location', inject(function ($urlRouter, $location) { 209 | const spy = spyOn(router.locationService, 'url'); 210 | $urlRouter.push($umf.compile('/hello/:name'), { name: 'world' }, { replace: true }); 211 | expect(spy).toHaveBeenCalled(); 212 | expect(spy.calls.mostRecent().args.slice(0, 2)).toEqual(['/hello/world', true]); 213 | })); 214 | 215 | it('can push location changes with no parameters', inject(function ($urlRouter, $location) { 216 | const spy = spyOn(router.locationService, 'url'); 217 | $urlRouter.push($umf.compile('/hello/:name', { state: { params: { name: '' } } })); 218 | expect(spy).toHaveBeenCalled(); 219 | expect(spy.calls.mostRecent().args[0]).toBe('/hello/'); 220 | })); 221 | 222 | it('can push an empty url', inject(function ($urlRouter, $location) { 223 | const spy = spyOn(router.locationService, 'url'); 224 | $urlRouter.push($umf.compile('/{id}', { state: { params: { id: { squash: true, value: null } } } })); 225 | expect(spy).toHaveBeenCalled(); 226 | expect(spy.calls.mostRecent().args[0]).toBe(''); 227 | })); 228 | 229 | // Angular 1.2 doesn't seem to support $location.url("") 230 | if (angular.version.minor >= 3) { 231 | // Test for https://github.com/angular-ui/ui-router/issues/3563 232 | it('updates url after an empty url is pushed', inject(function ($urlRouter, $location) { 233 | $lp.html5Mode(false); 234 | const spy = spyOn(router.locationService, 'url').and.callThrough(); 235 | $urlRouter.push($umf.compile('/foobar')); 236 | expect(spy.calls.mostRecent().args[0]).toBe('/foobar'); 237 | $urlRouter.push($umf.compile('/{id}', { state: { params: { id: { squash: true, value: null } } } })); 238 | expect(spy.calls.mostRecent().args[0]).toBe(''); 239 | expect(router.locationService.url()).toBe('/'); 240 | })); 241 | 242 | // Test #2 for https://github.com/angular-ui/ui-router/issues/3563 243 | it('updates html5mode url after an empty url is pushed', inject(function ($urlRouter, $location) { 244 | $lp.html5Mode(true); 245 | const spy = spyOn(router.locationService, 'url').and.callThrough(); 246 | $urlRouter.push($umf.compile('/foobar')); 247 | expect(spy.calls.mostRecent().args[0]).toBe('/foobar'); 248 | $urlRouter.push($umf.compile('/{id}', { state: { params: { id: { squash: true, value: null } } } })); 249 | expect(spy.calls.mostRecent().args[0]).toBe(''); 250 | expect(router.locationService.url()).toBe('/'); 251 | })); 252 | } 253 | 254 | it('can push location changes that include a #fragment', inject(function ($urlRouter, $location) { 255 | // html5mode disabled 256 | $lp.html5Mode(false); 257 | expect(html5Compat($lp.html5Mode())).toBe(false); 258 | $urlRouter.push($umf.compile('/hello/:name'), { name: 'world', '#': 'frag' }); 259 | expect($location.url()).toBe('/hello/world#frag'); 260 | expect($location.hash()).toBe('frag'); 261 | 262 | // html5mode enabled 263 | $lp.html5Mode(true); 264 | expect(html5Compat($lp.html5Mode())).toBe(true); 265 | $urlRouter.push($umf.compile('/hello/:name'), { name: 'world', '#': 'frag' }); 266 | expect($location.url()).toBe('/hello/world#frag'); 267 | expect($location.hash()).toBe('frag'); 268 | })); 269 | 270 | it('can read and sync a copy of location URL', inject(function ($urlRouter, $location) { 271 | $location.url('/old'); 272 | 273 | spyOn(router.locationService, 'url').and.callThrough(); 274 | $urlRouter.update(true); 275 | expect(router.locationService.url).toHaveBeenCalled(); 276 | 277 | $location.url('/new'); 278 | $urlRouter.update(); 279 | 280 | expect($location.url()).toBe('/old'); 281 | })); 282 | 283 | it('can read and sync a copy of location URL including query params', inject(function ($urlRouter, $location) { 284 | $location.url('/old?param=foo'); 285 | 286 | spyOn(router.locationService, 'url').and.callThrough(); 287 | $urlRouter.update(true); 288 | expect(router.locationService.url).toHaveBeenCalled(); 289 | 290 | $location.url('/new?param=bar'); 291 | $urlRouter.update(); 292 | 293 | expect($location.url()).toBe('/old?param=foo'); 294 | })); 295 | }); 296 | 297 | describe('URL generation', function () { 298 | it('should return null when UrlMatcher rejects parameters', inject(function ($urlRouter: UrlRouter) { 299 | $umf.type('custom', { is: (val) => val === 1138 }); 300 | const matcher = $umf.compile('/foo/{param:custom}'); 301 | 302 | expect($urlRouter.href(matcher, { param: 1138 })).toBe('#/foo/1138'); 303 | expect($urlRouter.href(matcher, { param: 5 })).toBeNull(); 304 | })); 305 | 306 | it('should handle the new html5Mode object config from Angular 1.3', inject(function ($urlRouter: UrlRouter) { 307 | $lp.html5Mode({ 308 | enabled: false, 309 | }); 310 | 311 | expect($urlRouter.href($umf.compile('/hello'))).toBe('#/hello'); 312 | })); 313 | 314 | it('should return URLs with #fragments', inject(function ($urlRouter: UrlRouter) { 315 | // html5mode disabled 316 | $lp.html5Mode(false); 317 | expect(html5Compat($lp.html5Mode())).toBe(false); 318 | expect($urlRouter.href($umf.compile('/hello/:name'), { name: 'world', '#': 'frag' })).toBe( 319 | '#/hello/world#frag' 320 | ); 321 | 322 | // html5mode enabled 323 | $lp.html5Mode(true); 324 | expect(html5Compat($lp.html5Mode())).toBe(true); 325 | expect($urlRouter.href($umf.compile('/hello/:name'), { name: 'world', '#': 'frag' })).toBe('/hello/world#frag'); 326 | })); 327 | 328 | it('should return URLs with #fragments when html5Mode is true & browser does not support pushState', inject(function ( 329 | $urlRouter: UrlRouter 330 | ) { 331 | $lp.html5Mode(true); 332 | $s['history'] = false; 333 | expect(html5Compat($lp.html5Mode())).toBe(true); 334 | expect($urlRouter.href($umf.compile('/hello/:name'), { name: 'world', '#': 'frag' })).toBe( 335 | '#/hello/world#frag' 336 | ); 337 | })); 338 | }); 339 | }); 340 | }); 341 | -------------------------------------------------------------------------------- /test/util/testUtilsNg1.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | // Promise testing support 4 | angular.module('ngMock').config(function ($provide, $locationProvider) { 5 | var oldFn = $locationProvider.html5Mode; 6 | $locationProvider.html5Mode = function () { 7 | var retval = oldFn.apply($locationProvider, arguments); 8 | return angular.isDefined(retval) && angular.isDefined(retval.enabled) ? retval.enabled : retval; 9 | }; 10 | 11 | $provide.decorator('$q', function ($delegate, $rootScope) { 12 | $delegate.flush = function () { 13 | $rootScope.$digest(); 14 | }; 15 | 16 | // Add callbacks to the promise that expose the resolved value/error 17 | function expose(promise) { 18 | // Don't add hooks to the same promise twice (shouldn't happen anyway) 19 | if (!promise.hasOwnProperty('$$resolved')) { 20 | promise.$$resolved = false; 21 | promise.then( 22 | function (value) { 23 | promise.$$resolved = { success: true, value: value }; 24 | }, 25 | function (error) { 26 | promise.$$resolved = { success: false, error: error }; 27 | } 28 | ); 29 | 30 | // We need to expose() any then()ed promises recursively 31 | var qThen = promise.then; 32 | promise.then = function () { 33 | return expose(qThen.apply(this, arguments)); 34 | }; 35 | } 36 | return promise; 37 | } 38 | 39 | // Wrap functions that return a promise 40 | angular.forEach(['when', 'all', 'reject'], function (name) { 41 | var qFunc = $delegate[name]; 42 | $delegate[name] = function () { 43 | return expose(qFunc.apply(this, arguments)); 44 | }; 45 | }); 46 | 47 | // Wrap defer() 48 | var qDefer = $delegate.defer; 49 | $delegate.defer = function () { 50 | var deferred = qDefer(); 51 | expose(deferred.promise); 52 | return deferred; 53 | }; 54 | 55 | return $delegate; 56 | }); 57 | }); 58 | 59 | try { 60 | // Animation testing support 61 | angular.module('mock.animate').config(function ($provide) { 62 | $provide.decorator('$animate', function ($delegate) { 63 | $delegate.flush = function () { 64 | while (this.queue.length > 0) { 65 | this.flushNext(this.queue[0].method); 66 | } 67 | }; 68 | return $delegate; 69 | }); 70 | }); 71 | } catch (e) {} 72 | 73 | export function testablePromise(promise) { 74 | if (!promise || !promise.then) throw new Error('Expected a promise, but got ' + jasmine.pp(promise) + '.'); 75 | if (!angular.isDefined(promise.$$resolved)) throw new Error('Promise has not been augmented by ngMock'); 76 | return promise; 77 | } 78 | 79 | export function resolvedPromise(promise) { 80 | var result = testablePromise(promise).$$resolved; 81 | if (!result) throw new Error('Promise is not resolved yet'); 82 | return result; 83 | } 84 | 85 | export function resolvedValue(promise) { 86 | var result = resolvedPromise(promise); 87 | if (!result.success) throw result.error; 88 | return result.value; 89 | } 90 | 91 | export function resolvedError(promise) { 92 | var result = resolvedPromise(promise); 93 | if (result.success) throw new Error('Promise was expected to fail but returned ' + jasmine.pp(result.value) + '.'); 94 | return result.error; 95 | } 96 | 97 | // Misc test utils 98 | export function caught(fn) { 99 | try { 100 | fn(); 101 | return null; 102 | } catch (e) { 103 | return e; 104 | } 105 | } 106 | 107 | // Usage of this helper should be replaced with a custom matcher in jasmine 2.0+ 108 | export function obj(object) { 109 | var o = {}; 110 | angular.forEach(object, function (val, i) { 111 | const key = (i as any) as string; 112 | if (!/^\$/.test(key) && key != '#') o[key] = val; 113 | }); 114 | return o; 115 | } 116 | 117 | export function html5Compat(html5mode) { 118 | return angular.isObject(html5mode) && html5mode.hasOwnProperty('enabled') ? html5mode.enabled : html5mode; 119 | } 120 | 121 | /** 122 | * The ng1 $exceptionHandler from angular-mocks will re-throw any exceptions thrown in a Promise. 123 | * This chunk of code decorates the handler, allowing a test to disable that behavior. 124 | * Inject $exceptionHandler and set `$exceptionHandler.disabled = true` 125 | */ 126 | export function decorateExceptionHandler($exceptionHandlerProvider) { 127 | var $get = $exceptionHandlerProvider.$get; 128 | 129 | $exceptionHandlerProvider.$get = function () { 130 | var realHandler = $get.apply($exceptionHandlerProvider, arguments); 131 | function passThrough(e) { 132 | if (!passThrough['disabled']) { 133 | realHandler.apply(null, arguments); 134 | } 135 | } 136 | return passThrough; 137 | }; 138 | } 139 | 140 | beforeEach(angular['mock'].module('ui.router.compat')); 141 | -------------------------------------------------------------------------------- /test/viewHookSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { StateService } from '@uirouter/core'; 3 | 4 | declare var inject; 5 | 6 | describe('view hooks', () => { 7 | let app, 8 | ctrl, 9 | $state: StateService, 10 | $q, 11 | $timeout: angular.ITimeoutService, 12 | log = ''; 13 | const component = { 14 | bindings: { cmpdata: '<' }, 15 | template: '{{$ctrl.cmpdata}}', 16 | }; 17 | 18 | const directive = { 19 | restrict: 'E', 20 | scope: { cmpdata: '=' }, 21 | bindToController: true, 22 | controller: function () {}, 23 | controllerAs: '$ctrl', 24 | template: '{{$ctrl.cmpdata}}', 25 | }; 26 | 27 | beforeEach(() => { 28 | app = angular.module('viewhooks', []); 29 | }); 30 | 31 | beforeEach( 32 | angular['mock'].module(($stateProvider) => { 33 | ctrl = function controller() { 34 | this.data = 'DATA'; 35 | }; 36 | 37 | if (angular.version.minor >= 5) { 38 | app.component('foo', angular.extend({}, component, { controller: ctrl })); 39 | app.component('bar', angular.extend({}, component)); 40 | app.component('baz', angular.extend({}, component)); 41 | } else if (angular.version.minor >= 2) { 42 | app.directive('foo', () => angular.extend({}, directive, { controller: ctrl })); 43 | app.directive('bar', () => angular.extend({}, directive)); 44 | app.directive('baz', () => angular.extend({}, directive)); 45 | } 46 | 47 | $stateProvider.state({ name: 'foo', url: '/foo', component: 'foo' }); 48 | $stateProvider.state({ name: 'bar', url: '/bar', component: 'bar' }); 49 | $stateProvider.state({ name: 'baz', url: '/baz', component: 'baz' }); 50 | $stateProvider.state({ name: 'redirect', redirectTo: 'baz' }); 51 | }) 52 | ); 53 | 54 | beforeEach(angular['mock'].module('viewhooks', 'ui.router')); 55 | 56 | beforeEach(inject((_$state_, _$q_, _$timeout_, $compile, $rootScope) => { 57 | $state = _$state_; 58 | $q = _$q_; 59 | $timeout = _$timeout_; 60 | $compile('
')($rootScope.$new()); 61 | })); 62 | 63 | describe('uiCanExit', () => { 64 | beforeEach(() => { 65 | log = ''; 66 | }); 67 | 68 | const initial = () => { 69 | $state.go('foo'); 70 | $q.flush(); 71 | $timeout.flush(); 72 | expect(log).toBe(''); 73 | expect($state.current.name).toBe('foo'); 74 | }; 75 | 76 | it("can cancel a transition that would exit the view's state by returning false", () => { 77 | $state.defaultErrorHandler(function () {}); 78 | ctrl.prototype.uiCanExit = function () { 79 | log += 'canexit;'; 80 | return false; 81 | }; 82 | initial(); 83 | 84 | $state.go('bar'); 85 | $q.flush(); 86 | $timeout.flush(); 87 | expect(log).toBe('canexit;'); 88 | expect($state.current.name).toBe('foo'); 89 | }); 90 | 91 | it('can allow the transition by returning true', () => { 92 | ctrl.prototype.uiCanExit = function () { 93 | log += 'canexit;'; 94 | return true; 95 | }; 96 | initial(); 97 | 98 | $state.go('bar'); 99 | $q.flush(); 100 | $timeout.flush(); 101 | expect(log).toBe('canexit;'); 102 | expect($state.current.name).toBe('bar'); 103 | }); 104 | 105 | it('can allow the transition by returning nothing', () => { 106 | ctrl.prototype.uiCanExit = function () { 107 | log += 'canexit;'; 108 | }; 109 | initial(); 110 | 111 | $state.go('bar'); 112 | $q.flush(); 113 | $timeout.flush(); 114 | expect(log).toBe('canexit;'); 115 | expect($state.current.name).toBe('bar'); 116 | }); 117 | 118 | it('can redirect the transition', () => { 119 | ctrl.prototype.uiCanExit = function (trans) { 120 | log += 'canexit;'; 121 | return trans.router.stateService.target('baz'); 122 | }; 123 | initial(); 124 | 125 | $state.go('bar'); 126 | $q.flush(); 127 | $timeout.flush(); 128 | expect(log).toBe('canexit;'); 129 | expect($state.current.name).toBe('baz'); 130 | }); 131 | 132 | it('can cancel the transition by returning a rejected promise', inject(($q, $state) => { 133 | ctrl.prototype.uiCanExit = function () { 134 | log += 'canexit;'; 135 | return $q.reject('nope'); 136 | }; 137 | initial(); 138 | 139 | $state.defaultErrorHandler(function () {}); 140 | $state.go('bar'); 141 | $q.flush(); 142 | $timeout.flush(); 143 | expect(log).toBe('canexit;'); 144 | expect($state.current.name).toBe('foo'); 145 | })); 146 | 147 | it('can wait for a promise and then reject the transition', inject(($timeout) => { 148 | $state.defaultErrorHandler(function () {}); 149 | ctrl.prototype.uiCanExit = function () { 150 | log += 'canexit;'; 151 | return $timeout(() => { 152 | log += 'delay;'; 153 | return false; 154 | }, 1000); 155 | }; 156 | initial(); 157 | 158 | $state.go('bar'); 159 | $q.flush(); 160 | $timeout.flush(); 161 | expect(log).toBe('canexit;delay;'); 162 | expect($state.current.name).toBe('foo'); 163 | })); 164 | 165 | it('can wait for a promise and then allow the transition', inject(($timeout) => { 166 | ctrl.prototype.uiCanExit = function () { 167 | log += 'canexit;'; 168 | return $timeout(() => { 169 | log += 'delay;'; 170 | }, 1000); 171 | }; 172 | initial(); 173 | 174 | $state.go('bar'); 175 | $q.flush(); 176 | $timeout.flush(); 177 | expect(log).toBe('canexit;delay;'); 178 | expect($state.current.name).toBe('bar'); 179 | })); 180 | 181 | it("has 'this' bound to the controller", () => { 182 | ctrl.prototype.uiCanExit = function () { 183 | log += this.data; 184 | }; 185 | initial(); 186 | 187 | $state.go('bar'); 188 | $q.flush(); 189 | $timeout.flush(); 190 | expect(log).toBe('DATA'); 191 | expect($state.current.name).toBe('bar'); 192 | }); 193 | 194 | it('receives the new Transition as the first argument', () => { 195 | const _state = $state; 196 | ctrl.prototype.uiCanExit = function (trans) { 197 | log += 'canexit;'; 198 | expect(typeof trans.treeChanges).toBe('function'); 199 | expect(trans.injector().get('$state')).toBe(_state); 200 | }; 201 | initial(); 202 | 203 | $state.go('bar'); 204 | $q.flush(); 205 | $timeout.flush(); 206 | expect(log).toBe('canexit;'); 207 | expect($state.current.name).toBe('bar'); 208 | }); 209 | 210 | // Test for https://github.com/angular-ui/ui-router/issues/3308 211 | it('should trigger once when answered truthy even if redirected', () => { 212 | ctrl.prototype.uiCanExit = function () { 213 | log += 'canexit;'; 214 | return true; 215 | }; 216 | initial(); 217 | 218 | $state.go('redirect'); 219 | $q.flush(); 220 | $timeout.flush(); 221 | expect(log).toBe('canexit;'); 222 | expect($state.current.name).toBe('baz'); 223 | }); 224 | 225 | // Test for https://github.com/angular-ui/ui-router/issues/3308 226 | it('should trigger only once if returns a redirect', () => { 227 | ctrl.prototype.uiCanExit = function () { 228 | log += 'canexit;'; 229 | return $state.target('bar'); 230 | }; 231 | initial(); 232 | 233 | $state.go('redirect'); 234 | $q.flush(); 235 | $timeout.flush(); 236 | expect(log).toBe('canexit;'); 237 | expect($state.current.name).toBe('bar'); 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /test/viewScrollSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | declare var inject; 3 | 4 | const module = angular['mock'].module; 5 | 6 | describe('uiView', function () { 7 | 'use strict'; 8 | 9 | beforeEach(module('ui.router')); 10 | 11 | describe('scrollIntoView', function () { 12 | let elem; 13 | 14 | beforeEach(function () { 15 | elem = [{ scrollIntoView: jasmine.createSpy('scrollIntoView') }]; 16 | }); 17 | 18 | it('should scroll element into view after timeout', inject(function ($uiViewScroll, $timeout) { 19 | $uiViewScroll(elem); 20 | expect(elem[0].scrollIntoView).not.toHaveBeenCalled(); 21 | 22 | $timeout.flush(); 23 | expect(elem[0].scrollIntoView).toHaveBeenCalled(); 24 | })); 25 | 26 | it('should return the promise from the timeout', inject(function ($uiViewScroll, $timeout) { 27 | const promise = $uiViewScroll(elem); 28 | 29 | $timeout.flush(); 30 | expect(elem[0].scrollIntoView).toHaveBeenCalled(); 31 | expect(promise).toBeDefined(); 32 | })); 33 | }); 34 | 35 | describe('useAnchorScroll', function () { 36 | beforeEach( 37 | module(function ($provide, $uiViewScrollProvider) { 38 | $provide.decorator('$anchorScroll', function ($delegate) { 39 | return jasmine.createSpy('$anchorScroll'); 40 | }); 41 | $uiViewScrollProvider.useAnchorScroll(); 42 | }) 43 | ); 44 | 45 | it('should call $anchorScroll', inject(function ($uiViewScroll, $anchorScroll) { 46 | $uiViewScroll(); 47 | expect($anchorScroll).toHaveBeenCalled(); 48 | })); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/viewSpec.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import { tail, curry, PathNode, PathUtils, ViewService, StateMatcher, StateBuilder, StateObject } from '@uirouter/core'; 3 | import { ng1ViewsBuilder, getNg1ViewConfigFactory } from '../src/statebuilders/views'; 4 | import { Ng1StateDeclaration } from '../src/interface'; 5 | declare var inject; 6 | 7 | describe('view', function () { 8 | let scope, $compile, $injector, elem, $controllerProvider, $urlMatcherFactoryProvider; 9 | let root: StateObject, states: { [key: string]: StateObject }; 10 | 11 | beforeEach( 12 | angular.mock.module('ui.router', function (_$provide_, _$controllerProvider_, _$urlMatcherFactoryProvider_) { 13 | _$provide_.factory('foo', function () { 14 | return 'Foo'; 15 | }); 16 | $controllerProvider = _$controllerProvider_; 17 | $urlMatcherFactoryProvider = _$urlMatcherFactoryProvider_; 18 | }) 19 | ); 20 | 21 | let register; 22 | const registerState = curry(function (_states, stateBuilder, config) { 23 | const state = StateObject.create(config); 24 | const built: StateObject = stateBuilder.build(state); 25 | return (_states[built.name] = built); 26 | }); 27 | 28 | beforeEach(inject(function ($rootScope, _$compile_, _$injector_) { 29 | scope = $rootScope.$new(); 30 | $compile = _$compile_; 31 | $injector = _$injector_; 32 | elem = angular.element('
'); 33 | 34 | states = {}; 35 | const matcher = new StateMatcher(states); 36 | const stateBuilder = new StateBuilder(matcher, $urlMatcherFactoryProvider); 37 | stateBuilder.builder('views', ng1ViewsBuilder); 38 | register = registerState(states, stateBuilder); 39 | root = register({ name: '' }); 40 | })); 41 | 42 | describe('controller handling', function () { 43 | let state, path: PathNode[], ctrlExpression; 44 | beforeEach(() => { 45 | ctrlExpression = null; 46 | const stateDeclaration: Ng1StateDeclaration = { 47 | name: 'foo', 48 | template: 'test', 49 | controllerProvider: [ 50 | 'foo', 51 | function (/* $stateParams, */ foo) { 52 | // todo: reimplement localized $stateParams 53 | ctrlExpression = /* $stateParams.type + */ foo + 'Controller as foo'; 54 | return ctrlExpression; 55 | }, 56 | ], 57 | }; 58 | 59 | state = register(stateDeclaration); 60 | const $view = new ViewService(null); 61 | $view._pluginapi._viewConfigFactory('ng1', getNg1ViewConfigFactory()); 62 | 63 | const _states = [root, state]; 64 | path = _states.map((_state) => new PathNode(_state)); 65 | PathUtils.applyViewConfigs($view, path, _states); 66 | }); 67 | 68 | it('uses the controllerProvider to get controller dynamically', inject(function ($view, $q) { 69 | $controllerProvider.register('AcmeFooController', function ($scope, foo) {}); 70 | elem.append($compile('
')(scope)); 71 | 72 | const view = tail(path).views[0]; 73 | view.load(); 74 | $q.flush(); 75 | expect(ctrlExpression).toEqual('FooController as foo'); 76 | })); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tsconfig.docgen.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@uirouter/core": ["src/includes/@uirouter/core/src"], 8 | "@uirouter/core/lib/globals": ["src/includes/@uirouter/core/src/globals"], 9 | "@uirouter/core/lib/state/stateRegistry": ["src/includes/@uirouter/core/src/state/stateRegistry"], 10 | "@uirouter/core/lib/router": ["src/includes/@uirouter/core/src/router"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "moduleResolution": "node", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "lib": ["es6", "dom"], 9 | "allowSyntheticDefaultImports": true, 10 | "outDir": "lib", 11 | "declaration": true, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "inlineSources": true 15 | }, 16 | "files": ["src/index.ts", "src/legacy/stateEvents.ts", "src/legacy/resolveService.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uirouter/angularjs", 3 | "readme": "README.md", 4 | "out": "_doc", 5 | "stripInternal": true, 6 | "excludeNotExported": true, 7 | "excludePrivate": true 8 | } 9 | --------------------------------------------------------------------------------