` tag) to a state. If the state has an associated
+ * URL, the directive will automatically generate & update the `href` attribute via
+ * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking
+ * the link will trigger a state transition with optional parameters.
*
- * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be
+ * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be
* handled natively by the browser.
*
- * You can also use relative state paths within ui-sref, just like the relative
+ * You can also use relative state paths within ui-sref, just like the relative
* paths passed to `$state.go()`. You just need to be aware that the path is relative
- * to the state that the link lives in, in other words the state that loaded the
+ * to the state that the link lives in, in other words the state that loaded the
* template containing the link.
*
* You can specify options to pass to {@link ui.router.state.$state#go $state.go()}
@@ -42,22 +79,22 @@ function stateContext(el) {
* and `reload`.
*
* @example
- * Here's an example of how you'd use ui-sref and how it would compile. If you have the
+ * Here's an example of how you'd use ui-sref and how it would compile. If you have the
* following template:
*
* Home | About | Next page
- *
+ *
*
*
- *
+ *
* Then the compiled html would be (assuming Html5Mode is off and current state is contacts):
*
* Home | About | Next page
- *
+ *
*
* -
* Joe
@@ -78,78 +115,86 @@ function stateContext(el) {
*/
$StateRefDirective.$inject = ['$state', '$timeout'];
function $StateRefDirective($state, $timeout) {
- var allowedOptions = ['location', 'inherit', 'reload', 'absolute'];
-
return {
restrict: 'A',
require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
link: function(scope, element, attrs, uiSrefActive) {
- var ref = parseStateRef(attrs.uiSref, $state.current.name);
- var params = null, url = null, base = stateContext(element) || $state.$current;
- // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
- var hrefKind = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
- 'xlink:href' : 'href';
- var newHref = null, isAnchor = element.prop("tagName").toUpperCase() === "A";
- var isForm = element[0].nodeName === "FORM";
- var attr = isForm ? "action" : hrefKind, nav = true;
-
- var options = { relative: base, inherit: true };
- var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {};
-
- angular.forEach(allowedOptions, function(option) {
- if (option in optionsOverride) {
- options[option] = optionsOverride[option];
- }
- });
+ var ref = parseStateRef(attrs.uiSref, $state.current.name);
+ var def = { state: ref.state, href: null, nav: true, params: null };
+ var type = getTypeInfo(element);
+ var active = uiSrefActive[1] || uiSrefActive[0];
- var update = function(newVal) {
- if (newVal) params = angular.copy(newVal);
- if (!nav) return;
+ def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {});
- newHref = $state.href(ref.state, params, options);
+ var update = function(val) {
+ if (val) def.params = angular.copy(val);
+ if (!def.nav) return;
- var activeDirective = uiSrefActive[1] || uiSrefActive[0];
- if (activeDirective) {
- activeDirective.$$addStateInfo(ref.state, params);
- }
- if (newHref === null) {
- nav = false;
- return false;
- }
- attrs.$set(attr, newHref);
+ def.href = $state.href(ref.state, def.params, def.options);
+ def.nav = (def.href !== null);
+
+ if (active) active.$$addStateInfo(ref.state, def.params);
+ if (def.nav) attrs.$set(type.attr, def.href);
};
if (ref.paramExpr) {
- scope.$watch(ref.paramExpr, function(newVal, oldVal) {
- if (newVal !== params) update(newVal);
- }, true);
- params = angular.copy(scope.$eval(ref.paramExpr));
+ scope.$watch(ref.paramExpr, function(val) { if (val !== def.params) update(val); }, true);
+ def.params = angular.copy(scope.$eval(ref.paramExpr));
}
update();
- if (isForm) return;
-
- element.bind("click", function(e) {
- var button = e.which || e.button;
- if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) {
- // HACK: This is to allow ng-clicks to be processed before the transition is initiated:
- var transition = $timeout(function() {
- $state.go(ref.state, params, options);
- });
- e.preventDefault();
-
- // if the state has no URL, ignore one preventDefault from the directive.
- var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0;
- e.preventDefault = function() {
- if (ignorePreventDefaultCount-- <= 0)
- $timeout.cancel(transition);
- };
- }
- });
+ if (!type.clickable) return;
+ element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; }));
}
};
}
+/**
+ * @ngdoc directive
+ * @name ui.router.state.directive:ui-state
+ *
+ * @requires ui.router.state.uiSref
+ *
+ * @restrict A
+ *
+ * @description
+ * Much like ui-sref, but will accept named $scope properties to evaluate for a state definition,
+ * params and override options.
+ *
+ * @param {string} ui-state 'stateName' can be any valid absolute or relative state
+ * @param {Object} ui-state-params params to pass to {@link ui.router.state.$state#href $state.href()}
+ * @param {Object} ui-state-opts options to pass to {@link ui.router.state.$state#go $state.go()}
+ */
+$StateRefDynamicDirective.$inject = ['$state', '$timeout'];
+function $StateRefDynamicDirective($state, $timeout) {
+ return {
+ restrict: 'A',
+ require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
+ link: function(scope, element, attrs, uiSrefActive) {
+ var type = getTypeInfo(element);
+ var active = uiSrefActive[1] || uiSrefActive[0];
+ var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null];
+ var watch = '[' + group.map(function(val) { return val || 'null'; }).join(', ') + ']';
+ var def = { state: null, params: null, options: null, href: null };
+
+ function runStateRefLink (group) {
+ def.state = group[0], def.params = group[1], def.options = group[2];
+ def.href = $state.href(def.state, def.params, def.options);
+
+ if (active) active.$$addStateInfo(ref.state, def.params);
+ if (def.href) attrs.$set(type.attr, def.href);
+ }
+
+ scope.$watch(watch, runStateRefLink, true);
+ runStateRefLink(scope.$eval(watch));
+
+ if (!type.clickable) return;
+ element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; }));
+ }
+ };
+}
+
+
/**
* @ngdoc directive
* @name ui.router.state.directive:ui-sref-active
@@ -269,13 +314,9 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
}
function addClass(el, className) { $timeout(function () { el.addClass(className); }); }
-
function removeClass(el, className) { el.removeClass(className); }
-
function anyMatch(state, params) { return $state.includes(state.name, params); }
-
function exactMatch(state, params) { return $state.is(state.name, params); }
-
}]
};
}
@@ -283,4 +324,5 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
angular.module('ui.router.state')
.directive('uiSref', $StateRefDirective)
.directive('uiSrefActive', $StateRefActiveDirective)
- .directive('uiSrefActiveEq', $StateRefActiveDirective);
+ .directive('uiSrefActiveEq', $StateRefActiveDirective)
+ .directive('uiState', $StateRefDynamicDirective);
diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js
index 9f696cff9..07e62a854 100644
--- a/test/stateDirectivesSpec.js
+++ b/test/stateDirectivesSpec.js
@@ -291,6 +291,71 @@ describe('uiStateRef', function() {
}));
});
+ describe('links with dynamic state definitions', function () {
+ var template;
+
+ beforeEach(inject(function($rootScope, $compile, $state) {
+ el = angular.element('state');
+ scope = $rootScope;
+ angular.extend(scope, { state: 'contacts', params: {} });
+ template = $compile(el)(scope);
+ scope.$digest();
+ }));
+
+ it('sets the correct initial href', function () {
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
+ });
+
+ it('updates to the new href', function () {
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
+
+ scope.state = 'contacts.item';
+ scope.params = { id: 5 };
+ scope.$digest();
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts/5');
+
+ scope.params.id = 25;
+ scope.$digest();
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts/25');
+ });
+
+ it('retains the old href if the new points to a non-state', function () {
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
+ scope.state = 'nostate';
+ scope.$digest();
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
+ });
+
+ it('accepts param overrides', inject(function ($compile) {
+ el = angular.element('state');
+ scope.state = 'contacts.item';
+ scope.params = { id: 10 };
+ template = $compile(el)(scope);
+ scope.$digest();
+ expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
+ }));
+
+ it('accepts option overrides', inject(function ($compile, $timeout, $state) {
+ var transitionOptions;
+
+ el = angular.element('state');
+ scope.state = 'contacts';
+ scope.opts = { reload: true };
+ template = $compile(el)(scope);
+ scope.$digest();
+
+ spyOn($state, 'go').andCallFake(function(state, params, options) {
+ transitionOptions = options;
+ });
+
+ triggerClick(template)
+ $timeout.flush();
+
+ expect(transitionOptions.reload).toEqual(true);
+ expect(transitionOptions.absolute).toBeUndefined();
+ }));
+ });
+
describe('forms', function() {
var el, scope;
@@ -362,33 +427,6 @@ describe('uiStateRef', function() {
expect($state.$current.name).toBe("contacts");
}));
});
-
- describe('transition options', function() {
-
- beforeEach(inject(function($rootScope, $compile, $state) {
- el = angular.element('Details');
- scope = $rootScope;
- scope.contact = { id: 5 };
-
- $compile(el)(scope);
- scope.$digest();
- }));
-
- it('uses allowed transition options', inject(function($q, $timeout, $state) {
- var transitionOptions;
-
- spyOn($state, 'go').andCallFake(function(state, params, options) {
- transitionOptions = options;
- });
-
- triggerClick(el);
- $timeout.flush();
-
- expect(transitionOptions.reload).toEqual(true);
- expect(transitionOptions.absolute).toEqual(true);
- expect(transitionOptions.notify).toBeUndefined();
- }));
- });
});
describe('uiSrefActive', function() {
diff --git a/test/stateSpec.js b/test/stateSpec.js
index 9b1ebb6e5..e504241c3 100644
--- a/test/stateSpec.js
+++ b/test/stateSpec.js
@@ -542,7 +542,7 @@ describe('state', function () {
$state.transitionTo('dynamicController', { type: "Acme" });
$q.flush();
expect(ctrlName).toEqual("AcmeFooController");
- }));+
+ }));
it('uses the templateProvider to get template dynamically', inject(function ($state, $q) {
$state.transitionTo('dynamicTemplate', { type: "Acme" });