Skip to content

Commit ea3d23d

Browse files
author
John Jenkins
committed
chore: more testing
1 parent 2a215e3 commit ea3d23d

File tree

13 files changed

+281
-29
lines changed

13 files changed

+281
-29
lines changed

src/client/client-host-ref.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRun
4949
$hostElement$: hostElement,
5050
$cmpMeta$: cmpMeta,
5151
$instanceValues$: new Map(),
52+
$serializerValues$: new Map(),
5253
};
5354
if (BUILD.isDev) {
5455
hostRef.$renderCount$ = 0;

src/compiler/transformers/decorators-to-static/convert-decorators.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ const visitClassDeclaration = (
125125
filteredMethodsAndFields,
126126
importAliasMap.get('PropSerialize'),
127127
'PropSerialize',
128+
importAliasMap.get('Prop'),
128129
);
129-
serializeDecoratorsToStatic(
130+
const deserializers = serializeDecoratorsToStatic(
130131
typeChecker,
131132
decoratedMembers,
132133
filteredMethodsAndFields,
133134
importAliasMap.get('AttrDeserialize'),
134135
'AttrDeserialize',
136+
importAliasMap.get('Prop'),
135137
);
136138
propDecoratorsToStatic(
137139
diagnostics,
@@ -141,6 +143,7 @@ const visitClassDeclaration = (
141143
filteredMethodsAndFields,
142144
importAliasMap.get('Prop'),
143145
serializers,
146+
deserializers,
144147
);
145148
stateDecoratorsToStatic(decoratedMembers, filteredMethodsAndFields, typeChecker, importAliasMap.get('State'));
146149
eventDecoratorsToStatic(

src/compiler/transformers/decorators-to-static/prop-decorator.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,18 @@ export const propDecoratorsToStatic = (
4242
): void => {
4343
const properties = decoratedProps
4444
.filter((prop) => ts.isPropertyDeclaration(prop) || ts.isGetAccessor(prop))
45-
.map((prop) => parsePropDecorator(diagnostics, typeChecker, program, prop, decoratorName, newMembers, serializers, deserializers))
45+
.map((prop) =>
46+
parsePropDecorator(
47+
diagnostics,
48+
typeChecker,
49+
program,
50+
prop,
51+
decoratorName,
52+
newMembers,
53+
serializers,
54+
deserializers,
55+
),
56+
)
4657
.filter((prop): prop is ts.PropertyAssignment => prop != null);
4758

4859
if (properties.length > 0) {
Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { flatOne } from '@utils';
21
import ts from 'typescript';
32

4-
import type * as d from '../../../declarations';
5-
import { convertValueToLiteral, createStaticGetter, retrieveTsDecorators } from '../transform-utils';
3+
import { convertValueToLiteral, createStaticGetter, retrieveTsDecorators, tsPropDeclName } from '../transform-utils';
64
import { getDecoratorParameters, isDecoratorNamed } from './decorator-utils';
75

86
export const serializeDecoratorsToStatic = (
@@ -11,37 +9,56 @@ export const serializeDecoratorsToStatic = (
119
newMembers: ts.ClassElement[],
1210
decoratorName: string,
1311
translateType: 'PropSerialize' | 'AttrDeserialize',
12+
propDecoratorName: string,
1413
) => {
14+
// we only care about `@Prop` decorated properties
15+
const props: string[] = decoratedProps
16+
.filter((prop) => ts.isPropertyDeclaration(prop) || ts.isGetAccessor(prop))
17+
.flatMap((prop) => {
18+
if (retrieveTsDecorators(prop)?.find(isDecoratorNamed(propDecoratorName))) {
19+
const { staticName } = tsPropDeclName(prop, typeChecker);
20+
return [staticName];
21+
}
22+
return [];
23+
});
24+
1525
const serializers = decoratedProps
1626
.filter(ts.isMethodDeclaration)
17-
.map((method) => parseSerializeDecorator(typeChecker, method, decoratorName));
18-
19-
const flatSerializers = flatOne(serializers);
27+
.flatMap((method) => parseSerializeDecorator(typeChecker, method, decoratorName, props));
2028

21-
if (flatSerializers.length > 0) {
29+
if (serializers.length > 0) {
2230
if (translateType === 'PropSerialize') {
23-
newMembers.push(createStaticGetter('serializers', convertValueToLiteral(flatSerializers)));
31+
newMembers.push(createStaticGetter('serializers', convertValueToLiteral(serializers)));
2432
} else {
25-
newMembers.push(createStaticGetter('deserializers', convertValueToLiteral(flatSerializers)));
33+
newMembers.push(createStaticGetter('deserializers', convertValueToLiteral(serializers)));
2634
}
2735
}
2836

29-
return flatSerializers;
37+
return serializers;
3038
};
3139

3240
const parseSerializeDecorator = (
3341
typeChecker: ts.TypeChecker,
3442
method: ts.MethodDeclaration,
3543
decoratorName: string,
36-
): d.ComponentCompilerChangeHandler[] => {
44+
props: string[],
45+
) => {
3746
const methodName = method.name.getText();
3847
const decorators = retrieveTsDecorators(method) ?? [];
39-
return decorators.filter(isDecoratorNamed(decoratorName)).map((decorator) => {
48+
49+
return decorators.filter(isDecoratorNamed(decoratorName)).flatMap((decorator) => {
4050
const [propName] = getDecoratorParameters<string>(decorator, typeChecker);
4151

42-
return {
43-
propName,
44-
methodName,
45-
};
52+
if (!props.includes(propName)) {
53+
// ignore if there's no corresponding @Prop decorated property
54+
return [];
55+
}
56+
57+
return [
58+
{
59+
propName,
60+
methodName,
61+
},
62+
];
4663
});
4764
};

src/declarations/stencil-private.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,6 +1757,7 @@ export interface HostRef {
17571757
$cmpMeta$: ComponentRuntimeMeta;
17581758
$hostElement$: HostElement;
17591759
$instanceValues$?: Map<string, any>;
1760+
$serializerValues$?: Map<string, string>;
17601761
$lazyInstance$?: ComponentInterface;
17611762
/**
17621763
* A promise that gets resolved if `BUILD.asyncLoading` is enabled and after the `componentDidLoad`

src/hydrate/platform/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta
151151
$cmpMeta$: cmpMeta,
152152
$hostElement$: elm,
153153
$instanceValues$: new Map(),
154+
$serializerValues$: new Map(),
154155
$renderCount$: 0,
155156
};
156157
hostRef.$onInstancePromise$ = new Promise((r) => (hostRef.$onInstanceResolve$ = r));

src/runtime/profile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const inspect = (ref: any) => {
5858
needsRerender: !!(flags & HOST_FLAGS.needsRerender),
5959
},
6060
instanceValues: hostRef.$instanceValues$,
61+
serializerValues: hostRef.$serializerValues$,
6162
ancestorComponent: hostRef.$ancestorComponent$,
6263
hostElement,
6364
lazyInstance: hostRef.$lazyInstance$,

src/runtime/proxy-component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,14 @@ export const proxyComponent = (
275275
plt.jmp(() => {
276276
const propName = attrNameToPropName.get(attrName);
277277
const hostRef = getHostRef(this);
278+
279+
if (hostRef.$serializerValues$.has(propName) && hostRef.$serializerValues$.get(propName) === newValue) {
280+
// The newValue is the same as a saved serialized value from a prop update.
281+
// The prop can be intentionally different from the attribute;
282+
// updating the underlying prop here can cause an infinite loop.
283+
return;
284+
}
285+
278286
// In a web component lifecycle the attributeChangedCallback runs prior to connectedCallback
279287
// in the case where an attribute was set inline.
280288
// ```html

src/runtime/set-value.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ export const setValue = (ref: d.RuntimeRef, propName: string, newVal: any, cmpMe
5353
// set our new value!
5454
hostRef.$instanceValues$.set(propName, newVal);
5555

56+
if (BUILD.reflect && cmpMeta.$attrsToReflect$) {
57+
if (instance && cmpMeta.$serializers$ && cmpMeta.$serializers$[propName]) {
58+
// this property has a serializer method
59+
60+
let attrVal = newVal;
61+
for (const methodName of cmpMeta.$serializers$[propName]) {
62+
// call the serializer methods
63+
attrVal = (instance as any)[methodName](attrVal, propName);
64+
}
65+
// keep the serialized value - it's used in `renderVdom()` (vdom-render.ts)
66+
// to set the attribute on the vnode
67+
hostRef.$serializerValues$.set(propName, attrVal);
68+
}
69+
}
70+
5671
if (BUILD.isDev) {
5772
if (hostRef.$flags$ & HOST_FLAGS.devOnRender) {
5873
consoleDevWarn(
@@ -104,6 +119,7 @@ export const setValue = (ref: d.RuntimeRef, propName: string, newVal: any, cmpMe
104119
return;
105120
}
106121
}
122+
107123
// looks like this value actually changed, so we've got work to do!
108124
// but only if we've already rendered, otherwise just chill out
109125
// queue that we need to do an update, but don't worry about queuing

src/runtime/test/attr-deserialize.spec.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, Method, Prop, State, AttrDeserialize, Element } from '@stencil/core';
1+
import { AttrDeserialize, Component, Element, Prop, State } from '@stencil/core';
22
import { newSpecPage } from '@stencil/core/testing';
33

44
import { withSilentWarn } from '../../testing/testing-utils';
@@ -48,6 +48,7 @@ describe('attribute deserialization', () => {
4848
// prop changes should not call deserializers
4949
root.prop1 = 100;
5050
await waitForChanges();
51+
5152
expect(rootInstance.method1Called).toBe(1);
5253
expect(rootInstance.method2Called).toBe(1);
5354

@@ -62,11 +63,13 @@ describe('attribute deserialization', () => {
6263

6364
// deserializer should not be called on state change
6465
rootInstance.someState = 'bye';
66+
await waitForChanges();
67+
6568
expect(rootInstance.method1Called).toBe(2);
6669
expect(rootInstance.method2Called).toBe(2);
6770
});
6871

69-
it('should watch correctly', async () => {
72+
it('should watch for changes correctly', async () => {
7073
@Component({ tag: 'cmp-a' })
7174
class CmpA {
7275
watchCalled = 0;
@@ -139,7 +142,7 @@ describe('attribute deserialization', () => {
139142
}
140143
}
141144

142-
const { root, rootInstance } = await newSpecPage({
145+
const { root, rootInstance, waitForChanges } = await newSpecPage({
143146
components: [CmpA],
144147
html: `<cmp-a></cmp-a>`,
145148
});
@@ -154,15 +157,23 @@ describe('attribute deserialization', () => {
154157

155158
// set different values
156159
root.setAttribute('prop-1', '100');
160+
await waitForChanges();
161+
157162
expect(rootInstance.method1).toHaveBeenCalledTimes(1);
158163
expect(root.prop1).toBe(100);
159164

160165
// special handling by deserializer
161166
root.setAttribute('prop-1', 'something');
162-
expect(rootInstance.method1).toHaveBeenCalledTimes(2);
167+
await waitForChanges();
168+
169+
// because the special handling returns a different value (1000) and the prop is reflected
170+
// the deserializer is called again to reflect the new value
171+
expect(rootInstance.method1).toHaveBeenCalledTimes(3);
163172
expect(root.prop1).toBe(1000);
164173

165174
root.setAttribute('json-prop', '{"a":99,"b":"bye"}');
175+
await waitForChanges();
176+
166177
expect(rootInstance.method2).toHaveBeenCalledTimes(1);
167178
expect(root.jsonProp).toEqual({ a: 99, b: 'bye' });
168179
});

0 commit comments

Comments
 (0)