Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/shaky-pianos-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@qwik.dev/core': major
---

BREAKING: (slightly) `-` handling in JSX event handlers has slightly changed. Now, if an event name starts with `-`, the rest of the name will be kept as-is, preserving casing. Otherwise, the event name is made lowercase. Any `-` characters in the middle of the name are preserved as-is. Previously, `-` were considered to mark the next letter as uppercase.
For example, `onCustomEvent$` will match `customevent`, `on-CustomEvent$` will match `CustomEvent`, and `onCustom-Event$` will match `custom-event`. Before, that last one would match `customEvent` instead.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"internalConsoleOptions": "neverOpen",
"program": "${workspaceFolder}/./node_modules/vitest/vitest.mjs",
"cwd": "${workspaceFolder}",
"args": ["--test-timeout", "999999", "--minWorkers", "1", "--maxWorkers", "1", "${file}"]
"args": ["--test-timeout", "999999", "--maxWorkers", "1", "${file}"]
}
]
}
2 changes: 1 addition & 1 deletion e2e/docs-e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "docs-e2e",
"description": "",
"private": true,
"author": "",
"devDependencies": {
"@playwright/test": "1.54.1",
Expand All @@ -10,6 +9,7 @@
"keywords": [],
"license": "ISC",
"main": "index.js",
"private": true,
"scripts": {
"test": "pnpm exec playwright test --config=playwright.config.ts --project=chromium",
"test-ui": "pnpm exec playwright test --config=playwright.config.ts --project=chromium --ui"
Expand Down
8 changes: 4 additions & 4 deletions e2e/qwik-react-e2e/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "qwik-react-test-app",
"description": "Qwik react test app",
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"private": true,
"devDependencies": {
"@qwik.dev/react": "workspace:*",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.7",
"react": "19.1.1",
"react-dom": "19.1.1"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"private": true,
"scripts": {
"build": "qwik build",
"build.client": "vite build",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@
"build.packages.insights": "pnpm -C ./packages/insights/ run build",
"build.platform": "tsx --require ./scripts/runBefore.ts scripts/index.ts --platform-binding",
"build.platform.copy": "tsx --require ./scripts/runBefore.ts scripts/index.ts --platform-binding-wasm-copy",
"build.qwik-react": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikreact",
"build.qwik-router": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikrouter",
"build.router": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikrouter --api",
"build.qwik-react": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikreact",
"build.validate": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --api --eslint --qwikrouter --platform-binding --wasm --validate",
"build.vite": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --insights --api --qwikrouter --eslint --platform-binding-wasm-copy",
"build.wasm": "tsx --require ./scripts/runBefore.ts scripts/index.ts --wasm",
Expand Down Expand Up @@ -236,11 +236,11 @@
"test.e2e.firefox": "playwright test starters --browser=firefox --config starters/playwright.config.ts",
"test.e2e.integrations.chromium": "playwright test e2e/adapters-e2e/tests --project=chromium --config e2e/adapters-e2e/playwright.config.ts",
"test.e2e.integrations.webkit": "playwright test e2e/adapters-e2e/tests --project=webkit --config e2e/adapters-e2e/playwright.config.ts",
"test.e2e.qwik-react.chromium": "playwright test e2e/qwik-react-e2e/tests --project=chromium --config e2e/qwik-react-e2e/playwright.config.ts",
"test.e2e.qwik-react.webkit": "playwright test e2e/qwik-react-e2e/tests --project=webkit --config e2e/qwik-react-e2e/playwright.config.ts",
"test.e2e.router": "playwright test starters/e2e/qwikrouter --browser=chromium --config starters/playwright.config.ts",
"test.e2e.run": "tsm scripts/e2e-cli.ts",
"test.e2e.webkit": "playwright test starters --browser=webkit --config starters/playwright.config.ts",
"test.e2e.qwik-react.chromium": "playwright test e2e/qwik-react-e2e/tests --project=chromium --config e2e/qwik-react-e2e/playwright.config.ts",
"test.e2e.qwik-react.webkit": "playwright test e2e/qwik-react-e2e/tests --project=webkit --config e2e/qwik-react-e2e/playwright.config.ts",
"test.rust": "make test",
"test.rust.bench": "make benchmark",
"test.rust.update": "make test-update",
Expand Down
5 changes: 2 additions & 3 deletions packages/docs/src/repl/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ export const Repl = component$((props: ReplProps) => {
if (vNew.version !== input.version) {
input.version = v.version;
}
}
// TODO this is broken, doesn't add the handler on the server
// { strategy: 'document-ready' }
},
{ strategy: 'document-ready' }
);

// Track input changes to rebuild the app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@
"mdFile": "router.verceledgeadapteroptions.md"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ contributors:
- Balastrong
- Jemsco
- shairez
updated_at: '2024-01-09T20:55:11Z'
- wmertens
updated_at: '2025-10-26T00:00:00Z'
created_at: '2023-03-20T23:45:13Z'
---

Expand Down Expand Up @@ -299,6 +300,10 @@ export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {

> Notice the use of the `QRL` type in `onTripleClick$: QRL<() => void>;`. It is like wrapping a function in `$()` but at the type level. If you had `const greet = $(() => "hi 👋");` and hovered over 'greet', you would see that 'greet' is of type `QRL<() => "hi 👋">`

Event names are case sensitive, but all DOM events except for `DOMContentLoaded` are lowercase. For a better DX, event names are always lowercased, so `onTripleClick$` becomes `tripleclick` under the hood.

To listen for a custom event with uppercase letters, you add a `-` after `on`. For example, to listen for a custom event named `CustomEvent`, you would use `on-CustomEvent$`. For a window event named `Hi-There`, you would use `window:on-Hi-There$`.

## Window and Document Events

So far, the discussion has focused on listening to events originating from elements. There are events such as `scroll` and `mousemove` that need to be listened to on the `window` or `document`. Qwik allows this by providing the `document:on` and `window:on` prefixes when listening for events.
Expand Down
56 changes: 24 additions & 32 deletions packages/docs/src/routes/playground/index!.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,33 @@ import {
$,
component$,
isBrowser,
useSignal,
useStore,
useStyles$,
useTask$,
useVisibleTask$,
} from '@qwik.dev/core';
import type { DocumentHead, RequestHandler } from '@qwik.dev/router';
import type { ReplAppInput } from '~/repl/types';
import { setReplCorsHeaders } from '~/utils/utils';
import { Header } from '../../components/header/header';
import { PanelToggle } from '../../components/panel-toggle/panel-toggle';
import type { ReplAppInput } from '../../repl/types';
import { Repl } from '../../repl/ui';
import { createPlaygroundShareUrl, parsePlaygroundShareUrl } from '../../repl/ui/repl-share-url';
import styles from './playground.css?inline';
import { setReplCorsHeaders } from '~/utils/utils';

export default component$(() => {
useStyles$(styles);

const store = useStore<PlaygroundStore>(() => {
const initStore: PlaygroundStore = {
files: playgroundApp.inputs,
version: '',
buildMode: 'development',
entryStrategy: 'segment',
colResizeActive: false,
colLeft: 50,
shareUrlTmr: null,
};
return initStore;
});
const colResizeActive = useSignal(false);
const colLeft = useSignal(50);
const shareUrlTmr = useSignal<any>(null);

const store = useStore<ReplAppInput>(() => ({
files: playgroundApp.inputs,
version: '',
buildMode: 'development',
entryStrategy: 'segment',
}));

const panelStore = useStore(() => ({
active: 'Input',
Expand All @@ -57,9 +55,9 @@ export default component$(() => {

if (isBrowser) {
if (store.version) {
clearTimeout(store.shareUrlTmr);
clearTimeout(shareUrlTmr.value);

store.shareUrlTmr = setTimeout(() => {
shareUrlTmr.value = setTimeout(() => {
const shareUrl = createPlaygroundShareUrl(store);
history.replaceState({}, '', shareUrl);
}, 1000);
Expand All @@ -68,19 +66,19 @@ export default component$(() => {
});

const pointerDown = $(() => {
store.colResizeActive = true;
colResizeActive.value = true;
});

const pointerMove = $((ev: PointerEvent) => {
if (store.colResizeActive) {
store.colLeft = (ev.clientX, ev.clientX / window.innerWidth) * 100;
store.colLeft = Math.max(25, store.colLeft);
store.colLeft = Math.min(75, store.colLeft);
if (colResizeActive.value) {
colLeft.value = (ev.clientX / window.innerWidth) * 100;
colLeft.value = Math.max(25, colLeft.value);
colLeft.value = Math.min(75, colLeft.value);
}
});

const pointerUp = $(() => {
store.colResizeActive = false;
colResizeActive.value = false;
});

return (
Expand All @@ -89,7 +87,7 @@ export default component$(() => {
playground: true,
'full-width': true,
'fixed-header': true,
'repl-resize-active': store.colResizeActive,
'repl-resize-active': colResizeActive.value,
}}
>
<Header />
Expand All @@ -101,7 +99,7 @@ export default component$(() => {
repl: true,
}}
style={{
gridTemplateColumns: `${store.colLeft}% ${100 - store.colLeft}%`,
gridTemplateColumns: `${colLeft.value}% ${100 - colLeft.value}%`,
}}
>
<Repl
Expand All @@ -119,7 +117,7 @@ export default component$(() => {
onPointerUp$={pointerUp}
onPointerOut$={pointerUp}
style={{
left: `calc(${store.colLeft}% - 6px)`,
left: `calc(${colLeft.value}% - 6px)`,
}}
/>
<PanelToggle panelStore={panelStore} />
Expand All @@ -131,12 +129,6 @@ export const head: DocumentHead = {
title: 'Playground',
};

export interface PlaygroundStore extends ReplAppInput {
colResizeActive: boolean;
colLeft: number;
shareUrlTmr: any;
}

export const onGet: RequestHandler = ({ cacheControl, headers }) => {
cacheControl({
public: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@
"url": "https://github.com/QwikDev/qwik.git",
"directory": "packages/qwik"
},
"sideEffects": false,
"scripts": {
"build.insights": "cd src/insights && vite build --mode lib --emptyOutDir"
},
"sideEffects": false,
"type": "module",
"types": "./public.d.ts"
}
73 changes: 32 additions & 41 deletions packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ import { ChoreType } from '../shared/util-chore-type';
import { escapeHTML } from '../shared/utils/character-escaping';
import { _OWNER } from '../shared/utils/constants';
import {
getEventNameFromJsxEvent,
getEventNameScopeFromJsxEvent,
fromCamelToKebabCase,
getEventDataFromHtmlAttribute,
isHtmlAttributeAnEventName,
isJsxPropertyAnEventName,
jsxEventToHtmlAttribute,
} from '../shared/utils/event-names';
import { getFileLocationFromJsx } from '../shared/utils/jsx-filename';
import {
Expand Down Expand Up @@ -604,23 +602,19 @@ export const vnode_diff = (
// We never tell the vNode about them saving us time and memory.
for (const key in constProps) {
let value = constProps[key];
if (isJsxPropertyAnEventName(key)) {
// So for event handlers we must add them to the vNode so that qwikloader can look them up
// But we need to mark them so that they don't get pulled into the diff.
const eventName = getEventNameFromJsxEvent(key);
const scope = getEventNameScopeFromJsxEvent(key);
if (eventName) {
vNewNode!.setProp(HANDLER_PREFIX + ':' + scope + ':' + eventName, value);
registerQwikLoaderEvent(eventName);
}

if (scope) {
// add an event attr with empty value for qwikloader element selector.
// We don't need value here. For ssr this value is a QRL,
// but for CSR value should be just empty
const htmlEvent = jsxEventToHtmlAttribute(key);
if (htmlEvent) {
vNewNode!.setAttr(htmlEvent, '', journal);
if (isHtmlAttributeAnEventName(key)) {
const data = getEventDataFromHtmlAttribute(key);
if (data) {
const scope = data[0];
const eventName = data[1];

if (eventName) {
vNewNode!.setProp(HANDLER_PREFIX + ':' + scope + ':' + eventName, value);
if (scope) {
// window and document need attrs so qwik loader can find them
vNewNode!.setAttr(key, '', journal);
}
registerQwikLoaderEvent(eventName);
}
}

Expand Down Expand Up @@ -769,7 +763,7 @@ export const vnode_diff = (
// Event handler needs to be patched onto the element.
if (!element.qDispatchEvent) {
element.qDispatchEvent = (event: Event, scope: QwikLoaderEventScope) => {
const eventName = event.type;
const eventName = fromCamelToKebabCase(event.type);
const eventProp = ':' + scope.substring(1) + ':' + eventName;
const qrls = [
vNode.getProp<QRL>(eventProp, null),
Expand Down Expand Up @@ -867,22 +861,13 @@ export const vnode_diff = (
};

const recordJsxEvent = (key: string, value: any) => {
const eventName = getEventNameFromJsxEvent(key);
const scope = getEventNameScopeFromJsxEvent(key);
if (eventName) {
const data = getEventDataFromHtmlAttribute(key);
if (data) {
const [scope, eventName] = data;
record(':' + scope + ':' + eventName, value);
// register an event for qwik loader
registerQwikLoaderEvent(eventName);
}

if (scope) {
// add an event attr with empty value for qwikloader element selector.
// We don't need value here. For ssr this value is a QRL,
// but for CSR value should be just empty
const htmlEvent = jsxEventToHtmlAttribute(key);
if (htmlEvent) {
record(htmlEvent, '');
}
patchEventDispatch = true;
}
};

Expand Down Expand Up @@ -910,8 +895,7 @@ export const vnode_diff = (
} else if (dstKey === undefined) {
// Destination exhausted: add remaining source keys
const srcValue = srcAttrs[srcIdx + 1];
if (isJsxPropertyAnEventName(srcKey)) {
patchEventDispatch = true;
if (isHtmlAttributeAnEventName(srcKey)) {
recordJsxEvent(srcKey, srcValue);
} else {
record(srcKey, srcValue);
Expand All @@ -923,17 +907,24 @@ export const vnode_diff = (
// Keys match: update if values differ
const srcValue = srcAttrs[srcIdx + 1];
const dstValue = dstAttrs[dstIdx + 1];
const isEventHandler = isHtmlAttributeAnEventName(srcKey);
if (srcValue !== dstValue) {
record(srcKey, srcValue);
// Update in place doesn't change array length
if (isEventHandler) {
recordJsxEvent(srcKey, srcValue);
} else {
record(srcKey, srcValue);
}
} else if (isEventHandler && !vnode.element.qDispatchEvent) {
// Special case: add event handlers after resume
recordJsxEvent(srcKey, srcValue);
}
// Update in place doesn't change array length
srcIdx += 2; // skip key and value
dstIdx += 2; // skip key and value
} else if (srcKey < dstKey) {
// Source has a key not in destination: add it
const srcValue = srcAttrs[srcIdx + 1];
if (isJsxPropertyAnEventName(srcKey)) {
patchEventDispatch = true;
if (isHtmlAttributeAnEventName(srcKey)) {
recordJsxEvent(srcKey, srcValue);
} else {
record(srcKey, srcValue);
Expand Down
Loading