Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions package-lock.json

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

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@stencil/core": "^2.6.0",
"@stencil/router": "^1.0.1",
"@stencil/sass": "^1.4.1",
"@types/jest": "25.2.3",
"@types/prismjs": "^1.16.5",
Expand Down
19 changes: 9 additions & 10 deletions src/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,16 @@ export class App {
index={this.index}
/>
<main role="main">
<stencil-router historyType="hash">
<stencil-route-switch scrollTopOffset={0}>
<stencil-route
<kompendium-router>
<kompendium-route-switch scrollTopOffset={0}>
<kompendium-route
url="/"
component="kompendium-markdown"
componentProps={{
text: this.data.readme,
}}
exact={true}
/>
<stencil-route
<kompendium-route
url="/component/:name/:section?"
component="kompendium-component"
componentProps={{
Expand All @@ -124,14 +123,14 @@ export class App {
this.examplePropsFactory,
}}
/>
<stencil-route
<kompendium-route
url="/type/:name"
component="kompendium-type"
componentProps={{
types: this.data.types,
}}
/>
<stencil-route
<kompendium-route
url="/debug/:name"
component="kompendium-debug"
componentProps={{
Expand All @@ -141,14 +140,14 @@ export class App {
this.examplePropsFactory,
}}
/>
<stencil-route
<kompendium-route
component="kompendium-guide"
componentProps={{
data: this.data,
}}
/>
</stencil-route-switch>
</stencil-router>
</kompendium-route-switch>
</kompendium-router>
</main>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/component/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
JsonDocsComponent,
JsonDocsTag,
} from '@stencil/core/internal';
import { MatchResults } from '@stencil/router';
import { MatchResults } from '../router/route-matching';
import { PropertyList } from './templates/props';
import { EventList } from './templates/events';
import { MethodList } from './templates/methods';
Expand Down
2 changes: 1 addition & 1 deletion src/components/debug/debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
JsonDocsComponent,
JsonDocsTag,
} from '@stencil/core/internal';
import { MatchResults } from '@stencil/router';
import { MatchResults } from '../router/route-matching';
import { PropsFactory } from '../playground/playground.types';

@Component({
Expand Down
129 changes: 129 additions & 0 deletions src/components/router/component-key.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { generateComponentKey } from './component-key';

describe('component-key', () => {
describe('generateComponentKey()', () => {
it('returns empty string for empty params', () => {
const key = generateComponentKey({});

expect(key).toBe('');
});

it('generates key for single parameter', () => {
const key = generateComponentKey({ id: '123' });

expect(key).toBe('id=123');
});

it('generates key for multiple parameters', () => {
const key = generateComponentKey({ id: '123', name: 'test' });

expect(key).toBe('id=123&name=test');
});

it('sorts parameters alphabetically', () => {
const key = generateComponentKey({
zebra: 'z',
apple: 'a',
banana: 'b',
});

expect(key).toBe('apple=a&banana=b&zebra=z');
});

it('generates deterministic keys (same input produces same output)', () => {
const params = { foo: 'bar', baz: 'qux' };

const key1 = generateComponentKey(params);
const key2 = generateComponentKey(params);

expect(key1).toBe(key2);
});

it('generates deterministic keys regardless of insertion order', () => {
const params1 = { a: '1', b: '2', c: '3' };
const params2 = { c: '3', a: '1', b: '2' };

const key1 = generateComponentKey(params1);
const key2 = generateComponentKey(params2);

expect(key1).toBe(key2);
expect(key1).toBe('a=1&b=2&c=3');
});

it('generates different keys for different parameter values', () => {
const key1 = generateComponentKey({ id: '123' });
const key2 = generateComponentKey({ id: '456' });

expect(key1).not.toBe(key2);
});

it('generates different keys for different parameter names', () => {
const key1 = generateComponentKey({ id: '123' });
const key2 = generateComponentKey({ name: '123' });

expect(key1).not.toBe(key2);
});

it('handles empty parameter values', () => {
const key = generateComponentKey({ id: '', name: 'test' });

expect(key).toBe('id=&name=test');
});

it('handles parameter values with special characters', () => {
const key = generateComponentKey({ id: 'user-123_test' });

expect(key).toBe('id=user-123_test');
});

it('handles parameter values with hyphens', () => {
const key = generateComponentKey({ name: 'my-component' });

expect(key).toBe('name=my-component');
});

it('handles parameter values with spaces', () => {
const key = generateComponentKey({ query: 'hello world' });

expect(key).toBe('query=hello world');
});

it('handles parameter values with URL-like characters', () => {
const key = generateComponentKey({ path: '/foo/bar' });

expect(key).toBe('path=/foo/bar');
});

it('handles numeric string values', () => {
const key = generateComponentKey({ userId: '123', postId: '456' });

expect(key).toBe('postId=456&userId=123');
});

it('handles multiple parameters with empty values', () => {
const key = generateComponentKey({ a: '', b: '', c: 'value' });

expect(key).toBe('a=&b=&c=value');
});

it('generates key compatible with query string format', () => {
const key = generateComponentKey({
name: 'kompendium-app',
section: 'properties',
});

expect(key).toBe('name=kompendium-app&section=properties');
expect(key).toContain('&');
expect(key.split('&').length).toBe(2);
});

it('handles real-world route parameters', () => {
const key = generateComponentKey({
name: 'kompendium-component',
section: 'methods',
});

expect(key).toBe('name=kompendium-component&section=methods');
});
});
});
13 changes: 13 additions & 0 deletions src/components/router/component-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Generate a stable key from route parameters for component recreation
* Keys are deterministic - same params always produce same key
* Used to force Stencil component recreation when route params change
* @param {Record<string, string>} params - Route parameters
* @returns {string} A stable, deterministic key
*/
export function generateComponentKey(params: Record<string, string>): string {
return Object.keys(params)
.sort()
.map((k) => `${k}=${params[k]}`)
.join('&');
}
48 changes: 48 additions & 0 deletions src/components/router/kompendium-route-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Component, h, Prop, State } from '@stencil/core';
import { getHashPath } from './route-matching';

/**
* Custom route switch component for Kompendium
* Manages navigation state and passes current path to child routes
*/
@Component({
tag: 'kompendium-route-switch',
shadow: false,
})
export class KompendiumRouteSwitch {
@Prop()
public scrollTopOffset?: number = 0;

@State()
private currentPath: string = '/';

constructor() {
this.handleHashChange = this.handleHashChange.bind(this);
}

connectedCallback(): void {
window.addEventListener('hashchange', this.handleHashChange);
this.handleHashChange();
}

disconnectedCallback(): void {
window.removeEventListener('hashchange', this.handleHashChange);
}

private handleHashChange(): void {
const newPath = getHashPath();
if (newPath !== this.currentPath) {
this.currentPath = newPath;
if (this.scrollTopOffset !== undefined) {
window.scrollTo(0, this.scrollTopOffset);
}
}
}

render() {
// Simply render child routes
// The @State currentPath will trigger re-render when hash changes
// Each route component will re-render and check if it matches
return <slot />;
}
}
Loading
Loading