From 02689ff17952438d6c4fdc9275cce87d37bc7fe5 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:12:19 +0700 Subject: [PATCH 01/16] - Adds `GoRouter.goRelative` - Adds `TypedRelativeGoRoute` --- .../lib/src/information_provider.dart | 27 ++ .../go_router/lib/src/misc/extensions.dart | 7 + packages/go_router/lib/src/route_data.dart | 22 ++ packages/go_router/lib/src/router.dart | 9 + packages/go_router/test/go_router_test.dart | 277 ++++++++++++++++++ 5 files changed, 342 insertions(+) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index dc979193b32..999cd530a25 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -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: { + ...currentUri.queryParameters, + ...newUri.queryParameters, + }); + + _setValue( + newUri.toString(), + RouteInformationState( + extra: extra, + type: NavigatingType.go, + ), + ); + } + /// Restores the current route matches with the `matchList`. void restore(String location, {required RouteMatchList matchList}) { _setValue( diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c137022b802..f5f654ce475 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -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, { diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index afe6159cecb..c6c2e13845b 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -390,6 +390,28 @@ class TypedGoRoute extends TypedRoute { final List> routes; } +/// A superclass for each typed go route descendant +@Target({TargetKind.library, TargetKind.classType}) +class TypedRelativeGoRoute extends TypedRoute { + /// Default const constructor + const TypedRelativeGoRoute({ + required this.path, + this.routes = const >[], + }); + + /// The relative path that corresponds to this route. + /// + /// See [GoRoute.path]. + /// + /// + final String path; + + /// Child route definitions. + /// + /// See [RouteBase.routes]. + final List> routes; +} + /// A superclass for each typed shell route descendant @Target({TargetKind.library, TargetKind.classType}) class TypedShellRoute extends TypedRoute { diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 2bc2057a3bb..5928e145cd4 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -340,6 +340,15 @@ class GoRouter implements RouterConfig { 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}'); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 10e6bb58d5b..01508675b01 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1874,6 +1874,283 @@ void main() { }); }); + group('go relative', () { + testWidgets('from default route', (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + 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 routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + 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 routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + expect(state.pathParameters, + {'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 routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + GoRoute( + path: 'person', + builder: (BuildContext context, GoRouterState state) { + expect(state.uri.queryParameters, + {'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 routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + 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 matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); + }); + + testWidgets('match no route', (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + path: 'family', + builder: (BuildContext context, GoRouterState state) => + const FamilyScreen('dummy'), + routes: [ + 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 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 routes = [ + GoRoute( + path: '/home', + builder: dummy, + routes: [ + 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 routes = [ + GoRoute( + path: '/home', + builder: dummy, + routes: [ + 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: {'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 routes = [ From e8ac4ee630301ed44e66b30f73c7bfcde0be9bad Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:27:23 +0700 Subject: [PATCH 02/16] add go_relative example for go_router --- .../go_router/example/lib/go_relative.dart | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 packages/go_router/example/lib/go_relative.dart diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart new file mode 100644 index 00000000000..114e3b2fdff --- /dev/null +++ b/packages/go_router/example/lib/go_relative.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app demonstrates how to use GoRoute.goRelative. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + routes: [ + GoRoute( + path: 'settings', + builder: (BuildContext context, GoRouterState state) { + return const SettingsScreen(); + }, + ), + ], + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goRelative('details'), + child: const Text('Go to the Details screen'), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: Column( + children: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('go back'), + ), + TextButton( + onPressed: () { + context.goRelative('settings'); + }, + child: const Text('go to settings'), + ), + ], + )), + ); + } +} + +/// The settings screen +class SettingsScreen extends StatelessWidget { + /// Constructs a [SettingsScreen] + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Settings Screen')), + body: const Center( + child: Text('Settings'), + ), + ); + } +} From c42151c71b80161a59d0e77c99f5d302704a1a05 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 15:32:11 +0700 Subject: [PATCH 03/16] add go_relative_test for the example --- .../go_router/example/lib/go_relative.dart | 2 +- .../example/test/go_relative_test.dart | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/go_router/example/test/go_relative_test.dart diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 114e3b2fdff..20f42dbb6ef 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -95,7 +95,7 @@ class DetailsScreen extends StatelessWidget { onPressed: () { context.goRelative('settings'); }, - child: const Text('go to settings'), + child: const Text('Go to the Settings screen'), ), ], )), diff --git a/packages/go_router/example/test/go_relative_test.dart b/packages/go_router/example/test/go_relative_test.dart new file mode 100644 index 00000000000..fbc884d5559 --- /dev/null +++ b/packages/go_router/example/test/go_relative_test.dart @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/go_relative.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.byType(example.HomeScreen), findsOneWidget); + + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go to the Settings screen')); + await tester.pumpAndSettle(); + expect(find.byType(example.SettingsScreen), findsOneWidget); + }); +} From 022db98454c8941aa0471ef0e3b2c3db80f5dc9a Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 29 May 2024 17:11:09 +0700 Subject: [PATCH 04/16] fix failed test --- packages/go_router/test/go_router_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 01508675b01..f84c0e8135e 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1952,7 +1952,6 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.go('/'); router.goRelative('family/$fid'); await tester.pumpAndSettle(); From 812498eb8747b6b573b6d3fcc44da56836734fe4 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 19:45:39 +0700 Subject: [PATCH 05/16] replace goRelative with go('./$path') --- .../go_router/example/lib/go_relative.dart | 4 ++-- .../lib/src/information_provider.dart | 10 +++++++-- .../go_router/lib/src/misc/extensions.dart | 7 ------- packages/go_router/lib/src/router.dart | 9 -------- packages/go_router/test/go_router_test.dart | 21 ++++++++++--------- 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 20f42dbb6ef..54f17293d2f 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -63,7 +63,7 @@ class HomeScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: () => context.goRelative('details'), + onPressed: () => context.go('./details'), child: const Text('Go to the Details screen'), ), ], @@ -93,7 +93,7 @@ class DetailsScreen extends StatelessWidget { ), TextButton( onPressed: () { - context.goRelative('settings'); + context.go('./settings'); }, child: const Text('Go to the Settings screen'), ), diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 999cd530a25..6588d07e35d 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'match.dart'; +import 'path_utils.dart'; /// The type of the navigation. /// @@ -135,11 +136,16 @@ class GoRouteInformationProvider extends RouteInformationProvider } void _setValue(String location, Object state) { - final Uri uri = Uri.parse(location); + Uri uri = Uri.parse(location); + + // Check for relative location + if (location.startsWith('./')) { + uri = concatenateUris(_value.uri, uri); + } final bool shouldNotify = _valueHasChanged(newLocationUri: uri, newState: state); - _value = RouteInformation(uri: Uri.parse(location), state: state); + _value = RouteInformation(uri: uri, state: state); if (shouldNotify) { notifyListeners(); } diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index f5f654ce475..c137022b802 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -24,13 +24,6 @@ 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, { diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 5928e145cd4..2bc2057a3bb 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -340,15 +340,6 @@ class GoRouter implements RouterConfig { 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}'); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index f84c0e8135e..2e361ffe0b2 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1892,7 +1892,7 @@ void main() { ]; final GoRouter router = await createRouter(routes, tester); - router.goRelative('login'); + router.go('./login'); await tester.pumpAndSettle(); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -1915,7 +1915,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.go('/home'); - router.goRelative('login'); + router.go('./login'); await tester.pumpAndSettle(); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -1953,11 +1953,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative('family/$fid'); + router.go('./family/$fid'); await tester.pumpAndSettle(); expect(find.byType(FamilyScreen), findsOneWidget); - router.goRelative('person/$pid'); + router.go('./person/$pid'); await tester.pumpAndSettle(); expect(find.byType(PersonScreen), findsOneWidget); }); @@ -1994,11 +1994,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative('family?fid=$fid'); + router.go('./family?fid=$fid'); await tester.pumpAndSettle(); expect(find.byType(FamilyScreen), findsOneWidget); - router.goRelative('person?pid=$pid'); + router.go('./person?pid=$pid'); await tester.pumpAndSettle(); expect(find.byType(PersonScreen), findsOneWidget); }); @@ -2035,7 +2035,7 @@ void main() { errorBuilder: (BuildContext context, GoRouterState state) => TestErrorScreen(state.error!), ); - router.goRelative('family/person/$pid'); + router.go('./family/person/$pid'); await tester.pumpAndSettle(); expect(find.byType(TestErrorScreen), findsOneWidget); @@ -2106,7 +2106,7 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); final String loc = 'page1/${Uri.encodeComponent(param1)}'; - router.goRelative(loc); + router.go('./$loc'); await tester.pumpAndSettle(); expect(find.byType(DummyScreen), findsOneWidget); @@ -2137,10 +2137,11 @@ void main() { final GoRouter router = await createRouter(routes, tester, initialLocation: '/home'); - router.goRelative(Uri( + final String loc = Uri( path: 'page1', queryParameters: {'param1': param1}, - ).toString()); + ).toString(); + router.go('./$loc'); await tester.pumpAndSettle(); expect(find.byType(DummyScreen), findsOneWidget); From 07b63ad218f26d8faddcd83eebf4718e024476d7 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:01:24 +0700 Subject: [PATCH 06/16] Commit missing files during merge --- .../lib/src/information_provider.dart | 27 ---------- packages/go_router/lib/src/path_utils.dart | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 6588d07e35d..a019761922f 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -178,33 +178,6 @@ 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: { - ...currentUri.queryParameters, - ...newUri.queryParameters, - }); - - _setValue( - newUri.toString(), - RouteInformationState( - extra: extra, - type: NavigatingType.go, - ), - ); - } - /// Restores the current route matches with the `matchList`. void restore(String location, {required RouteMatchList matchList}) { _setValue( diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 872f7786b26..a2a9175600f 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -118,6 +118,56 @@ String concatenatePaths(String parentPath, String childPath) { return '${parentPath == '/' ? '' : parentPath}/$childPath'; } +/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths , then merges their query parameters. +/// +/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. +Uri concatenateUris(Uri parentUri, Uri childUri) { + final Uri newUri = parentUri.replace( + path: concatenatePaths(parentUri.path, childUri.path), + queryParameters: { + ...parentUri.queryParameters, + ...childUri.queryParameters, + }, + ); + + return newUri; +} + +/// Normalizes the location string. +String canonicalUri(String loc) { + if (loc.isEmpty) { + throw GoException('Location cannot be empty.'); + } + String canon = Uri.parse(loc).toString(); + canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon; + final Uri uri = Uri.parse(canon); + + // remove trailing slash except for when you shouldn't, e.g. + // /profile/ => /profile + // / => / + // /login?from=/ => /login?from=/ + canon = uri.path.endsWith('/') && + uri.path != '/' && + !uri.hasQuery && + !uri.hasFragment + ? canon.substring(0, canon.length - 1) + : canon; + + // replace '/?', except for first occurrence, from path only + // /login/?from=/ => /login?from=/ + // /?from=/ => /?from=/ + final int pathStartIndex = uri.host.isNotEmpty + ? uri.toString().indexOf(uri.host) + uri.host.length + : uri.hasScheme + ? uri.toString().indexOf(uri.scheme) + uri.scheme.length + : 0; + if (pathStartIndex < canon.length) { + canon = canon.replaceFirst('/?', '?', pathStartIndex + 1); + } + + return canon; +} + /// Builds an absolute path for the provided route. String? fullPathForRoute( RouteBase targetRoute, String parentFullpath, List routes) { From c025d866a6568d1882089b1947561ef5d05824bf Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:05:26 +0700 Subject: [PATCH 07/16] update changelog & version --- packages/go_router/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 137f8d2329d..20ecdca50ac 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.2.8 + +- Allows going to a path relatively by prefixing `./` + ## 14.2.7 - Fixes issue so that the parseRouteInformationWithContext can handle non-http Uris. From 3fb646303e2ad80828789cc86c14d5ea75da96c9 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:19:23 +0700 Subject: [PATCH 08/16] Prevent concatenateUris from adding trailing redundant '?'. Add test for `concatenateUris` --- packages/go_router/lib/src/path_utils.dart | 15 ++++++--- packages/go_router/test/path_utils_test.dart | 33 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index a2a9175600f..045fec41e63 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -122,12 +122,19 @@ String concatenatePaths(String parentPath, String childPath) { /// /// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. Uri concatenateUris(Uri parentUri, Uri childUri) { + // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. + final Map? newParameters = + parentUri.queryParameters.isNotEmpty && + childUri.queryParameters.isNotEmpty + ? { + ...parentUri.queryParameters, + ...childUri.queryParameters, + } + : null; + final Uri newUri = parentUri.replace( path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: { - ...parentUri.queryParameters, - ...childUri.queryParameters, - }, + queryParameters: newParameters, ); return newUri; diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index a495e4d1a05..881a0b6ae04 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -92,4 +92,37 @@ void main() { verifyThrows('/', ''); verifyThrows('', ''); }); + + test('concatenateUris', () { + void verify(String pathA, String pathB, String expected) { + final String result = + concatenateUris(Uri.parse(pathA), Uri.parse(pathB)).toString(); + expect(result, expected); + } + + verify('/a', 'b/c', '/a/b/c'); + verify('/', 'b', '/b'); + verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); + }); + + test('canonicalUri', () { + void verify(String path, String expected) => + expect(canonicalUri(path), expected); + verify('/a', '/a'); + verify('/a/', '/a'); + verify('/', '/'); + verify('/a/b/', '/a/b'); + verify('https://www.example.com/', 'https://www.example.com/'); + verify('https://www.example.com/a', 'https://www.example.com/a'); + verify('https://www.example.com/a/', 'https://www.example.com/a'); + verify('https://www.example.com/a/b/', 'https://www.example.com/a/b'); + verify('https://www.example.com/?', 'https://www.example.com/'); + verify('https://www.example.com/?a=b', 'https://www.example.com/?a=b'); + verify('https://www.example.com/?a=/', 'https://www.example.com/?a=/'); + verify('https://www.example.com/a/?b=c', 'https://www.example.com/a?b=c'); + verify('https://www.example.com/#a/', 'https://www.example.com/#a/'); + + expect(() => canonicalUri('::::'), throwsA(isA())); + expect(() => canonicalUri(''), throwsA(anything)); + }); } From 205d09600113f73ba7d3d465e20eb6ecc79a2b12 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 19 Jun 2024 20:27:35 +0700 Subject: [PATCH 09/16] Add more `concatenateUris` test. Fix joining params --- packages/go_router/lib/src/path_utils.dart | 2 +- packages/go_router/test/path_utils_test.dart | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 045fec41e63..174932a3e67 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -124,7 +124,7 @@ String concatenatePaths(String parentPath, String childPath) { Uri concatenateUris(Uri parentUri, Uri childUri) { // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. final Map? newParameters = - parentUri.queryParameters.isNotEmpty && + parentUri.queryParameters.isNotEmpty || childUri.queryParameters.isNotEmpty ? { ...parentUri.queryParameters, diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 881a0b6ae04..78e11dd67f3 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -102,6 +102,10 @@ void main() { verify('/a', 'b/c', '/a/b/c'); verify('/', 'b', '/b'); + + // Test with parameters + verify('/a?fid=f1', 'b/c', '/a/b/c?fid=f1'); + verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); }); From 8e4db8eb1890ac7ad59a305660fe6868dc3312e7 Mon Sep 17 00:00:00 2001 From: Thang Date: Wed, 10 Jul 2024 11:13:08 +0700 Subject: [PATCH 10/16] update example description --- packages/go_router/example/lib/go_relative.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index 54f17293d2f..f59faa9e76c 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -/// This sample app demonstrates how to use GoRoute.goRelative. +/// This sample app demonstrates how to use go relatively with GoRouter.go('./$path'). void main() => runApp(const MyApp()); /// The route configuration. From 0ead0afbf3b4172cd5c9beae4af66fca8cabab71 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 19 Jul 2024 18:31:53 +0700 Subject: [PATCH 11/16] Make concatenateUris not merging parameters, only take them from childUri --- packages/go_router/lib/src/path_utils.dart | 20 ++++++-------------- packages/go_router/test/go_router_test.dart | 2 +- packages/go_router/test/path_utils_test.dart | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 174932a3e67..c4b7521e6d9 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -118,25 +118,17 @@ String concatenatePaths(String parentPath, String childPath) { return '${parentPath == '/' ? '' : parentPath}/$childPath'; } -/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths , then merges their query parameters. +/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths, and take only the child's parameters. /// -/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?fid=1&pid=2. +/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2. Uri concatenateUris(Uri parentUri, Uri childUri) { - // Merge query parameters from both Uris. We don't return an empty map to prevent trailing '?'. - final Map? newParameters = - parentUri.queryParameters.isNotEmpty || - childUri.queryParameters.isNotEmpty - ? { - ...parentUri.queryParameters, - ...childUri.queryParameters, - } - : null; - - final Uri newUri = parentUri.replace( + Uri newUri = parentUri.replace( path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: newParameters, + queryParameters: childUri.queryParameters, ); + // Parse the new normalized uri to remove unnecessary parts, like the trailing '?'. + newUri = Uri.parse(canonicalUri(newUri.toString())); return newUri; } diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 2e361ffe0b2..12db2e21a07 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1981,7 +1981,7 @@ void main() { path: 'person', builder: (BuildContext context, GoRouterState state) { expect(state.uri.queryParameters, - {'fid': fid, 'pid': pid}); + {'pid': pid}); return const PersonScreen('dummy', 'dummy'); }, ), diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index 78e11dd67f3..d25a56f246e 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -104,9 +104,9 @@ void main() { verify('/', 'b', '/b'); // Test with parameters - verify('/a?fid=f1', 'b/c', '/a/b/c?fid=f1'); + verify('/a?fid=f1', 'b/c', '/a/b/c'); verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); - verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?fid=f1&pid=p2'); + verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?pid=p2'); }); test('canonicalUri', () { From 9407200514c145b7391805a159addf79150b37f1 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 19 Jul 2024 19:54:30 +0700 Subject: [PATCH 12/16] Refactor example & its test --- .../go_router/example/lib/go_relative.dart | 70 +++++++++++-------- .../example/test/go_relative_test.dart | 8 +++ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/go_router/example/lib/go_relative.dart b/packages/go_router/example/lib/go_relative.dart index f59faa9e76c..72076dcbea0 100644 --- a/packages/go_router/example/lib/go_relative.dart +++ b/packages/go_router/example/lib/go_relative.dart @@ -8,6 +8,19 @@ import 'package:go_router/go_router.dart'; /// This sample app demonstrates how to use go relatively with GoRouter.go('./$path'). void main() => runApp(const MyApp()); +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + /// The route configuration. final GoRouter _router = GoRouter( routes: [ @@ -36,19 +49,6 @@ final GoRouter _router = GoRouter( ], ); -/// The main app. -class MyApp extends StatelessWidget { - /// Constructs a [MyApp] - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp.router( - routerConfig: _router, - ); - } -} - /// The home screen class HomeScreen extends StatelessWidget { /// Constructs a [HomeScreen] @@ -83,22 +83,23 @@ class DetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text('Details Screen')), body: Center( - child: Column( - children: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: const Text('go back'), - ), - TextButton( - onPressed: () { - context.go('./settings'); - }, - child: const Text('Go to the Settings screen'), - ), - ], - )), + child: Column( + children: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('Go back'), + ), + TextButton( + onPressed: () { + context.go('./settings'); + }, + child: const Text('Go to the Settings screen'), + ), + ], + ), + ), ); } } @@ -112,8 +113,15 @@ class SettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Settings Screen')), - body: const Center( - child: Text('Settings'), + body: Column( + children: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('Go back'), + ), + ], ), ); } diff --git a/packages/go_router/example/test/go_relative_test.dart b/packages/go_router/example/test/go_relative_test.dart index fbc884d5559..ee18d01dd76 100644 --- a/packages/go_router/example/test/go_relative_test.dart +++ b/packages/go_router/example/test/go_relative_test.dart @@ -17,5 +17,13 @@ void main() { await tester.tap(find.text('Go to the Settings screen')); await tester.pumpAndSettle(); expect(find.byType(example.SettingsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.text('Go back')); + await tester.pumpAndSettle(); + expect(find.byType(example.HomeScreen), findsOneWidget); }); } From 68305396579a6d5e3028522952cf940fd98d6cd3 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 6 Sep 2024 10:30:21 +0700 Subject: [PATCH 13/16] Remove TypedRelativeGoRoute --- packages/go_router/lib/src/route_data.dart | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index c6c2e13845b..afe6159cecb 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -390,28 +390,6 @@ class TypedGoRoute extends TypedRoute { final List> routes; } -/// A superclass for each typed go route descendant -@Target({TargetKind.library, TargetKind.classType}) -class TypedRelativeGoRoute extends TypedRoute { - /// Default const constructor - const TypedRelativeGoRoute({ - required this.path, - this.routes = const >[], - }); - - /// The relative path that corresponds to this route. - /// - /// See [GoRoute.path]. - /// - /// - final String path; - - /// Child route definitions. - /// - /// See [RouteBase.routes]. - final List> routes; -} - /// A superclass for each typed shell route descendant @Target({TargetKind.library, TargetKind.classType}) class TypedShellRoute extends TypedRoute { From 43eb497f15927eedb3491c4072f8e40a205d8635 Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 6 Sep 2024 10:46:59 +0700 Subject: [PATCH 14/16] Add missing import --- packages/go_router/lib/src/path_utils.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index c4b7521e6d9..247f17db14c 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'misc/errors.dart'; import 'route.dart'; final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?'); From 2dd80d97014cb84927c8b6d10e73490500f60fdd Mon Sep 17 00:00:00 2001 From: Thang Date: Tue, 24 Sep 2024 23:49:55 +0700 Subject: [PATCH 15/16] add fragment test to concatenateUris --- packages/go_router/lib/src/path_utils.dart | 3 +-- packages/go_router/test/path_utils_test.dart | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart index 247f17db14c..5114e1e7690 100644 --- a/packages/go_router/lib/src/path_utils.dart +++ b/packages/go_router/lib/src/path_utils.dart @@ -123,9 +123,8 @@ String concatenatePaths(String parentPath, String childPath) { /// /// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2. Uri concatenateUris(Uri parentUri, Uri childUri) { - Uri newUri = parentUri.replace( + Uri newUri = childUri.replace( path: concatenatePaths(parentUri.path, childUri.path), - queryParameters: childUri.queryParameters, ); // Parse the new normalized uri to remove unnecessary parts, like the trailing '?'. diff --git a/packages/go_router/test/path_utils_test.dart b/packages/go_router/test/path_utils_test.dart index d25a56f246e..6acc4ba052c 100644 --- a/packages/go_router/test/path_utils_test.dart +++ b/packages/go_router/test/path_utils_test.dart @@ -104,9 +104,15 @@ void main() { verify('/', 'b', '/b'); // Test with parameters - verify('/a?fid=f1', 'b/c', '/a/b/c'); + verify('/a?fid=f1', 'b/c?', '/a/b/c'); verify('/a', 'b/c?pid=p2', '/a/b/c?pid=p2'); verify('/a?fid=f1', 'b/c?pid=p2', '/a/b/c?pid=p2'); + + // Test with fragment + verify('/a#f', 'b/c#f2', '/a/b/c#f2'); + + // Test with fragment and parameters + verify('/a?fid=f1#f', 'b/c?pid=p2#', '/a/b/c?pid=p2#'); }); test('canonicalUri', () { From d1ebfeb12ac33c9c5584b3cbe308a5a0e2c9136a Mon Sep 17 00:00:00 2001 From: Thang Date: Fri, 8 Nov 2024 19:20:38 +0700 Subject: [PATCH 16/16] bump version to 14.4.2 in pubspec.yaml --- packages/go_router/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index ce916a07f3d..2e5cceb4c79 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 14.4.1 +version: 14.4.2 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22