UNPKG

19.3 kBJavaScriptView Raw
1/** @publicapi @module directives */ /** */
2import { extend, filter, isDefined, isFunction, isString, kebobString, noop, parse, ResolveContext, tail, trace, unnestR, } from '@uirouter/core';
3import { ng as angular } from '../angular';
4import { getLocals } from '../services';
5import { 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 */
131export var uiView;
132// eslint-disable-next-line prefer-const
133uiView = [
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 */
293function $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 */
356var hasComponentImpl = typeof angular.module('ui.router')['component'] === 'function';
357/** @hidden incrementing id */
358var _uiCanExitId = 0;
359/** @hidden TODO: move these callbacks to $view and/or `/hooks/components.ts` or something */
360function 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}
420angular.module('ui.router.state').directive('uiView', uiView);
421angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill);
422//# sourceMappingURL=viewDirective.js.map
\No newline at end of file