Skip to content

Commit

Permalink
feat(router-lite): activeClass router configuration (#1733)
Browse files Browse the repository at this point in the history
* feat(router-lite): activeClass router configuration

* chore(router-lite): cleanup
  • Loading branch information
Sayan751 committed Apr 7, 2023
1 parent 5bde983 commit bd18fde
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 10 deletions.
6 changes: 6 additions & 0 deletions docs/user-docs/router-lite/navigating.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,12 @@ This can also be seen in the live example below.
Note that the [navigation model](./navigation-model.md) also offers a [`isActive` property](./navigation-model.md#using-the-isactive-property).
{% endhint %}

### "active" CSS class

The `active` bindable can be used for other purposes, other than adding CSS classes to the element.
However, if that's what you need mostly the `active` property for, you may choose to configure the [`activeClass` property](./router-configuration.md#configure-active-class) in the router configuration.
When configured, the `load` custom attribute will add that configured class to the element when the associated routing instruction is active.

## Using the Router API

Along with the custom attributes on the markup-side, the router-lite also offers the `IRouter#load` method that can be used to perform navigation, with the complete capabilities of the JavaScript at your disposal.
Expand Down
8 changes: 8 additions & 0 deletions docs/user-docs/router-lite/router-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,11 @@ As you interact with this example, you can see that there is absolutely no chang
### Override configured history strategy

You can use the [navigation options](./navigating.md#using-navigation-options) to override the configured history strategy for individual routing instructions.

## Configure active class

Using the `activeClass` option you can add a class name to the router configuration.
This class name is used by the [`load` custom attribute](./navigating.md#using-the-load-custom-attribute) when the associated instruction is active.
The default value for this option is `null`, which also means that the `load` custom attribute won't add any class proactively.
Note that the router-lite does not define any CSS class out-of-the-box.
If you want to use this feature, make sure that you defines the class as well in your stylesheet.
5 changes: 3 additions & 2 deletions packages/__tests__/router-lite/_shared/create-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,19 @@ type RouterTestStartOptions<TAppRoot> = {
useHash?: boolean;
registrations?: any[];
historyStrategy?: HistoryStrategy;
activeClass?: string | null;
};

/**
* Simpler fixture creation.
*/
export async function start<TAppRoot>({ appRoot, useHash = false, registrations = [], historyStrategy = 'replace' }: RouterTestStartOptions<TAppRoot>) {
export async function start<TAppRoot>({ appRoot, useHash = false, registrations = [], historyStrategy = 'replace', activeClass }: RouterTestStartOptions<TAppRoot>) {
const ctx = TestContext.create();
const { container } = ctx;

container.register(
TestRouterConfiguration.for(LogLevel.warn),
RouterConfiguration.customize({ useUrlFragmentHash: useHash, historyStrategy }),
RouterConfiguration.customize({ useUrlFragmentHash: useHash, historyStrategy, activeClass }),
...registrations,
);

Expand Down
105 changes: 105 additions & 0 deletions packages/__tests__/router-lite/resources/load.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,111 @@ describe('router-lite/resources/load.spec.ts', function () {
await au.stop(true);
});

it('adds activeClass when configured', async function () {
@customElement({ name: 'fo-o', template: '' })
class Foo { }

@route({
routes: [
{ id: 'foo', path: 'foo/:id', component: Foo }
]
})
@customElement({
name: 'ro-ot',
template: `
<a load="route:foo; params.bind:{id: 1}"></a>
<a load="route:foo/2"></a>
<au-viewport></au-viewport>`
})
class Root { }

const activeClass = 'au-rl-active';
const { au, host, container } = await start({ appRoot: Root, registrations: [Foo], activeClass });
const queue = container.get(IPlatform).domWriteQueue;
await queue.yield();

const anchors = host.querySelectorAll('a');
const a1 = { href: 'foo/1', active: false };
const a2 = { href: 'foo/2', active: false };
assertAnchorsWithClass(anchors, [a1, a2], activeClass, 'round#1');

anchors[1].click();
await queue.yield();
a2.active = true;
assertAnchorsWithClass(anchors, [a1, a2], activeClass, 'round#2');

anchors[0].click();
await queue.yield();
a1.active = true;
a2.active = false;
assertAnchorsWithClass(anchors, [a1, a2], activeClass, 'round#3');

await au.stop(true);

function assertAnchorsWithClass(anchors: HTMLAnchorElement[] | NodeListOf<HTMLAnchorElement>, expected: { href: string; active?: boolean }[], activeClass: string | null = null, message: string = ''): void {
const len = anchors.length;
assert.strictEqual(len, expected.length, `${message} length`);
for (let i = 0; i < len; i++) {
const anchor = anchors[i];
const item = expected[i];
assert.strictEqual(anchor.href.endsWith(item.href), true, `${message} - #${i} href - actual: ${anchor.href} - expected: ${item.href}`);
assert.strictEqual(anchor.classList.contains(activeClass), !!item.active, `${message} - #${i} active`);
}
}
});

it('does not add activeClass when not configured', async function () {
@customElement({ name: 'fo-o', template: '' })
class Foo { }

@route({
routes: [
{ id: 'foo', path: 'foo/:id', component: Foo }
]
})
@customElement({
name: 'ro-ot',
template: `
<a load="route:foo; params.bind:{id: 1}"></a>
<a load="route:foo/2"></a>
<au-viewport></au-viewport>`
})
class Root { }

const { au, host, container } = await start({ appRoot: Root, registrations: [Foo] });
const queue = container.get(IPlatform).domWriteQueue;
await queue.yield();

const anchors = host.querySelectorAll('a');
const a1 = { href: 'foo/1', active: false };
const a2 = { href: 'foo/2', active: false };
assertAnchorsWithoutClass(anchors, [a1, a2], 'round#1');

anchors[1].click();
await queue.yield();
a2.active = true;
assertAnchorsWithoutClass(anchors, [a1, a2], 'round#2');

anchors[0].click();
await queue.yield();
a1.active = true;
a2.active = false;
assertAnchorsWithoutClass(anchors, [a1, a2], 'round#3');

await au.stop(true);

function assertAnchorsWithoutClass(anchors: HTMLAnchorElement[] | NodeListOf<HTMLAnchorElement>, expected: { href: string; active?: boolean }[], message: string = ''): void {
const len = anchors.length;
assert.strictEqual(len, expected.length, `${message} length`);
for (let i = 0; i < len; i++) {
const anchor = anchors[i];
const item = expected[i];
assert.strictEqual(anchor.href.endsWith(item.href), true, `${message} - #${i} href - actual: ${anchor.href} - expected: ${item.href}`);
assert.strictEqual(anchor.classList.value, 'au', `${message} - #${i} active`);
}
}
});

it('un-configured parameters are added to the querystring', async function () {
@customElement({ name: 'fo-o', template: '' })
class Foo { }
Expand Down
9 changes: 8 additions & 1 deletion packages/router-lite/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DI } from '@aurelia/kernel';
import type { Params, RouteContextLike, RouteableComponent, ViewportInstruction, ViewportInstructionTree } from './instructions';
import type { RouteNode } from './route-tree';
import type { Transition } from './router';
import { IRouteContext } from './route-context';
import type { IRouteContext } from './route-context';

export type HistoryStrategy = 'none' | 'replace' | 'push';
export type ValueOrFunc<T extends string> = T | ((instructions: ViewportInstructionTree) => T);
Expand Down Expand Up @@ -41,6 +41,12 @@ export class RouterOptions {
* The default value is `true`.
*/
public readonly useNavigationModel: boolean,
/**
* The class that is added to the element by the `load` custom attribute, if the associated instruction is active.
* If no value is provided while configuring router, no class will be added.
* The default value is `null`.
*/
public readonly activeClass: string | null,
) { }

public static create(input: IRouterOptions): RouterOptions {
Expand All @@ -50,6 +56,7 @@ export class RouterOptions {
input.historyStrategy ?? 'push',
input.buildTitle ?? null,
input.useNavigationModel ?? true,
input.activeClass ?? null,
);
}

Expand Down
3 changes: 0 additions & 3 deletions packages/router-lite/src/resources/href.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
bindable,
ICustomAttributeViewModel,
ICustomAttributeController,
IEventTarget,
INode,
IWindow,
getRef, CustomAttribute
Expand Down Expand Up @@ -32,7 +31,6 @@ export class HrefCustomAttribute implements ICustomAttributeViewModel {
@bindable({ mode: BindingMode.toView })
public value: unknown;

// private eventListener!: IDisposable;
private isInitialized: boolean = false;
private isEnabled: boolean;

Expand All @@ -43,7 +41,6 @@ export class HrefCustomAttribute implements ICustomAttributeViewModel {
public readonly $controller!: ICustomAttributeController<this>;

public constructor(
@IEventTarget private readonly target: IEventTarget,
@INode private readonly el: INode<HTMLElement>,
@IRouter private readonly router: IRouter,
@IRouteContext private readonly ctx: IRouteContext,
Expand Down
14 changes: 10 additions & 4 deletions packages/router-lite/src/resources/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
customAttribute,
bindable,
ICustomAttributeViewModel,
IEventTarget,
INode,
CustomElement,
} from '@aurelia/runtime-html';
Expand Down Expand Up @@ -37,12 +36,11 @@ export class LoadCustomAttribute implements ICustomAttributeViewModel {

private href: string | null = null;
private instructions: ViewportInstructionTree | null = null;
// private eventListener: IDisposable | null = null;
private navigationEndListener: IDisposable | null = null;
private readonly isEnabled: boolean;
private readonly activeClass: string | null;

public constructor(
@IEventTarget private readonly target: IEventTarget,
@INode private readonly el: INode<HTMLElement>,
@IRouter private readonly router: IRouter,
@IRouterEvents private readonly events: IRouterEvents,
Expand All @@ -51,6 +49,7 @@ export class LoadCustomAttribute implements ICustomAttributeViewModel {
) {
// Ensure the element is not explicitly marked as external.
this.isEnabled = !el.hasAttribute('external') && !el.hasAttribute('data-external');
this.activeClass = router.options.activeClass;
}

public binding(): void {
Expand All @@ -60,7 +59,14 @@ export class LoadCustomAttribute implements ICustomAttributeViewModel {
this.valueChanged();
this.navigationEndListener = this.events.subscribe('au:router:navigation-end', _e => {
this.valueChanged();
this.active = this.instructions !== null && this.router.isActive(this.instructions, this.context!);
const active = this.active = this.instructions !== null && this.router.isActive(this.instructions, this.context!);
const activeClass = this.activeClass;
if (activeClass === null) return;
if (active) {
this.el.classList.add(activeClass);
} else {
this.el.classList.remove(activeClass);
}
});
}

Expand Down

0 comments on commit bd18fde

Please sign in to comment.