Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use strict';

import type { ValueProcessor } from '../../types';
import { ValueProcessorTarget } from '../../types';
import { ReanimatedError } from '../../errors';
import createPropsBuilder from '../createPropsBuilder';

type TestStyle = {
width?: number;
margin?: string | number;
borderRadius?: number;
padding?: number;
shadowColor?: number;
shadowOpacity?: number;
shadowRadius?: number;
height?: number;
};

type ConfigEntry = boolean | { process: ValueProcessor } | 'loop';

type TestConfig = Record<keyof TestStyle, ConfigEntry>;

const BASE_CONFIG: TestConfig = {
width: false,
margin: false,
borderRadius: false,
padding: false,
shadowColor: false,
shadowOpacity: false,
shadowRadius: false,
height: false,
};

const createBuilder = (configOverrides: Partial<TestConfig>) => {
const config: TestConfig = { ...BASE_CONFIG, ...configOverrides };

return createPropsBuilder<TestStyle, TestConfig>({
config,
processConfigValue(configValue) {
if (configValue === true) {
return (value: unknown) => value;
}

if (configValue === 'loop') {
return configValue;
}

if (
configValue &&
typeof configValue === 'object' &&
'process' in configValue &&
typeof configValue.process === 'function'
) {
return configValue.process;
}

return undefined;
},
});
};

describe(createPropsBuilder, () => {
test('skips undefined values unless includeUndefined is true', () => {
const builder = createBuilder({
width: true,
margin: true,
borderRadius: true,
});

const style: TestStyle = {
width: undefined,
margin: 'auto',
borderRadius: 10,
};

expect(builder.build(style)).toEqual({
margin: 'auto',
borderRadius: 10,
});

expect(
builder.build(style, { includeUndefined: true })
).toEqual({
width: undefined,
margin: 'auto',
borderRadius: 10,
});
});

test('ignores properties not present in config', () => {
const builder = createBuilder({ width: true });

const style: TestStyle = {
width: 120,
height: 300,
};

expect(builder.build(style)).toEqual({ width: 120 });
});

test('passes provided context to processors', () => {
const processor = jest.fn().mockReturnValue(24);
const builder = createBuilder({
borderRadius: { process: processor },
});

builder.build({ borderRadius: 12 }, {
target: ValueProcessorTarget.CSS,
});

expect(processor).toHaveBeenCalledWith(12, {
target: ValueProcessorTarget.CSS,
});
});

test('uses default target context when target not set', () => {
const processor = jest.fn().mockReturnValue(10);
const builder = createBuilder({
padding: { process: processor },
});

builder.build({ padding: 5 });

expect(processor).toHaveBeenCalledWith(5, {
target: ValueProcessorTarget.Default,
});
});

test('merges record results without overwriting original props', () => {
const builder = createBuilder({
shadowColor: {
process: () => ({
shadowOpacity: 0.5,
shadowRadius: 6,
}),
},
shadowOpacity: true,
shadowRadius: true,
});

const style: TestStyle = {
shadowColor: 0xff0000,
shadowOpacity: 0.8,
};

expect(builder.build(style)).toEqual({
shadowOpacity: 0.8,
shadowRadius: 6,
});
});

test('allows processors to return undefined based on includeUndefined option', () => {
const builder = createBuilder({
width: {
process: () => undefined,
},
});

expect(builder.build({ width: 10 })).toEqual({});
expect(
builder.build({ width: 10 }, { includeUndefined: true })
).toEqual({ width: undefined });
});

test('throws when processor resolution exceeds maximum depth', () => {
expect(() =>
createBuilder({
width: 'loop',
})
).toThrow(new ReanimatedError('Max process depth for props builder reached for property width'));
});
});

This file was deleted.

4 changes: 2 additions & 2 deletions packages/react-native-reanimated/src/common/style/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import {
processTransform,
processTransformOrigin,
} from './processors';
import type { StyleBuilderConfig } from './types';
import type { PropsBuilderConfig } from './types';

const colorAttributes = { process: processColor };

export const BASE_PROPERTIES_CONFIG: StyleBuilderConfig<PlainStyle> = {
export const BASE_PROPERTIES_CONFIG: PropsBuilderConfig<PlainStyle> = {
/** Layout and Positioning */
// FLEXBOX
flex: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';
import { ReanimatedError } from '../errors';
import type {
UnknownRecord,
ValueProcessor,
ValueProcessorContext,
} from '../types';
import { ValueProcessorTarget } from '../types';
import { isRecord } from '../utils';

const MAX_PROCESS_DEPTH = 10;

type CreatePropsBuilderParams<TPropsConfig> = {
config: TPropsConfig;
processConfigValue: (
configValue: TPropsConfig[keyof TPropsConfig]
) => ValueProcessor | TPropsConfig[keyof TPropsConfig] | undefined;
};

export type PropsBuilderResult<TProps> = {
build(
props: TProps,
options?: {
includeUndefined?: boolean;
target?: ValueProcessorTarget;
}
): UnknownRecord;
};

export default function createPropsBuilder<
TProps extends UnknownRecord,
TPropsConfig extends UnknownRecord,
>({
processConfigValue,
config,
}: CreatePropsBuilderParams<TPropsConfig>): PropsBuilderResult<TProps> {
const processedConfig = Object.entries(config).reduce<
Record<string, ValueProcessor>
>((acc, [key, configValue]) => {
let processedValue: ReturnType<typeof processConfigValue> =
configValue as TPropsConfig[keyof TPropsConfig];

let depth = 0;
while (processedValue) {
if (++depth > MAX_PROCESS_DEPTH) {
throw new ReanimatedError(
`Max process depth for props builder reached for property ${key}`
);
}

if (typeof processedValue === 'function') {
acc[key] = processedValue as ValueProcessor;
break;
}

processedValue = processConfigValue(processedValue);
}

return acc;
}, {});

return {
build(
props: Readonly<UnknownRecord>,
{
includeUndefined = false,
target = ValueProcessorTarget.Default,
}: {
includeUndefined?: boolean;
target?: ValueProcessorTarget;
} = {}
) {
'worklet';
const context: ValueProcessorContext = { target };

return Object.entries(props).reduce<UnknownRecord>(
(acc, [key, value]) => {
const processor = processedConfig[key];

if (!processor) {
// Props is not supported, skip it
return acc;
}

const processedValue = processor(value, context);

const valueIsRecord = isRecord(value);
const processedValueIsRecord = isRecord(processedValue);

if (processedValue === undefined && !includeUndefined) {
// Skip if value is undefined and we don't want to include undefined values
return acc;
}

if (processedValueIsRecord && !valueIsRecord) {
for (const processedKey in processedValue) {
if (!(processedKey in props)) {
acc[processedKey] = processedValue[processedKey];
}
}
} else {
acc[key] = processedValue;
}

return acc;
},
{}
);
},
};
}
Loading
Loading