1.0.0-dev
This commit is contained in:
parent
58d0c51c4e
commit
064c831db0
27 changed files with 1050 additions and 36 deletions
46
.gitignore
vendored
46
.gitignore
vendored
|
@ -25,5 +25,49 @@ doc/api/
|
|||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
.idea
|
23
.idea/angel_route.iml
Normal file
23
.idea/angel_route.iml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/route/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/router/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/web/hash/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/web/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/web/push_state/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/web/shared/packages" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="application" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
38
.idea/misc.xml
Normal file
38
.idea/misc.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||
<entry key="Project Default">
|
||||
<profile-state>
|
||||
<expanded-state>
|
||||
<State>
|
||||
<id />
|
||||
</State>
|
||||
<State>
|
||||
<id>General</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>XPath</id>
|
||||
</State>
|
||||
</expanded-state>
|
||||
<selected-state>
|
||||
<State>
|
||||
<id>AngularJS</id>
|
||||
</State>
|
||||
</selected-state>
|
||||
</profile-state>
|
||||
</entry>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/angel_route.iml" filepath="$PROJECT_DIR$/.idea/angel_route.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/runConfigurations/All_Tests.xml
Normal file
6
.idea/runConfigurations/All_Tests.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/all_tests.dart" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
137
README.md
137
README.md
|
@ -1,2 +1,137 @@
|
|||
# angel_route
|
||||
Advanced routing API, supports both server and browser.
|
||||
A powerful, isomorphic routing library for Dart.
|
||||
|
||||
This API is a huge improvement over the original [Angel](https://github.com/angel-dart/angel)
|
||||
routing system, and thus deserves to be its own individual project.
|
||||
|
||||
`angel_route` exposes a routing system that takes the shape of a tree. This tree structure
|
||||
can be easily navigated, in a fashion somewhat similar to a filesystem. The `Router` API
|
||||
is a very straightforward interface that allows for your code to take a shape similar to
|
||||
the route tree. Users of Laravel and Express will be very happy.
|
||||
|
||||
`angel_route` does not require the use of [Angel](https://github.com/angel-dart/angel),
|
||||
and has no dependencies. Thus, it can be used in any application, regardless of
|
||||
framework. This includes Web apps, Flutter apps, CLI apps, and smaller servers which do
|
||||
not need all the features of the Angel framework.
|
||||
|
||||
# Contents
|
||||
|
||||
* [Examples](#examples)
|
||||
* [Routing](#routing)
|
||||
* [Tree Hierarchy and Path Resolution](#hierarchy)
|
||||
* [In the Browser](#in-the-browser)
|
||||
* [Route State](#route-state)
|
||||
* [Route Parameters](#route-parameters)
|
||||
|
||||
# Examples
|
||||
|
||||
## Routing
|
||||
If you use [Angel](https://github.com/angel-dart/angel), every `Angel` instance is
|
||||
a `Router` in itself.
|
||||
|
||||
```dart
|
||||
|
||||
main() {
|
||||
final router = new Router();
|
||||
|
||||
router.get('/users', () {});
|
||||
|
||||
router.post('/users/:id/timeline', (String id) {});
|
||||
|
||||
router.get('/square_root/:id([0-9]+)', (String id) {
|
||||
final n = num.parse(id);
|
||||
return {'result': pow(n, 2) };
|
||||
});
|
||||
|
||||
router.group('/show/:id', (router) {
|
||||
router.get('/reviews', (id) {
|
||||
return someQuery(id).reviews;
|
||||
});
|
||||
|
||||
// Optionally restrict params to a RegExp
|
||||
router.get('/reviews/:reviewId([A-Za-z0-9_]+)', (id, reviewId) {
|
||||
return someQuery(id).reviews.firstWhere(
|
||||
(r) => r.id == reviewId);
|
||||
});
|
||||
}, before: [put, middleware, here]);
|
||||
}
|
||||
```
|
||||
|
||||
The default `Router` does not give any notification of routes being changed, because
|
||||
there is no inherent stream of URL's for it to listen to. This is good, because a server
|
||||
needs a lot of flexibility with which to handle requests.
|
||||
|
||||
## Hierarchy
|
||||
|
||||
```dart
|
||||
main() {
|
||||
final foo = new Route('/');
|
||||
final bar = foo.child('bar');
|
||||
final baz = foo.child('baz');
|
||||
|
||||
final a = bar.child('a');
|
||||
|
||||
/*
|
||||
* Relative paths:
|
||||
* a.resolve('../baz') = baz;
|
||||
* bar.resolve('a') = a;
|
||||
*
|
||||
* Absolute paths:
|
||||
* a.resolve('/bar/a') = a;
|
||||
*/
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
main() {
|
||||
final router = new Router();
|
||||
|
||||
router.group('/user/:id', (router) {
|
||||
router.get('/balance', (id) async {
|
||||
final user = await someQuery(id);
|
||||
return user.balance;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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`
|
||||
whenever no route is matched.
|
||||
|
||||
```dart
|
||||
main() {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
`angel_route` will also automatically intercept `<a>` elements and redirect them to
|
||||
your routes.
|
||||
|
||||
To prevent this for a given anchor, do any of the following:
|
||||
* Do not provide an `href`
|
||||
* Provide a `download` or `target` attribute on the element
|
||||
* Set `rel="external"`
|
||||
|
||||
# Route State
|
||||
Routes can have state via the `Extensible` class, which is a simple proxy over a `Map`.
|
||||
This does not require reflection, and can be used in all Dart environments.
|
||||
|
||||
```dart
|
||||
main() {
|
||||
final router = new BrowserRouter();
|
||||
// ..
|
||||
router.onRoute.listen((route) {
|
||||
if (route == null)
|
||||
throw 404;
|
||||
else route.state.foo = 'bar';
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
# Route Parameters
|
||||
Routes can have parameters, as seen in the above examples.
|
||||
If a parameter is a numeber, then it will automatically be parsed.
|
|
@ -1,3 +1,5 @@
|
|||
library angel_route;
|
||||
|
||||
export 'src/route.dart';
|
||||
export 'src/route.dart';
|
||||
export 'src/router.dart';
|
||||
export 'src/routing_exception.dart';
|
123
lib/browser.dart
Normal file
123
lib/browser.dart
Normal file
|
@ -0,0 +1,123 @@
|
|||
import 'dart:async' show Stream, StreamController;
|
||||
import 'dart:convert' show JSON;
|
||||
import 'dart:html' show AnchorElement, window;
|
||||
import 'angel_route.dart';
|
||||
|
||||
final RegExp _hash = new RegExp(r'^#/');
|
||||
|
||||
abstract class BrowserRouter extends Router {
|
||||
Stream<Route> get onRoute;
|
||||
|
||||
factory BrowserRouter({bool hash: false, bool listen: true, Route root}) {
|
||||
return hash
|
||||
? new _HashRouter(listen: listen, root: root)
|
||||
: new _PushStateRouter(listen: listen, root: root);
|
||||
}
|
||||
|
||||
BrowserRouter._([Route root]) : super(root);
|
||||
|
||||
void go(String path, [Map params]);
|
||||
void goTo(Route route, [Map params]);
|
||||
void listen();
|
||||
}
|
||||
|
||||
class _BrowserRouterImpl extends Router implements BrowserRouter {
|
||||
Route _current;
|
||||
StreamController<Route> _onRoute = new StreamController<Route>();
|
||||
Route get currentRoute => _current;
|
||||
|
||||
@override
|
||||
Stream<Route> get onRoute => _onRoute.stream;
|
||||
|
||||
_BrowserRouterImpl({bool listen, Route root}) : super(root) {
|
||||
if (listen) this.listen();
|
||||
prepareAnchors();
|
||||
}
|
||||
|
||||
@override
|
||||
void go(String path, [Map params]) {
|
||||
final resolved = resolve(path);
|
||||
|
||||
if (resolved != null)
|
||||
goTo(resolved, params);
|
||||
else
|
||||
throw new RoutingException.noSuchRoute(path);
|
||||
}
|
||||
|
||||
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']);
|
||||
});
|
||||
}
|
||||
|
||||
$a.attributes['dynamic'] = 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HashRouter extends _BrowserRouterImpl {
|
||||
_HashRouter({bool listen, Route root}) : super(listen: listen, root: root) {
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void goTo(Route route, [Map params]) {
|
||||
route.state.properties.addAll(params ?? {});
|
||||
window.location.hash = '#/${route.makeUri(params)}';
|
||||
_onRoute.add(route);
|
||||
}
|
||||
|
||||
@override
|
||||
void listen() {
|
||||
window.onHashChange.listen((_) {
|
||||
final path = window.location.hash.replaceAll(_hash, '');
|
||||
final resolved = resolve(path);
|
||||
|
||||
if (resolved == null || (path.isEmpty && resolved == root)) {
|
||||
_onRoute.add(_current = null);
|
||||
} else if (resolved != null && resolved != _current) {
|
||||
goTo(resolved);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PushStateRouter extends _BrowserRouterImpl {
|
||||
_PushStateRouter({bool listen, Route root})
|
||||
: super(listen: listen, root: root) {
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void goTo(Route route, [Map params]) {
|
||||
window.history.pushState(
|
||||
{'path': route.path, 'params': params ?? {}, 'properties': properties},
|
||||
route.name ?? route.path,
|
||||
route.makeUri(params));
|
||||
_onRoute.add(_current = route..state.properties.addAll(params ?? {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void listen() {
|
||||
window.onPopState.listen((e) {
|
||||
if (e.state is Map && e.state.containsKey('path')) {
|
||||
final resolved = resolve(e.state['path']);
|
||||
|
||||
if (resolved != _current) {
|
||||
properties.addAll(e.state['properties'] ?? {});
|
||||
_onRoute.add(_current = resolved
|
||||
..state.properties.addAll(e.state['params'] ?? {}));
|
||||
}
|
||||
} else
|
||||
_onRoute.add(_current = null);
|
||||
});
|
||||
}
|
||||
}
|
31
lib/src/extensible.dart
Normal file
31
lib/src/extensible.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
final RegExp _equ = new RegExp(r'=$');
|
||||
final RegExp _sym = new RegExp(r'Symbol\("([^"]+)"\)');
|
||||
|
||||
/// Supports accessing members of a Map as though they were actual members.
|
||||
///
|
||||
/// No longer requires reflection. :)
|
||||
@proxy
|
||||
class Extensible {
|
||||
/// A set of custom properties that can be assigned to the server.
|
||||
///
|
||||
/// Useful for configuration and extension.
|
||||
Map properties = {};
|
||||
|
||||
noSuchMethod(Invocation invocation) {
|
||||
if (invocation.memberName != null) {
|
||||
String name = _sym.firstMatch(invocation.memberName.toString()).group(1);
|
||||
|
||||
if (invocation.isMethod) {
|
||||
return Function.apply(properties[name], invocation.positionalArguments,
|
||||
invocation.namedArguments);
|
||||
} else if (invocation.isGetter) {
|
||||
return properties[name];
|
||||
} else if (invocation.isSetter) {
|
||||
return properties[name.replaceAll(_equ, '')] =
|
||||
invocation.positionalArguments.first;
|
||||
}
|
||||
}
|
||||
|
||||
super.noSuchMethod(invocation);
|
||||
}
|
||||
}
|
|
@ -1,66 +1,160 @@
|
|||
final RegExp _rgxEnd = new RegExp(r'\$');
|
||||
final RegExp _rgxStart = new RegExp(r'\^');
|
||||
import 'extensible.dart';
|
||||
import 'routing_exception.dart';
|
||||
|
||||
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||
final RegExp _rgxEnd = new RegExp(r'\$+$');
|
||||
final RegExp _rgxStart = new RegExp(r'^\^+');
|
||||
final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
|
||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
String _matcherify(String path, {bool expand: true}) {
|
||||
var p = path.replaceAll(new RegExp(r'\/\*$'), "*").replaceAll('/', r'\/');
|
||||
|
||||
if (expand) {
|
||||
var match = _param.firstMatch(p);
|
||||
|
||||
while (match != null) {
|
||||
if (match.group(3) == null)
|
||||
p = p.replaceAll(match[0], '([^\/]+)');
|
||||
else
|
||||
p = p.replaceAll(match[0], '(${match[3]})');
|
||||
match = _param.firstMatch(p);
|
||||
}
|
||||
}
|
||||
|
||||
p = p.replaceAll(new RegExp('\\*'), '.*');
|
||||
|
||||
p = '^$p\$';
|
||||
return p;
|
||||
}
|
||||
|
||||
String _pathify(String path) {
|
||||
var p = path.replaceAll(_straySlashes, '');
|
||||
|
||||
Map<String, String> replace = {};
|
||||
|
||||
for (Match match in _param.allMatches(p)) {
|
||||
if (match[3] != null) replace[match[0]] = ':${match[1]}';
|
||||
}
|
||||
|
||||
replace.forEach((k, v) {
|
||||
p = p.replaceAll(k, v);
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
class Route {
|
||||
final List<Route> _children = [];
|
||||
final List _handlers = [];
|
||||
RegExp _matcher;
|
||||
String _name;
|
||||
Route _parent;
|
||||
String _path;
|
||||
String _pathified;
|
||||
RegExp _resolver;
|
||||
List<Route> get children => new List.unmodifiable(_children);
|
||||
List get handlers => new List.unmodifiable(_handlers);
|
||||
RegExp get matcher => _matcher;
|
||||
final String method;
|
||||
final String name;
|
||||
String get name => _name;
|
||||
Route get parent => _parent;
|
||||
String get path => _path;
|
||||
final Extensible state = new Extensible();
|
||||
|
||||
Route get absoluteParent {
|
||||
Route result = this;
|
||||
|
||||
while (result.parent != null) result = result.parent;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Backtracks up the hierarchy, and builds
|
||||
/// a sequential list of all handlers from both
|
||||
/// this route, and every found parent route.
|
||||
///
|
||||
/// The resulting list puts handlers from routes
|
||||
/// higher in the tree at lower indices. Thus,
|
||||
/// this can be used in a routing-enabled application
|
||||
/// to evaluate multiple middleware on a single route,
|
||||
/// and apply them to all children.
|
||||
List get handlerSequence {
|
||||
final result = [];
|
||||
var r = this;
|
||||
|
||||
while (r != null) {
|
||||
result.insertAll(0, r.handlers);
|
||||
r = r.parent;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Route(Pattern path,
|
||||
{Iterable<Route> children: const [],
|
||||
Iterable handlers: const [],
|
||||
this.method: "GET",
|
||||
this.name: null}) {
|
||||
String name: null}) {
|
||||
if (children != null) _children.addAll(children);
|
||||
if (handlers != null) _handlers.addAll(handlers);
|
||||
_name = name;
|
||||
|
||||
if (path is RegExp) {
|
||||
_matcher = path;
|
||||
_path = path.pattern;
|
||||
} else {
|
||||
_matcher = new RegExp(_path = path
|
||||
.toString()
|
||||
.replaceAll(_straySlashes, '')
|
||||
.replaceAll(new RegExp(r'\/\*$'), "*")
|
||||
.replaceAll(new RegExp('\/'), r'\/')
|
||||
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
|
||||
.replaceAll(new RegExp('\\*'), '.*'));
|
||||
_matcher = new RegExp(
|
||||
_matcherify(path.toString().replaceAll(_straySlashes, '')));
|
||||
_path = _pathified = _pathify(path.toString());
|
||||
_resolver = new RegExp(_matcherify(
|
||||
path.toString().replaceAll(_straySlashes, ''),
|
||||
expand: false));
|
||||
}
|
||||
}
|
||||
|
||||
factory Route.join(Route parent, Route child) {
|
||||
final String path1 = parent.path.replaceAll(_straySlashes, '');
|
||||
final String path2 = child.path.replaceAll(_straySlashes, '');
|
||||
final String pattern1 = parent.matcher.pattern.replaceAll(_rgxEnd, '');
|
||||
final String pattern2 = child.matcher.pattern.replaceAll(_rgxStart, '');
|
||||
final String path1 = parent.path
|
||||
.replaceAll(_rgxStart, '')
|
||||
.replaceAll(_rgxEnd, '')
|
||||
.replaceAll(_straySlashes, '');
|
||||
final String path2 = child.path
|
||||
.replaceAll(_rgxStart, '')
|
||||
.replaceAll(_rgxEnd, '')
|
||||
.replaceAll(_straySlashes, '');
|
||||
final String pattern1 = parent.matcher.pattern
|
||||
.replaceAll(_rgxEnd, '')
|
||||
.replaceAll(_rgxStraySlashes, '');
|
||||
final String pattern2 = child.matcher.pattern
|
||||
.replaceAll(_rgxStart, '')
|
||||
.replaceAll(_rgxStraySlashes, '');
|
||||
|
||||
final route = new Route(new RegExp('$pattern1/$pattern2'),
|
||||
final route = new Route('$path1/$path2',
|
||||
children: child.children,
|
||||
handlers: child.handlers,
|
||||
method: child.method,
|
||||
name: child.name);
|
||||
|
||||
String separator = (pattern1.isEmpty || pattern1 == '^') ? '' : '\\/';
|
||||
|
||||
return route
|
||||
..parent = parent
|
||||
.._path = '$path1/$path2';
|
||||
.._matcher = new RegExp('$pattern1$separator$pattern2')
|
||||
.._parent = parent;
|
||||
}
|
||||
|
||||
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
|
||||
return routes.map((route) => addChild(route, join: join)).toList();
|
||||
}
|
||||
|
||||
Route addChild(Route route, {bool join: true}) {
|
||||
Route created = join ? new Route.join(this, route) : route;
|
||||
Route created = join ? new Route.join(this, route) : route.._parent = this;
|
||||
_children.add(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
/// Assigns a name to this route.
|
||||
Route as(String name) => this.._name = name;
|
||||
|
||||
Route child(Pattern path,
|
||||
{Iterable<Route> children: const [],
|
||||
Iterable handlers: const [],
|
||||
|
@ -71,28 +165,109 @@ class Route {
|
|||
return addChild(route);
|
||||
}
|
||||
|
||||
Match match(String path) => matcher.firstMatch(path);
|
||||
/// Generates a URI to this route with the given parameters.
|
||||
String makeUri([Map<String, dynamic> params]) {
|
||||
String result = _pathified;
|
||||
if (params != null) {
|
||||
for (String key in (params.keys)) {
|
||||
result = result.replaceAll(
|
||||
new RegExp(":$key" + r"\??"), params[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
Route resolve(String path) {
|
||||
if (path.isEmpty ||
|
||||
path == '.' ||
|
||||
path.replaceAll(_straySlashes, '').isEmpty) {
|
||||
return result.replaceAll("*", "");
|
||||
}
|
||||
|
||||
Match match(String path) =>
|
||||
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
|
||||
|
||||
/// Extracts route parameters from a given path.
|
||||
Map parseParameters(String requestPath) {
|
||||
Map result = {};
|
||||
|
||||
Iterable<String> values = _parseParameters(requestPath.replaceAll(_straySlashes, ''));
|
||||
Iterable<Match> matches = _param.allMatches(
|
||||
_pathified.replaceAll(new RegExp('\/'), r'\/'));
|
||||
for (int i = 0; i < matches.length; i++) {
|
||||
Match match = matches.elementAt(i);
|
||||
String paramName = match.group(1);
|
||||
String value = values.elementAt(i);
|
||||
num numValue = num.parse(value, (_) => double.NAN);
|
||||
if (!numValue.isNaN)
|
||||
result[paramName] = numValue;
|
||||
else
|
||||
result[paramName] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_parseParameters(String requestPath) sync* {
|
||||
Match routeMatch = matcher.firstMatch(requestPath);
|
||||
for (int i = 1; i <= routeMatch.groupCount; i++)
|
||||
yield routeMatch.group(i);
|
||||
}
|
||||
|
||||
Route resolve(String path, [bool filter(Route route)]) {
|
||||
final _filter = filter ?? (_) => true;
|
||||
|
||||
if (path.isEmpty || path == '.' && _filter(this)) {
|
||||
return this;
|
||||
} else if (path.replaceAll(_straySlashes, '').isEmpty) {
|
||||
for (Route route in children) {
|
||||
final stub = route.path.replaceAll(this.path, '');
|
||||
|
||||
if (stub == '/' || stub.isEmpty && _filter(route)) return route;
|
||||
}
|
||||
|
||||
if (_filter(this))
|
||||
return this;
|
||||
else
|
||||
return null;
|
||||
} else if (path == '..') {
|
||||
if (parent != null)
|
||||
return parent;
|
||||
else
|
||||
throw new RoutingException.orphan();
|
||||
} else if (path.startsWith('/') &&
|
||||
path.length > 1 &&
|
||||
path[1] != '/' &&
|
||||
absoluteParent != null) {
|
||||
return absoluteParent.resolve(path.substring(1), _filter);
|
||||
} else {
|
||||
final segments = path.split('/');
|
||||
|
||||
if (segments[0] == '..') {
|
||||
if (parent != null)
|
||||
return parent.resolve(segments.skip(1).join('/'), _filter);
|
||||
else
|
||||
throw new RoutingException.orphan();
|
||||
}
|
||||
|
||||
for (Route route in children) {
|
||||
final subPath = '${this.path}/${segments[0]}';
|
||||
|
||||
if (route.match(subPath) != null) {
|
||||
if (segments.length == 1)
|
||||
if (route.match(subPath) != null ||
|
||||
route._resolver.firstMatch(subPath) != null) {
|
||||
if (segments.length == 1 && _filter(route))
|
||||
return route;
|
||||
else {
|
||||
return route.resolve(segments.skip(1).join('/'));
|
||||
return route.resolve(segments.skip(1).join('/'), _filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match the whole route, if nothing else works
|
||||
for (Route route in children) {
|
||||
if ((route.match(path) != null ||
|
||||
route._resolver.firstMatch(path) != null) &&
|
||||
_filter(route))
|
||||
return route;
|
||||
else if ((route.match('/$path') != null ||
|
||||
route._resolver.firstMatch('/$path') != null) &&
|
||||
_filter(route)) return route;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
168
lib/src/router.dart
Normal file
168
lib/src/router.dart
Normal file
|
@ -0,0 +1,168 @@
|
|||
import 'extensible.dart';
|
||||
import 'route.dart';
|
||||
|
||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
class Router extends Extensible {
|
||||
/// Additional filters to be run on designated requests.
|
||||
Map<String, dynamic> requestMiddleware = {};
|
||||
|
||||
/// The single [Route] that serves as the root of the hierarchy.
|
||||
final Route root;
|
||||
|
||||
Router([Route root]) : this.root = root ?? new Route('/', name: '<root>');
|
||||
|
||||
/// Adds a route that responds to the given path
|
||||
/// for requests with the given method (case-insensitive).
|
||||
/// Provide '*' as the method to respond to all methods.
|
||||
Route addRoute(String method, Pattern path, Object handler,
|
||||
{List middleware}) {
|
||||
List handlers = [];
|
||||
|
||||
handlers
|
||||
..addAll(middleware ?? [])
|
||||
..add(handler);
|
||||
|
||||
return root.child(path, handlers: handlers, method: method);
|
||||
}
|
||||
|
||||
/// Creates a visual representation of the route hierarchy and
|
||||
/// passes it to a callback. If none is provided, `print` is called.
|
||||
void dumpTree(
|
||||
{callback(String tree), header: 'Dumping route tree:', tab: ' '}) {
|
||||
var tabs = 0;
|
||||
final buf = new StringBuffer();
|
||||
|
||||
void dumpRoute(Route route, {String replace: null}) {
|
||||
for (var i = 0; i < tabs; i++) buf.write(tab);
|
||||
buf.write('- ${route.method} ');
|
||||
|
||||
final p =
|
||||
replace != null ? route.path.replaceAll(replace, '') : route.path;
|
||||
|
||||
if (p.isEmpty)
|
||||
buf.write("'/'");
|
||||
else
|
||||
buf.write("'${p.replaceAll(_straySlashes, '')}'");
|
||||
|
||||
buf.write(' => ');
|
||||
|
||||
if (route.handlers.isNotEmpty)
|
||||
buf.writeln('${route.handlers.length} handler(s)');
|
||||
else
|
||||
buf.writeln();
|
||||
|
||||
tabs++;
|
||||
route.children.forEach((r) => dumpRoute(r, replace: route.path));
|
||||
tabs--;
|
||||
}
|
||||
|
||||
if (header != null && header.isNotEmpty) buf.writeln(header);
|
||||
|
||||
dumpRoute(root);
|
||||
(callback ?? print)(buf);
|
||||
}
|
||||
|
||||
/// Creates a route, and allows you to add child routes to it
|
||||
/// via a [Router] instance.
|
||||
///
|
||||
/// Returns the created route.
|
||||
/// You can also register middleware within the router.
|
||||
Route group(Pattern path, void callback(Router router),
|
||||
{Iterable middleware: const [],
|
||||
String method: "*",
|
||||
String name: null,
|
||||
String namespace: null}) {
|
||||
final route =
|
||||
root.child(path, handlers: middleware, method: method, name: name);
|
||||
final router = new Router(route);
|
||||
callback(router);
|
||||
|
||||
// Let's copy middleware, heeding the optional middleware namespace.
|
||||
String middlewarePrefix = namespace != null ? "$namespace." : "";
|
||||
|
||||
Map copiedMiddleware = new Map.from(router.requestMiddleware);
|
||||
for (String middlewareName in copiedMiddleware.keys) {
|
||||
requestMiddleware["$middlewarePrefix$middlewareName"] =
|
||||
copiedMiddleware[middlewareName];
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/// Assigns a middleware to a name for convenience.
|
||||
registerMiddleware(String name, middleware) {
|
||||
requestMiddleware[name] = middleware;
|
||||
}
|
||||
|
||||
/// Finds the first [Route] that matches the given path.
|
||||
///
|
||||
/// You can pass an additional filter to determine which
|
||||
/// routes count as matches.
|
||||
Route resolve(String path, [bool filter(Route route)]) =>
|
||||
root.resolve(path, filter);
|
||||
|
||||
/// Incorporates another [Router]'s routes into this one's.
|
||||
///
|
||||
/// If `hooked` is set to `true` and a [Service] is provided,
|
||||
/// then that service will be wired to a [HookedService] proxy.
|
||||
/// If a `namespace` is provided, then any middleware
|
||||
/// from the provided [Router] will be prefixed by that namespace,
|
||||
/// with a dot.
|
||||
/// For example, if the [Router] has a middleware 'y', and the `namespace`
|
||||
/// is 'x', then that middleware will be available as 'x.y' in the main router.
|
||||
/// These namespaces can be nested.
|
||||
void use(Pattern path, Router router,
|
||||
{bool hooked: true, String namespace: null}) {
|
||||
// Let's copy middleware, heeding the optional middleware namespace.
|
||||
String middlewarePrefix = namespace != null ? "$namespace." : "";
|
||||
|
||||
Map copiedMiddleware = new Map.from(router.requestMiddleware);
|
||||
for (String middlewareName in copiedMiddleware.keys) {
|
||||
requestMiddleware["$middlewarePrefix$middlewareName"] =
|
||||
copiedMiddleware[middlewareName];
|
||||
}
|
||||
|
||||
root.addChild(router.root);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to any request matching the given path.
|
||||
Route all(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('*', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a DELETE request.
|
||||
Route delete(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('DELETE', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a GET request.
|
||||
Route get(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('GET', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a HEAD request.
|
||||
Route head(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('HEAD', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a OPTIONS request.
|
||||
Route options(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('OPTIONS', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a POST request.
|
||||
Route post(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('POST', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a PATCH request.
|
||||
Route patch(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('PATCH', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a PUT request.
|
||||
Route put(Pattern path, Object handler, {List middleware}) {
|
||||
return addRoute('PUT', path, handler, middleware: middleware);
|
||||
}
|
||||
}
|
14
lib/src/routing_exception.dart
Normal file
14
lib/src/routing_exception.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
abstract class RoutingException extends Exception {
|
||||
factory RoutingException(String message) => new _RoutingExceptionImpl(message);
|
||||
factory RoutingException.orphan() => new _RoutingExceptionImpl("Tried to resolve path '..' on a route that has no parent.");
|
||||
factory RoutingException.noSuchRoute(String path) => new _RoutingExceptionImpl("Tried to navigate to non-existent route: '$path'.");
|
||||
}
|
||||
|
||||
class _RoutingExceptionImpl implements RoutingException {
|
||||
final String message;
|
||||
|
||||
_RoutingExceptionImpl(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
name: angel_route
|
||||
description: Advanced routing API, supports both server and browser.
|
||||
description: A powerful, isomorphic routing library for Dart.
|
||||
version: 1.0.0-dev
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_route
|
||||
dev_dependencies:
|
||||
browser: ">=0.10.0 < 0.11.0"
|
||||
test: ">=0.12.15 <0.13.0"
|
|
@ -1,8 +1,8 @@
|
|||
import '../lib/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'route/all_tests.dart' as route;
|
||||
import 'router/all_tests.dart' as router;
|
||||
|
||||
main() {
|
||||
final foo = new Route('/foo');
|
||||
final bar = foo.child('/bar');
|
||||
print(foo.path);
|
||||
print(bar.path);
|
||||
group('route', route.main);
|
||||
group('router', router.main);
|
||||
}
|
1
test/packages
Symbolic link
1
test/packages
Symbolic link
|
@ -0,0 +1 @@
|
|||
../packages
|
10
test/route/all_tests.dart
Normal file
10
test/route/all_tests.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'no_params.dart' as no_params;
|
||||
import 'parse_params.dart' as parse_params;
|
||||
import 'with_params.dart' as with_params;
|
||||
|
||||
main() {
|
||||
group('parse params', parse_params.main);
|
||||
group('no params', no_params.main);
|
||||
group('with params', with_params.main);
|
||||
}
|
57
test/route/no_params.dart
Normal file
57
test/route/no_params.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
final foo = new Route('/foo', handlers: ['bar']);
|
||||
final bar = foo.child('/bar');
|
||||
final baz = bar.child('//////baz//////', handlers: ['hello', 'world']);
|
||||
|
||||
test('matching', () {
|
||||
expect(foo.children.length, equals(1));
|
||||
expect(foo.handlers.length, equals(1));
|
||||
expect(foo.handlerSequence.length, equals(1));
|
||||
expect(foo.path, equals('foo'));
|
||||
expect(foo.match('/foo'), isNotNull);
|
||||
expect(foo.match('/bar'), isNull);
|
||||
expect(foo.match('/foolish'), isNull);
|
||||
expect(foo.parent, isNull);
|
||||
expect(foo.absoluteParent, equals(foo));
|
||||
|
||||
expect(bar.path, equals('foo/bar'));
|
||||
expect(bar.children.length, equals(1));
|
||||
expect(bar.handlers, isEmpty);
|
||||
expect(bar.handlerSequence.length, equals(1));
|
||||
expect(bar.match('/foo/bar'), isNotNull);
|
||||
expect(bar.match('/bar'), isNull);
|
||||
expect(bar.match('/foo/bar/2'), isNull);
|
||||
expect(bar.parent, equals(foo));
|
||||
expect(baz.absoluteParent, equals(foo));
|
||||
|
||||
expect(baz.children, isEmpty);
|
||||
expect(baz.handlers.length, equals(2));
|
||||
expect(baz.handlerSequence.length, equals(3));
|
||||
expect(baz.path, equals('foo/bar/baz'));
|
||||
expect(baz.match('/foo/bar/baz'), isNotNull);
|
||||
expect(baz.match('/foo/bat/baz'), isNull);
|
||||
expect(baz.match('/foo/bar/baz/1'), isNull);
|
||||
expect(baz.parent, equals(bar));
|
||||
expect(baz.absoluteParent, equals(foo));
|
||||
});
|
||||
|
||||
test('hierarchy', () {
|
||||
expect(foo.resolve('bar'), equals(bar));
|
||||
expect(foo.resolve('bar/baz'), equals(baz));
|
||||
|
||||
expect(bar.resolve('..'), equals(foo));
|
||||
expect(bar.resolve('/bar/baz'), equals(baz));
|
||||
expect(bar.resolve('../bar'), equals(bar));
|
||||
|
||||
expect(baz.resolve('..'), equals(bar));
|
||||
expect(baz.resolve('../..'), equals(foo));
|
||||
expect(baz.resolve('../baz'), equals(baz));
|
||||
expect(baz.resolve('../../bar'), equals(bar));
|
||||
expect(baz.resolve('../../bar/baz'), equals(baz));
|
||||
expect(baz.resolve('/bar'), equals(bar));
|
||||
expect(baz.resolve('/bar/baz'), equals(baz));
|
||||
});
|
||||
}
|
1
test/route/packages
Symbolic link
1
test/route/packages
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../packages
|
26
test/route/parse_params.dart
Normal file
26
test/route/parse_params.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
p(x) {
|
||||
print(x);
|
||||
return x;
|
||||
}
|
||||
|
||||
main() {
|
||||
final foo = new Route('/foo/:id');
|
||||
final bar = foo.child('/bar/:barId/baz');
|
||||
|
||||
test('make uri', () {
|
||||
expect(p(foo.makeUri({'id': 1337})), equals('foo/1337'));
|
||||
expect(p(bar.makeUri({'id': 1337, 'barId': 12})),
|
||||
equals('foo/1337/bar/12/baz'));
|
||||
});
|
||||
|
||||
test('parse', () {
|
||||
final fooParams = foo.parseParameters('foo/1337/////');
|
||||
expect(p(fooParams), equals({'id': 1337}));
|
||||
|
||||
final barParams = bar.parseParameters('/////foo/1337/bar/12/baz');
|
||||
expect(p(barParams), equals({'id': 1337, 'barId': 12}));
|
||||
});
|
||||
}
|
3
test/route/with_params.dart
Normal file
3
test/route/with_params.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
main() {
|
||||
|
||||
}
|
42
test/router/all_tests.dart
Normal file
42
test/router/all_tests.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
main() {
|
||||
final router = new Router();
|
||||
final indexRoute = router.get('/', () => ':)');
|
||||
final userById = router.delete('/user/:id/detail', (id) => num.parse(id));
|
||||
|
||||
Route lower;
|
||||
final letters = router.group('/letter///', (router) {
|
||||
lower = router
|
||||
.get('/:id([A-Za-z])', (id) => ABC.indexOf(id[0]))
|
||||
.child('////lower', handlers: [(String id) => id.toLowerCase()[0]]);
|
||||
|
||||
lower.parent
|
||||
.child('/upper', handlers: [(String id) => id.toUpperCase()[0]]);
|
||||
});
|
||||
|
||||
router.dumpTree();
|
||||
|
||||
test('extensible', () {
|
||||
router.two = 2;
|
||||
expect(router.properties['two'], equals(2));
|
||||
});
|
||||
|
||||
test('hierarchy', () {
|
||||
expect(lower.absoluteParent, equals(router.root));
|
||||
expect(lower.parent.path, equals('letter/:id'));
|
||||
expect(lower.resolve('../upper').path, equals('letter/:id/upper'));
|
||||
expect(lower.resolve('/user/34/detail'), equals(userById));
|
||||
});
|
||||
|
||||
test('resolve', () {
|
||||
expect(router.resolve('/'), equals(indexRoute));
|
||||
expect(router.resolve('user/1337/detail'), equals(userById));
|
||||
expect(router.resolve('/user/1337/detail'), equals(userById));
|
||||
expect(router.resolve('letter/a/lower'), equals(lower));
|
||||
expect(router.resolve('letter/2/lower'), isNull);
|
||||
});
|
||||
}
|
1
test/router/packages
Symbolic link
1
test/router/packages
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../packages
|
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>
|
33
web/shared/basic.dart
Normal file
33
web/shared/basic.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
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.onRoute.listen((route) {
|
||||
if (route == null) {
|
||||
$h1.text = 'No Active Route';
|
||||
$ul.children
|
||||
..clear()
|
||||
..add(new LIElement()..text = '(empty)');
|
||||
} else {
|
||||
$h1.text = 'Active Route: ${route.path}';
|
||||
$ul.children
|
||||
..clear()
|
||||
..addAll(route.handlerSequence
|
||||
.map((handler) => new LIElement()..text = handler.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('a', 'a handler');
|
||||
|
||||
router.group('b', (router) {
|
||||
router.get('a', 'b/a handler');
|
||||
router.get('b', 'b/b handler', middleware: ['b/b middleware']);
|
||||
}, middleware: ['b middleware']);
|
||||
|
||||
router.get('c', 'c handler');
|
||||
|
||||
router.dumpTree();
|
||||
}
|
Loading…
Reference in a new issue