diff --git a/.changeset/witty-balloons-thank.md b/.changeset/witty-balloons-thank.md
new file mode 100644
index 00000000000..66b28520792
--- /dev/null
+++ b/.changeset/witty-balloons-thank.md
@@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': patch
+---
+
+fix: serialize var prop
diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts
index d7565a4b622..6a42e8f3da5 100644
--- a/packages/qwik/src/core/client/vnode-diff.ts
+++ b/packages/qwik/src/core/client/vnode-diff.ts
@@ -723,8 +723,7 @@ export const vnode_diff = (
const jsxAttrs = [] as ClientAttrs;
const props = jsx.varProps;
for (const key in props) {
- let value = props[key];
- value = serializeAttribute(key, value, scopedStyleIdPrefix);
+ const value = props[key];
if (value != null) {
mapArray_set(jsxAttrs, key, value, 0);
}
@@ -791,7 +790,7 @@ export const vnode_diff = (
value = untrack(() => value.value);
}
- vnode_setAttr(journal, vnode, key, value);
+ vnode_setAttr(journal, vnode, key, serializeAttribute(key, value, scopedStyleIdPrefix));
if (value === null) {
// if we set `null` than attribute was removed and we need to shorten the dstLength
dstLength = dstAttrs.length;
@@ -830,20 +829,20 @@ export const vnode_diff = (
if (dstKey && isHtmlAttributeAnEventName(dstKey)) {
patchEventDispatch = true;
dstIdx++;
- } else {
- record(dstKey!, null);
+ } else if (dstKey) {
+ record(dstKey, null);
dstIdx--;
}
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
} else if (dstKey == null) {
// Destination has more keys, so we need to insert them from source.
const isEvent = isJsxPropertyAnEventName(srcKey);
- if (isEvent) {
+ if (srcKey && isEvent) {
// Special handling for events
patchEventDispatch = true;
recordJsxEvent(srcKey, srcAttrs[srcIdx]);
- } else {
- record(srcKey!, srcAttrs[srcIdx]);
+ } else if (srcKey) {
+ record(srcKey, srcAttrs[srcIdx]);
}
srcIdx++;
srcKey = srcIdx < srcLength ? srcAttrs[srcIdx++] : null;
@@ -874,11 +873,11 @@ export const vnode_diff = (
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
} else {
// Source is missing the key, so we need to remove it from destination.
- if (isHtmlAttributeAnEventName(dstKey)) {
+ if (dstKey && isHtmlAttributeAnEventName(dstKey)) {
patchEventDispatch = true;
dstIdx++;
- } else {
- record(dstKey!, null);
+ } else if (dstKey) {
+ record(dstKey, null);
dstIdx--;
}
dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null;
diff --git a/packages/qwik/src/core/tests/attributes.spec.tsx b/packages/qwik/src/core/tests/attributes.spec.tsx
index 07dfc480fb9..320499aeb91 100644
--- a/packages/qwik/src/core/tests/attributes.spec.tsx
+++ b/packages/qwik/src/core/tests/attributes.spec.tsx
@@ -119,50 +119,84 @@ describe.each([
);
});
- it('should bind checked attribute', async () => {
- const BindCmp = component$(() => {
- const show = useSignal(false);
- return (
- <>
-
-
{show.value.toString()}
- >
+ describe('binding', () => {
+ it('should bind checked attribute', async () => {
+ const BindCmp = component$(() => {
+ const show = useSignal(false);
+ return (
+ <>
+
+ {show.value.toString()}
+ >
+ );
+ });
+
+ const { vNode, document } = await render(, { debug });
+
+ expect(vNode).toMatchVDOM(
+
+
+
+ false
+
+
+ );
+
+ // simulate checkbox click
+ const input = document.querySelector('input')!;
+ input.checked = true;
+ await trigger(document.body, 'input', 'input');
+
+ expect(vNode).toMatchVDOM(
+
+
+
+ true
+
+
);
});
- const { vNode, document } = await render(, { debug });
+ it('should bind textarea value', async () => {
+ const Cmp = component$(() => {
+ const value = useSignal('123');
+ return (
+
+
+
+
+ );
+ });
+ const { document } = await render(, { debug });
- expect(vNode).toMatchVDOM(
-
-
-
- false
-
-
- );
+ await expect(document.querySelector('div')).toMatchDOM(
+
+
+
+
+ );
- // simulate checkbox click
- const input = document.querySelector('input')!;
- input.checked = true;
- await trigger(document.body, 'input', 'input');
+ // simulate input
+ const textarea = document.querySelector('textarea')!;
+ textarea.value = 'abcd';
+ await trigger(document.body, textarea, 'input');
- expect(vNode).toMatchVDOM(
-
-
-
- true
-
-
- );
+ await expect(document.querySelector('div')).toMatchDOM(
+
+
+
+
+ );
+ });
});
it('should render preventdefault attribute', async () => {
diff --git a/packages/qwik/src/core/tests/render-styles.spec.tsx b/packages/qwik/src/core/tests/render-styles.spec.tsx
index f933a63ca8f..23298cdd78a 100644
--- a/packages/qwik/src/core/tests/render-styles.spec.tsx
+++ b/packages/qwik/src/core/tests/render-styles.spec.tsx
@@ -1,9 +1,15 @@
import {
Fragment as Component,
component$,
+ createContextId,
Fragment,
Fragment as Signal,
+ type Signal as SignalType,
useStore,
+ useContext,
+ useComputed$,
+ useSignal,
+ useContextProvider,
} from '@qwik.dev/core';
import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
import { describe, expect, it } from 'vitest';
@@ -70,4 +76,71 @@ describe.each([
);
});
+
+ it('should render styles from computed and context', async () => {
+ const Ctx = createContextId<{
+ val: SignalType>;
+ abc: SignalType;
+ }>('test');
+
+ const Child = component$(() => {
+ const ctx = useContext(Ctx);
+ // use computed to add context value as dependency
+ const color = useComputed$(() => (ctx.abc.value > 0 ? 'red' : 'green'));
+ return (
+ // use spread props to convert div props to var props
+
+ Abcd
+
+ );
+ });
+
+ const Parent = component$(() => {
+ const signal = useSignal(0);
+ // use computed to create a new object
+ const comp = useComputed$>(() => ({
+ 'data-value': signal.value,
+ }));
+ useContextProvider(Ctx, {
+ val: comp,
+ abc: signal,
+ });
+ return (
+
+
+
+
+ );
+ });
+
+ const { vNode, container } = await render(, { debug });
+
+ expect(vNode).toMatchVDOM(
+
+
+
+ );
+
+ await trigger(container.element, 'button', 'click');
+
+ expect(vNode).toMatchVDOM(
+
+
+
+ );
+ });
});
diff --git a/packages/qwik/src/core/tests/use-signal.spec.tsx b/packages/qwik/src/core/tests/use-signal.spec.tsx
index 8b8c03441ad..c2c8de9a911 100644
--- a/packages/qwik/src/core/tests/use-signal.spec.tsx
+++ b/packages/qwik/src/core/tests/use-signal.spec.tsx
@@ -553,86 +553,6 @@ describe.each([
});
});
- describe('binding', () => {
- it('should bind checked attribute', async () => {
- const BindCmp = component$(() => {
- const show = useSignal(false);
- return (
- <>
-
- {show.value.toString()}
- >
- );
- });
-
- const { vNode, document } = await render(, { debug });
-
- expect(vNode).toMatchVDOM(
-
-
-
- false
-
-
- );
-
- // simulate checkbox click
- const input = document.querySelector('input')!;
- input.checked = true;
- await trigger(document.body, 'input', 'input');
-
- expect(vNode).toMatchVDOM(
-
-
-
- true
-
-
- );
- });
-
- it('should bind textarea value', async () => {
- const Cmp = component$(() => {
- const value = useSignal('123');
- return (
-
-
-
-
- );
- });
- const { document } = await render(, { debug });
-
- await expect(document.querySelector('div')).toMatchDOM(
-
-
-
-
- );
-
- // simulate input
- const textarea = document.querySelector('textarea')!;
- textarea.value = 'abcd';
- await trigger(document.body, textarea, 'input');
-
- await expect(document.querySelector('div')).toMatchDOM(
-
-
-
-
- );
- });
- });
-
describe('regression', () => {
it('#4249 - should render signal text with double condition', async () => {
const Issue4249 = component$(() => {