Skip to content

Commit 85488dc

Browse files
committed
Router optimizations
1 parent e70fe4a commit 85488dc

23 files changed

Lines changed: 629 additions & 960 deletions

src/router/directives/state-directives.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { removeFrom } from "../../shared/common.ts";
1111
import {
1212
assign,
1313
arrayFrom,
14-
entries,
1514
isArray,
1615
isNullOrUndefined,
1716
isObject,
1817
isString,
18+
keys,
1919
} from "../../shared/utils.ts";
2020
import { getInheritedData } from "../../shared/dom.ts";
2121
import type { RawParams } from "../params/interface.ts";
@@ -84,6 +84,12 @@ function getClasses(stateList: ActiveClassState[]): string[] {
8484
return classes;
8585
}
8686

87+
function appendUniqueClasses(target: string[], source: string[]): void {
88+
source.forEach((className) => {
89+
if (!target.includes(className)) target.push(className);
90+
});
91+
}
92+
8793
/**
8894
* Parses an `ng-sref` expression into a target state name and parameter expression.
8995
*/
@@ -573,11 +579,11 @@ export function StateRefActiveDirective(
573579
function setStatesFromDefinitionObject(statesDefinition: unknown): void {
574580
if (isObject(statesDefinition)) {
575581
states = [];
576-
const stateEntries = entries(
577-
statesDefinition as Record<string, unknown>,
578-
);
582+
const definition = statesDefinition as Record<string, unknown>;
583+
584+
keys(definition).forEach((activeClass) => {
585+
const stateOrName = definition[activeClass];
579586

580-
stateEntries.forEach(([activeClass, stateOrName]) => {
581587
if (isString(stateOrName)) {
582588
addStateForClass(stateOrName, activeClass);
583589
} else if (isArray(stateOrName)) {
@@ -657,7 +663,9 @@ export function StateRefActiveDirective(
657663
appendSplitClasses(exactClasses, activeEqClass);
658664
}
659665

660-
const addClasses = uniqueStrings(fuzzyClasses.concat(exactClasses));
666+
const addClasses = uniqueStrings(fuzzyClasses);
667+
668+
appendUniqueClasses(addClasses, exactClasses);
661669

662670
const removeClasses: string[] = [];
663671

src/router/params/interface.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,37 +139,38 @@ export interface ParamDeclaration {
139139
/**
140140
* The parameter's `array` mode
141141
*
142-
* Explicitly specifies the array mode of a URL parameter
142+
* Explicitly specifies the array mode of a query parameter. Path parameters
143+
* are always treated as single values.
143144
*
144145
* - If `false`, the parameter value will be treated (encoded/decoded) as a single value
145-
* - If `true`, the parameter value will be treated (encoded/decoded) as an array of values.
146+
* - If `true`, a query parameter value will be treated (encoded/decoded) as an array of values.
146147
* - If `auto` (for query parameters only), if multiple values for a single parameter are present
147148
* in the URL (e.g.: /foo?bar=1&bar=2&bar=3) then the values are mapped to an array (e.g.:
148149
* `{ foo: [ '1', '2', '3' ] }`). However, if only one value is present (e.g.: /foo?bar=1)
149150
* then the value is treated as single value (e.g.: { foo: '1' }).
150151
*
151-
* If you specified a [[type]] for the parameter, the value will be treated as an array
152-
* of the specified [[ParamType]].
152+
* If you specified a [[type]] for a query parameter, the value will be treated
153+
* as an array of the specified [[ParamType]].
153154
*
154155
* #### Example:
155156
* ```js
156157
* {
157158
* name: 'foo',
158-
* url: '/foo/{arrayParam:int}`,
159+
* url: '/foo?arrayParam',
159160
* params: {
160161
* arrayParam: { array: true }
161162
* }
162163
* }
163164
*
164-
* // After the transition, URL should be '/foo/1-2-3'
165+
* // After the transition, URL should be '/foo?arrayParam=1&arrayParam=2&arrayParam=3'
165166
* $state.go("foo", { arrayParam: [ 1, 2, 3 ] });
166167
* ```
167168
*
168169
* @default `false` for path parameters, such as `url: '/foo/:pathParam'`
169170
* @default `auto` for query parameters, such as `url: '/foo?queryParam'`
170-
* @default `true` if the parameter name ends in `[]`, such as `url: '/foo/{implicitArrayParam:int[]}'`
171+
* @default `true` for query parameters if the parameter name ends in `[]`, such as `url: '/foo?implicitArrayParam[]'`
171172
*/
172-
array?: boolean;
173+
array?: boolean | "auto";
173174

174175
/**
175176
* Squash mode: omit default parameter values in URL

src/router/params/param-type.spec.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ describe("ParamType", () => {
1414
it("returns the original type when array mode is disabled", () => {
1515
const type = createType();
1616

17-
expect(type.$asArray(false, true)).toBe(type);
17+
expect(type.$asArray(false)).toBe(type);
1818
});
1919

20-
it("rejects auto array mode for path parameters", () => {
20+
it("wraps a type when array mode is enabled", () => {
2121
const type = createType();
2222

23-
expect(() => type.$asArray("auto", false)).toThrowError(
24-
"'auto' array mode is for query parameters only",
25-
);
23+
expect(type.$asArray("auto")).not.toBe(type);
2624
});
2725
});

src/router/params/param-type.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export class ParamType {
3131
this.pattern = /.*/;
3232
this.inherit = true;
3333
assign(this, def);
34-
this.name = undefined;
3534
}
3635
// consider these four methods to be "abstract methods" that should be overridden
3736

@@ -86,14 +85,10 @@ export class ParamType {
8685
* - url: "/path?queryParam=1 will create $stateParams.queryParam: 1
8786
* - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2]
8887
* @param {boolean |'auto'} mode
89-
* @param {boolean} isSearch
9088
*/
91-
$asArray(mode: boolean | "auto", isSearch: boolean): ParamType {
89+
$asArray(mode: boolean | "auto"): ParamType {
9290
if (!mode) return this;
9391

94-
if (mode === "auto" && !isSearch)
95-
throw new Error("'auto' array mode is for query parameters only");
96-
9792
return new ArrayParamType(this, mode);
9893
}
9994
}
@@ -122,15 +117,12 @@ class ArrayParamType extends ParamType {
122117

123118
this._type = type;
124119
this._arrayMode = mode;
125-
126-
assign(this, {
127-
dynamic: type.dynamic,
128-
name: type.name,
129-
pattern: type.pattern,
130-
inherit: type.inherit,
131-
raw: type.raw,
132-
$arrayMode: mode,
133-
});
120+
this.dynamic = type.dynamic;
121+
this.name = type.name as string | undefined;
122+
this.pattern = type.pattern;
123+
this.inherit = type.inherit;
124+
this.raw = type.raw;
125+
this.$arrayMode = mode;
134126
}
135127

136128
/** @internal */
@@ -156,22 +148,22 @@ class ArrayParamType extends ParamType {
156148

157149
const arr = this._arrayWrap(val);
158150

159-
const result: unknown[] = [];
160-
161151
const type = this._type;
162152

163-
arr.forEach((item) => {
164-
result.push(type[method](item));
165-
});
166-
167153
if (allTruthyMode) {
168-
for (let i = 0; i < result.length; i++) {
169-
if (!result[i]) return false;
154+
for (let i = 0; i < arr.length; i++) {
155+
if (!type[method](arr[i])) return false;
170156
}
171157

172158
return true;
173159
}
174160

161+
const result: unknown[] = [];
162+
163+
arr.forEach((item) => {
164+
result.push(type[method](item));
165+
});
166+
175167
return this._arrayUnwrap(result);
176168
}
177169

src/router/params/param.ts

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { isInjectable } from "../../shared/predicates.ts";
22
import {
3-
assign,
43
hasOwn,
54
isArray,
65
isDefined,
@@ -13,6 +12,7 @@ import { ParamType } from "./param-type.ts";
1312
import type {
1413
ParamDeclaration,
1514
ParamDefaultValueFactory,
15+
ParamDefaultValueProvider,
1616
RawParams,
1717
Replace,
1818
} from "./interface.ts";
@@ -62,11 +62,13 @@ function getParamDeclaration(
6262
): ParamDeclaration {
6363
const { dynamic } = state;
6464

65-
const defaultConfig = isDefined(dynamic) ? { dynamic } : {};
66-
6765
const paramConfig = unwrapShorthand(state?.params?.[paramName]);
6866

69-
return assign(defaultConfig, paramConfig);
67+
if (isDefined(dynamic) && !hasOwn(paramConfig, "dynamic")) {
68+
paramConfig.dynamic = dynamic;
69+
}
70+
71+
return paramConfig;
7072
}
7173

7274
/**
@@ -88,7 +90,9 @@ function unwrapShorthand(cfg: unknown): ParamDeclaration {
8890
? paramConfig.value
8991
: getStaticDefaultValue;
9092

91-
return assign(paramConfig, { _fn });
93+
paramConfig._fn = _fn as ParamDefaultValueProvider;
94+
95+
return paramConfig;
9296
}
9397

9498
/**
@@ -172,32 +176,39 @@ function getReplace(
172176
isOptional: boolean,
173177
squash: string | boolean,
174178
): Replace[] {
175-
const defaultPolicy = [
176-
{ from: "", to: isOptional || arrayMode ? undefined : "" },
177-
{ from: null, to: isOptional || arrayMode ? undefined : "" },
178-
] as Replace[];
179-
180179
const replace = isArray(config.replace) ? config.replace : [];
181180

182-
if (isString(squash)) replace.push({ from: squash, to: undefined });
181+
const result: Replace[] = [];
183182

184-
const configuredKeys: Array<string | null> = [];
183+
let hasEmptyReplace = false;
185184

186-
replace.forEach((item) => {
187-
configuredKeys.push(item.from);
188-
});
185+
let hasNullReplace = false;
189186

190-
const result: Replace[] = [];
187+
for (let i = 0; i < replace.length; i++) {
188+
const item = replace[i];
191189

192-
defaultPolicy.forEach((item) => {
193-
if (configuredKeys.indexOf(item.from) === -1) {
194-
result.push(item);
190+
if (item.from === "") {
191+
hasEmptyReplace = true;
192+
} else if (item.from === null) {
193+
hasNullReplace = true;
195194
}
196-
});
195+
}
196+
197+
const defaultReplacement = isOptional || arrayMode ? undefined : "";
198+
199+
if (!hasEmptyReplace) result.push({ from: "", to: defaultReplacement });
200+
201+
if (!hasNullReplace) {
202+
result.push({ from: null as unknown as string, to: defaultReplacement });
203+
}
204+
205+
for (let i = 0; i < replace.length; i++) {
206+
const item = replace[i];
197207

198-
replace.forEach((item) => {
199208
result.push(item);
200-
});
209+
}
210+
211+
if (isString(squash)) result.push({ from: squash, to: undefined });
201212

202213
return result;
203214
}
@@ -207,13 +218,11 @@ function getArrayMode(
207218
location: DefTypeValue,
208219
config: ParamDeclaration,
209220
): boolean | "auto" {
210-
const arrayDefaults = {
211-
array: location === DefType._SEARCH ? "auto" : false,
212-
};
221+
if (location !== DefType._SEARCH) return false;
213222

214-
const arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {};
223+
if (isDefined(config.array)) return config.array;
215224

216-
return assign(arrayDefaults, arrayParamNomenclature, config).array;
225+
return id.endsWith("[]") ? true : "auto";
217226
}
218227

219228
export class Param {
@@ -228,7 +237,6 @@ export class Param {
228237
inherit: boolean;
229238
array: boolean | "auto";
230239
config: ParamDeclaration;
231-
matchingKeys: RawParams | undefined;
232240
/** @internal */
233241
_defaultValueCache?: { defaultValue: unknown };
234242
/** @internal */
@@ -256,9 +264,7 @@ export class Param {
256264
type = getType(config, type, location, id, urlConfig._paramTypes);
257265
const arrayMode = getArrayMode(id, location, config);
258266

259-
type = arrayMode
260-
? type && type.$asArray(arrayMode, location === DefType._SEARCH)
261-
: type;
267+
type = arrayMode ? type && type.$asArray(arrayMode) : type;
262268
const isOptional =
263269
config.value !== undefined || location === DefType._SEARCH;
264270

@@ -289,7 +295,6 @@ export class Param {
289295
this.inherit = inherit;
290296
this.array = arrayMode;
291297
this.config = config;
292-
this.matchingKeys = undefined;
293298
this._runtime = runtime;
294299
}
295300

@@ -306,7 +311,14 @@ export class Param {
306311
* @param {undefined} [value]
307312
*/
308313
value(value?: unknown): unknown {
309-
value = this._replaceSpecialValues(value);
314+
for (let i = 0; i < this.replace.length; i++) {
315+
const tuple = this.replace[i];
316+
317+
if (tuple.from === value) {
318+
value = tuple.to;
319+
break;
320+
}
321+
}
310322

311323
return isUndefined(value)
312324
? this._getDefaultValue()
@@ -346,21 +358,6 @@ export class Param {
346358
return defaultValue;
347359
}
348360

349-
/** @internal */
350-
_replaceSpecialValues(value: unknown): unknown {
351-
for (let i = 0; i < this.replace.length; i++) {
352-
const tuple = this.replace[i];
353-
354-
if (tuple.from === value) return tuple.to;
355-
}
356-
357-
return value;
358-
}
359-
360-
isSearch(): boolean {
361-
return this.location === DefType._SEARCH;
362-
}
363-
364361
/**
365362
* @param {null} value
366363
*/
@@ -433,7 +430,15 @@ export class Param {
433430
values1: RawParams = {},
434431
values2: RawParams = {},
435432
): boolean {
436-
return Param.changed(params, values1, values2).length === 0;
433+
for (let i = 0; i < params.length; i++) {
434+
const param = params[i];
435+
436+
if (!param.type.equals(values1[param.id], values2[param.id])) {
437+
return false;
438+
}
439+
}
440+
441+
return true;
437442
}
438443

439444
/**

0 commit comments

Comments
 (0)