Skip to content

Commit 6b496c2

Browse files
authored
feat: add support for standalone angular applications (#994)
* Updated uiSref directive to be an standalone directive * Updated uiSrefActive directive to be a standalone directive * Updated uiSrefStatus directive to be a standalone directive * Updated uiView component to be a standalone component * Imported directives instead of declaring them in UIRouterModule * Created a root provider function for UIRouter. * Fixed bug in uiView component not loading the CommonModule. * Updated downstream sample app for the tests. * Updated UISrefActive to use hostDirectives to implement the UISrefStatus directive. * Added provideUiRouter to global exports. * added a standalone v18 test project. * Added standalone version for downstream projects. * Updated package minor version * Add documentation for provideUIRouter() function. * Revert "Updated downstream sample app for the tests." This reverts commit 2955b86. * updated package version from 14.1.0 to 15.0.0. This change is introducing a potencial breaking change if projects use ui-view component as the bootstrap component.
1 parent e7b978e commit 6b496c2

28 files changed

+531
-7
lines changed

downstream_projects.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"projects": {
44
"sample-app-angular": "https://github.com/ui-router/sample-app-angular.git",
55
"angular18": "./test-angular-versions/v18",
6+
"angular18standalone": "./test-angular-versions/v18-standalone",
67
"typescript54": "./test-typescript-versions/typescript5.4"
78
}
89
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@uirouter/angular",
33
"description": "State-based routing for Angular",
4-
"version": "14.0.0",
4+
"version": "15.0.0",
55
"scripts": {
66
"clean": "shx rm -rf lib lib-esm _bundles _doc dist",
77
"compile": "npm run clean && ngc",

src/directives/uiSref.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import { ReplaySubject, Subscription } from 'rxjs';
1717
* @internal
1818
* # blah blah blah
1919
*/
20-
@Directive({ selector: 'a[uiSref]' })
20+
@Directive({
21+
selector: 'a[uiSref]',
22+
standalone: true
23+
})
2124
export class AnchorUISref {
2225
constructor(public _el: ElementRef, public _renderer: Renderer2) {}
2326

@@ -78,6 +81,7 @@ export class AnchorUISref {
7881
@Directive({
7982
selector: '[uiSref]',
8083
exportAs: 'uiSref',
84+
standalone: true
8185
})
8286
export class UISref implements OnChanges {
8387
/**

src/directives/uiSrefActive.ts

+5
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ import { Subscription } from 'rxjs';
8282
*/
8383
@Directive({
8484
selector: '[uiSrefActive],[uiSrefActiveEq]',
85+
hostDirectives: [{
86+
directive: UISrefStatus,
87+
outputs: ['uiSrefStatus']
88+
}],
89+
standalone: true
8590
})
8691
export class UISrefActive {
8792
private _classes: string[] = [];

src/directives/uiSrefStatus.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,9 @@ function mergeSrefStatus(left: SrefStatus, right: SrefStatus): SrefStatus {
180180
* This API is subject to change.
181181
*/
182182
@Directive({
183-
selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]',
183+
selector: '[uiSrefStatus]',
184184
exportAs: 'uiSrefStatus',
185+
standalone: true
185186
})
186187
export class UISrefStatus {
187188
/** current statuses of the state/params the uiSref directive is linking to */

src/directives/uiView.ts

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from '@uirouter/core';
3434
import { Ng2ViewConfig } from '../statebuilders/views';
3535
import { MergeInjector } from '../mergeInjector';
36+
import { CommonModule } from '@angular/common';
3637

3738
/** @hidden */
3839
let id = 0;
@@ -110,6 +111,8 @@ const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
110111
@Component({
111112
selector: 'ui-view, [ui-view]',
112113
exportAs: 'uiView',
114+
standalone: true,
115+
imports: [CommonModule],
113116
template: `
114117
<ng-template #componentTarget></ng-template>
115118
<ng-content *ngIf="!_componentRef"></ng-content>

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export * from './statebuilders/lazyLoad';
77
export * from './statebuilders/views';
88
export * from './uiRouterConfig';
99
export * from './uiRouterNgModule';
10+
export * from './provideUiRouter';
1011

1112
export * from '@uirouter/core';

src/provideUiRouter.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { EnvironmentProviders, makeEnvironmentProviders } from "@angular/core";
2+
import { locationStrategy, makeRootProviders, RootModule } from "./uiRouterNgModule";
3+
import { _UIROUTER_INSTANCE_PROVIDERS, _UIROUTER_SERVICE_PROVIDERS } from "./providers";
4+
5+
/**
6+
* Sets up providers necessary to enable UI-Router for the application. Intended as a replacement
7+
* for [[UIRouterModule.forRoot]] in newer standalone based applications.
8+
*
9+
* Example:
10+
* ```js
11+
* const routerConfig = {
12+
* otherwise: '/home',
13+
* states: [homeState, aboutState]
14+
* };
15+
*
16+
* const appConfig: ApplicationConfig = {
17+
* providers: [
18+
* provideZoneChangeDetection({ eventCoalescing: true }),
19+
* provideUIRouter(routerConfig)
20+
* ]
21+
* };
22+
*
23+
* bootstrapApplication(AppComponent, appConfig)
24+
* .catch((err) => console.error(err));
25+
* ```
26+
*
27+
* @param config declarative UI-Router configuration
28+
* @returns an `EnvironmentProviders` which provides the [[UIRouter]] singleton instance
29+
*/
30+
export function provideUIRouter(config: RootModule = {}): EnvironmentProviders {
31+
return makeEnvironmentProviders([
32+
_UIROUTER_INSTANCE_PROVIDERS,
33+
_UIROUTER_SERVICE_PROVIDERS,
34+
locationStrategy(config.useHash),
35+
...makeRootProviders(config),
36+
]);
37+
}

src/uiRouterNgModule.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import {
77
Injector,
88
APP_INITIALIZER,
99
} from '@angular/core';
10-
import { CommonModule, LocationStrategy, HashLocationStrategy, PathLocationStrategy } from '@angular/common';
10+
import { LocationStrategy, HashLocationStrategy, PathLocationStrategy } from '@angular/common';
1111
import { _UIROUTER_DIRECTIVES } from './directives/directives';
12-
import { UIView } from './directives/uiView';
1312
import { UrlRuleHandlerFn, TargetState, TargetStateDef, UIRouter, TransitionService } from '@uirouter/core';
1413
import { _UIROUTER_INSTANCE_PROVIDERS, _UIROUTER_SERVICE_PROVIDERS } from './providers';
1514

@@ -71,8 +70,9 @@ export function locationStrategy(useHash) {
7170
* This enables UI-Router to automatically register the states with the [[StateRegistry]] at bootstrap (and during lazy load).
7271
*/
7372
@NgModule({
74-
imports: [CommonModule],
75-
declarations: [_UIROUTER_DIRECTIVES],
73+
imports: [
74+
_UIROUTER_DIRECTIVES
75+
],
7676
exports: [_UIROUTER_DIRECTIVES],
7777
})
7878
export class UIRouterModule {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# V18
2+
3+
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.7.
4+
5+
## Development server
6+
7+
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
8+
9+
## Code scaffolding
10+
11+
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12+
13+
## Build
14+
15+
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
16+
17+
## Running unit tests
18+
19+
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20+
21+
## Running end-to-end tests
22+
23+
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
24+
25+
## Further help
26+
27+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{
2+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3+
"version": 1,
4+
"cli": {
5+
"packageManager": "yarn"
6+
},
7+
"newProjectRoot": "projects",
8+
"projects": {
9+
"v18": {
10+
"projectType": "application",
11+
"schematics": {
12+
"@schematics/angular:class": {
13+
"skipTests": true
14+
},
15+
"@schematics/angular:component": {
16+
"skipTests": true
17+
},
18+
"@schematics/angular:directive": {
19+
"skipTests": true
20+
},
21+
"@schematics/angular:guard": {
22+
"skipTests": true
23+
},
24+
"@schematics/angular:interceptor": {
25+
"skipTests": true
26+
},
27+
"@schematics/angular:pipe": {
28+
"skipTests": true
29+
},
30+
"@schematics/angular:resolver": {
31+
"skipTests": true
32+
},
33+
"@schematics/angular:service": {
34+
"skipTests": true
35+
}
36+
},
37+
"root": "",
38+
"sourceRoot": "src",
39+
"prefix": "app",
40+
"architect": {
41+
"build": {
42+
"builder": "@angular-devkit/build-angular:application",
43+
"options": {
44+
"outputPath": "dist/v18",
45+
"index": "src/index.html",
46+
"browser": "src/main.ts",
47+
"polyfills": [
48+
"zone.js"
49+
],
50+
"tsConfig": "tsconfig.app.json",
51+
"assets": [
52+
{
53+
"glob": "**/*",
54+
"input": "public"
55+
}
56+
],
57+
"styles": [
58+
"src/styles.css"
59+
],
60+
"scripts": []
61+
},
62+
"configurations": {
63+
"production": {
64+
"budgets": [
65+
{
66+
"type": "initial",
67+
"maximumWarning": "500kB",
68+
"maximumError": "1MB"
69+
},
70+
{
71+
"type": "anyComponentStyle",
72+
"maximumWarning": "2kB",
73+
"maximumError": "4kB"
74+
}
75+
],
76+
"outputHashing": "all"
77+
},
78+
"development": {
79+
"optimization": false,
80+
"extractLicenses": false,
81+
"sourceMap": true
82+
}
83+
},
84+
"defaultConfiguration": "production"
85+
},
86+
"serve": {
87+
"builder": "@angular-devkit/build-angular:dev-server",
88+
"configurations": {
89+
"production": {
90+
"buildTarget": "v18:build:production"
91+
},
92+
"development": {
93+
"buildTarget": "v18:build:development"
94+
}
95+
},
96+
"defaultConfiguration": "development"
97+
},
98+
"extract-i18n": {
99+
"builder": "@angular-devkit/build-angular:extract-i18n"
100+
},
101+
"test": {
102+
"builder": "@angular-devkit/build-angular:karma",
103+
"options": {
104+
"polyfills": [
105+
"zone.js",
106+
"zone.js/testing"
107+
],
108+
"tsConfig": "tsconfig.spec.json",
109+
"assets": [
110+
{
111+
"glob": "**/*",
112+
"input": "public"
113+
}
114+
],
115+
"styles": [
116+
"src/styles.css"
117+
],
118+
"scripts": []
119+
}
120+
}
121+
}
122+
}
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from 'cypress';
2+
3+
export default defineConfig({
4+
video: false,
5+
e2e: {
6+
setupNodeEvents(on, config) {},
7+
baseUrl: 'http://localhost:4000',
8+
supportFile: false
9+
},
10+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
describe('Angular app', () => {
2+
beforeEach(() => {
3+
window.sessionStorage.clear();
4+
});
5+
6+
it('loads', () => {
7+
cy.visit('');
8+
});
9+
10+
it('loads home state by default', () => {
11+
cy.visit('');
12+
cy.url().should('include', '/home');
13+
});
14+
15+
it('renders uisref as links', () => {
16+
cy.visit('');
17+
cy.get('a').contains('home');
18+
cy.get('a').contains('about');
19+
cy.get('a').contains('lazy');
20+
cy.get('a').contains('lazy.child');
21+
cy.get('a').contains('lazy.child.viewtarget');
22+
});
23+
24+
it('renders home', () => {
25+
cy.visit('/home');
26+
cy.get('a').contains('home').should('have.class', 'active');
27+
cy.get('a').contains('about').should('not.have.class', 'active');
28+
cy.get('#default').contains('home works');
29+
});
30+
31+
it('renders about', () => {
32+
cy.visit('/home');
33+
cy.visit('/about');
34+
cy.get('a').contains('home').should('not.have.class', 'active');
35+
cy.get('a').contains('about').should('have.class', 'active');
36+
cy.get('#default').contains('about works');
37+
});
38+
39+
it('loads lazy routes', () => {
40+
cy.visit('/home');
41+
cy.visit('/lazy');
42+
cy.get('a').contains('home').should('not.have.class', 'active');
43+
cy.get('a').contains('lazy').should('have.class', 'active');
44+
cy.get('#default').contains('lazy works');
45+
});
46+
47+
it('routes to lazy routes', () => {
48+
cy.visit('/lazy');
49+
cy.get('a').contains('home').should('not.have.class', 'active');
50+
cy.get('a').contains('lazy').should('have.class', 'active');
51+
cy.get('#default').contains('lazy works');
52+
});
53+
54+
it('routes to lazy child routes', () => {
55+
cy.visit('/lazy/child');
56+
cy.get('a').contains('home').should('not.have.class', 'active');
57+
cy.get('a').contains('lazy.child').should('have.class', 'active');
58+
cy.get('#default').contains('lazy.child works');
59+
});
60+
61+
it('targets named views', () => {
62+
cy.visit('/lazy/child/viewtarget');
63+
cy.get('a').contains('home').should('not.have.class', 'active');
64+
cy.get('a').contains('lazy.child').should('have.class', 'active');
65+
cy.get('#default').contains('lazy.child works');
66+
cy.get('#header').contains('lazy.child.viewtarget works');
67+
cy.get('#footer').contains('lazy.child.viewtarget works');
68+
});
69+
});

0 commit comments

Comments
 (0)