Skip to content

Commit 85e0c73

Browse files
authored
Merge pull request #7929 from QwikDev/v2-spread-props-element-node
fix: handling spread props on element node
2 parents 442273b + 47c9284 commit 85e0c73

File tree

6 files changed

+421
-7
lines changed

6 files changed

+421
-7
lines changed

.changeset/plain-eggs-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: handling spread props on element node

packages/qwik/src/core/tests/attributes.spec.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,200 @@ describe.each([
533533
);
534534
});
535535

536+
describe('spread props', () => {
537+
describe('class attribute from component props', () => {
538+
it('should use class attribute from element', async () => {
539+
const Cmp = component$((props: PropsOf<'div'>) => {
540+
return <div {...props} class={[props.class, 'component']} />;
541+
});
542+
543+
const Parent = component$(() => {
544+
return <Cmp class="test" />;
545+
});
546+
547+
const { vNode } = await render(<Parent />, { debug });
548+
expect(vNode).toMatchVDOM(
549+
<Component>
550+
<Component>
551+
<div class="test component"></div>
552+
</Component>
553+
</Component>
554+
);
555+
});
556+
557+
it('should use class attribute from props', async () => {
558+
const Cmp = component$((props: PropsOf<'div'>) => {
559+
return <div class={[props.class, 'component']} {...props} />;
560+
});
561+
562+
const Parent = component$(() => {
563+
return <Cmp class="test" />;
564+
});
565+
566+
const { vNode } = await render(<Parent />, { debug });
567+
expect(vNode).toMatchVDOM(
568+
<Component>
569+
<Component>
570+
<div class="test"></div>
571+
</Component>
572+
</Component>
573+
);
574+
});
575+
});
576+
577+
describe('class attribute without component props', () => {
578+
it('should use class attribute from props', async () => {
579+
const Cmp = component$((props: PropsOf<'div'>) => {
580+
return <div class="test component" {...props} />;
581+
});
582+
583+
const Parent = component$(() => {
584+
return <Cmp class="test" />;
585+
});
586+
587+
const { vNode } = await render(<Parent />, { debug });
588+
expect(vNode).toMatchVDOM(
589+
<Component>
590+
<Component>
591+
<div class="test"></div>
592+
</Component>
593+
</Component>
594+
);
595+
});
596+
597+
it('should use class attribute from element', async () => {
598+
const Cmp = component$((props: PropsOf<'div'>) => {
599+
return <div {...props} class="test component" />;
600+
});
601+
602+
const Parent = component$(() => {
603+
return <Cmp class="test" />;
604+
});
605+
606+
const { vNode } = await render(<Parent />, { debug });
607+
expect(vNode).toMatchVDOM(
608+
<Component>
609+
<Component>
610+
<div class="test component"></div>
611+
</Component>
612+
</Component>
613+
);
614+
});
615+
});
616+
617+
it('should handle multiple spread component props', async () => {
618+
const Cmp = component$((props: PropsOf<'div'>) => {
619+
return <div {...props} class="test component" {...props} />;
620+
});
621+
622+
const Parent = component$(() => {
623+
return <Cmp class="test" />;
624+
});
625+
626+
const { vNode } = await render(<Parent />, { debug });
627+
628+
expect(vNode).toMatchVDOM(
629+
<Component>
630+
<Component>
631+
<div class="test"></div>
632+
</Component>
633+
</Component>
634+
);
635+
});
636+
637+
it('should handle multiple spread component and element props', async () => {
638+
const Cmp = component$((props: PropsOf<'div'>) => {
639+
const attrs: Record<string, any> = {
640+
class: 'test2',
641+
};
642+
return <div {...props} class="test component" {...attrs} />;
643+
});
644+
645+
const Parent = component$(() => {
646+
return <Cmp class="test" />;
647+
});
648+
649+
const { vNode } = await render(<Parent />, { debug });
650+
651+
expect(vNode).toMatchVDOM(
652+
<Component>
653+
<Component>
654+
<div class="test2"></div>
655+
</Component>
656+
</Component>
657+
);
658+
});
659+
660+
it('should handle multiple spread element and component props', async () => {
661+
const Cmp = component$((props: PropsOf<'div'>) => {
662+
const attrs: Record<string, any> = {
663+
class: 'test2',
664+
};
665+
return <div {...attrs} class="test component" {...props} />;
666+
});
667+
668+
const Parent = component$(() => {
669+
return <Cmp class="test" />;
670+
});
671+
672+
const { vNode } = await render(<Parent />, { debug });
673+
674+
expect(vNode).toMatchVDOM(
675+
<Component>
676+
<Component>
677+
<div class="test"></div>
678+
</Component>
679+
</Component>
680+
);
681+
});
682+
683+
it('should handle multiple spread element and component props before normal props', async () => {
684+
const Cmp = component$((props: PropsOf<'div'>) => {
685+
const attrs: Record<string, any> = {
686+
class: 'test2',
687+
};
688+
return <div {...attrs} {...props} class="test component" />;
689+
});
690+
691+
const Parent = component$(() => {
692+
return <Cmp class="test" />;
693+
});
694+
695+
const { vNode } = await render(<Parent />, { debug });
696+
697+
expect(vNode).toMatchVDOM(
698+
<Component>
699+
<Component>
700+
<div class="test component"></div>
701+
</Component>
702+
</Component>
703+
);
704+
});
705+
706+
it('should handle multiple spread element and component props after normal props', async () => {
707+
const Cmp = component$((props: PropsOf<'div'>) => {
708+
const attrs: Record<string, any> = {
709+
class: 'test2',
710+
};
711+
return <div class="test component" {...props} {...attrs} />;
712+
});
713+
714+
const Parent = component$(() => {
715+
return <Cmp class="test" />;
716+
});
717+
718+
const { vNode } = await render(<Parent />, { debug });
719+
720+
expect(vNode).toMatchVDOM(
721+
<Component>
722+
<Component>
723+
<div class="test2"></div>
724+
</Component>
725+
</Component>
726+
);
727+
});
728+
});
729+
536730
describe('class attribute', () => {
537731
it('should render class attribute', async () => {
538732
const Cmp = component$(() => {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
source: packages/qwik/src/optimizer/core/src/test.rs
3+
assertion_line: 4600
4+
expression: output
5+
---
6+
==INPUT==
7+
8+
9+
import { component$ } from '@qwik.dev/core';
10+
11+
export default component$((props) => {
12+
return <div {...props} class={[props.class, 'component']} />;
13+
});
14+
15+
============================= test.js ==
16+
17+
import { componentQrl } from "@qwik.dev/core";
18+
import { qrl } from "@qwik.dev/core";
19+
const i_LUXeXe0DQrg = ()=>import("./test.tsx_test_component_LUXeXe0DQrg");
20+
export default /*#__PURE__*/ componentQrl(/*#__PURE__*/ qrl(i_LUXeXe0DQrg, "test_component_LUXeXe0DQrg"));
21+
22+
23+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;AAGE,6BAAe,6EAEZ\"}")
24+
============================= test.tsx_test_component_LUXeXe0DQrg.js (ENTRY POINT)==
25+
26+
import { _fnSignal } from "@qwik.dev/core";
27+
import { _getConstProps } from "@qwik.dev/core";
28+
import { _getVarProps } from "@qwik.dev/core";
29+
import { _jsxSplit } from "@qwik.dev/core";
30+
export const test_component_LUXeXe0DQrg = (props)=>{
31+
return /*#__PURE__*/ _jsxSplit("div", {
32+
..._getVarProps(props),
33+
..._getConstProps(props),
34+
class: _fnSignal((p0)=>[
35+
p0.class,
36+
'component'
37+
], [
38+
props
39+
], '[p0.class,"component"]')
40+
}, null, null, 0, "u6_0");
41+
};
42+
43+
44+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;0CAG4B,CAAC;IAC1B,qBAAO,UAAC;wBAAQ;0BAAA;QAAO,KAAK,kBAAE;gBAAC,GAAM,KAAK;gBAAE;aAAY;;;;AACzD\"}")
45+
/*
46+
{
47+
"origin": "test.tsx",
48+
"name": "test_component_LUXeXe0DQrg",
49+
"entry": null,
50+
"displayName": "test.tsx_test_component",
51+
"hash": "LUXeXe0DQrg",
52+
"canonicalFilename": "test.tsx_test_component_LUXeXe0DQrg",
53+
"path": "",
54+
"extension": "js",
55+
"parent": null,
56+
"ctxKind": "function",
57+
"ctxName": "component$",
58+
"captures": false,
59+
"loc": [
60+
78,
61+
159
62+
],
63+
"paramNames": [
64+
"props"
65+
]
66+
}
67+
*/
68+
== DIAGNOSTICS ==
69+
70+
[]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
source: packages/qwik/src/optimizer/core/src/test.rs
3+
assertion_line: 4617
4+
expression: output
5+
---
6+
==INPUT==
7+
8+
9+
import { component$ } from '@qwik.dev/core';
10+
11+
export default component$((props) => {
12+
return <div {...props} class={[props.class, 'component']} {...props} />;
13+
});
14+
15+
============================= test.js ==
16+
17+
import { componentQrl } from "@qwik.dev/core";
18+
import { qrl } from "@qwik.dev/core";
19+
const i_LUXeXe0DQrg = ()=>import("./test.tsx_test_component_LUXeXe0DQrg");
20+
export default /*#__PURE__*/ componentQrl(/*#__PURE__*/ qrl(i_LUXeXe0DQrg, "test_component_LUXeXe0DQrg"));
21+
22+
23+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;AAGE,6BAAe,6EAEZ\"}")
24+
============================= test.tsx_test_component_LUXeXe0DQrg.js (ENTRY POINT)==
25+
26+
import { _fnSignal } from "@qwik.dev/core";
27+
import { _getConstProps } from "@qwik.dev/core";
28+
import { _getVarProps } from "@qwik.dev/core";
29+
import { _jsxSplit } from "@qwik.dev/core";
30+
export const test_component_LUXeXe0DQrg = (props)=>{
31+
return /*#__PURE__*/ _jsxSplit("div", {
32+
..._getVarProps(props),
33+
..._getConstProps(props),
34+
class: _fnSignal((p0)=>[
35+
p0.class,
36+
'component'
37+
], [
38+
props
39+
], '[p0.class,"component"]'),
40+
..._getVarProps(props)
41+
}, _getConstProps(props), null, 0, "u6_0");
42+
};
43+
44+
45+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;0CAG4B,CAAC;IAC1B,qBAAO,UAAC;wBAAQ;0BAAA;QAAO,KAAK,kBAAE;gBAAC,GAAM,KAAK;gBAAE;aAAY;;;wBAAM;sBAAA;AAC/D\"}")
46+
/*
47+
{
48+
"origin": "test.tsx",
49+
"name": "test_component_LUXeXe0DQrg",
50+
"entry": null,
51+
"displayName": "test.tsx_test_component",
52+
"hash": "LUXeXe0DQrg",
53+
"canonicalFilename": "test.tsx_test_component_LUXeXe0DQrg",
54+
"path": "",
55+
"extension": "js",
56+
"parent": null,
57+
"ctxKind": "function",
58+
"ctxName": "component$",
59+
"captures": false,
60+
"loc": [
61+
78,
62+
170
63+
],
64+
"paramNames": [
65+
"props"
66+
]
67+
}
68+
*/
69+
== DIAGNOSTICS ==
70+
71+
[]

packages/qwik/src/optimizer/core/src/test.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4595,6 +4595,40 @@ fn should_convert_rest_props() {
45954595
});
45964596
}
45974597

4598+
#[test]
4599+
fn should_merge_attributes_with_spread_props() {
4600+
test_input!(TestInput {
4601+
code: r#"
4602+
import { component$ } from '@qwik.dev/core';
4603+
4604+
export default component$((props) => {
4605+
return <div {...props} class={[props.class, 'component']} />;
4606+
});
4607+
"#
4608+
.to_string(),
4609+
transpile_ts: true,
4610+
transpile_jsx: true,
4611+
..TestInput::default()
4612+
});
4613+
}
4614+
4615+
#[test]
4616+
fn should_merge_attributes_with_spread_props_before_and_after() {
4617+
test_input!(TestInput {
4618+
code: r#"
4619+
import { component$ } from '@qwik.dev/core';
4620+
4621+
export default component$((props) => {
4622+
return <div {...props} class={[props.class, 'component']} {...props} />;
4623+
});
4624+
"#
4625+
.to_string(),
4626+
transpile_ts: true,
4627+
transpile_jsx: true,
4628+
..TestInput::default()
4629+
});
4630+
}
4631+
45984632
// TODO(misko): Make this test work by implementing strict serialization.
45994633
// #[test]
46004634
// fn example_of_synchronous_qrl_that_cant_be_serialized() {

0 commit comments

Comments
 (0)