Finally done?

This commit is contained in:
thosakwe 2016-11-27 17:24:30 -05:00
parent 2420042d4d
commit 826cb90ffe
11 changed files with 366 additions and 33 deletions

View file

@ -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
View 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);
}
});
}
}

View file

@ -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;
}
}

View file

@ -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 ?? {});
}

View file

@ -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:

View file

@ -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
View 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
View 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>

View 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
View 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
View 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();
}