Skip to content

Commit a67284a

Browse files
authored
(feat) (svelte2tsx) Type check event forward from element on component (#303)
* event type check spec * transform component event handler * type check event bubbled from the element on component * bubble svelte:window and svelte:body * Component event bubble * bubble event again * tweak event type 1. $on return of function 2. wildcard event is CustomEvent now * tweak event type alias names * move computation of event def to event-handler
1 parent 35d4a67 commit a67284a

File tree

79 files changed

+318
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+318
-98
lines changed

packages/svelte2tsx/README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ function render() {
3333
<>
3434
<h1>hello {world}</h1>
3535
</>;
36-
return { props: { world }, slots: {} };
36+
return { props: { world }, slots: {}, events: {} };
3737
}
3838

3939
export default class {
4040
$$prop_def = __sveltets_partial(render().props);
41-
$$slot_def = render().slots;
41+
$$slot_def = render().slots
42+
$on = __sveltets_eventDef(render().events).$on;
4243
}
4344
```
4445

packages/svelte2tsx/src/htmlxtojsx.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const stripDoctype = (str: MagicString) => {
2626
if (result) str.remove(result.index, result.index + result[0].length);
2727
};
2828

29-
// eslint-disable-next-line max-len
3029
export function convertHtmlxToJsx(
3130
str: MagicString,
3231
ast: Node,
@@ -70,11 +69,22 @@ export function convertHtmlxToJsx(
7069
);
7170
}
7271
} else {
73-
//We don't know the type of the event handler
7472
if (attr.expression) {
73+
const on = 'on';
7574
//for handler assignment, we changeIt to call to our __sveltets_ensureFunction
76-
str.overwrite(attr.start, attr.expression.start, '{...__sveltets_ensureFunction((');
77-
str.overwrite(attr.expression.end, attr.end, '))}');
75+
str.appendRight(
76+
attr.start, `{__sveltets_instanceOf(${parent.name}).$`
77+
);
78+
const eventNameIndex = htmlx.indexOf(':', attr.start) + 1;
79+
str.overwrite(
80+
htmlx.indexOf(on, attr.start) + on.length,
81+
eventNameIndex,
82+
`('`
83+
);
84+
const eventEnd = htmlx.lastIndexOf('=', attr.expression.start);
85+
str.overwrite(eventEnd, attr.expression.start, `', `);
86+
str.overwrite(attr.expression.end, attr.end, ')}');
87+
str.move(attr.start, attr.end, parent.end);
7888
} else {
7989
//for passthrough handlers, we just remove
8090
str.remove(attr.start, attr.end);

packages/svelte2tsx/src/interfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
1414
scriptTag: Node;
1515
scriptDestination: number;
1616
slots: Map<string, Map<string, string>>;
17+
events: Map<string, string | string[]>;
1718
isTsFile: boolean;
1819
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Node } from 'estree-walker';
2+
3+
export function createEventHandlerTransformer() {
4+
const events = new Map<string, string | string[]>();
5+
6+
const handleEventHandler = (node: Node, parent: Node) => {
7+
const eventName = node.name;
8+
9+
const handleEventHandlerBubble = () => {
10+
const componentEventDef = `__sveltets_instanceOf(${parent.name})`;
11+
const exp = `__sveltets_bubbleEventDef(${componentEventDef}.$on, '${eventName}')`;
12+
13+
const exist = events.get(eventName);
14+
events.set(eventName, exist ? [].concat(exist, exp) : exp);
15+
};
16+
17+
// pass-through/ bubble
18+
if (!node.expression) {
19+
if (parent.type === 'InlineComponent') {
20+
handleEventHandlerBubble();
21+
} else {
22+
events.set(
23+
eventName,
24+
getEventDefExpressionForNonCompoent(eventName, parent)
25+
);
26+
}
27+
}
28+
};
29+
30+
return {
31+
handleEventHandler,
32+
getEvents: () => events,
33+
};
34+
}
35+
36+
function getEventDefExpressionForNonCompoent(eventName: string, ele: Node) {
37+
switch (ele.type) {
38+
case 'Element':
39+
return `__sveltets_mapElementEvent('${eventName}')`;
40+
case 'Body':
41+
return `__sveltets_mapBodyEvent('${eventName}')`;
42+
case 'Window':
43+
return `__sveltets_mapWindowEvent('${eventName}')`;
44+
default:
45+
break;
46+
}
47+
}
48+
49+
export function eventMapToString(events: Map<string, string | string[]>) {
50+
return '{' +
51+
Array.from(events.entries()).map(eventMapEntryToString).join(', ') +
52+
'}';
53+
54+
}
55+
56+
function eventMapEntryToString([eventName, expression]: [
57+
string,
58+
string | string[]
59+
]) {
60+
return `'${eventName}':${
61+
Array.isArray(expression) ? `[${expression}]` : expression
62+
}`;
63+
}

packages/svelte2tsx/src/svelte2tsx.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseHtmlx } from './htmlxparser';
66
import { convertHtmlxToJsx } from './htmlxtojsx';
77
import { Node } from 'estree-walker';
88
import * as ts from 'typescript';
9+
import { createEventHandlerTransformer, eventMapToString } from './nodes/event-handler';
910
import { findExortKeyword } from './utils/tsAst';
1011
import { InstanceScriptProcessResult, CreateRenderFunctionPara } from './interfaces';
1112
import { createRenderFunctionGetterStr, createClassGetters } from './nodes/exportgetters';
@@ -45,6 +46,7 @@ type TemplateProcessResult = {
4546
moduleScriptTag: Node;
4647
/** To be added later as a comment on the default class export */
4748
componentDocumentation: string | null;
49+
events: Map<string, string | string[]>;
4850
};
4951

