Finally done?
This commit is contained in:
parent
2420042d4d
commit
826cb90ffe
11 changed files with 366 additions and 33 deletions
14
README.md
14
README.md
|
@ -1,6 +1,6 @@
|
|||
# angel_route
|
||||
|
||||
![version 1.0.0-dev+13](https://img.shields.io/badge/version-1.0.0--dev+13-red.svg)
|
||||
![version 1.0.0-dev+14](https://img.shields.io/badge/version-1.0.0--dev+14-red.svg)
|
||||
![build status](https://travis-ci.org/angel-dart/route.svg)
|
||||
|
||||
A powerful, isomorphic routing library for Dart.
|
||||
|
@ -91,6 +91,13 @@ main() {
|
|||
main() {
|
||||
final router = new Router();
|
||||
|
||||
router
|
||||
.chain('middleware1')
|
||||
.chain('other_middleware')
|
||||
.get('/hello', () {
|
||||
print('world');
|
||||
});
|
||||
|
||||
router.group('/user/:id', (router) {
|
||||
router.get('/balance', (id) async {
|
||||
final user = await someQuery(id);
|
||||
|
@ -104,7 +111,7 @@ See [the tests](test/route/no_params.dart) for good examples.
|
|||
|
||||
# In the Browser
|
||||
Supports both hashed routes and pushState. The `BrowserRouter` interface exposes
|
||||
a `Stream<Route> onRoute`, which can be listened to for changes. It will fire `null`
|
||||
a `Stream<RoutingResult> onRoute`, which can be listened to for changes. It will fire `null`
|
||||
whenever no route is matched.
|
||||
|
||||
`angel_route` will also automatically intercept `<a>` elements and redirect them to
|
||||
|
@ -129,6 +136,9 @@ main() {
|
|||
}
|
||||
```
|
||||
|
||||
For applications where you need to access a chain of handlers, consider using
|
||||
`onResolve` instead. You can see an example in `web/shared/basic.dart`.
|
||||
|
||||
# Route Parameters
|
||||
Routes can have parameters, as seen in the above examples.
|
||||
If a parameter is a number, then it will automatically be parsed.
|
149
lib/browser.dart
Normal file
149
lib/browser.dart
Normal file
|
@ -0,0 +1,149 @@
|
|||
import 'dart:async' show Stream, StreamController;
|
||||
import 'dart:html' show AnchorElement, window;
|
||||
import 'angel_route.dart';
|
||||
|
||||
final RegExp _hash = new RegExp(r'^#/');
|
||||
|
||||
/// A variation of the [Router] support both hash routing and push state.
|
||||
abstract class BrowserRouter extends Router {
|
||||
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
||||
Stream<RoutingResult> get onResolve;
|
||||
|
||||
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
||||
Stream<Route> get onRoute;
|
||||
|
||||
/// Set `hash` to true to use hash routing instead of push state.
|
||||
/// `listen` as `true` will call `listen` after initialization.
|
||||
factory BrowserRouter({bool hash: false, bool listen: true}) {
|
||||
return hash
|
||||
? new _HashRouter(listen: listen)
|
||||
: new _PushStateRouter(listen: listen);
|
||||
}
|
||||
|
||||
BrowserRouter._() : super();
|
||||
|
||||
void _goTo(String path);
|
||||
|
||||
/// Navigates to the path generated by calling
|
||||
/// [navigate] with the given [linkParams].
|
||||
///
|
||||
/// This always navigates to an absolute path.
|
||||
void go(List linkParams);
|
||||
|
||||
/// Begins listen for location changes.
|
||||
void listen();
|
||||
|
||||
/// Identical to [all].
|
||||
Route on(Pattern path, handler, {List middleware}) =>
|
||||
all(path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
class _BrowserRouterImpl extends Router implements BrowserRouter {
|
||||
Route _current;
|
||||
StreamController<RoutingResult> _onResolve =
|
||||
new StreamController<RoutingResult>();
|
||||
StreamController<Route> _onRoute = new StreamController<Route>();
|
||||
Route get currentRoute => _current;
|
||||
|
||||
@override
|
||||
Stream<RoutingResult> get onResolve => _onResolve.stream;
|
||||
|
||||
@override
|
||||
Stream<Route> get onRoute => _onRoute.stream;
|
||||
|
||||
_BrowserRouterImpl({bool listen}) : super() {
|
||||
if (listen) this.listen();
|
||||
prepareAnchors();
|
||||
}
|
||||
|
||||
@override
|
||||
void go(List linkParams) => _goTo(navigate(linkParams));
|
||||
|
||||
void prepareAnchors() {
|
||||
final anchors = window.document.querySelectorAll('a:not([dynamic])');
|
||||
|
||||
for (final AnchorElement $a in anchors) {
|
||||
if ($a.attributes.containsKey('href') &&
|
||||
!$a.attributes.containsKey('download') &&
|
||||
!$a.attributes.containsKey('target') &&
|
||||
$a.attributes['rel'] != 'external') {
|
||||
$a.onClick.listen((e) {
|
||||
e.preventDefault();
|
||||
go($a.attributes['href'].split('/').where((str) => str.isNotEmpty));
|
||||
});
|
||||
}
|
||||
|
||||
$a.attributes['dynamic'] = 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HashRouter extends _BrowserRouterImpl {
|
||||
_HashRouter({bool listen}) : super(listen: listen) {
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void _goTo(String uri) {
|
||||
window.location.hash = '#$uri';
|
||||
}
|
||||
|
||||
@override
|
||||
void listen() {
|
||||
window.onHashChange.listen((_) {
|
||||
final path = window.location.hash.replaceAll(_hash, '');
|
||||
final resolved = resolveAbsolute(path);
|
||||
|
||||
if (resolved == null) {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
} else if (resolved != null && resolved.route != _current) {
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = resolved.route);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PushStateRouter extends _BrowserRouterImpl {
|
||||
_PushStateRouter({bool listen, Route root}) : super(listen: listen) {
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void _goTo(String uri) {
|
||||
final resolved = resolveAbsolute(uri);
|
||||
|
||||
if (resolved == null) {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
} else {
|
||||
final route = resolved.route;
|
||||
window.history.pushState(
|
||||
{'path': route.path, 'params': {}, 'properties': properties},
|
||||
route.name ?? route.path,
|
||||
uri);
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void listen() {
|
||||
window.onPopState.listen((e) {
|
||||
if (e.state is Map && e.state.containsKey('path')) {
|
||||
final resolved = resolveAbsolute(e.state['path']);
|
||||
|
||||
if (resolved != null && resolved.route != _current) {
|
||||
properties.addAll(e.state['properties'] ?? {});
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = resolved.route
|
||||
..state.properties.addAll(e.state['params'] ?? {}));
|
||||
}
|
||||
} else {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ final RegExp _slashDollar = new RegExp(r'/+\$');
|
|||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
|
||||
class Router {
|
||||
class Router extends Extensible {
|
||||
final List _middleware = [];
|
||||
final Map<Pattern, Router> _mounted = {};
|
||||
final List<Route> _routes = [];
|
||||
|
@ -57,6 +57,12 @@ class Router {
|
|||
return route.._path = _pathify(path);
|
||||
}
|
||||
|
||||
/// Prepends the given middleware to any routes created
|
||||
/// by the resulting router.
|
||||
///
|
||||
/// The resulting router can be chained, too.
|
||||
_ChainedRouter chain(middleware) => new _ChainedRouter(this, middleware);
|
||||
|
||||
/// Returns a [Router] with a duplicated version of this tree.
|
||||
Router clone() {
|
||||
final router = new Router(debug: debug);
|
||||
|
@ -165,7 +171,7 @@ class Router {
|
|||
/// ```dart
|
||||
/// router.navigate(['users/:id', {'id': '1337'}, 'profile']);
|
||||
/// ```
|
||||
String navigate(List linkParams, {bool absolute: true}) {
|
||||
String navigate(Iterable linkParams, {bool absolute: true}) {
|
||||
final List<String> segments = [];
|
||||
Router search = this;
|
||||
Route lastRoute;
|
||||
|
@ -234,13 +240,14 @@ class Router {
|
|||
}
|
||||
|
||||
RoutingResult _dumpResult(String path, RoutingResult result) {
|
||||
_printDebug('Resolved "/$path" to ${result.deepestRoute}');
|
||||
_printDebug('Resolved "/$path" to ${result.route}');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Finds the first [Route] that matches the given path,
|
||||
/// with the given method.
|
||||
RoutingResult resolve(String absolute, String relative, {String method: 'GET'}) {
|
||||
RoutingResult resolve(String absolute, String relative,
|
||||
{String method: 'GET'}) {
|
||||
final cleanAbsolute = absolute.replaceAll(_straySlashes, '');
|
||||
final cleanRelative = relative.replaceAll(_straySlashes, '');
|
||||
final segments = cleanRelative.split('/').where((str) => str.isNotEmpty);
|
||||
|
@ -253,24 +260,31 @@ class Router {
|
|||
final match = route._head.firstMatch(segments.first);
|
||||
|
||||
if (match != null) {
|
||||
final cleaned = segments.first.replaceFirst(match[0], '');
|
||||
final tail = cleanRelative
|
||||
.replaceAll(route._head, '')
|
||||
.replaceAll(_straySlashes, '');
|
||||
_printDebug('Matched head "${match[0]}" to $route. Tail: "$tail"');
|
||||
route.router.debug = route.router.debug || debug;
|
||||
final nested =
|
||||
route.router.resolve(cleanAbsolute, tail, method: method);
|
||||
return _dumpResult(
|
||||
cleanRelative,
|
||||
new RoutingResult(
|
||||
match: match,
|
||||
nested: nested,
|
||||
params: route.parseParameters(cleanRelative),
|
||||
sourceRoute: route,
|
||||
sourceRouter: this,
|
||||
tail: tail));
|
||||
|
||||
if (cleaned.isEmpty) {
|
||||
_printDebug('Matched relative "$cleanRelative" to head ${route._head
|
||||
.pattern} on $route. Tail: "$tail"');
|
||||
route.router.debug = route.router.debug || debug;
|
||||
final nested =
|
||||
route.router.resolve(cleanAbsolute, tail, method: method);
|
||||
return _dumpResult(
|
||||
cleanRelative,
|
||||
new RoutingResult(
|
||||
match: match,
|
||||
nested: nested,
|
||||
params: route.parseParameters(cleanRelative),
|
||||
shallowRoute: route,
|
||||
shallowRouter: this,
|
||||
tail: tail));
|
||||
}
|
||||
}
|
||||
} else if (route.method == '*' || route.method == method) {
|
||||
}
|
||||
|
||||
if (route.method == '*' || route.method == method) {
|
||||
final match = route.match(cleanRelative);
|
||||
|
||||
if (match != null) {
|
||||
|
@ -279,8 +293,8 @@ class Router {
|
|||
new RoutingResult(
|
||||
match: match,
|
||||
params: route.parseParameters(cleanRelative),
|
||||
sourceRoute: route,
|
||||
sourceRouter: this));
|
||||
shallowRoute: route,
|
||||
shallowRouter: this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +303,11 @@ class Router {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Returns the result of [resolve] with [path] passed as
|
||||
/// both `absolute` and `relative`.
|
||||
RoutingResult resolveAbsolute(String path, {String method: 'GET'}) =>
|
||||
resolve(path, path, method: method);
|
||||
|
||||
/// Finds every possible [Route] that matches the given path,
|
||||
/// with the given method.
|
||||
Iterable<RoutingResult> resolveAll(String absolute, String relative,
|
||||
|
@ -303,12 +322,12 @@ class Router {
|
|||
else
|
||||
break;
|
||||
|
||||
result.deepestRouter._routes.remove(result.deepestRoute);
|
||||
result.router._routes.remove(result.route);
|
||||
result = router.resolve(absolute, relative, method: method);
|
||||
}
|
||||
|
||||
_printDebug(
|
||||
'Results of $method "/${absolute.replaceAll(_straySlashes, '')}": ${results.map((r) => r.deepestRoute).toList()}');
|
||||
'Results of $method "/${absolute.replaceAll(_straySlashes, '')}": ${results.map((r) => r.route).toList()}');
|
||||
return results;
|
||||
}
|
||||
|
||||
|
@ -381,3 +400,40 @@ class Router {
|
|||
return addRoute('PUT', path, handler, middleware: middleware);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChainedRouter extends Router {
|
||||
final List _handlers = [];
|
||||
Router _root;
|
||||
|
||||
_ChainedRouter.empty();
|
||||
|
||||
_ChainedRouter(Router root, middleware) {
|
||||
this._root = root;
|
||||
_handlers.add(middleware);
|
||||
}
|
||||
|
||||
@override
|
||||
Route addRoute(String method, Pattern path, handler,
|
||||
{List middleware: const []}) {
|
||||
return _root.addRoute(method, path, handler,
|
||||
middleware: []..addAll(_handlers)..addAll(middleware ?? []));
|
||||
}
|
||||
|
||||
@override
|
||||
SymlinkRoute mount(Pattern path, Router router,
|
||||
{bool hooked: true, String namespace: null}) {
|
||||
final route =
|
||||
super.mount(path, router, hooked: hooked, namespace: namespace);
|
||||
route.router._middleware.insertAll(0, _handlers);
|
||||
return route;
|
||||
}
|
||||
|
||||
@override
|
||||
_ChainedRouter chain(middleware) {
|
||||
final piped = new _ChainedRouter.empty().._root = _root;
|
||||
piped._handlers.addAll([]
|
||||
..addAll(_handlers)
|
||||
..add(middleware));
|
||||
return piped;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ class RoutingResult {
|
|||
final Match match;
|
||||
final RoutingResult nested;
|
||||
final Map<String, dynamic> params = {};
|
||||
final Route sourceRoute;
|
||||
final Router sourceRouter;
|
||||
final Route shallowRoute;
|
||||
final Router shallowRouter;
|
||||
final String tail;
|
||||
|
||||
RoutingResult get deepest {
|
||||
|
@ -16,11 +16,11 @@ class RoutingResult {
|
|||
return search;
|
||||
}
|
||||
|
||||
Route get deepestRoute => deepest.sourceRoute;
|
||||
Router get deepestRouter => deepest.sourceRouter;
|
||||
Route get route => deepest.shallowRoute;
|
||||
Router get router => deepest.shallowRouter;
|
||||
|
||||
List get handlers {
|
||||
return []..addAll(sourceRouter.middleware)..addAll(sourceRoute.handlers);
|
||||
return []..addAll(shallowRouter.middleware)..addAll(shallowRoute.handlers);
|
||||
}
|
||||
|
||||
List get allHandlers {
|
||||
|
@ -39,8 +39,8 @@ class RoutingResult {
|
|||
{this.match,
|
||||
Map<String, dynamic> params: const {},
|
||||
this.nested,
|
||||
this.sourceRoute,
|
||||
this.sourceRouter,
|
||||
this.shallowRoute,
|
||||
this.shallowRouter,
|
||||
this.tail}) {
|
||||
this.params.addAll(params ?? {});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: angel_route
|
||||
description: A powerful, isomorphic routing library for Dart.
|
||||
version: 1.0.0-dev+13
|
||||
version: 1.0.0-dev+14
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_route
|
||||
dev_dependencies:
|
||||
|
|
|
@ -159,6 +159,12 @@ main() {
|
|||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('together'));
|
||||
});
|
||||
|
||||
test('fallback', () async {
|
||||
final res = await client.patch('$url/beatles/spanil_clakcer');
|
||||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('together'));
|
||||
});
|
||||
});
|
||||
|
||||
test('deep nested', () async {
|
||||
|
@ -183,6 +189,11 @@ main() {
|
|||
await expect404(client.get('$url/beatles2'));
|
||||
});
|
||||
|
||||
test('method', () async {});
|
||||
test('method', () async {
|
||||
await expect404(client.head(url));
|
||||
await expect404(client.patch('$url/people'));
|
||||
await expect404(client.post('$url/people/0'));
|
||||
await expect404(client.delete('$url/beatles2/spinal_clacker'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
5
web/hash/basic.dart
Normal file
5
web/hash/basic.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:angel_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
main() => basic(new BrowserRouter(hash: true));
|
||||
|
31
web/hash/basic.html
Normal file
31
web/hash/basic.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Hash Router</title>
|
||||
<style>
|
||||
#routes li {
|
||||
display: inline;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ul id="routes">
|
||||
<li><a href="/a">Route A</a></li>
|
||||
<li><a href="/b">Route B</a></li>
|
||||
<li><a href="/b/a">Route B/A</a></li>
|
||||
<li><a href="/b/b">Route B/B</a></li>
|
||||
<li><a href="/c">Route C</a></li>
|
||||
</ul>
|
||||
<h1>No Active Route</h1>
|
||||
<i>Handler Sequence:</i>
|
||||
<ul id="handlers">
|
||||
<li>(empty)</li>
|
||||
</ul>
|
||||
<script src="basic.dart" type="application/dart"></script>
|
||||
<script src="packages/browser/dart.js"></script>
|
||||
</body>
|
||||
</html>
|
5
web/push_state/basic.dart
Normal file
5
web/push_state/basic.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:angel_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
main() => basic(new BrowserRouter());
|
||||
|
31
web/push_state/basic.html
Normal file
31
web/push_state/basic.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Push State Router</title>
|
||||
<style>
|
||||
#routes li {
|
||||
display: inline;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ul id="routes">
|
||||
<li><a href="/a">Route A</a></li>
|
||||
<li><a href="/b">Route B</a></li>
|
||||
<li><a href="/b/a">Route B/A</a></li>
|
||||
<li><a href="/b/b">Route B/B</a></li>
|
||||
<li><a href="/c">Route C</a></li>
|
||||
</ul>
|
||||
<h1>No Active Route</h1>
|
||||
<i>Handler Sequence:</i>
|
||||
<ul id="handlers">
|
||||
<li>(empty)</li>
|
||||
</ul>
|
||||
<script src="basic.dart" type="application/dart"></script>
|
||||
<script src="packages/browser/dart.js"></script>
|
||||
</body>
|
||||
</html>
|
35
web/shared/basic.dart
Normal file
35
web/shared/basic.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'dart:html';
|
||||
import 'package:angel_route/browser.dart';
|
||||
|
||||
basic(BrowserRouter router) {
|
||||
final $h1 = window.document.querySelector('h1');
|
||||
final $ul = window.document.getElementById('handlers');
|
||||
|
||||
router.onResolve.listen((result) {
|
||||
final route = result?.route;
|
||||
|
||||
if (route == null) {
|
||||
$h1.text = 'No Active Route';
|
||||
$ul.children
|
||||
..clear()
|
||||
..add(new LIElement()..text = '(empty)');
|
||||
} else {
|
||||
$h1.text = 'Active Route: ${route.name ?? route.path}';
|
||||
$ul.children
|
||||
..clear()
|
||||
..addAll(result.allHandlers
|
||||
.map((handler) => new LIElement()..text = handler.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('a', 'a handler');
|
||||
|
||||
router.group('b', (router) {
|
||||
router.get('a', 'b/a handler').as('b/a');
|
||||
router.get('b', 'b/b handler', middleware: ['b/b middleware']).as('b/b');
|
||||
}, middleware: ['b middleware']);
|
||||
|
||||
router.get('c', 'c handler');
|
||||
|
||||
router.dumpTree();
|
||||
}
|
Loading…
Reference in a new issue