From 5f2536b51df93bfd51098c48220d647e7ad3954c Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 28 Aug 2024 12:41:35 +0200 Subject: [PATCH] fix: handle preact signals in array correctly (#11834) * fix: handle preact signals in array correctly * feat: serialize signals in object --- .changeset/eleven-cameras-sing.md | 5 ++ .../src/components/SignalsInArray.jsx | 8 +++ .../src/components/SignalsInObject.jsx | 8 +++ .../preact-component/src/pages/signals.astro | 5 ++ packages/astro/test/preact-component.test.js | 40 ++++++++++++++ packages/integrations/preact/src/client.ts | 31 +++++++++-- packages/integrations/preact/src/signals.ts | 55 +++++++++++++++---- packages/integrations/preact/src/types.ts | 6 +- 8 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 .changeset/eleven-cameras-sing.md create mode 100644 packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx create mode 100644 packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx diff --git a/.changeset/eleven-cameras-sing.md b/.changeset/eleven-cameras-sing.md new file mode 100644 index 0000000000..51092c495a --- /dev/null +++ b/.changeset/eleven-cameras-sing.md @@ -0,0 +1,5 @@ +--- +'@astrojs/preact': patch +--- + +Preact signals are now serialized correctly in arrays when they are given to components. diff --git a/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx b/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx new file mode 100644 index 0000000000..69940f7306 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx @@ -0,0 +1,8 @@ +import { h } from 'preact'; + +export default ({ signalsArray }) => { + return
+

{signalsArray[0]} {signalsArray[3]}

+

{signalsArray[1].value}-{signalsArray[2].value}-{signalsArray[4].value}

+
+} diff --git a/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx b/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx new file mode 100644 index 0000000000..6187ce8c59 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx @@ -0,0 +1,8 @@ +import { h } from 'preact'; + +export default ({ signalsObject }) => { + return
+

{signalsObject.title}

+

{signalsObject.counter.value}

+
+} diff --git a/packages/astro/test/fixtures/preact-component/src/pages/signals.astro b/packages/astro/test/fixtures/preact-component/src/pages/signals.astro index b68fde36d6..37b43a73c7 100644 --- a/packages/astro/test/fixtures/preact-component/src/pages/signals.astro +++ b/packages/astro/test/fixtures/preact-component/src/pages/signals.astro @@ -1,7 +1,10 @@ --- import { signal } from '@preact/signals'; import Signals from '../components/Signals'; +import SignalsInArray from '../components/SignalsInArray'; +import SignalsInObject from '../components/SignalsInObject'; const count = signal(1); +const secondCount = signal(2); --- @@ -10,5 +13,7 @@ const count = signal(1); + + diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.js index e8c89d5bf4..f5b5c7233b 100644 --- a/packages/astro/test/preact-component.test.js +++ b/packages/astro/test/preact-component.test.js @@ -100,4 +100,44 @@ describe('Preact component', () => { assert.notEqual(sigs1.count, undefined); assert.equal(sigs1.count, sigs2.count); }); + + it('Can use signals in array', async () => { + const html = await fixture.readFile('/signals/index.html'); + const $ = cheerio.load(html); + const element = $('.preact-signal-array'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[2]).attr('data-preact-signals'); + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsArray: [ + ['p0', 1], + ['p0', 2], + ['p1', 4], + ], + }); + + assert.equal(element.find('h1').text(), "I'm not a signal 12345"); + assert.equal(element.find('p').text(), '1-1-2'); + }); + + it('Can use signals in object', async () => { + const html = await fixture.readFile('/signals/index.html'); + const $ = cheerio.load(html); + const element = $('.preact-signal-object'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[3]).attr('data-preact-signals'); + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsObject: [['p0', 'counter']], + }); + + assert.equal(element.find('h1').text(), 'I am a title'); + assert.equal(element.find('p').text(), '1'); + }); }); diff --git a/packages/integrations/preact/src/client.ts b/packages/integrations/preact/src/client.ts index 3fb36d22a1..fa20a99283 100644 --- a/packages/integrations/preact/src/client.ts +++ b/packages/integrations/preact/src/client.ts @@ -18,13 +18,34 @@ export default (element: HTMLElement) => let signalsRaw = element.dataset.preactSignals; if (signalsRaw) { const { signal } = await import('@preact/signals'); - let signals: Record = JSON.parse(element.dataset.preactSignals!); + let signals: Record = JSON.parse( + element.dataset.preactSignals!, + ); for (const [propName, signalId] of Object.entries(signals)) { - if (!sharedSignalMap.has(signalId)) { - const signalValue = signal(props[propName]); - sharedSignalMap.set(signalId, signalValue); + if (Array.isArray(signalId)) { + signalId.forEach(([id, indexOrKeyInProps]) => { + const mapValue = props[propName][indexOrKeyInProps]; + let valueOfSignal = mapValue; + + // not an property key + if(typeof indexOrKeyInProps !== 'string') { + valueOfSignal = mapValue[0]; + indexOrKeyInProps = mapValue[1]; + } + + if (!sharedSignalMap.has(id)) { + const signalValue = signal(valueOfSignal); + sharedSignalMap.set(id, signalValue); + } + props[propName][indexOrKeyInProps] = sharedSignalMap.get(id); + }); + } else { + if (!sharedSignalMap.has(signalId)) { + const signalValue = signal(props[propName]); + sharedSignalMap.set(signalId, signalValue); + } + props[propName] = sharedSignalMap.get(signalId); } - props[propName] = sharedSignalMap.get(signalId); } } diff --git a/packages/integrations/preact/src/signals.ts b/packages/integrations/preact/src/signals.ts index ea022b4222..f8bae590fa 100644 --- a/packages/integrations/preact/src/signals.ts +++ b/packages/integrations/preact/src/signals.ts @@ -1,6 +1,13 @@ import type { Context } from './context.js'; import { incrementId } from './context.js'; -import type { AstroPreactAttrs, PropNameToSignalMap, SignalLike } from './types.js'; +import type { + ArrayObjectMapping, + AstroPreactAttrs, + PropNameToSignalMap, + SignalLike, + Signals, + SignalToKeyOrIndexMap, +} from './types.js'; function isSignal(x: any): x is SignalLike { return x != null && typeof x === 'object' && typeof x.peek === 'function' && 'value' in x; @@ -28,22 +35,38 @@ export function serializeSignals( map: PropNameToSignalMap, ) { // Check for signals - const signals: Record = {}; + const signals: Signals = {}; for (const [key, value] of Object.entries(props)) { - if (isSignal(value)) { + const isPropArray = Array.isArray(value); + const isPropObject = !isSignal(value) && typeof props[key] === 'object' && !isPropArray; + + if (isPropObject || isPropArray) { + const values = isPropObject ? Object.keys(props[key]) : value; + values.forEach((valueKey: number | string, valueIndex: number) => { + const signal = isPropObject ? props[key][valueKey] : valueKey; + if (isSignal(signal)) { + const keyOrIndex = isPropObject ? valueKey.toString() : valueIndex; + + props[key] = isPropObject + ? Object.assign({}, props[key], { [keyOrIndex]: signal.peek() }) + : props[key].map((v: SignalLike, i: number) => + i === valueIndex ? [signal.peek(), i] : v, + ); + + const currentMap = (map.get(key) || []) as SignalToKeyOrIndexMap; + map.set(key, [...currentMap, [signal, keyOrIndex]]); + + const currentSignals = (signals[key] || []) as ArrayObjectMapping; + signals[key] = [...currentSignals, [getSignalId(ctx, signal), keyOrIndex]]; + } + }); + } else if (isSignal(value)) { // Set the value to the current signal value // This mutates the props on purpose, so that it will be serialized correct. props[key] = value.peek(); map.set(key, value); - let id: string; - if (ctx.signals.has(value)) { - id = ctx.signals.get(value)!; - } else { - id = incrementId(ctx); - ctx.signals.set(value, id); - } - signals[key] = id; + signals[key] = getSignalId(ctx, value); } } @@ -51,3 +74,13 @@ export function serializeSignals( attrs['data-preact-signals'] = JSON.stringify(signals); } } + +function getSignalId(ctx: Context, item: SignalLike) { + let id = ctx.signals.get(item); + if (!id) { + id = incrementId(ctx); + ctx.signals.set(item, id); + } + + return id; +} diff --git a/packages/integrations/preact/src/types.ts b/packages/integrations/preact/src/types.ts index 93f65bbc20..e1c56ca30b 100644 --- a/packages/integrations/preact/src/types.ts +++ b/packages/integrations/preact/src/types.ts @@ -7,7 +7,11 @@ export type SignalLike = { peek(): any; }; -export type PropNameToSignalMap = Map; +export type ArrayObjectMapping = [string, number | string][]; +export type Signals = Record; + +export type SignalToKeyOrIndexMap = [SignalLike, number | string][]; +export type PropNameToSignalMap = Map; export type AstroPreactAttrs = { ['data-preact-signals']?: string;