5052
class Scope {
@@ -282,6 +284,8 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
282284
str.remove(node.start, node.end);
283285
};
284286

287+
const { handleEventHandler, getEvents } = createEventHandlerTransformer();
288+
285289
const onHtmlxWalk = (node: Node, parent: Node, prop: string) => {
286290
if (
287291
prop == 'params' &&
@@ -318,6 +322,9 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
318322
case 'ArrowFunctionExpression':
319323
enterArrowFunctionExpression();
320324
break;
325+
case 'EventHandler':
326+
handleEventHandler(node, parent);
327+
break;
321328
case 'VariableDeclarator':
322329
isDeclaration = true;
323330
break;
@@ -362,6 +369,7 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult {
362369
moduleScriptTag,
363370
scriptTag,
364371
slots,
372+
events: getEvents(),
365373
uses$$props,
366374
uses$$restProps,
367375
componentDocumentation,
@@ -526,7 +534,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
526534
if (
527535
(ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
528536
parent.operator !==
529-
ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */
537+
ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */
530538
) {
531539
let simpleOperator: string;
532540
if (parent.operator === ts.SyntaxKind.PlusPlusToken) {
@@ -853,6 +861,7 @@ function addComponentExport(
853861
className ? `${className} ` : ''
854862
}{\n $$prop_def = ${propDef}\n $$slot_def = render().slots` +
855863
createClassGetters(getters) +
864+
`\n $on = __sveltets_eventDef(render().events)` +
856865
'\n}';
857866

858867
str.append(statement);
@@ -891,6 +900,7 @@ function createRenderFunction({
891900
scriptDestination,
892901
slots,
893902
getters,
903+
events,
894904
exportedNames,
895905
isTsFile,
896906
uses$$props,
@@ -932,9 +942,11 @@ function createRenderFunction({
932942
.join(', ') +
933943
'}';
934944

935-
const returnString = `\nreturn { props: ${exportedNames.createPropsStr(
936-
isTsFile,
937-
)}, slots: ${slotsAsDef}, getters: ${createRenderFunctionGetterStr(getters)} }}`;
945+
const returnString =
946+
`\nreturn { props: ${exportedNames.createPropsStr(
947+
isTsFile
948+
)}, slots: ${slotsAsDef}, getters: ${createRenderFunctionGetterStr(getters)}` +
949+
`, events: ${eventMapToString(events)} }}`;
938950
str.append(returnString);
939951
}
940952

@@ -950,6 +962,7 @@ export function svelte2tsx(
950962
slots,
951963
uses$$props,
952964
uses$$restProps,
965+
events,
953966
componentDocumentation,
954967
} = processSvelteTemplate(str);
955968

@@ -991,6 +1004,7 @@ export function svelte2tsx(
9911004
scriptTag,
9921005
scriptDestination: instanceScriptTarget,
9931006
slots,
1007+
events,
9941008
getters,
9951009
exportedNames,
9961010
isTsFile: options?.isTsFile,

packages/svelte2tsx/svelte-shims.d.ts

+42-18
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
11
declare module '*.svelte' {
22
export default class {
3-
$$prop_def: any;
4-
$$slot_def: any;
3+
$$prop_def: any;
4+
$$slot_def: any;
5+
6+
$on(event: string, handler: (e: Event) => any): () => void
57
}
68
}
79

8-
type AConstructorTypeOf<T> = new (...args:any[]) => T;
10+
type AConstructorTypeOf<T> = new (...args: any[]) => T;
911

1012
type SvelteAction<U extends any[]> = (node: HTMLElement, ...args:U) => {
1113
update?: (...args:U) => void,
1214
destroy?: () => void
1315
} | void
1416

15-
1617
type SvelteTransitionConfig = {
17-
delay?: number,
18-
duration?: number,
19-
easing?: (t: number) => number,
20-
css?: (t: number, u: number) => string,
21-
tick?: (t: number, u: number) => void
18+
delay?: number,
19+
duration?: number,
20+
easing?: (t: number) => number,
21+
css?: (t: number, u: number) => string,
22+
tick?: (t: number, u: number) => void
2223
}
2324

24-
type SvelteTransition<U extends any[]> = (node: Element, ...args:U) => SvelteTransitionConfig | (() => SvelteTransitionConfig)
25+
type SvelteTransition<U extends any[]> = (node: Element, ...args: U) => SvelteTransitionConfig | (() => SvelteTransitionConfig)
2526

26-
type SvelteAnimation<U extends any[]> = (node: Element, move: { from: DOMRect, to: DOMRect}, ...args:U) => {
27+
type SvelteAnimation<U extends any[]> = (node: Element, move: { from: DOMRect, to: DOMRect }, ...args: U) => {
2728
delay?: number,
28-
duration?: number,
29-
easing?: (t: number) => number,
30-
css?: (t: number, u: number) => string,
31-
tick?: (t: number, u: number) => void
29+
duration?: number,
30+
easing?: (t: number) => number,
31+
css?: (t: number, u: number) => string,
32+
tick?: (t: number, u: number) => void
3233
}
3334

34-
type SvelteAllProps = { [index: string]: any }
35+
type SvelteAllProps = { [index: string]: any }
3536
type SvelteRestProps = { [index: string]: any }
36-
type SvelteStore<T> = { subscribe: (run: (value:T) => any, invalidate?: any) => any }
37+
type SvelteStore<T> = { subscribe: (run: (value: T) => any, invalidate?: any) => any }
3738
type SvelteComponent = import('*.svelte').default
39+
type SvelteEventRecord = Record<string, Event | Event[]>
40+
type SvelteExtractEvent<T> = T extends any[] ? T[number] : T;
41+
type SvelteOnEvent<T, K extends keyof T> = (
42+
event: K,
43+
handler: (e: SvelteExtractEvent<T[K]>) => any
44+
) => () => void;
45+
type SvelteRestEvent = (event: string, handler: (e: CustomEvent) => any) => () => void
46+
type SvelteOnAllEvent<T> = SvelteOnEvent<T, keyof T> & SvelteRestEvent
3847

3948
declare var process: NodeJS.Process & { browser: boolean }
4049

@@ -49,8 +58,23 @@ declare function __sveltets_restPropsType(): SvelteRestProps
4958
declare function __sveltets_partial<T>(obj: T): Partial<T>;
5059
declare function __sveltets_partial_with_any<T>(obj: T): Partial<T> & SvelteAllProps
5160
declare function __sveltets_with_any<T>(obj: T): T & SvelteAllProps
52-
declare function __sveltets_store_get<T=any>(store: SvelteStore<T>): T
61+
declare function __sveltets_store_get<T = any>(store: SvelteStore<T>): T
5362
declare function __sveltets_any(dummy: any): any;
5463
declare function __sveltets_empty(dummy: any): {};
5564
declare function __sveltets_componentType(): AConstructorTypeOf<SvelteComponent>
5665
declare function __sveltets_invalidate<T>(getValue: () => T): T
66+
declare function __sveltets_eventDef<T extends SvelteEventRecord>(def: T): SvelteOnAllEvent<T>
67+
declare function __sveltets_mapWindowEvent<K extends keyof HTMLBodyElementEventMap>(
68+
event: K
69+
): HTMLBodyElementEventMap[K];
70+
declare function __sveltets_mapBodyEvent<K extends keyof WindowEventMap>(
71+
event: K
72+
): WindowEventMap[K];
73+
declare function __sveltets_mapElementEvent<K extends keyof HTMLElementEventMap>(
74+
event: K
75+
): HTMLElementEventMap[K];
76+
declare function __sveltets_bubbleEventDef<
77+
T extends SvelteEventRecord,
78+
TEvent,
79+
TKey extends keyof T = TEvent extends keyof T ? TEvent : string
80+
>(on: SvelteOnAllEvent<T>, event: TEvent): T[TKey];

packages/svelte2tsx/test/htmlx2jsx/samples/directive-quoted/expected.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<><h1 onclick={()=>console.log("click")}>Hello</h1>
2-
<Component {...__sveltets_ensureFunction((test))}/>
2+
<Component />{__sveltets_instanceOf(Component).$on('click', test)}
33
<img {...__sveltets_ensureAction(action,thing)} />
44
<img {...__sveltets_ensureTransition(fade, params)} />
55
<img {...__sveltets_ensureType(Boolean, !!(classthing))} />
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<><Component {...__sveltets_ensureFunction((() => click()))} {...__sveltets_ensureFunction((() => log('hi')))}/></>
1+
<><Component />{__sveltets_instanceOf(Component).$on('event', () => click())}{__sveltets_instanceOf(Component).$on('UpperCaseEvent', () => log('hi'))}</>

packages/svelte2tsx/test/sourcemaps/event-binding.html

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<></>;function render() {
2-
<><Component {...__sveltets_ensureFunction((__sveltets_store_get(check) ? method1 : method2))} />
3-
1==== 2==================
2+
<><Component />{__sveltets_instanceOf(Component).$on('click', __sveltets_store_get(check) ? method1 : method2)}
3+
1==== 2==================
44
<button onclick={__sveltets_store_get(check) ? method1 : method2} >Bla</button></>
55
3==== 4==================
6-
return { props: {}, slots: {}, getters: {} }}
6+
return { props: {}, slots: {}, getters: {}, events: {} }}
77

88
export default class {
99
$$prop_def = __sveltets_partial(render().props)
1010
$$slot_def = render().slots
11+
$on = __sveltets_eventDef(render().events)
1112
}
1213
!Expected
1314
<Component on:click={$check ? method1 : method2} />

packages/svelte2tsx/test/sourcemaps/let.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
;
55
<>
66
</>
7-
return { props: {}, slots: {}, getters: {} }}
7+
return { props: {}, slots: {}, getters: {}, events: {} }}
88

99
export default class {
1010
$$prop_def = __sveltets_partial(render().props)
1111
$$slot_def = render().slots
12+
$on = __sveltets_eventDef(render().events)
1213
}
1314
!Expected
1415
<script>

packages/svelte2tsx/test/sourcemaps/repl.html

+4-3
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@
165165
rollupUrl={rollupUrl}
166166
orientation={mobile ? 'columns' : 'rows'}
167167
fixed={mobile}
168-
{...__sveltets_ensureFunction((handle_change))}
168+
169169
injectedJS={mapbox_setup}
170170
relaxed
171-
/>
171+
/>{__sveltets_instanceOf(Repl).$on('change', handle_change)}
172172
</div>
173173
</div>
174174

@@ -177,11 +177,12 @@
177177
</>}}}
178178
</div>
179179
</>
180-
return { props: {slug: slug , chapter: chapter}, slots: {}, getters: {} }}
180+
return { props: {slug: slug , chapter: chapter}, slots: {}, getters: {}, events: {} }}
181181

182182
export default class {
183183
$$prop_def = __sveltets_partial(render().props)
184184
$$slot_def = render().slots
185+
$on = __sveltets_eventDef(render().events)
185186
}
186187
!Expected
187188
<script context="module">

packages/svelte2tsx/test/svelte2tsx/samples/array-binding-export/expected.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
let [a,b,c] = [1,2,3];
44
;
55
<></>
6-
return { props: {a: a , b: b , c: c}, slots: {}, getters: {} }}
6+
return { props: {a: a , b: b , c: c}, slots: {}, getters: {}, events: {} }}
77

88
export default class Input__SvelteComponent_ {
99
$$prop_def = __sveltets_partial(render().props)
1010
$$slot_def = render().slots
11+
$on = __sveltets_eventDef(render().events)
1112
}

0 commit comments

Comments
 (0)