Skip to content

Commit

Permalink
feat(navigation): Allow to listen for active navigation changes
Browse files Browse the repository at this point in the history
This adds the native `EventTarget` class to the Navigation allowing to add listeners to it.
Allowing to dispatch the `updateActive` event when the active navigation changed.

The idea is to make it reactive in a native / framework agnostic way.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jun 14, 2024
1 parent 10d140b commit 808ab7c
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 4 deletions.
62 changes: 62 additions & 0 deletions __tests__/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, it, expect, vi } from 'vitest'
import { Navigation } from '../lib/navigation/navigation'
import { View } from '../lib/navigation/view'

const mockView = (id = 'view', order = 1) => new View({ id, order, name: 'View', icon: '<svg></svg>', getContents: () => Promise.reject(new Error()) })

describe('Navigation', () => {
it('Can register a view', async () => {
const navigation = new Navigation()
const view = mockView()
navigation.register(view)

expect(navigation.views).toEqual([view])
})

it('Throws when registering the same view twice', async () => {
const navigation = new Navigation()
const view = mockView()
navigation.register(view)
expect(() => navigation.register(view)).toThrow(/already registered/)
expect(navigation.views).toEqual([view])
})

it('Can remove a view', async () => {
const navigation = new Navigation()
const view = mockView()
navigation.register(view)
expect(navigation.views).toEqual([view])
navigation.remove(view.id)
expect(navigation.views).toEqual([])
})

it('Can set a view as active', async () => {
const navigation = new Navigation()
const view = mockView()
navigation.register(view)

expect(navigation.active).toBe(null)

navigation.setActive(view)
expect(navigation.active).toEqual(view)
})

it('Emits event when setting a view as active', async () => {
const navigation = new Navigation()
const view = mockView()
navigation.register(view)

// add listener
const listener = vi.fn()
navigation.addEventListener('updateActive', listener)

navigation.setActive(view)
expect(listener).toHaveBeenCalledOnce()
// So it was called, we then expect the first argument of the first call to be the event with the view as the detail
expect(listener.mock.calls[0][0].detail).toBe(view)
})
})
27 changes: 23 additions & 4 deletions lib/navigation/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { TypedEventTarget } from 'typescript-event-target'
import { View } from './view'
import logger from '../utils/logger'

export class Navigation {
/**
* The event emitted when the navigation view was updated.
* It contains the new active view in the `detail` attribute.
*/
type UpdateActiveViewEvent = CustomEvent<View | null>

export class Navigation extends TypedEventTarget<{ updateActive: UpdateActiveViewEvent }> {

private _views: View[] = []
private _currentView: View | null = null

register(view: View) {
/**
* Register a new view on the navigation
* @param view The view to register
* @throws `Error` is thrown if a view with the same id is already registered
*/
register(view: View): void {
if (this._views.find(search => search.id === view.id)) {
throw new Error(`View id ${view.id} is already registered`)
}

this._views.push(view)
}

remove(id: string) {
remove(id: string): void {
const index = this._views.findIndex(view => view.id === id)
if (index !== -1) {
this._views.splice(index, 1)
Expand All @@ -29,8 +41,15 @@ export class Navigation {
return this._views
}

setActive(view: View | null) {
/**
* Set the currently active view
* @fires UpdateActiveViewEvent
* @param view New active view
*/
setActive(view: View | null): void {
this._currentView = view
const event: UpdateActiveViewEvent = new CustomEvent<View | null>('updateActive', { detail: view })
this.dispatchTypedEvent('updateActive', event)
}

get active(): View | null {
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@nextcloud/router": "^3.0.1",
"cancelable-promise": "^4.3.1",
"is-svg": "^5.0.1",
"typescript-event-target": "^1.1.1",
"webdav": "^5.6.0"
}
}

0 comments on commit 808ab7c

Please sign in to comment.