1 | /** @publicapi @module directives */ /** */
|
2 | import { extend, filter, isDefined, isFunction, isString, kebobString, noop, parse, ResolveContext, tail, trace, unnestR, } from '@uirouter/core';
|
3 | import { ng as angular } from '../angular';
|
4 | import { getLocals } from '../services';
|
5 | import { Ng1ViewConfig } from '../statebuilders/views';
|
6 | /**
|
7 | * `ui-view`: A viewport directive which is filled in by a view from the active state.
|
8 | *
|
9 | * ### Attributes
|
10 | *
|
11 | * - `name`: (Optional) A view name.
|
12 | * The name should be unique amongst the other views in the same state.
|
13 | * You can have views of the same name that live in different states.
|
14 | * The ui-view can be targeted in a View using the name ([[Ng1StateDeclaration.views]]).
|
15 | *
|
16 | * - `autoscroll`: an expression. When it evaluates to true, the `ui-view` will be scrolled into view when it is activated.
|
17 | * Uses [[$uiViewScroll]] to do the scrolling.
|
18 | *
|
19 | * - `onload`: Expression to evaluate whenever the view updates.
|
20 | *
|
21 | * #### Example:
|
22 | * A view can be unnamed or named.
|
23 | * ```html
|
24 | * <!-- Unnamed -->
|
25 | * <div ui-view></div>
|
26 | *
|
27 | * <!-- Named -->
|
28 | * <div ui-view="viewName"></div>
|
29 | *
|
30 | * <!-- Named (different style) -->
|
31 | * <ui-view name="viewName"></ui-view>
|
32 | * ```
|
33 | *
|
34 | * You can only have one unnamed view within any template (or root html). If you are only using a
|
35 | * single view and it is unnamed then you can populate it like so:
|
36 | *
|
37 | * ```html
|
38 | * <div ui-view></div>
|
39 | * $stateProvider.state("home", {
|
40 | * template: "<h1>HELLO!</h1>"
|
41 | * })
|
42 | * ```
|
43 | *
|
44 | * The above is a convenient shortcut equivalent to specifying your view explicitly with the
|
45 | * [[Ng1StateDeclaration.views]] config property, by name, in this case an empty name:
|
46 | *
|
47 | * ```js
|
48 | * $stateProvider.state("home", {
|
49 | * views: {
|
50 | * "": {
|
51 | * template: "<h1>HELLO!</h1>"
|
52 | * }
|
53 | * }
|
54 | * })
|
55 | * ```
|
56 | *
|
57 | * But typically you'll only use the views property if you name your view or have more than one view
|
58 | * in the same template. There's not really a compelling reason to name a view if its the only one,
|
59 | * but you could if you wanted, like so:
|
60 | *
|
61 | * ```html
|
62 | * <div ui-view="main"></div>
|
63 | * ```
|
64 | *
|
65 | * ```js
|
66 | * $stateProvider.state("home", {
|
67 | * views: {
|
68 | * "main": {
|
69 | * template: "<h1>HELLO!</h1>"
|
70 | * }
|
71 | * }
|
72 | * })
|
73 | * ```
|
74 | *
|
75 | * Really though, you'll use views to set up multiple views:
|
76 | *
|
77 | * ```html
|
78 | * <div ui-view></div>
|
79 | * <div ui-view="chart"></div>
|
80 | * <div ui-view="data"></div>
|
81 | * ```
|
82 | *
|
83 | * ```js
|
84 | * $stateProvider.state("home", {
|
85 | * views: {
|
86 | * "": {
|
87 | * template: "<h1>HELLO!</h1>"
|
88 | * },
|
89 | * "chart": {
|
90 | * template: "<chart_thing/>"
|
91 | * },
|
92 | * "data": {
|
93 | * template: "<data_thing/>"
|
94 | * }
|
95 | * }
|
96 | * })
|
97 | * ```
|
98 | *
|
99 | * #### Examples for `autoscroll`:
|
100 | * ```html
|
101 | * <!-- If autoscroll present with no expression,
|
102 | * then scroll ui-view into view -->
|
103 | * <ui-view autoscroll/>
|
104 | *
|
105 | * <!-- If autoscroll present with valid expression,
|
106 | * then scroll ui-view into view if expression evaluates to true -->
|
107 | * <ui-view autoscroll='true'/>
|
108 | * <ui-view autoscroll='false'/>
|
109 | * <ui-view autoscroll='scopeVariable'/>
|
110 | * ```
|
111 | *
|
112 | * Resolve data:
|
113 | *
|
114 | * The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this
|
115 | * can be customized using [[Ng1ViewDeclaration.resolveAs]]). This can be then accessed from the template.
|
116 | *
|
117 | * Note that when `controllerAs` is being used, `$resolve` is set on the controller instance *after* the
|
118 | * controller is instantiated. The `$onInit()` hook can be used to perform initialization code which
|
119 | * depends on `$resolve` data.
|
120 | *
|
121 | * #### Example:
|
122 | * ```js
|
123 | * $stateProvider.state('home', {
|
124 | * template: '<my-component user="$resolve.user"></my-component>',
|
125 | * resolve: {
|
126 | * user: function(UserService) { return UserService.fetchUser(); }
|
127 | * }
|
128 | * });
|
129 | * ```
|
130 | */
|
131 | export var uiView;
|
132 | // eslint-disable-next-line prefer-const
|
133 | uiView = [
|
134 | '$view',
|
135 | '$animate',
|
136 | '$uiViewScroll',
|
137 | '$interpolate',
|
138 | '$q',
|
139 | function $ViewDirective($view, $animate, $uiViewScroll, $interpolate, $q) {
|
140 | function getRenderer() {
|
141 | return {
|
142 | enter: function (element, target, cb) {
|
143 | if (angular.version.minor > 2) {
|
144 | $animate.enter(element, null, target).then(cb);
|
145 | }
|
146 | else {
|
147 | $animate.enter(element, null, target, cb);
|
148 | }
|
149 | },
|
150 | leave: function (element, cb) {
|
151 | if (angular.version.minor > 2) {
|
152 | $animate.leave(element).then(cb);
|
153 | }
|
154 | else {
|
155 | $animate.leave(element, cb);
|
156 | }
|
157 | },
|
158 | };
|
159 | }
|
160 | function configsEqual(config1, config2) {
|
161 | return config1 === config2;
|
162 | }
|
163 | var rootData = {
|
164 | $cfg: { viewDecl: { $context: $view._pluginapi._rootViewContext() } },
|
165 | $uiView: {},
|
166 | };
|
167 | var directive = {
|
168 | count: 0,
|
169 | restrict: 'ECA',
|
170 | terminal: true,
|
171 | priority: 400,
|
172 | transclude: 'element',
|
173 | compile: function (tElement, tAttrs, $transclude) {
|
174 | return function (scope, $element, attrs) {
|
175 | var onloadExp = attrs['onload'] || '', autoScrollExp = attrs['autoscroll'], renderer = getRenderer(), inherited = $element.inheritedData('$uiView') || rootData, name = $interpolate(attrs['uiView'] || attrs['name'] || '')(scope) || '$default';
|
176 | var previousEl, currentEl, currentScope, viewConfig;
|
177 | var activeUIView = {
|
178 | $type: 'ng1',
|
179 | id: directive.count++,
|
180 | name: name,
|
181 | fqn: inherited.$uiView.fqn ? inherited.$uiView.fqn + '.' + name : name,
|
182 | config: null,
|
183 | configUpdated: configUpdatedCallback,
|
184 | get creationContext() {
|
185 | // The context in which this ui-view "tag" was created
|
186 | var fromParentTagConfig = parse('$cfg.viewDecl.$context')(inherited);
|
187 | // Allow <ui-view name="foo"><ui-view name="bar"></ui-view></ui-view>
|
188 | // See https://github.com/angular-ui/ui-router/issues/3355
|
189 | var fromParentTag = parse('$uiView.creationContext')(inherited);
|
190 | return fromParentTagConfig || fromParentTag;
|
191 | },
|
192 | };
|
193 | trace.traceUIViewEvent('Linking', activeUIView);
|
194 | function configUpdatedCallback(config) {
|
195 | if (config && !(config instanceof Ng1ViewConfig))
|
196 | return;
|
197 | if (configsEqual(viewConfig, config))
|
198 | return;
|
199 | trace.traceUIViewConfigUpdated(activeUIView, config && config.viewDecl && config.viewDecl.$context);
|
200 | viewConfig = config;
|
201 | updateView(config);
|
202 | }
|
203 | $element.data('$uiView', { $uiView: activeUIView });
|
204 | updateView();
|
205 | var unregister = $view.registerUIView(activeUIView);
|
206 | scope.$on('$destroy', function () {
|
207 | trace.traceUIViewEvent('Destroying/Unregistering', activeUIView);
|
208 | unregister();
|
209 | });
|
210 | function cleanupLastView() {
|
211 | if (previousEl) {
|
212 | trace.traceUIViewEvent('Removing (previous) el', previousEl.data('$uiView'));
|
213 | previousEl.remove();
|
214 | previousEl = null;
|
215 | }
|
216 | if (currentScope) {
|
217 | trace.traceUIViewEvent('Destroying scope', activeUIView);
|
218 | currentScope.$destroy();
|
219 | currentScope = null;
|
220 | }
|
221 | if (currentEl) {
|
222 | var _viewData_1 = currentEl.data('$uiViewAnim');
|
223 | trace.traceUIViewEvent('Animate out', _viewData_1);
|
224 | renderer.leave(currentEl, function () {
|
225 | _viewData_1.$$animLeave.resolve();
|
226 | previousEl = null;
|
227 | });
|
228 | previousEl = currentEl;
|
229 | currentEl = null;
|
230 | }
|
231 | }
|
232 | function updateView(config) {
|
233 | var newScope = scope.$new();
|
234 | var animEnter = $q.defer(), animLeave = $q.defer();
|
235 | var $uiViewData = {
|
236 | $cfg: config,
|
237 | $uiView: activeUIView,
|
238 | };
|
239 | var $uiViewAnim = {
|
240 | $animEnter: animEnter.promise,
|
241 | $animLeave: animLeave.promise,
|
242 | $$animLeave: animLeave,
|
243 | };
|
244 | /**
|
245 | * @ngdoc event
|
246 | * @name ui.router.state.directive:ui-view#$viewContentLoading
|
247 | * @eventOf ui.router.state.directive:ui-view
|
248 | * @eventType emits on ui-view directive scope
|
249 | * @description
|
250 | *
|
251 | * Fired once the view **begins loading**, *before* the DOM is rendered.
|
252 | *
|
253 | * @param {Object} event Event object.
|
254 | * @param {string} viewName Name of the view.
|
255 | */
|
256 | newScope.$emit('$viewContentLoading', name);
|
257 | var cloned = $transclude(newScope, function (clone) {
|
258 | clone.data('$uiViewAnim', $uiViewAnim);
|
259 | clone.data('$uiView', $uiViewData);
|
260 | renderer.enter(clone, $element, function onUIViewEnter() {
|
261 | animEnter.resolve();
|
262 | if (currentScope)
|
263 | currentScope.$emit('$viewContentAnimationEnded');
|
264 | if ((isDefined(autoScrollExp) && !autoScrollExp) || scope.$eval(autoScrollExp)) {
|
265 | $uiViewScroll(clone);
|
266 | }
|
267 | });
|
268 | cleanupLastView();
|
269 | });
|
270 | currentEl = cloned;
|
271 | currentScope = newScope;
|
272 | /**
|
273 | * @ngdoc event
|
274 | * @name ui.router.state.directive:ui-view#$viewContentLoaded
|
275 | * @eventOf ui.router.state.directive:ui-view
|
276 | * @eventType emits on ui-view directive scope
|
277 | * @description *
|
278 | * Fired once the view is **loaded**, *after* the DOM is rendered.
|
279 | *
|
280 | * @param {Object} event Event object.
|
281 | */
|
282 | currentScope.$emit('$viewContentLoaded', config || viewConfig);
|
283 | currentScope.$eval(onloadExp);
|
284 | }
|
285 | };
|
286 | },
|
287 | };
|
288 | return directive;
|
289 | },
|
290 | ];
|
291 | $ViewDirectiveFill.$inject = ['$compile', '$controller', '$transitions', '$view', '$q'];
|
292 | /** @hidden */
|
293 | function $ViewDirectiveFill($compile, $controller, $transitions, $view, $q) {
|
294 | var getControllerAs = parse('viewDecl.controllerAs');
|
295 | var getResolveAs = parse('viewDecl.resolveAs');
|
296 | return {
|
297 | restrict: 'ECA',
|
298 | priority: -400,
|
299 | compile: function (tElement) {
|
300 | var initial = tElement.html();
|
301 | tElement.empty();
|
302 | return function (scope, $element) {
|
303 | var data = $element.data('$uiView');
|
304 | if (!data) {
|
305 | $element.html(initial);
|
306 | $compile($element.contents())(scope);
|
307 | return;
|
308 | }
|
309 | var cfg = data.$cfg || { viewDecl: {}, getTemplate: noop };
|
310 | var resolveCtx = cfg.path && new ResolveContext(cfg.path);
|
311 | $element.html(cfg.getTemplate($element, resolveCtx) || initial);
|
312 | trace.traceUIViewFill(data.$uiView, $element.html());
|
313 | var link = $compile($element.contents());
|
314 | var controller = cfg.controller;
|
315 | var controllerAs = getControllerAs(cfg);
|
316 | var resolveAs = getResolveAs(cfg);
|
317 | var locals = resolveCtx && getLocals(resolveCtx);
|
318 | scope[resolveAs] = locals;
|
319 | if (controller) {
|
320 | var controllerInstance = ($controller(controller, extend({}, locals, { $scope: scope, $element: $element })));
|
321 | if (controllerAs) {
|
322 | scope[controllerAs] = controllerInstance;
|
323 | scope[controllerAs][resolveAs] = locals;
|
324 | }
|
325 | // TODO: Use $view service as a central point for registering component-level hooks
|
326 | // Then, when a component is created, tell the $view service, so it can invoke hooks
|
327 | // $view.componentLoaded(controllerInstance, { $scope: scope, $element: $element });
|
328 | // scope.$on('$destroy', () => $view.componentUnloaded(controllerInstance, { $scope: scope, $element: $element }));
|
329 | $element.data('$ngControllerController', controllerInstance);
|
330 | $element.children().data('$ngControllerController', controllerInstance);
|
331 | registerControllerCallbacks($q, $transitions, controllerInstance, scope, cfg);
|
332 | }
|
333 | // Wait for the component to appear in the DOM
|
334 | if (isString(cfg.component)) {
|
335 | var kebobName = kebobString(cfg.component);
|
336 | var tagRegexp_1 = new RegExp("^(x-|data-)?" + kebobName + "$", 'i');
|
337 | var getComponentController = function () {
|
338 | var directiveEl = [].slice
|
339 | .call($element[0].children)
|
340 | .filter(function (el) { return el && el.tagName && tagRegexp_1.exec(el.tagName); });
|
341 | return directiveEl && angular.element(directiveEl).data("$" + cfg.component + "Controller");
|
342 | };
|
343 | var deregisterWatch_1 = scope.$watch(getComponentController, function (ctrlInstance) {
|
344 | if (!ctrlInstance)
|
345 | return;
|
346 | registerControllerCallbacks($q, $transitions, ctrlInstance, scope, cfg);
|
347 | deregisterWatch_1();
|
348 | });
|
349 | }
|
350 | link(scope);
|
351 | };
|
352 | },
|
353 | };
|
354 | }
|
355 | /** @hidden */
|
356 | var hasComponentImpl = typeof angular.module('ui.router')['component'] === 'function';
|
357 | /** @hidden incrementing id */
|
358 | var _uiCanExitId = 0;
|
359 | /** @hidden TODO: move these callbacks to $view and/or `/hooks/components.ts` or something */
|
360 | function registerControllerCallbacks($q, $transitions, controllerInstance, $scope, cfg) {
|
361 | // Call $onInit() ASAP
|
362 | if (isFunction(controllerInstance.$onInit) &&
|
363 | !((cfg.viewDecl.component || cfg.viewDecl.componentProvider) && hasComponentImpl)) {
|
364 | controllerInstance.$onInit();
|
365 | }
|
366 | var viewState = tail(cfg.path).state.self;
|
367 | var hookOptions = { bind: controllerInstance };
|
368 | // Add component-level hook for onUiParamsChanged
|
369 | if (isFunction(controllerInstance.uiOnParamsChanged)) {
|
370 | var resolveContext = new ResolveContext(cfg.path);
|
371 | var viewCreationTrans_1 = resolveContext.getResolvable('$transition$').data;
|
372 | // Fire callback on any successful transition
|
373 | var paramsUpdated = function ($transition$) {
|
374 | // Exit early if the $transition$ is the same as the view was created within.
|
375 | // Exit early if the $transition$ will exit the state the view is for.
|
376 | if ($transition$ === viewCreationTrans_1 || $transition$.exiting().indexOf(viewState) !== -1)
|
377 | return;
|
378 | var toParams = $transition$.params('to');
|
379 | var fromParams = $transition$.params('from');
|
380 | var getNodeSchema = function (node) { return node.paramSchema; };
|
381 | var toSchema = $transition$.treeChanges('to').map(getNodeSchema).reduce(unnestR, []);
|
382 | var fromSchema = $transition$.treeChanges('from').map(getNodeSchema).reduce(unnestR, []);
|
383 | // Find the to params that have different values than the from params
|
384 | var changedToParams = toSchema.filter(function (param) {
|
385 | var idx = fromSchema.indexOf(param);
|
386 | return idx === -1 || !fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id]);
|
387 | });
|
388 | // Only trigger callback if a to param has changed or is new
|
389 | if (changedToParams.length) {
|
390 | var changedKeys_1 = changedToParams.map(function (x) { return x.id; });
|
391 | // Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
|
392 | var newValues = filter(toParams, function (val, key) { return changedKeys_1.indexOf(key) !== -1; });
|
393 | controllerInstance.uiOnParamsChanged(newValues, $transition$);
|
394 | }
|
395 | };
|
396 | $scope.$on('$destroy', $transitions.onSuccess({}, paramsUpdated, hookOptions));
|
397 | }
|
398 | // Add component-level hook for uiCanExit
|
399 | if (isFunction(controllerInstance.uiCanExit)) {
|
400 | var id_1 = _uiCanExitId++;
|
401 | var cacheProp_1 = '_uiCanExitIds';
|
402 | // Returns true if a redirect transition already answered truthy
|
403 | var prevTruthyAnswer_1 = function (trans) {
|
404 | return !!trans && ((trans[cacheProp_1] && trans[cacheProp_1][id_1] === true) || prevTruthyAnswer_1(trans.redirectedFrom()));
|
405 | };
|
406 | // If a user answered yes, but the transition was later redirected, don't also ask for the new redirect transition
|
407 | var wrappedHook = function (trans) {
|
408 | var promise;
|
409 | var ids = (trans[cacheProp_1] = trans[cacheProp_1] || {});
|
410 | if (!prevTruthyAnswer_1(trans)) {
|
411 | promise = $q.when(controllerInstance.uiCanExit(trans));
|
412 | promise.then(function (val) { return (ids[id_1] = val !== false); });
|
413 | }
|
414 | return promise;
|
415 | };
|
416 | var criteria = { exiting: viewState.name };
|
417 | $scope.$on('$destroy', $transitions.onBefore(criteria, wrappedHook, hookOptions));
|
418 | }
|
419 | }
|
420 | angular.module('ui.router.state').directive('uiView', uiView);
|
421 | angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill);
|
422 | //# sourceMappingURL=viewDirective.js.map |
\ | No newline at end of file |