Skip to content

Commit 633573e

Browse files
feat(uiCanExit): Add uiCanExit hook for routed components
1 parent 7cd8dc9 commit 633573e

File tree

2 files changed

+90
-78
lines changed

2 files changed

+90
-78
lines changed

src/directives/uiView.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
} from '@angular/core';
66
import {ReflectorReader, reflector} from '../private_import_core';
77

8-
import {UIRouter} from "ui-router-core";
8+
import {
9+
UIRouter, isFunction, Transition, parse, HookResult, TransitionHookFn, State, prop, StateDeclaration
10+
} from "ui-router-core";
911
import {trace} from "ui-router-core";
1012
import {ViewContext, ViewConfig, ActiveUIView} from "ui-router-core";
1113
import {Ng2ViewConfig} from "../statebuilders/views";
1214
import {ResolveContext, NATIVE_INJECTOR_TOKEN} from "ui-router-core";
1315
import {flattenR} from "ui-router-core";
1416
import {MergeInjector} from "../mergeInjector";
17+
import { Subscription } from 'rxjs/Subscription';
1518

1619
/** @hidden */
1720
let id = 0;
@@ -134,8 +137,13 @@ export class UIView {
134137
@ViewChild('componentTarget', {read: ViewContainerRef}) componentTarget: ViewContainerRef;
135138
@Input('name') name: string;
136139
@Input('ui-view') set _name(val: string) { this.name = val; }
140+
/** The reference to the component currently inside the viewport */
137141
componentRef: ComponentRef<any>;
138-
deregister: Function;
142+
/** Deregisters the ui-view from the view service */
143+
deregisterUIView: Function;
144+
/** Deregisters the master uiCanExit transition hook */
145+
deregisterHook: Function;
146+
/** Data about the this UIView */
139147
uiViewData: ActiveUIView = <any> {};
140148
parent: ParentUIViewInject;
141149

@@ -150,8 +158,9 @@ export class UIView {
150158
}
151159

152160
ngOnInit() {
153-
let parentFqn = this.parent.fqn;
154-
let name = this.name || '$default';
161+
const router = this.router;
162+
const parentFqn = this.parent.fqn;
163+
const name = this.name || '$default';
155164

156165
this.uiViewData = {
157166
$type: 'ng2',
@@ -163,7 +172,31 @@ export class UIView {
163172
config: undefined
164173
};
165174

166-
this.deregister = this.router.viewService.registerUIView(this.uiViewData);
175+
this.deregisterHook = router.transitionService.onBefore({}, trans => this.applyUiCanExitHook(trans));
176+
this.deregisterUIView = router.viewService.registerUIView(this.uiViewData);
177+
}
178+
179+
/**
180+
* For each transition, checks the component loaded in the ui-view for:
181+
*
182+
* - has a uiCanExit() component hook
183+
* - is being exited
184+
*
185+
* If both are true, adds the uiCanExit component function as a hook to that singular Transition.
186+
*/
187+
applyUiCanExitHook(trans: Transition) {
188+
const instance = this.componentRef && this.componentRef.instance;
189+
const uiCanExitFn: TransitionHookFn = instance && instance.uiCanExit;
190+
191+
if (isFunction(uiCanExitFn)) {
192+
const state: StateDeclaration = parse("uiViewData.config.viewDecl.$context.self")(this);
193+
194+
if (trans.exiting().indexOf(state) !== -1) {
195+
trans.onStart({}, function(trans) {
196+
return uiCanExitFn.call(instance, trans);
197+
});
198+
}
199+
}
167200
}
168201

169202
disposeLast() {
@@ -172,7 +205,8 @@ export class UIView {
172205
}
173206

174207
ngOnDestroy() {
175-
if (this.deregister) this.deregister();
208+
if (this.deregisterUIView) this.deregisterUIView();
209+
if (this.deregisterHook) this.deregisterHook();
176210
this.disposeLast();
177211
}
178212

@@ -214,8 +248,6 @@ export class UIView {
214248

215249
// Wire resolves to @Input()s
216250
this.applyInputBindings(this.componentRef, context, componentClass);
217-
218-
// TODO: wire uiCanExit and uiOnParamsChanged callbacks
219251
}
220252

221253
/**

src/interface.ts

Lines changed: 50 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** */
33

44
import { StateDeclaration, _ViewDeclaration, Transition, HookResult } from "ui-router-core";
5-
import { Type } from "@angular/core";
5+
import { Type, Component } from "@angular/core";
66
import { NgModuleToLoad } from "./lazyLoad/lazyLoadNgModule";
77

88
/**
@@ -293,111 +293,91 @@ export interface Ng2ViewDeclaration extends _ViewDeclaration {
293293
*
294294
* When using a [[component]] declaration (`component: MyComponent`), each input binding for the component is supplied
295295
* data from a resolve of the same name, by default. You may supply data from a different resolve name by mapping it here.
296+
* This might be useful if you want to reuse the same resolve value with various components with different input binding names.
296297
*
297298
* Each key in this object is the name of one of the component's input bindings.
298299
* Each value is the name of the resolve that should be provided to that binding.
299300
*
300-
* Any component bindings that are omitted from this map get the default behavior of mapping to a resolve of the
301-
* same name.
301+
* Any component bindings that are omitted from this map get the default behavior of mapping to a resolve of the * same name.
302302
*
303303
* #### Example:
304304
* ```js
305-
* $stateProvider.state('foo', {
306-
* resolve: {
307-
* foo: function(FooService) { return FooService.get(); },
308-
* bar: function(BarService) { return BarService.get(); }
309-
* },
310-
* component: 'Baz',
311-
* // The component's `baz` binding gets data from the `bar` resolve
312-
* // The component's `foo` binding gets data from the `foo` resolve (default behavior)
305+
* export const fooState = {
306+
* name: 'foo',
307+
* component: MyComponent,
308+
* resolve: [
309+
* { token: 'users', deps: [UserService], resolveFn: getUsers }
310+
* ],
313311
* bindings: {
314-
* baz: 'bar'
312+
* resolveData: 'users'
315313
* }
316-
* });
314+
* }
317315
*
318-
* app.component('Baz', {
319-
* templateUrl: 'baz.html',
320-
* controller: 'BazController',
321-
* bindings: {
322-
* foo: '<', // foo binding
323-
* baz: '<' // baz binding
324-
* }
325-
* });
316+
* export function getUsers(userservice) {
317+
* return userservice.getUsers();
318+
* }
319+
*
320+
* @Component() {
321+
* }
322+
* class MyComponent {
323+
* @Input() resolveData;
324+
* constructor() { }
325+
* }
326326
* ```
327327
*
328328
*/
329329
bindings?: { [key: string]: string };
330330
}
331331

332332
/**
333-
* @hidden
334-
*
335333
* The shape of a controller for a view (and/or component), defining the controller callbacks.
336334
*
337-
* A view in UI-Router is comprised of either a `component` ([[Ng2ViewDeclaration.component]]) or a combination of a
338-
* `template` (or `templateProvider`) and a `controller` (or `controllerProvider`).
339-
*
340-
* The `controller` object (or the `component`'s controller object) can define component-level controller callbacks,
341-
* which UI-Router will call at the appropriate times. These callbacks are similar to Transition Hooks
342-
* ([[IHookRegistry]]), but are only called if the view is currently active.
335+
* A UI-Router view has an Angular `Component` (see [[Ng2ViewDeclaration.component]]).
336+
* The `Component` may define component-level hooks which UI-Router will call at the appropriate times.
337+
* These callbacks are similar to Transition Hooks ([[IHookRegistry]]), but are only called if the view/component is currently active.
343338
*
344339
* This interface defines the UI-Router component callbacks.
345-
*
346-
* TODO: this should extend the ng2 Component interface
347340
*/
348-
export interface Ng2Component {
341+
export interface Ng2Component extends Component {
349342
/**
350-
* This callback is called when parameter values have changed.
343+
* This callback is called when the a routed component's state is about to be exited.
351344
*
352-
* This callback can be used to respond to changing parameter values in the current state, or in parent/child states.
353-
* This callback is especially handy when using dynamic parameters ([[ParamDeclaration.dynamic]])
354-
*
355-
* Called when:
356-
* - The view is still active
357-
* - A new transition has completed successfully
358-
* - The state for the view (controller) was not reloaded
359-
* - At least one parameter value was changed
360-
*
361-
* Called with:
362-
* @param newValues an object containing the changed parameter values
363-
* @param $transition$ the new Transition which triggered this callback
364-
*
365-
* @example:
366-
* ```js
367-
*
368-
* angular.module('foo').controller('FancyCtrl', function() {
369-
* this.uiOnParamsChanged = function(newParams) {
370-
* console.log("new params: ", newParams);
371-
* }
372-
* });
373-
* ```
374-
*/
375-
uiOnParamsChanged(newValues: any, $transition$: Transition): void;
376-
377-
/**
378-
* This callback is called when the view's state is about to be exited.
345+
* The callback can be used to cancel or alter the new Transition that would otherwise exit the component's state.
379346
*
380347
* This callback is used to inform a view that it is about to be exited, due to a new [[Transition]].
381348
* The callback can ask for user confirmation, and cancel or alter the new Transition. The callback should
382349
* return a value, or a promise for a value. If a promise is returned, the new Transition waits until the
383350
* promise settles.
384351
*
385-
*
386352
* Called when:
387-
* - The view is still active
353+
* - The component is still active inside a `ui-view`
388354
* - A new Transition is about to run
389355
* - The new Transition will exit the view's state
390356
*
391357
* Called with:
392-
* - This callback is injected in the new Transition's context
358+
* - The `Transition` that is about to exit the component's state
393359
*
394-
* Relevant return Values:
395-
* - `false`: The transition is cancelled.
396-
* - A rejected promise: The transition is cancelled.
397-
* - [[TargetState]]: The transition is redirected to the new target state.
398-
* - Anything else: the transition will continue normally (the state and view will be deactivated)
360+
* #### Example:
361+
* ```js
362+
* @Component({
363+
* template: '<input type="text">'
364+
* })
365+
* class MyComponent {
366+
* dirty = true;
367+
*
368+
* constructor(public confirmService: confirmService) {
369+
*
370+
* }
371+
*
372+
* uiCanExit(newTransition: Transition) {
373+
* if (this.dirty && newTransition.to() !== 'logout') {
374+
* return this.confirmService.confirm("Exit without saving changes?");
375+
* }
376+
* }
377+
* }
378+
* ```
399379
*
400-
* @return a value, or a promise for a value.
380+
* @return a hook result which may cancel or alter the pending Transition (see [[HookResult]])
401381
*/
402-
uiCanExit(): HookResult;
382+
uiCanExit(newTransition?: Transition): HookResult;
403383
}

0 commit comments

Comments
 (0)