From 826cb90ffe287e522ac82a831fbe20995b41861f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 27 Nov 2016 17:24:30 -0500 Subject: [PATCH] Finally done? --- README.md | 14 +++- lib/browser.dart | 149 ++++++++++++++++++++++++++++++++++++ lib/src/router.dart | 100 ++++++++++++++++++------ lib/src/routing_result.dart | 14 ++-- pubspec.yaml | 2 +- test/server_test.dart | 13 +++- web/hash/basic.dart | 5 ++ web/hash/basic.html | 31 ++++++++ web/push_state/basic.dart | 5 ++ web/push_state/basic.html | 31 ++++++++ web/shared/basic.dart | 35 +++++++++ 11 files changed, 366 insertions(+), 33 deletions(-) create mode 100644 lib/browser.dart create mode 100644 web/hash/basic.dart create mode 100644 web/hash/basic.html create mode 100644 web/push_state/basic.dart create mode 100644 web/push_state/basic.html create mode 100644 web/shared/basic.dart diff --git a/README.md b/README.md index 20ebc4be..27e78111 100644 --- a/README.md +++ b/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 onRoute`, which can be listened to for changes. It will fire `null` +a `Stream onRoute`, which can be listened to for changes. It will fire `null` whenever no route is matched. `angel_route` will also automatically intercept `` 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. \ No newline at end of file diff --git a/lib/browser.dart b/lib/browser.dart new file mode 100644 index 00000000..fe9330fa --- /dev/null +++ b/lib/browser.dart @@ -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 get onResolve; + + /// Fires whenever the active route changes. Fires `null` if none is selected (404). + Stream 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 _onResolve = + new StreamController(); + StreamController _onRoute = new StreamController(); + Route get currentRoute => _current; + + @override + Stream get onResolve => _onResolve.stream; + + @override + Stream 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); + } + }); + } +} diff --git a/lib/src/router.dart b/lib/src/router.dart index b06754b6..1158c267 100644 --- a/lib/src/router.dart +++ b/lib/src/router.dart @@ -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 _mounted = {}; final List _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 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 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; + } +} diff --git a/lib/src/routing_result.dart b/lib/src/routing_result.dart index 9d147eae..ba252d58 100644 --- a/lib/src/routing_result.dart +++ b/lib/src/routing_result.dart @@ -4,8 +4,8 @@ class RoutingResult { final Match match; final RoutingResult nested; final Map 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 params: const {}, this.nested, - this.sourceRoute, - this.sourceRouter, + this.shallowRoute, + this.shallowRouter, this.tail}) { this.params.addAll(params ?? {}); } diff --git a/pubspec.yaml b/pubspec.yaml index 20f567d2..f492c7c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 homepage: https://github.com/angel-dart/angel_route dev_dependencies: diff --git a/test/server_test.dart b/test/server_test.dart index b8aa1f29..b2d58c05 100644 --- a/test/server_test.dart +++ b/test/server_test.dart @@ -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')); + }); }); } diff --git a/web/hash/basic.dart b/web/hash/basic.dart new file mode 100644 index 00000000..b5ba5e99 --- /dev/null +++ b/web/hash/basic.dart @@ -0,0 +1,5 @@ +import 'package:angel_route/browser.dart'; +import '../shared/basic.dart'; + +main() => basic(new BrowserRouter(hash: true)); + diff --git a/web/hash/basic.html b/web/hash/basic.html new file mode 100644 index 00000000..e2e0e86b --- /dev/null +++ b/web/hash/basic.html @@ -0,0 +1,31 @@ + + + + + + Hash Router + + + + +

No Active Route

+Handler Sequence: +
    +
  • (empty)
  • +
+ + + + \ No newline at end of file diff --git a/web/push_state/basic.dart b/web/push_state/basic.dart new file mode 100644 index 00000000..d889a70d --- /dev/null +++ b/web/push_state/basic.dart @@ -0,0 +1,5 @@ +import 'package:angel_route/browser.dart'; +import '../shared/basic.dart'; + +main() => basic(new BrowserRouter()); + diff --git a/web/push_state/basic.html b/web/push_state/basic.html new file mode 100644 index 00000000..c2b6faca --- /dev/null +++ b/web/push_state/basic.html @@ -0,0 +1,31 @@ + + + + + + Push State Router + + + + +

No Active Route

+Handler Sequence: +
    +
  • (empty)
  • +
+ + + + \ No newline at end of file diff --git a/web/shared/basic.dart b/web/shared/basic.dart new file mode 100644 index 00000000..f65eb059 --- /dev/null +++ b/web/shared/basic.dart @@ -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(); +}