Skip to content

Commit 168e497

Browse files
committed
Fix view directive and expand scroll service to accept elements
1 parent c17ebea commit 168e497

9 files changed

Lines changed: 170 additions & 117 deletions

File tree

@types/router/state/views.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class ViewConfig {
4747
viewDecl: import("./interface.ts").ViewDeclaration,
4848
factory: import("../template-factory.js").TemplateFactoryProvider,
4949
);
50+
$id: number;
5051
/**
5152
* @type {Array<import('../path/path-node.js').PathNode>}
5253
*/
@@ -67,10 +68,14 @@ export class ViewConfig {
6768
* @type {string | undefined}
6869
*/
6970
template: string | undefined;
70-
/** @type {Number} */ $id: number;
71+
/** @type {boolean} */
7172
loaded: boolean;
7273
getTemplate: (ngView: any, context: ResolveContext) => string;
73-
load(): Promise<this>;
74+
/**
75+
*
76+
* @returns {Promise<ViewConfig>}
77+
*/
78+
load(): Promise<ViewConfig>;
7479
controller: any;
7580
/**
7681
* Gets the controller for a view configuration.

@types/router/view/interface.d.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { ViewDeclaration } from "../state/interface.ts";
2-
import { PathNode } from "../path/path-node.js";
1+
import { ViewConfig } from "../state/views.js";
32
/** The context ref can be anything that has a `name` and a `parent` reference to another IContextRef */
43
export interface ViewContext {
54
name: string;
@@ -13,35 +12,12 @@ export interface ActiveUIView {
1312
/** The ng-view's fully qualified name */
1413
fqn: string;
1514
/** The ViewConfig that is currently loaded into the ng-view */
16-
config: ViewConfig;
15+
config: ViewConfig | null;
1716
/** The state context in which the ng-view tag was created. */
1817
creationContext: ViewContext;
1918
/** A callback that should apply a ViewConfig (or clear the ng-view, if config is undefined) */
2019
configUpdated: (config: ViewConfig) => void;
2120
}
22-
/**
23-
* This interface represents a [[_ViewDeclaration]] that is bound to a [[PathNode]].
24-
*
25-
* A `ViewConfig` is the runtime definition of a single view.
26-
*
27-
* During a transition, `ViewConfig`s are created for each [[_ViewDeclaration]] defined on each "entering" [[StateObject]].
28-
* Then, the [[ViewService]] finds any matching `ng-view`(s) in the DOM, and supplies the ng-view
29-
* with the `ViewConfig`. The `ng-view` then loads itself using the information found in the `ViewConfig`.
30-
*
31-
* A `ViewConfig` if matched with a `ng-view` by finding all `ng-view`s which were created in the
32-
* context named by the `ngVIewContextAnchor`, and finding the `ng-view` or child `ng-view` that matches
33-
* the `ngVIewName` address.
34-
*/
35-
export interface ViewConfig {
36-
$id: number;
37-
/** The normalized view declaration from [[State.views]] */
38-
viewDecl: ViewDeclaration;
39-
/** The node the ViewConfig is bound to */
40-
path: PathNode[];
41-
loaded: boolean;
42-
/** Fetches templates, runs dynamic (controller|template)Provider code, lazy loads Components, etc */
43-
load(): Promise<ViewConfig>;
44-
}
4521
export interface ViewTuple {
4622
ngView: ActiveUIView | undefined;
4723
viewConfig: ViewConfig;

@types/services/anchor-scroll/interface.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface AnchorScrollService {
22
/**
33
* Invoke anchor scrolling.
44
*/
5-
(hash?: string | number): void;
5+
(hashOrElement?: string | number | HTMLElement): void;
66
/**
77
* Vertical scroll offset.
88
* Can be a number, a function returning a number,

src/router/directives/view-directive.js

Lines changed: 103 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { filter, tail, unnestR } from "../../shared/common.js";
1+
import { tail, unnestR } from "../../shared/common.js";
22
import { hasAnimate, isDefined, isFunction } from "../../shared/utils.js";
33
import { parse } from "../../shared/hof.js";
44
import { ResolveContext } from "../resolve/resolve-context.js";
@@ -210,12 +210,12 @@ export function $ViewDirective($view, $animate, $anchorScroll, $interpolate) {
210210
)(scope) || "$default";
211211

212212
/**
213-
* @type {HTMLElement}
213+
* @type {HTMLElement | null}
214214
*/
215215
let previousEl;
216216

217217
/**
218-
* @type {HTMLElement}
218+
* @type {HTMLElement | null}
219219
*/
220220
let currentEl;
221221

@@ -229,6 +229,7 @@ export function $ViewDirective($view, $animate, $anchorScroll, $interpolate) {
229229
*/
230230
let viewConfig;
231231

232+
/** @type {import("../view/interface.ts").ActiveUIView} */
232233
const activeUIView = {
233234
id: /** @type {number} */ (directive.count)++, // Global sequential ID for ng-view tags added to DOM
234235
name, // ng-view name (<div ng-view="name"></div>
@@ -333,24 +334,39 @@ export function $ViewDirective($view, $animate, $anchorScroll, $interpolate) {
333334
* @param {string} viewName Name of the view.
334335
*/
335336
newScope.$emit("$viewContentLoading", name);
336-
currentEl = $transclude(newScope, function (clone) {
337-
setCacheData(clone, "$ngViewAnim", $ngViewAnim);
338-
setCacheData(clone, "$ngView", $ngViewData);
339-
renderer.enter(clone, $element, function () {
340-
animEnter.resolve();
341-
342-
if (currentScope)
343-
currentScope.$emit("$viewContentAnimationEnded");
344-
345-
if (
346-
(isDefined(autoScrollExp) && !autoScrollExp) ||
347-
(autoScrollExp && scope.$eval(autoScrollExp))
348-
) {
349-
/** @type {ng.AnchorScrollService} */ ($anchorScroll)(clone);
350-
}
351-
});
352-
cleanupLastView();
353-
});
337+
currentEl = /** @type {HTMLElement} */ (
338+
/** @type {ng.TranscludeFn} */ ($transclude)(newScope, (clone) => {
339+
setCacheData(
340+
/** @type {HTMLElement} */ (clone),
341+
"$ngViewAnim",
342+
$ngViewAnim,
343+
);
344+
setCacheData(
345+
/** @type {HTMLElement} */ (clone),
346+
"$ngView",
347+
$ngViewData,
348+
);
349+
renderer.enter(
350+
/** @type {HTMLElement} */ (clone),
351+
$element,
352+
() => {
353+
animEnter.resolve(undefined);
354+
355+
if (currentScope)
356+
currentScope.$emit("$viewContentAnimationEnded");
357+
358+
if (
359+
(isDefined(autoScrollExp) && !autoScrollExp) ||
360+
(autoScrollExp && scope.$eval(autoScrollExp))
361+
) {
362+
$anchorScroll(/** @type {HTMLElement} */ (clone));
363+
}
364+
},
365+
);
366+
cleanupLastView();
367+
})
368+
);
369+
354370
currentScope = newScope;
355371
/**
356372
* Fired once the view is **loaded**, *after* the DOM is rendered.
@@ -497,9 +513,9 @@ let _uiCanExitId = 0;
497513
/**
498514
* @ignore TODO: move these callbacks to $view and/or `/hooks/components.ts` or something
499515
* @param {ng.TransitionService} $transitions
500-
* @param {object | (() => object)} controllerInstance
516+
* @param {any} controllerInstance
501517
* @param {ng.Scope} $scope
502-
* @param {{ viewDecl: { component: any; componentProvider: any; }; path: string | any[]; }} cfg
518+
* @param {{ viewDecl: { component: any; componentProvider: any; }; path: import("../transition/transition.js").PathNode[]; }} cfg
503519
*/
504520
function registerControllerCallbacks(
505521
$transitions,
@@ -525,50 +541,79 @@ function registerControllerCallbacks(
525541
const viewCreationTrans = resolveContext.getResolvable("$transition$").data;
526542

527543
// Fire callback on any successful transition
528-
const paramsUpdated = ($transition$) => {
544+
const paramsUpdated = (
545+
/** @type {ng.Transition | undefined} */ $transition$,
546+
) => {
547+
if (!$transition$) return;
548+
529549
// Exit early if the $transition$ is the same as the view was created within.
530550
// Exit early if the $transition$ will exit the state the view is for.
531551
if (
532552
$transition$ === viewCreationTrans ||
533553
$transition$.exiting().indexOf(viewState) !== -1
534-
)
554+
) {
535555
return;
556+
}
557+
536558
const toParams = $transition$.params("to");
537559

538560
const fromParams = $transition$.params("from");
539561

540-
const getNodeSchema = (node) => node.paramSchema;
562+
const getNodeSchema = (/** @type {{ paramSchema: any }} */ node) =>
563+
node.paramSchema;
541564

542-
const toSchema = $transition$
543-
.treeChanges("to")
544-
.map(getNodeSchema)
545-
.reduce(unnestR, []);
565+
const treeChanges = $transition$ && $transition$.treeChanges;
546566

547-
const fromSchema = $transition$
548-
.treeChanges("from")
549-
.map(getNodeSchema)
550-
.reduce(unnestR, []);
567+
const toNodes = isFunction(treeChanges)
568+
? (treeChanges.call($transition$, "to") ?? [])
569+
: [];
551570

552-
// Find the to params that have different values than the from params
553-
const changedToParams = toSchema.filter((param) => {
554-
const idx = fromSchema.indexOf(param);
571+
const fromNodes = isFunction(treeChanges)
572+
? (treeChanges.call($transition$, "from") ?? [])
573+
: [];
555574

556-
return (
557-
idx === -1 ||
558-
!fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id])
559-
);
560-
});
575+
const toSchema =
576+
/** @type {import("../transition/transition.js").PathNode[]} */ (
577+
toNodes
578+
)
579+
.map(getNodeSchema)
580+
.reduce(unnestR, []);
581+
582+
const fromSchema =
583+
/** @type {import("../transition/transition.js").PathNode[]} */ (
584+
fromNodes
585+
)
586+
.map(getNodeSchema)
587+
.reduce(unnestR, []);
588+
589+
// Find the to params that have different values than the from params
590+
const changedToParams = toSchema.filter(
591+
(/** @type {{ id: string | number; }} */ param) => {
592+
const idx = fromSchema.indexOf(param);
593+
594+
return (
595+
idx === -1 ||
596+
!fromSchema[idx].type.equals(
597+
toParams[param.id],
598+
fromParams[param.id],
599+
)
600+
);
601+
},
602+
);
561603

562604
// Only trigger callback if a to param has changed or is new
563605
if (changedToParams.length) {
564-
const changedKeys = changedToParams.map((x) => x.id);
606+
const changedKeys = /** @type {any[]} */ (
607+
changedToParams.map((/** @type {{ id: any; }} */ x) => x.id)
608+
);
565609

566610
// Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
567-
const newValues = filter(
568-
toParams,
569-
(val, key) => changedKeys.indexOf(key) !== -1,
570-
);
611+
/** @type {Record<string, any>} */
612+
const newValues = {};
571613

614+
changedKeys.forEach((key) => {
615+
if (key in toParams) newValues[key] = toParams[key];
616+
});
572617
controllerInstance.uiOnParamsChanged(newValues, $transition$);
573618
}
574619
};
@@ -585,14 +630,22 @@ function registerControllerCallbacks(
585630

586631
const cacheProp = "_uiCanExitIds";
587632

588-
// Returns true if a redirect transition already answered truthy
589-
const prevTruthyAnswer = (trans) =>
633+
/**
634+
* Returns true if any transition in the redirect chain already answered truthy
635+
* @param {import("../transition/transition.js").Transition | null | undefined} trans
636+
* @returns {boolean}
637+
*/
638+
const prevTruthyAnswer = (
639+
/** @type {ng.Transition & Record<String, any>} */ trans,
640+
) =>
590641
!!trans &&
591642
((trans[cacheProp] && trans[cacheProp][id] === true) ||
592643
prevTruthyAnswer(trans.redirectedFrom()));
593644

594645
// If a user answered yes, but the transition was later redirected, don't also ask for the new redirect transition
595-
const wrappedHook = (trans) => {
646+
const wrappedHook = (
647+
/** @type {ng.Transition & Record<String, any>} */ trans,
648+
) => {
596649
let promise;
597650

598651
const ids = (trans[cacheProp] = trans[cacheProp] || {});

src/router/state/views.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export class ViewConfig {
125125
* @param {import('../template-factory.js').TemplateFactoryProvider} factory
126126
*/
127127
constructor(path, viewDecl, factory) {
128+
this.$id = -1;
128129
/**
129130
* @type {Array<import('../path/path-node.js').PathNode>}
130131
*/
@@ -147,7 +148,10 @@ export class ViewConfig {
147148
*/
148149
this.template = undefined;
149150

150-
/** @type {Number} */ this.$id = id++;
151+
/** @type {Number} */
152+
this.$id = id++;
153+
154+
/** @type {boolean} */
151155
this.loaded = false;
152156
this.getTemplate = (
153157
/** @type {any} */ ngView,
@@ -163,6 +167,10 @@ export class ViewConfig {
163167
: this.template;
164168
}
165169

170+
/**
171+
*
172+
* @returns {Promise<ViewConfig>}
173+
*/
166174
async load() {
167175
const context = new ResolveContext(this.path);
168176

src/router/view/interface.ts

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { ViewDeclaration } from "../state/interface.ts";
2-
import { PathNode } from "../path/path-node.js";
1+
import { ViewConfig } from "../state/views.js";
32

43
/** The context ref can be anything that has a `name` and a `parent` reference to another IContextRef */
54
export interface ViewContext {
@@ -15,41 +14,13 @@ export interface ActiveUIView {
1514
/** The ng-view's fully qualified name */
1615
fqn: string;
1716
/** The ViewConfig that is currently loaded into the ng-view */
18-
config: ViewConfig;
17+
config: ViewConfig | null;
1918
/** The state context in which the ng-view tag was created. */
2019
creationContext: ViewContext;
2120
/** A callback that should apply a ViewConfig (or clear the ng-view, if config is undefined) */
2221
configUpdated: (config: ViewConfig) => void;
2322
}
2423

25-
/**
26-
* This interface represents a [[_ViewDeclaration]] that is bound to a [[PathNode]].
27-
*
28-
* A `ViewConfig` is the runtime definition of a single view.
29-
*
30-
* During a transition, `ViewConfig`s are created for each [[_ViewDeclaration]] defined on each "entering" [[StateObject]].
31-
* Then, the [[ViewService]] finds any matching `ng-view`(s) in the DOM, and supplies the ng-view
32-
* with the `ViewConfig`. The `ng-view` then loads itself using the information found in the `ViewConfig`.
33-
*
34-
* A `ViewConfig` if matched with a `ng-view` by finding all `ng-view`s which were created in the
35-
* context named by the `ngVIewContextAnchor`, and finding the `ng-view` or child `ng-view` that matches
36-
* the `ngVIewName` address.
37-
*/
38-
export interface ViewConfig {
39-
/* The unique id for the ViewConfig instance */
40-
$id: number;
41-
/** The normalized view declaration from [[State.views]] */
42-
viewDecl: ViewDeclaration;
43-
44-
/** The node the ViewConfig is bound to */
45-
path: PathNode[];
46-
47-
loaded: boolean;
48-
49-
/** Fetches templates, runs dynamic (controller|template)Provider code, lazy loads Components, etc */
50-
load(): Promise<ViewConfig>;
51-
}
52-
5324
// A uiView and its matching viewConfig
5425
export interface ViewTuple {
5526
ngView: ActiveUIView | undefined;

0 commit comments

Comments
 (0)