
package.src.router.directives.view-directive.js Maven / Gradle / Ivy
import { filter, tail, unnestR } from "../../shared/common";
import { isDefined, isFunction, isString } from "../../shared/utils";
import { kebobString } from "../../shared/strings";
import { parse } from "../../shared/hof";
import { ResolveContext } from "../resolve/resolve-context";
import { trace } from "../common/trace";
import { Ng1ViewConfig } from "../state/views";
import { JQLite } from "../../shared/jqlite/jqlite";
import { getLocals } from "../state/state-registry";
/**
* `ui-view`: A viewport directive which is filled in by a view from the active state.
*
* ### Attributes
*
* - `name`: (Optional) A view name.
* The name should be unique amongst the other views in the same state.
* You can have views of the same name that live in different states.
* The ui-view can be targeted in a View using the name ([[Ng1StateDeclaration.views]]).
*
* - `autoscroll`: an expression. When it evaluates to true, the `ui-view` will be scrolled into view when it is activated.
* Uses [[$ngViewScroll]] to do the scrolling.
*
* - `onload`: Expression to evaluate whenever the view updates.
*
* #### Example:
* A view can be unnamed or named.
* ```html
*
*
*
*
*
*
*
*
* ```
*
* You can only have one unnamed view within any template (or root html). If you are only using a
* single view and it is unnamed then you can populate it like so:
*
* ```html
*
* $stateProvider.state("home", {
* template: "HELLO!
"
* })
* ```
*
* The above is a convenient shortcut equivalent to specifying your view explicitly with the
* [[Ng1StateDeclaration.views]] config property, by name, in this case an empty name:
*
* ```js
* $stateProvider.state("home", {
* views: {
* "": {
* template: "HELLO!
"
* }
* }
* })
* ```
*
* But typically you'll only use the views property if you name your view or have more than one view
* in the same template. There's not really a compelling reason to name a view if its the only one,
* but you could if you wanted, like so:
*
* ```html
*
* ```
*
* ```js
* $stateProvider.state("home", {
* views: {
* "main": {
* template: "HELLO!
"
* }
* }
* })
* ```
*
* Really though, you'll use views to set up multiple views:
*
* ```html
*
*
*
* ```
*
* ```js
* $stateProvider.state("home", {
* views: {
* "": {
* template: "HELLO!
"
* },
* "chart": {
* template: " "
* },
* "data": {
* template: " "
* }
* }
* })
* ```
*
* #### Examples for `autoscroll`:
* ```html
*
*
*
*
*
*
*
* ```
*
* Resolve data:
*
* The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this
* can be customized using [[Ng1ViewDeclaration.resolveAs]]). This can be then accessed from the template.
*
* Note that when `controllerAs` is being used, `$resolve` is set on the controller instance *after* the
* controller is instantiated. The `$onInit()` hook can be used to perform initialization code which
* depends on `$resolve` data.
*
* #### Example:
* ```js
* $stateProvider.state('home', {
* template: ' ',
* resolve: {
* user: function(UserService) { return UserService.fetchUser(); }
* }
* });
* ```
*/
export let ngView = [
"$view",
"$animate",
"$ngViewScroll",
"$interpolate",
function $ViewDirective($view, $animate, $ngViewScroll, $interpolate) {
function getRenderer() {
return {
enter: function (element, target, cb) {
$animate.enter(element, null, target).then(cb);
},
leave: function (element, cb) {
$animate.leave(element).then(cb);
},
};
}
function configsEqual(config1, config2) {
return config1 === config2;
}
const rootData = {
$cfg: { viewDecl: { $context: $view._pluginapi._rootViewContext() } },
$ngView: {},
};
const directive = {
count: 0,
restrict: "EA",
terminal: true,
priority: 400,
transclude: "element",
compile: function (tElement, tAttrs, $transclude) {
return function (scope, $element, attrs) {
const onloadExp = attrs["onload"] || "",
autoScrollExp = attrs["autoscroll"],
renderer = getRenderer(),
inherited = $element.inheritedData("$ngView") || rootData,
name =
$interpolate(attrs["ngView"] || attrs["name"] || "")(scope) ||
"$default";
let previousEl, currentEl, currentScope, viewConfig;
const activeUIView = {
$type: "ng1",
id: directive.count++, // Global sequential ID for ui-view tags added to DOM
name: name, // ui-view name (
fqn: inherited.$ngView.fqn
? inherited.$ngView.fqn + "." + name
: name, // fully qualified name, describes location in DOM
config: null, // The ViewConfig loaded (from a state.views definition)
configUpdated: configUpdatedCallback, // Called when the matching ViewConfig changes
get creationContext() {
// The context in which this ui-view "tag" was created
const fromParentTagConfig = parse("$cfg.viewDecl.$context")(
inherited,
);
// Allow
// See https://github.com/angular-ui/ui-router/issues/3355
const fromParentTag = parse("$ngView.creationContext")(inherited);
return fromParentTagConfig || fromParentTag;
},
};
trace.traceUIViewEvent("Linking", activeUIView);
function configUpdatedCallback(config) {
if (config && !(config instanceof Ng1ViewConfig)) return;
if (configsEqual(viewConfig, config)) return;
trace.traceUIViewConfigUpdated(
activeUIView,
config && config.viewDecl && config.viewDecl.$context,
);
viewConfig = config;
updateView(config);
}
$element.data("$ngView", { $ngView: activeUIView });
updateView();
const unregister = $view.registerUIView(activeUIView);
scope.$on("$destroy", function () {
trace.traceUIViewEvent("Destroying/Unregistering", activeUIView);
unregister();
});
function cleanupLastView() {
if (previousEl) {
trace.traceUIViewEvent(
"Removing (previous) el",
previousEl.data("$ngView"),
);
previousEl.remove();
previousEl = null;
}
if (currentScope) {
trace.traceUIViewEvent("Destroying scope", activeUIView);
currentScope.$destroy();
currentScope = null;
}
if (currentEl) {
const _viewData = currentEl.data("$ngViewAnim");
trace.traceUIViewEvent("Animate out", _viewData);
renderer.leave(currentEl, function () {
_viewData.$$animLeave.resolve();
previousEl = null;
});
previousEl = currentEl;
currentEl = null;
}
}
function updateView(config) {
const newScope = scope.$new();
const animEnter = $q.defer(),
animLeave = $q.defer();
const $ngViewData = {
$cfg: config,
$ngView: activeUIView,
};
const $ngViewAnim = {
$animEnter: animEnter.promise,
$animLeave: animLeave.promise,
$$animLeave: animLeave,
};
/**
* Fired once the view **begins loading**, *before* the DOM is rendered.
*
* @param {Object} event Event object.
* @param {string} viewName Name of the view.
*/
newScope.$emit("$viewContentLoading", name);
const cloned = $transclude(newScope, function (clone) {
clone.data("$ngViewAnim", $ngViewAnim);
clone.data("$ngView", $ngViewData);
renderer.enter(clone, $element, function () {
animEnter.resolve();
if (currentScope)
currentScope.$emit("$viewContentAnimationEnded");
if (
(isDefined(autoScrollExp) && !autoScrollExp) ||
scope.$eval(autoScrollExp)
) {
$ngViewScroll(clone);
}
});
cleanupLastView();
});
currentEl = cloned;
currentScope = newScope;
/**
* Fired once the view is **loaded**, *after* the DOM is rendered.
*
* @param {Object} event Event object.
*/
currentScope.$emit("$viewContentLoaded", config || viewConfig);
currentScope.$eval(onloadExp);
}
};
},
};
return directive;
},
];
$ViewDirectiveFill.$inject = [
"$compile",
"$controller",
"$transitions",
"$view",
];
export function $ViewDirectiveFill($compile, $controller, $transitions, $view) {
const getControllerAs = parse("viewDecl.controllerAs");
const getResolveAs = parse("viewDecl.resolveAs");
return {
restrict: "EA",
priority: -400,
compile: function (tElement) {
const initial = tElement.html();
tElement.empty();
return function (scope, $element) {
const data = $element.data("$ngView");
if (!data) {
$element.html(initial);
$compile($element[0].contentDocument || $element[0].childNodes)(
scope,
);
return;
}
const cfg = data.$cfg || { viewDecl: {}, getTemplate: () => {} };
const resolveCtx = cfg.path && new ResolveContext(cfg.path);
$element.html(cfg.getTemplate($element, resolveCtx) || initial);
trace.traceUIViewFill(data.$ngView, $element.html());
const link = $compile(
$element[0].contentDocument || $element[0].childNodes,
);
const controller = cfg.controller;
const controllerAs = getControllerAs(cfg);
const resolveAs = getResolveAs(cfg);
const locals = resolveCtx && getLocals(resolveCtx);
scope[resolveAs] = locals;
if (controller) {
const controllerInstance = $controller(
controller,
Object.assign({}, locals, { $scope: scope, $element: $element }),
);
if (controllerAs) {
scope[controllerAs] = controllerInstance;
scope[controllerAs][resolveAs] = locals;
}
// TODO: Use $view service as a central point for registering component-level hooks
// Then, when a component is created, tell the $view service, so it can invoke hooks
// $view.componentLoaded(controllerInstance, { $scope: scope, $element: $element });
// scope.$on('$destroy', () => $view.componentUnloaded(controllerInstance, { $scope: scope, $element: $element }));
$element.data("$ngControllerController", controllerInstance);
$element
.children()
.data("$ngControllerController", controllerInstance);
registerControllerCallbacks(
$q,
$transitions,
controllerInstance,
scope,
cfg,
);
}
// Wait for the component to appear in the DOM
if (isString(cfg.component)) {
const kebobName = kebobString(cfg.component);
const tagRegexp = new RegExp(`^(x-|data-)?${kebobName}$`, "i");
const getComponentController = () => {
const directiveEl = [].slice
.call($element[0].children)
.filter((el) => el && el.tagName && tagRegexp.exec(el.tagName));
return (
directiveEl &&
JQLite(directiveEl).data(`$${cfg.component}Controller`)
);
};
const deregisterWatch = scope.$watch(
getComponentController,
function (ctrlInstance) {
if (!ctrlInstance) return;
registerControllerCallbacks(
$transitions,
ctrlInstance,
scope,
cfg,
);
deregisterWatch();
},
);
}
link(scope);
};
},
};
}
/** @ignore */
/** @ignore incrementing id */
let _uiCanExitId = 0;
/** @ignore TODO: move these callbacks to $view and/or `/hooks/components.ts` or something */
function registerControllerCallbacks(
$transitions,
controllerInstance,
$scope,
cfg,
) {
// Call $onInit() ASAP
if (
isFunction(controllerInstance.$onInit) &&
!(cfg.viewDecl.component || cfg.viewDecl.componentProvider)
) {
controllerInstance.$onInit();
}
const viewState = tail(cfg.path).state.self;
const hookOptions = { bind: controllerInstance };
// Add component-level hook for onUiParamsChanged
if (isFunction(controllerInstance.uiOnParamsChanged)) {
const resolveContext = new ResolveContext(cfg.path);
const viewCreationTrans = resolveContext.getResolvable("$transition$").data;
// Fire callback on any successful transition
const paramsUpdated = ($transition$) => {
// Exit early if the $transition$ is the same as the view was created within.
// Exit early if the $transition$ will exit the state the view is for.
if (
$transition$ === viewCreationTrans ||
$transition$.exiting().indexOf(viewState) !== -1
)
return;
const toParams = $transition$.params("to");
const fromParams = $transition$.params("from");
const getNodeSchema = (node) => node.paramSchema;
const toSchema = $transition$
.treeChanges("to")
.map(getNodeSchema)
.reduce(unnestR, []);
const fromSchema = $transition$
.treeChanges("from")
.map(getNodeSchema)
.reduce(unnestR, []);
// Find the to params that have different values than the from params
const changedToParams = toSchema.filter((param) => {
const idx = fromSchema.indexOf(param);
return (
idx === -1 ||
!fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id])
);
});
// Only trigger callback if a to param has changed or is new
if (changedToParams.length) {
const changedKeys = changedToParams.map((x) => x.id);
// Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
const newValues = filter(
toParams,
(val, key) => changedKeys.indexOf(key) !== -1,
);
controllerInstance.uiOnParamsChanged(newValues, $transition$);
}
};
$scope.$on(
"$destroy",
$transitions.onSuccess({}, paramsUpdated, hookOptions),
);
}
// Add component-level hook for uiCanExit
if (isFunction(controllerInstance.uiCanExit)) {
const id = _uiCanExitId++;
const cacheProp = "_uiCanExitIds";
// Returns true if a redirect transition already answered truthy
const prevTruthyAnswer = (trans) =>
!!trans &&
((trans[cacheProp] && trans[cacheProp][id] === true) ||
prevTruthyAnswer(trans.redirectedFrom()));
// If a user answered yes, but the transition was later redirected, don't also ask for the new redirect transition
const wrappedHook = (trans) => {
let promise;
const ids = (trans[cacheProp] = trans[cacheProp] || {});
if (!prevTruthyAnswer(trans)) {
promise = Promise.resolve(controllerInstance.uiCanExit(trans));
promise.then((val) => (ids[id] = val !== false));
}
return promise;
};
const criteria = { exiting: viewState.name };
$scope.$on(
"$destroy",
$transitions.onBefore(criteria, wrappedHook, hookOptions),
);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy