diff --git a/src/router/links.ts b/src/router/links.ts index 32d1a142..667003d2 100644 --- a/src/router/links.ts +++ b/src/router/links.ts @@ -63,7 +63,17 @@ export const interceptLinks = (container: Element = document.body): (() => void) if (anchor.hasAttribute('download')) return; if (anchor.origin !== window.location.origin) return; // External link - const path = anchor.pathname + anchor.search + anchor.hash; + // Detect hash-routing mode: links written as href="#/page" + // In this case, anchor.hash contains the route path + let path: string; + if (anchor.hash && anchor.hash.startsWith('#/')) { + // Hash-routing mode: extract path from the hash + // e.g., href="#/page?foo=bar" → path = "/page?foo=bar" + path = anchor.hash.slice(1); // Remove leading # + } else { + // History mode or regular anchor: use pathname + search + hash + path = anchor.pathname + anchor.search + anchor.hash; + } e.preventDefault(); navigate(path); diff --git a/src/store/create-store.ts b/src/store/create-store.ts index 8856819e..466dff25 100644 --- a/src/store/create-store.ts +++ b/src/store/create-store.ts @@ -2,7 +2,14 @@ * Store creation logic. */ -import { batch, computed, signal, type ReadonlySignal, type Signal } from '../reactive/index'; +import { + batch, + computed, + signal, + untrack, + type ReadonlySignal, + type Signal, +} from '../reactive/index'; import { notifyDevtoolsStateChange, registerDevtoolsStore } from './devtools'; import { applyPlugins } from './plugins'; import { getStore, hasStore, registerStore } from './registry'; @@ -56,9 +63,15 @@ export const createStore = < * trigger reactive updates. This differs from frameworks like Pinia that * use deep reactivity. To update nested state, replace the entire object. * + * Uses `untrack()` to prevent accidental dependency tracking when called + * from within reactive contexts (e.g., `effect()` or `computed()`). + * * @internal */ - const getCurrentState = (): S => ({ ...stateProxy }); + const getCurrentState = (): S => + untrack(() => { + return { ...stateProxy }; + }); /** * Notifies subscribers of state changes. diff --git a/src/view/directives/on.ts b/src/view/directives/on.ts index f7bc024b..ee9229c7 100644 --- a/src/view/directives/on.ts +++ b/src/view/directives/on.ts @@ -1,4 +1,4 @@ -import { evaluate, evaluateRaw } from '../evaluate'; +import { evaluateRaw } from '../evaluate'; import type { DirectiveHandler } from '../types'; /** @@ -24,8 +24,9 @@ export const handleOn = (eventName: string): DirectiveHandler => { } } - // Otherwise evaluate as expression (e.g., "handleClick($event)" or "count++") - evaluate(expression, eventContext); + // Otherwise evaluate as expression using evaluateRaw to allow signal mutations + // (e.g., "count.value++" or "handleClick($event)") + evaluateRaw(expression, eventContext); }; el.addEventListener(eventName, handler); diff --git a/tests/component.test.ts b/tests/component.test.ts index 89937e23..31c4c819 100644 --- a/tests/component.test.ts +++ b/tests/component.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { component, html } from '../src/component/index'; +import { component, defineComponent, html } from '../src/component/index'; describe('component/html', () => { it('creates HTML from template literal', () => { @@ -598,3 +598,167 @@ describe('component/component', () => { el.remove(); }); }); + +describe('component/defineComponent', () => { + it('returns an HTMLElement subclass', () => { + const tagName = `test-define-class-${Date.now()}`; + const ElementClass = defineComponent(tagName, { + props: {}, + render: () => html`