Skip to content

Commit 61208e1

Browse files
committed
fix(core): actual sane casing in events
1 parent 408c018 commit 61208e1

File tree

13 files changed

+65
-74
lines changed

13 files changed

+65
-74
lines changed

.changeset/shaky-pianos-wait.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
'@qwik.dev/core': major
33
---
44

5-
BREAKING: (slightly) custom event handlers no longer use a `-` prefix to denote case sensitive events. Instead, write the event name in kebab-case directly. For example, the custom event `CustomEvent` should now be written as `on-Custom-Event$` instead of `on-CustomEvent$`. The handler will be called for events named `CustomEvent` but also for `-custom-event`, which should not be a problem in practice.
5+
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.
6+
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.

packages/docs/src/routes/docs/(qwik)/core/events/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {
302302
303303
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.
304304

305-
To listen for a custom event with uppercase letters, you must use kebab-case. For example, to listen for a custom event named `CustomEvent`, you would use `on-Custom-Event$` or `on-custom-event$`. The handler would then be called for events named: `CustomEvent`, `-custom-event`, `Custom-event` and `-customEvent`. In practice, this combining of events should not be a problem, but you can check the exact event name in the handler if needed.
305+
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$`.
306306

307307
## Window and Document Events
308308

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
fromCamelToKebabCase,
3030
getEventDataFromHtmlAttribute,
3131
isHtmlAttributeAnEventName,
32-
isJsxPropertyAnEventName,
3332
} from '../shared/utils/event-names';
3433
import { getFileLocationFromJsx } from '../shared/utils/jsx-filename';
3534
import {
@@ -623,11 +622,6 @@ export const vnode_diff = (
623622
continue;
624623
}
625624

626-
if (isJsxPropertyAnEventName(key)) {
627-
// ignore jsx properties
628-
continue;
629-
}
630-
631625
if (key === 'ref') {
632626
if (isSignal(value)) {
633627
value.value = element;

packages/qwik/src/core/readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Register a listener on the current component's host element.
160160

161161
Used to programmatically add event listeners. Useful from custom `use*` methods, which do not have access to the JSX. Otherwise, it's adding a JSX listener in the `<div>` is a better idea.
162162

163-
Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem. You can always check the exact event name in the handler if needed.
163+
Events are case sensitive.
164164

165165
@see `useOn`, `useOnWindow`, `useOnDocument`.
166166

@@ -172,7 +172,7 @@ Register a listener on `window`.
172172

173173
Used to programmatically add event listeners. Useful from custom `use*` methods, which do not have access to the JSX.
174174

175-
Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem. You can always check the exact event name in the handler if needed.
175+
Events are case sensitive.
176176

177177
@see `useOn`, `useOnWindow`, `useOnDocument`.
178178

@@ -186,7 +186,7 @@ Register a listener on `document`.
186186

187187
Used to programmatically add event listeners. Useful from custom `use*` methods, which do not have access to the JSX.
188188

189-
Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem. You can always check the exact event name in the handler if needed.
189+
Events are case sensitive.
190190

191191
@see `useOn`, `useOnWindow`, `useOnDocument`.
192192

packages/qwik/src/core/shared/utils/event-names.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,23 @@ export function jsxEventToHtmlAttribute(jsxEvent: string): string | null {
4747

4848
if (idx !== -1) {
4949
const name = jsxEvent.slice(idx, -1);
50-
return createEventName(name, prefix);
50+
return name === 'DOMContentLoaded'
51+
? // The only DOM event that is not all lowercase
52+
prefix + '-d-o-m-content-loaded'
53+
: createEventName(
54+
name.charAt(0) === '-'
55+
? // marker for case sensitive event name
56+
name.slice(1)
57+
: name.toLowerCase(),
58+
prefix
59+
);
5160
}
5261
}
5362
return null; // Return null if not matching expected format
5463
}
5564

5665
export function createEventName(event: string, prefix: EventNameHtmlScope): string {
57-
const eventName = event === 'DOMContentLoaded' ? '-d-o-m-content-loaded' : event.toLowerCase();
66+
const eventName = fromCamelToKebabCase(event);
5867
return prefix + eventName;
5968
}
6069

@@ -86,15 +95,9 @@ export function isPreventDefault(key: string): boolean {
8695
return key.startsWith('preventdefault:');
8796
}
8897

89-
/**
90-
* Converts a camelCase string to kebab-case. This is used for event names.
91-
*
92-
* However, we do not escape `-` characters that are already present in the string. This means that
93-
* both `dbl-click` and `dblClick` will convert to `dbl-click`. This is intentional, it means that
94-
* `onCustom-Event$` works for both `custom-event` and `customEvent`.
95-
*/
98+
/** Converts a camelCase string to kebab-case. This is used for event names. */
9699
export const fromCamelToKebabCase = (text: string): string => {
97-
return text.replace(/([A-Z])/g, '-$1').toLowerCase();
100+
return text.replace(/([A-Z-])/g, (a) => '-' + a.toLowerCase());
98101
};
99102

100103
export const getEventDataFromHtmlAttribute = (htmlKey: string): [string, string] | null => {

packages/qwik/src/core/shared/utils/event-names.unit.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ import { jsxEventToHtmlAttribute } from './event-names';
1010
*/
1111
const testCases = [
1212
// default scope
13-
{ jsx: 'onClick$', html: 'on:click' },
13+
{ jsx: 'onClick$', html: 'on:click', eventName: 'click' },
1414
{ jsx: 'onDblClick$', html: 'on:dblclick' },
15-
{ jsx: 'on-customEvent$', html: 'on:-customevent' },
16-
{ jsx: 'on-CustomEvent$', html: 'on:-customevent' },
17-
{ jsx: 'onCustom-Event$', html: 'on:custom-event' },
18-
{ jsx: 'on-Custom-Event$', html: 'on:-custom-event' },
19-
{ jsx: 'on-Custom-event$', html: 'on:-custom-event' },
20-
{ jsx: 'onCustom-event$', html: 'on:custom-event' },
21-
{ jsx: 'on-custom-event$', html: 'on:-custom-event' },
22-
{ jsx: 'on--CustomEvent$', html: 'on:--customevent' },
15+
{ jsx: 'on--CustomEvent$', html: 'on:---custom-event' },
16+
{ jsx: 'on-Custom-Event$', html: 'on:-custom---event' },
17+
{ jsx: 'on-custom-event$', html: 'on:custom--event' },
18+
{ jsx: 'on-CustomEvent$', html: 'on:-custom-event' },
19+
{ jsx: 'on-customEvent$', html: 'on:custom-event' },
20+
{ jsx: 'onCustom-event$', html: 'on:custom--event' },
21+
{ jsx: 'onCustom-Event$', html: 'on:custom--event' },
2322
// exception for DOMContentLoaded
2423
{ jsx: 'onDOMContentLoaded$', html: 'on:-d-o-m-content-loaded' },
2524
// window scope

packages/qwik/src/core/tests/render-api.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const ManyEventsComponent = component$(() => {
8484
inlinedQrl(() => {}, 's_useOnFocus')
8585
);
8686
useOn(
87-
'-My-Custom',
87+
'My-Custom',
8888
inlinedQrl(() => {}, 's_useOnMyCustom')
8989
);
9090
return (
@@ -98,7 +98,7 @@ const ManyEventsComponent = component$(() => {
9898
<button
9999
onClick$={inlinedQrl(() => {}, 's_click2')}
100100
onBlur$={inlinedQrl(() => {}, 's_blur1')}
101-
onAnother-custom$={inlinedQrl(() => {}, 's_anotherCustom1')}
101+
on-anotherCustom$={inlinedQrl(() => {}, 's_anotherCustom1')}
102102
>
103103
click
104104
</button>
@@ -499,7 +499,7 @@ describe('render api', () => {
499499
qwikLoader: 'module',
500500
});
501501
expect(result.html).toContain(
502-
'(window.qwikevents||(window.qwikevents=[])).push("focus", "-my-custom", "click", "dblclick", "another-custom", "blur")'
502+
'(window.qwikevents||(window.qwikevents=[])).push("focus", "-my---custom", "click", "dblclick", "another-custom", "blur")'
503503
);
504504
});
505505
});

packages/qwik/src/core/tests/use-on.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ describe.each([
624624
const Counter = component$((props: { initial: number }) => {
625625
const count = useSignal(props.initial);
626626
useOn(
627-
'-Some-Custom-Event',
627+
'SomeCustomEvent',
628628
$(() => count.value++)
629629
);
630630
return <button>Count: {count.value}!</button>;
@@ -652,7 +652,7 @@ describe.each([
652652
it('should update counter for jsx event', async () => {
653653
const Counter = component$((props: { initial: number }) => {
654654
const count = useSignal(props.initial);
655-
return <button on-Some-Custom-Event$={() => count.value++}>Count: {count.value}!</button>;
655+
return <button on-SomeCustomEvent$={() => count.value++}>Count: {count.value}!</button>;
656656
});
657657

658658
const { vNode, container } = await render(<Counter initial={123} />, { debug });

packages/qwik/src/core/use/use-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export interface ContextId<STATE> {
122122
*/
123123
// </docs>
124124
export const createContextId = <STATE = unknown>(name: string): ContextId<STATE> => {
125-
assertTrue(/^[\w/.-]+$/.test(name), 'Context name must only contain A-Z,a-z,0-9, _', name);
125+
assertTrue(/^[\w/.-]+$/.test(name), 'Context name must only contain A-Z,a-z,0-9,_,.,-', name);
126126
return /*#__PURE__*/ Object.freeze({
127127
id: fromCamelToKebabCase(name),
128128
} as any);

packages/qwik/src/core/use/use-on.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
} from '../shared/jsx/types/jsx-qwik-attributes';
99
import type { HostElement } from '../shared/types';
1010
import { USE_ON_LOCAL, USE_ON_LOCAL_FLAGS, USE_ON_LOCAL_SEQ_IDX } from '../shared/utils/markers';
11-
import { EventNameHtmlScope, createEventName } from '../shared/utils/event-names';
11+
import { EventNameHtmlScope, fromCamelToKebabCase } from '../shared/utils/event-names';
1212

1313
export type EventQRL<T extends string = AllEventKeys> =
1414
| QRL<EventHandler<EventFromName<T>, Element>>
@@ -23,11 +23,7 @@ export type EventQRL<T extends string = AllEventKeys> =
2323
* Used to programmatically add event listeners. Useful from custom `use*` methods, which do not
2424
* have access to the JSX. Otherwise, it's adding a JSX listener in the `<div>` is a better idea.
2525
*
26-
* Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to
27-
* a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use
28-
* `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for
29-
* `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem.
30-
* You can always check the exact event name in the handler if needed.
26+
* Events are case sensitive.
3127
*
3228
* @public
3329
* @see `useOn`, `useOnWindow`, `useOnDocument`.
@@ -46,11 +42,7 @@ export const useOn = <T extends KnownEventNames>(event: T | T[], eventQrl: Event
4642
* Used to programmatically add event listeners. Useful from custom `use*` methods, which do not
4743
* have access to the JSX.
4844
*
49-
* Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to
50-
* a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use
51-
* `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for
52-
* `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem.
53-
* You can always check the exact event name in the handler if needed.
45+
* Events are case sensitive.
5446
*
5547
* @public
5648
* @see `useOn`, `useOnWindow`, `useOnDocument`.
@@ -85,11 +77,7 @@ export const useOnDocument = <T extends KnownEventNames>(event: T | T[], eventQr
8577
* Used to programmatically add event listeners. Useful from custom `use*` methods, which do not
8678
* have access to the JSX.
8779
*
88-
* Event names are converted to lowercase (except for `DOMContentLoaded`). If you need to listen to
89-
* a case-sensitive custom event, use kebab-case. For example, to listen to `CustomEvent`, use
90-
* `-Custom-Event` or `-custom-event`. This will listen for `CustomEvent`, but also for
91-
* `-custom-event`, `Custom-event` and `-customEvent`. In practice, this should not be a problem.
92-
* You can always check the exact event name in the handler if needed.
80+
* Events are case sensitive.
9381
*
9482
* @public
9583
* @see `useOn`, `useOnWindow`, `useOnDocument`.
@@ -124,10 +112,10 @@ const _useOn = (prefix: EventNameHtmlScope, eventName: string | string[], eventQ
124112
if (eventQrl) {
125113
if (Array.isArray(eventName)) {
126114
for (const event of eventName) {
127-
addEvent(createEventName(event, prefix), eventQrl);
115+
addEvent(prefix + fromCamelToKebabCase(event), eventQrl);
128116
}
129117
} else {
130-
addEvent(createEventName(eventName, prefix), eventQrl);
118+
addEvent(prefix + fromCamelToKebabCase(eventName), eventQrl);
131119
}
132120
}
133121
};

0 commit comments

Comments
 (0)