Skip to content

Commit 389dfd5

Browse files
fix(ui-sref): Update ui-sref/state href when states are added/removed
fix(ui-state): Support one time bindings in ng 1.3 Closes #3131 Closes #3054
1 parent 6f22898 commit 389dfd5

File tree

3 files changed

+122
-40
lines changed

3 files changed

+122
-40
lines changed

karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function karmaServedFiles(ngVersion) {
3232
var webpackConfig = require('./webpack.config.js');
3333
webpackConfig.entry = {};
3434
webpackConfig.plugins = [];
35+
webpackConfig.devtool = 'inline-source-map';
3536

3637
module.exports = function(config) {
3738
var ngVersion = config.ngversion || "1.5.0";

src/ng1/directives/stateDirectives.ts

+65-38
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ng as angular } from "../../angular";
99
import { IAugmentedJQuery, ITimeoutService, IScope, IInterpolateService } from "angular";
1010

1111
import {
12-
Obj, extend, forEach, toJson, tail, isString, isObject, parse,
12+
Obj, extend, forEach, toJson, tail, isString, isObject, parse, noop,
1313
PathNode, StateOrName, StateService, TransitionService, State, UIRouter
1414
} from "ui-router-core";
1515
import { UIViewData } from "./viewDirective";
@@ -50,14 +50,14 @@ function getTypeInfo(el: IAugmentedJQuery): TypeInfo {
5050
}
5151

5252
/** @hidden */
53-
function clickHook(el: IAugmentedJQuery, $state: StateService, $timeout: ITimeoutService, type: TypeInfo, current: Function) {
53+
function clickHook(el: IAugmentedJQuery, $state: StateService, $timeout: ITimeoutService, type: TypeInfo, getDef: () => Def) {
5454
return function (e: JQueryMouseEventObject) {
55-
var button = e.which || e.button, target = current();
55+
var button = e.which || e.button, target = getDef();
5656

5757
if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) {
5858
// HACK: This is to allow ng-clicks to be processed before the transition is initiated:
5959
var transition = $timeout(function () {
60-
$state.go(target.state, target.params, target.options);
60+
$state.go(target.uiState, target.uiStateParams, target.uiStateOpts);
6161
});
6262
e.preventDefault();
6363

@@ -142,38 +142,42 @@ function defaultOpts(el: IAugmentedJQuery, $state: StateService) {
142142
* @param {string} ui-sref 'stateName' can be any valid absolute or relative state
143143
* @param {Object} ui-sref-opts options to pass to [[StateService.go]]
144144
*/
145-
let uiSref = ['$state', '$timeout',
146-
function $StateRefDirective($state: StateService, $timeout: ITimeoutService) {
145+
let uiSref = ['$uiRouter', '$timeout',
146+
function $StateRefDirective($uiRouter: UIRouter, $timeout: ITimeoutService) {
147+
let $state = $uiRouter.stateService;
148+
147149
return {
148150
restrict: 'A',
149151
require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
150152
link: function (scope: IScope, element: IAugmentedJQuery, attrs: any, uiSrefActive: any) {
151153
var ref = parseStateRef(attrs.uiSref, $state.current.name);
152-
var def: Def = { state: ref.state, href: null, params: null, options: null };
154+
var def = { uiState: ref.state } as Def;
153155
var type = getTypeInfo(element);
154156
var active = uiSrefActive[1] || uiSrefActive[0];
155157
var unlinkInfoFn: Function = null;
156158
var hookFn;
157159

158-
def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {});
160+
def.uiStateOpts = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {});
159161

160162
var update = function (val?: any) {
161-
if (val) def.params = angular.copy(val);
162-
def.href = $state.href(ref.state, def.params, def.options);
163+
if (val) def.uiStateParams = angular.copy(val);
164+
def.href = $state.href(ref.state, def.uiStateParams, def.uiStateOpts);
163165

164166
if (unlinkInfoFn) unlinkInfoFn();
165-
if (active) unlinkInfoFn = active.$$addStateInfo(ref.state, def.params);
167+
if (active) unlinkInfoFn = active.$$addStateInfo(ref.state, def.uiStateParams);
166168
if (def.href !== null) attrs.$set(type.attr, def.href);
167169
};
168170

169171
if (ref.paramExpr) {
170172
scope.$watch(ref.paramExpr, function (val) {
171-
if (val !== def.params) update(val);
173+
if (val !== def.uiStateParams) update(val);
172174
}, true);
173-
def.params = angular.copy(scope.$eval(ref.paramExpr));
175+
def.uiStateParams = angular.copy(scope.$eval(ref.paramExpr));
174176
}
175177
update();
176178

179+
scope.$on('$destroy', <any> $uiRouter.stateRegistry.onStatesChanged(() => update()));
180+
177181
if (!type.clickable) return;
178182
hookFn = clickHook(element, $state, $timeout, type, function () {
179183
return def;
@@ -187,52 +191,75 @@ let uiSref = ['$state', '$timeout',
187191
}];
188192

189193
/**
190-
* `ui-state`: A dynamic version of `ui-sref`
194+
* `ui-state`: A dynamic version of the `ui-sref` directive
195+
*
196+
* Much like ui-sref, but it `$observe`s inputs and `$watch`es/evaluates values.
197+
*
198+
* The `ui-sref` directive takes a string literal, which is split into 1) state name and 2) parameter values expression.
199+
* It does not `$observe` the input string and `$watch`es only the parameter value expression.
200+
* Because of this, `ui-sref` is fairly lightweight, but does no deal well with with srefs that dynamically change.
201+
*
202+
*
203+
* On the other hand, the `ui-state` directive is fully dynamic.
204+
* It is useful for building dynamic links, such as data–driven navigation links.
205+
*
206+
* It consists of three attributes:
207+
*
208+
* - `ui-state="expr"`: The state to link to; the `expr` string is evaluated and `$watch`ed
209+
* - `ui-state-params="expr"`: The state params to link to; the `expr` string is evaluated and `$watch`ed
210+
* - `ui-state-opts="expr"`: The transition options for the link; the `expr` string is evaluated and `$watch`ed
191211
*
192-
* Much like ui-sref, but will accept named $scope properties to evaluate for a state definition,
193-
* params and override options.
212+
* In angular 1.3 and above, a one time binding may be used if you know specific bindings will not change, i.e:
213+
* `ui-params="::foo.params"`.
214+
*
215+
* Like `ui-sref`, this directive also works with `ui-sref-active` and `ui-sref-active-eq`.
194216
*
195217
* @example
196218
* ```html
197219
*
198-
* <li ng-repeat="nav in navlinks">
199-
* <a ui-state="nav.statename">{{nav.description}}</a>
220+
* <li ng-repeat="nav in navlinks" ui-sref-active="active">
221+
* <a ui-state="nav.statename" ui-state-params="nav.params">{{nav.description}}</a>
200222
* </li>
201223
* ```
202-
*
203-
* @param {string} ui-state 'stateName' can be any valid absolute or relative state
204-
* @param {Object} ui-state-params params to pass to [[StateService.href]]
205-
* @param {Object} ui-state-opts options to pass to [[StateService.go]]
206224
*/
207-
let uiState = ['$state', '$timeout',
208-
function $StateRefDynamicDirective($state: StateService, $timeout: ITimeoutService) {
225+
let uiState = ['$uiRouter', '$timeout',
226+
function $StateRefDynamicDirective($uiRouter: UIRouter, $timeout: ITimeoutService) {
227+
let $state = $uiRouter.stateService;
228+
209229
return {
210230
restrict: 'A',
211231
require: ['?^uiSrefActive', '?^uiSrefActiveEq'],
212232
link: function (scope: IScope, element: IAugmentedJQuery, attrs: any, uiSrefActive: any) {
213233
var type = getTypeInfo(element);
214234
var active = uiSrefActive[1] || uiSrefActive[0];
215-
var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null];
216-
var watch = '[' + group.map(function (val) {
217-
return val || 'null';
218-
}).join(', ') + ']';
219-
var def: Def = { state: null, params: null, options: null, href: null };
235+
var def = {} as Def;
236+
let inputAttrs = ['uiState', 'uiStateParams', 'uiStateOpts'];
237+
let watchDeregFns = inputAttrs.reduce((acc, attr) => (acc[attr] = noop, acc), {});
220238
var unlinkInfoFn: Function = null;
221239
var hookFn;
222240

223-
function runStateRefLink(group: any[]) {
224-
def.state = group[0];
225-
def.params = group[1];
226-
def.options = group[2];
227-
def.href = $state.href(def.state, def.params, def.options);
241+
function update() {
242+
def.href = $state.href(def.uiState, def.uiStateParams, def.uiStateOpts);
228243

229244
if (unlinkInfoFn) unlinkInfoFn();
230-
if (active) unlinkInfoFn = active.$$addStateInfo(def.state, def.params);
245+
if (active) unlinkInfoFn = active.$$addStateInfo(def.uiState, def.uiStateParams);
231246
if (def.href) attrs.$set(type.attr, def.href);
232247
}
233248

234-
scope.$watch(watch, runStateRefLink, true);
235-
runStateRefLink(scope.$eval(watch));
249+
inputAttrs.forEach((field) => {
250+
def[field] = attrs[field] ? scope.$eval(attrs[field]) : null;
251+
252+
attrs.$observe(field, (expr) => {
253+
watchDeregFns[field]();
254+
watchDeregFns[field] = scope.$watch(expr, (newval) => {
255+
def[field] = newval;
256+
update();
257+
}, true);
258+
})
259+
});
260+
261+
scope.$on('$destroy', <any> $uiRouter.stateRegistry.onStatesChanged(() => update()));
262+
update();
236263

237264
if (!type.clickable) return;
238265
hookFn = clickHook(element, $state, $timeout, type, function () {
@@ -456,7 +483,7 @@ let uiSrefActive = ['$state', '$stateParams', '$interpolate', '$transitions', '$
456483
};
457484
}];
458485

459-
interface Def { state: string; href: string; params: Obj; options: any;
486+
interface Def { uiState: string; href: string; uiStateParams: Obj; uiStateOpts: any;
460487
}
461488
angular.module('ui.router.state')
462489
.directive('uiSref', uiSref)

test/stateDirectivesSpec.js

+56-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ describe('uiStateRef', function() {
1212
_locationProvider = $locationProvider;
1313
$stateProvider.state('top', {
1414
url: ''
15+
}).state('other', {
16+
url: '/other/:id',
17+
template: 'other'
1518
}).state('contacts', {
1619
url: '/contacts',
1720
template: '<a ui-sref=".item({ id: 5 })" class="item">Person</a> <ui-view></ui-view>'
@@ -365,6 +368,14 @@ describe('uiStateRef', function() {
365368

366369
}));
367370

371+
it('updates to a new href when it points to a new state', function () {
372+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
373+
scope.state = 'other';
374+
scope.params = { id: '123' };
375+
scope.$digest();
376+
expect(angular.element(template[0]).attr('href')).toBe('#/other/123');
377+
});
378+
368379
it('retains the old href if the new points to a non-state', function () {
369380
expect(angular.element(template[0]).attr('href')).toBe('#/contacts');
370381
scope.state = 'nostate';
@@ -373,14 +384,57 @@ describe('uiStateRef', function() {
373384
});
374385

375386
it('accepts param overrides', inject(function ($compile) {
376-
el = angular.element('<a ui-state="state" ui-state-params="params">state</a>');
377387
scope.state = 'contacts.item';
378388
scope.params = { id: 10 };
389+
scope.$digest();
390+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
391+
}));
392+
393+
it('accepts param overrides', inject(function ($compile) {
394+
scope.state = 'contacts.item';
395+
scope.params = { id: 10 };
396+
scope.$digest();
397+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
398+
399+
scope.params.id = 22;
400+
scope.$digest();
401+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/22');
402+
}));
403+
404+
it('watches attributes', inject(function ($compile) {
405+
el = angular.element('<a ui-state="{{exprvar}}" ui-state-params="params">state</a>');
379406
template = $compile(el)(scope);
407+
408+
scope.exprvar = 'state1';
409+
scope.state1 = 'contacts.item';
410+
scope.state2 = 'other';
411+
scope.params = { id: 10 };
380412
scope.$digest();
381413
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
414+
415+
scope.exprvar = 'state2';
416+
scope.$digest();
417+
expect(angular.element(template[0]).attr('href')).toBe('#/other/10');
382418
}));
383419

420+
if (angular.version.minor >= 3) {
421+
it('allows one-time-binding on ng1.3+', inject(function ($compile) {
422+
el = angular.element('<a ui-state="::state" ui-state-params="::params">state</a>');
423+
424+
scope.state = 'contacts.item';
425+
scope.params = {id: 10};
426+
template = $compile(el)(scope);
427+
scope.$digest();
428+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
429+
430+
scope.state = 'other';
431+
scope.params = {id: 22};
432+
433+
scope.$digest();
434+
expect(angular.element(template[0]).attr('href')).toBe('#/contacts/10');
435+
}));
436+
}
437+
384438
it('accepts option overrides', inject(function ($compile, $timeout, $state) {
385439
var transitionOptions;
386440

@@ -402,7 +456,7 @@ describe('uiStateRef', function() {
402456
}));
403457
});
404458

405-
describe('links with dynamic state definitions', function () {
459+
describe('links with dyna mic state definitions', function () {
406460
var template;
407461

408462
beforeEach(inject(function($rootScope, $compile, $state) {

0 commit comments

Comments
 (0)