Skip to content

Commit c4d1403

Browse files
committed
Merge pull request #139 from nateabele/directive
First pass at state directive for auto-generating link URLs
2 parents 2da2710 + 5bcd4cc commit c4d1403

File tree

6 files changed

+137
-5
lines changed

6 files changed

+137
-5
lines changed

Gruntfile.js

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = function (grunt) {
3636
'src/urlRouter.js',
3737
'src/state.js',
3838
'src/viewDirective.js',
39+
'src/stateDirectives.js',
3940
'src/compat.js'
4041
],
4142
dest: '<%= builddir %>/<%= pkg.name %>.js'

src/state.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
266266
return $state.$current.includes[findState(stateOrName).name];
267267
};
268268

269-
$state.href = function (stateOrName, params) {
270-
var state = findState(stateOrName), nav = state.navigable;
271-
if (!nav) throw new Error("State '" + state + "' is not navigable");
269+
$state.href = function (stateOrName, params, options) {
270+
options = extend({ lossy: true }, options || {});
271+
var state = findState(stateOrName);
272+
var nav = options.lossy ? state.navigable : state;
273+
274+
if (!nav || !nav.url) throw new Error("State '" + state + "' " + (
275+
options.lossy ? "does not have a URL or navigable parent" : "is not navigable"
276+
));
272277
return nav.url.format(normalize(state.params, params || {}));
273278
};
274279

src/stateDirectives.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function parseStateRef(ref) {
2+
var parsed = ref.match(/^([^(]+?)\s*(\((.*)\))?$/);
3+
if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'");
4+
return { state: parsed[1], paramExpr: parsed[3] || null };
5+
}
6+
7+
$StateRefDirective.$inject = ['$state'];
8+
function $StateRefDirective($state) {
9+
return {
10+
restrict: 'A',
11+
link: function(scope, element, attrs) {
12+
var ref = parseStateRef(attrs.uiSref);
13+
var params = null, url = null;
14+
var isForm = element[0].nodeName === "FORM";
15+
var attr = isForm ? "action" : "href", nav = true;
16+
17+
var update = function(newVal) {
18+
if (newVal) params = newVal;
19+
if (!nav) return;
20+
21+
try {
22+
element[0][attr] = $state.href(ref.state, params, { lossy: true });
23+
} catch (e) {
24+
nav = false;
25+
}
26+
};
27+
28+
if (ref.paramExpr) {
29+
scope.$watch(ref.paramExpr, function(newVal, oldVal) {
30+
if (newVal !== oldVal) update(newVal);
31+
}, true);
32+
params = scope.$eval(ref.paramExpr);
33+
}
34+
update();
35+
36+
if (isForm) return;
37+
38+
element.bind("click", function(e) {
39+
$state.transitionTo(ref.state, params);
40+
e.preventDefault();
41+
});
42+
}
43+
};
44+
}
45+
46+
angular.module('ui.state').directive('uiSref', $StateRefDirective);

test/stateDirectivesSpec.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
describe('uiStateRef', function() {
2+
3+
beforeEach(module('ui.state'));
4+
5+
beforeEach(module(function($stateProvider) {
6+
$stateProvider.state('index', {
7+
url: '/'
8+
}).state('contacts', {
9+
url: '/contacts'
10+
}).state('contacts.item', {
11+
url: '/:id'
12+
}).state('contacts.item.detail', {});
13+
}));
14+
15+
describe('links', function() {
16+
var el, scope;
17+
18+
beforeEach(inject(function($rootScope, $compile) {
19+
el = angular.element('<a ui-sref="contacts.item.detail({ id: contact.id })">Details</a>');
20+
scope = $rootScope;
21+
scope.contact = { id: 5 };
22+
scope.$apply();
23+
24+
$compile(el)(scope);
25+
scope.$digest();
26+
}));
27+
28+
29+
it('should generate the correct href', function() {
30+
expect(el.attr('href')).toBe('/contacts/5');
31+
});
32+
33+
it('should update the href when parameters change', function() {
34+
expect(el.attr('href')).toBe('/contacts/5');
35+
scope.contact.id = 6;
36+
scope.$apply();
37+
expect(el.attr('href')).toBe('/contacts/6');
38+
});
39+
40+
it('should transition states when clicked', inject(function($state, $stateParams, $document, $q) {
41+
expect($state.$current.name).toEqual('');
42+
43+
var e = $document[0].createEvent("MouseEvents");
44+
e.initMouseEvent("click");
45+
el[0].dispatchEvent(e);
46+
47+
$q.flush();
48+
expect($state.current.name).toEqual('contacts.item.detail');
49+
expect($stateParams).toEqual({ id: "5" });
50+
}));
51+
});
52+
53+
describe('forms', function() {
54+
var el, scope;
55+
56+
beforeEach(inject(function($rootScope, $compile) {
57+
el = angular.element('<form ui-sref="contacts.item.detail({ id: contact.id })"></form>');
58+
scope = $rootScope;
59+
scope.contact = { id: 5 };
60+
scope.$apply();
61+
62+
$compile(el)(scope);
63+
scope.$digest();
64+
}));
65+
66+
it('should generate the correct action', function() {
67+
expect(el.attr('action')).toBe('/contacts/5');
68+
});
69+
});
70+
});

test/stateSpec.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,16 @@ describe('state', function () {
256256

257257
describe('.href()', function () {
258258
it('aborts on un-navigable states', inject(function ($state) {
259-
expect(function() { $state.href("A"); }).toThrow("State 'A' is not navigable");
259+
expect(function() { $state.href("A"); }).toThrow(
260+
"State 'A' does not have a URL or navigable parent"
261+
);
262+
expect(function() { $state.href("about.sidebar", null, { lossy: false }); }).toThrow(
263+
"State 'about.sidebar' is not navigable"
264+
);
265+
}));
266+
267+
it('generates a parent state URL when lossy is true', inject(function ($state) {
268+
expect($state.href("about.sidebar", null, { lossy: true })).toEqual("/about");
260269
}));
261270

262271
it('generates a URL without parameters', inject(function ($state) {

test/test-config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ files = [
1717
'src/urlRouter.js',
1818
'src/state.js',
1919
'src/viewDirective.js',
20+
'src/stateDirectives.js',
2021
'src/compat.js',
21-
22+
2223
'test/*Spec.js',
2324
// 'test/compat/matchers.js',
2425
// 'test/compat/*Spec.js',

0 commit comments

Comments
 (0)