Skip to content

[go_router_builder][go_router] Add support for relative routes #6823

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## 14.1.3

- Adds `GoRouter.goRelative`

## 14.1.3

- Improves the logging of routes when `debugLogDiagnostics` is enabled or `debugKnownRoutes() is called. Explains the position of shell routes in the route tree. Prints the widget name of the routes it is building.

## 14.1.2
Expand Down
27 changes: 27 additions & 0 deletions packages/go_router/lib/src/information_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ class GoRouteInformationProvider extends RouteInformationProvider
);
}

/// Relatively go to [relativeLocation].
void goRelative(String relativeLocation, {Object? extra}) {
assert(
!relativeLocation.startsWith('/'),
"Relative locations must not start with a '/'.",
);

final Uri currentUri = value.uri;
Uri newUri = Uri.parse(
currentUri.path.endsWith('/')
? '${currentUri.path}$relativeLocation'
: '${currentUri.path}/$relativeLocation',
);
newUri = newUri.replace(queryParameters: <String, dynamic>{
...currentUri.queryParameters,
...newUri.queryParameters,
});

_setValue(
newUri.toString(),
RouteInformationState<void>(
extra: extra,
type: NavigatingType.go,
),
);
}

/// Restores the current route matches with the `matchList`.
void restore(String location, {required RouteMatchList matchList}) {
_setValue(
Expand Down
7 changes: 7 additions & 0 deletions packages/go_router/lib/src/misc/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ extension GoRouterHelper on BuildContext {
void go(String location, {Object? extra}) =>
GoRouter.of(this).go(location, extra: extra);

/// Navigate relative to a location.
void goRelative(String location, {Object? extra}) =>
GoRouter.of(this).goRelative(
location,
extra: extra,
);

/// Navigate to a named route.
void goNamed(
String name, {
Expand Down
22 changes: 22 additions & 0 deletions packages/go_router/lib/src/route_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,28 @@ class TypedGoRoute<T extends GoRouteData> extends TypedRoute<T> {
final List<TypedRoute<RouteData>> routes;
}

/// A superclass for each typed go route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedRelativeGoRoute<T extends GoRouteData> extends TypedRoute<T> {
/// Default const constructor
const TypedRelativeGoRoute({
required this.path,
this.routes = const <TypedRoute<RouteData>>[],
});

/// The relative path that corresponds to this route.
///
/// See [GoRoute.path].
///
///
final String path;

/// Child route definitions.
///
/// See [RouteBase.routes].
final List<TypedRoute<RouteData>> routes;
}

/// A superclass for each typed shell route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
Expand Down
9 changes: 9 additions & 0 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,15 @@ class GoRouter implements RouterConfig<RouteMatchList> {
routeInformationProvider.go(location, extra: extra);
}

/// Navigate to a URI location by appending [relativeLocation] to the current [GoRouterState.matchedLocation] w/ optional query parameters, e.g.
void goRelative(
String relativeLocation, {
Object? extra,
}) {
log('going relative to $relativeLocation');
routeInformationProvider.goRelative(relativeLocation, extra: extra);
}

/// Restore the RouteMatchList
void restore(RouteMatchList matchList) {
log('restoring ${matchList.uri}');
Expand Down
277 changes: 277 additions & 0 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,283 @@ void main() {
});
});

group('go relative', () {
testWidgets('from default route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];

final GoRouter router = await createRouter(routes, tester);
router.goRelative('login');
await tester.pumpAndSettle();
expect(find.byType(LoginScreen), findsOneWidget);
});

testWidgets('from non-default route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];

final GoRouter router = await createRouter(routes, tester);
router.go('/home');
router.goRelative('login');
await tester.pumpAndSettle();
expect(find.byType(LoginScreen), findsOneWidget);
});

testWidgets('match w/ path params', (WidgetTester tester) async {
const String fid = 'f2';
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
name: 'person',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.pathParameters,
<String, String>{'fid': fid, 'pid': pid});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');
router.go('/');

router.goRelative('family/$fid');
await tester.pumpAndSettle();
expect(find.byType(FamilyScreen), findsOneWidget);

router.goRelative('person/$pid');
await tester.pumpAndSettle();
expect(find.byType(PersonScreen), findsOneWidget);
});

testWidgets('match w/ query params', (WidgetTester tester) async {
const String fid = 'f2';
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person',
builder: (BuildContext context, GoRouterState state) {
expect(state.uri.queryParameters,
<String, String>{'fid': fid, 'pid': pid});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');

router.goRelative('family?fid=$fid');
await tester.pumpAndSettle();
expect(find.byType(FamilyScreen), findsOneWidget);

router.goRelative('person?pid=$pid');
await tester.pumpAndSettle();
expect(find.byType(PersonScreen), findsOneWidget);
});

testWidgets('too few params', (WidgetTester tester) async {
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) =>
const PersonScreen('dummy', 'dummy'),
),
],
),
],
),
];
// await expectLater(() async {
final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/home',
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
router.goRelative('family/person/$pid');
await tester.pumpAndSettle();
expect(find.byType(TestErrorScreen), findsOneWidget);

final List<RouteMatchBase> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
});

testWidgets('match no route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person',
builder: (BuildContext context, GoRouterState state) =>
const PersonScreen('dummy', 'dummy'),
),
],
),
],
),
];

final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/home',
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
router.go('person');

await tester.pumpAndSettle();
expect(find.byType(TestErrorScreen), findsOneWidget);

final List<RouteMatchBase> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
});

testWidgets('preserve path param spaces and slashes',
(WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: dummy,
routes: <RouteBase>[
GoRoute(
path: 'page1/:param1',
builder: (BuildContext c, GoRouterState s) {
expect(s.pathParameters['param1'], param1);
return const DummyScreen();
},
),
],
)
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');
final String loc = 'page1/${Uri.encodeComponent(param1)}';
router.goRelative(loc);

await tester.pumpAndSettle();
expect(find.byType(DummyScreen), findsOneWidget);

final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.pathParameters['param1'], param1);
});

testWidgets('preserve query param spaces and slashes',
(WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: dummy,
routes: <RouteBase>[
GoRoute(
path: 'page1',
builder: (BuildContext c, GoRouterState s) {
expect(s.uri.queryParameters['param1'], param1);
return const DummyScreen();
},
),
],
)
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');

router.goRelative(Uri(
path: 'page1',
queryParameters: <String, dynamic>{'param1': param1},
).toString());

await tester.pumpAndSettle();
expect(find.byType(DummyScreen), findsOneWidget);

final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.uri.queryParameters['param1'], param1);
});
});

group('redirects', () {
testWidgets('top-level redirect', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
Expand Down
4 changes: 4 additions & 0 deletions packages/go_router_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.7.1

- Adds `TypedRelativeGoRoute` annotation which supports relative routes.

## 2.7.0

- Adds an example and a test with `onExit`.
Expand Down
Loading