Skip to content

Commit fe09e60

Browse files
committed
feat: Added support for non-parts props. Fixes #3
1 parent 77fd36c commit fe09e60

File tree

1 file changed

+124
-32
lines changed

1 file changed

+124
-32
lines changed

src/index.tsx

Lines changed: 124 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import {
2-
type Value,
2+
canAnimate as _canAnimate,
33
type Format,
4-
SlottedTag,
5-
slottedStyles,
6-
partitionParts,
74
NumberFlowLite,
5+
PartitionedParts,
6+
partitionParts,
87
prefersReducedMotion,
9-
canAnimate as _canAnimate,
8+
slottedStyles,
9+
SlottedTag,
10+
type Value,
1011
} from 'number-flow';
12+
import {
13+
Accessor,
14+
createEffect,
15+
createMemo,
16+
createSignal,
17+
onMount,
18+
splitProps,
19+
VoidProps,
20+
} from 'solid-js';
1121
import { JSX } from 'solid-js/jsx-runtime';
12-
import { createEffect, createMemo, createSignal, onMount, splitProps } from 'solid-js';
1322
import { Dynamic } from 'solid-js/web';
14-
export type { Value, Format, Trend } from 'number-flow';
23+
export type { Format, Trend, Value } from 'number-flow';
1524

1625
// Can't wait to not have to do this in React 19:
1726
const OBSERVED_ATTRIBUTES = ['parts'] as const;
@@ -31,10 +40,10 @@ export type NumberFlowProps = JSX.HTMLAttributes<NumberFlowElement> & {
3140
animated?: boolean;
3241
respectMotionPreference?: boolean;
3342
willChange?: boolean;
34-
// animateDependencies?: React.DependencyList
3543
onAnimationsStart?: () => void;
3644
onAnimationsFinish?: () => void;
3745
trend?: (typeof NumberFlowElement)['prototype']['trend'];
46+
continuous?: (typeof NumberFlowElement)['prototype']['continuous'];
3847
opacityTiming?: (typeof NumberFlowElement)['prototype']['opacityTiming'];
3948
transformTiming?: (typeof NumberFlowElement)['prototype']['transformTiming'];
4049
spinTiming?: (typeof NumberFlowElement)['prototype']['spinTiming'];
@@ -47,65 +56,148 @@ const formatters: Record<string, Intl.NumberFormat> = {};
4756

4857
NumberFlowElement.define();
4958

50-
export default function NumberFlow(props: NumberFlowProps) {
51-
const localesString = createMemo(
52-
() => (props.locales ? JSON.stringify(props.locales) : ''),
53-
[props.locales],
54-
);
55-
const formatString = createMemo(() => (props.format ? JSON.stringify(props.format) : ''));
56-
const parts = createMemo(() => {
57-
const formatter = (formatters[`${localesString()}:${formatString()}`] ??= new Intl.NumberFormat(
58-
props.locales,
59-
props.format,
60-
));
59+
// ===========================================================================
60+
// IMPLEMENTATION (Equivalent to the React Class Component)
61+
// ===========================================================================
62+
type NumberFlowImplProps = Omit<NumberFlowProps, 'value' | 'locales' | 'format'> & {
63+
innerRef: NumberFlowElement | undefined;
64+
parts: Accessor<PartitionedParts>;
65+
};
6166

62-
return partitionParts(props.value, formatter);
67+
/** Used for `prevProps` because accessing signals always gives "latest" values, we don't want that. */
68+
type NumberFlowImplProps_NoSignals = Omit<NumberFlowImplProps, 'parts'> & {
69+
parts: PartitionedParts;
70+
};
71+
72+
function NumberFlowImpl(props: VoidProps<NumberFlowImplProps>) {
73+
let el: NumberFlowElement | undefined;
74+
75+
const updateNonPartsProps = (prevProps?: NumberFlowImplProps_NoSignals) => {
76+
if (!el) return;
77+
78+
// el.manual = !props.isolate; (Not sure why but this breaks the animations, so isolate might not work right now. I personally think it has a very niche usecase though).
79+
if (props.animated != null) el.animated = props.animated;
80+
if (props.respectMotionPreference != null)
81+
el.respectMotionPreference = props.respectMotionPreference;
82+
if (props.trend != null) el.trend = props.trend;
83+
if (props.continuous != null) el.continuous = props.continuous;
84+
if (props.opacityTiming) el.opacityTiming = props.opacityTiming;
85+
if (props.transformTiming) el.transformTiming = props.transformTiming;
86+
if (props.spinTiming) el.spinTiming = props.spinTiming;
87+
88+
if (prevProps?.onAnimationsStart)
89+
el.removeEventListener('animationsstart', prevProps.onAnimationsStart);
90+
if (props.onAnimationsStart) el.addEventListener('animationsstart', props.onAnimationsStart);
91+
92+
if (prevProps?.onAnimationsFinish)
93+
el.removeEventListener('animationsfinish', prevProps.onAnimationsFinish);
94+
if (props.onAnimationsFinish) el.addEventListener('animationsfinish', props.onAnimationsFinish);
95+
};
96+
97+
onMount(() => {
98+
updateNonPartsProps();
99+
if (el) {
100+
el.parts = props.parts();
101+
}
102+
});
103+
104+
createEffect((prevProps?: NumberFlowImplProps_NoSignals) => {
105+
updateNonPartsProps(prevProps);
106+
if (props.isolate) {
107+
return;
108+
}
109+
if (prevProps?.parts === props.parts()) {
110+
return;
111+
}
112+
el?.willUpdate();
113+
114+
// The returned should not have any signals (because accessing it in the next
115+
// call will contain the "current" value). We want it to be "previous".
116+
return {
117+
...props,
118+
parts: props.parts(),
119+
};
63120
});
64121

122+
/**
123+
* It's exactly like a signal setter, but we're setting two things:
124+
* - innerRef (from props)
125+
* - this ref
126+
*/
127+
const handleRef = (elRef: NumberFlowElement) => {
128+
props.innerRef = elRef;
129+
el = elRef;
130+
};
131+
65132
const [_used, others] = splitProps(props, [
66-
// For Root
67-
'value',
68-
'locales',
69-
'format',
70-
// For impl
133+
'parts',
134+
// From Impl
71135
'class',
72136
'willChange',
137+
// These are set in updateNonPartsProps, so ignore them here:
73138
'animated',
74139
'respectMotionPreference',
75140
'isolate',
76141
'trend',
142+
'continuous',
77143
'opacityTiming',
78144
'transformTiming',
79145
'spinTiming',
80146
]);
81147

148+
// Manual Attribute setter onMount.
82149
onMount(() => {
83150
// This is a workaround until this gets fixed: https://github.com/solidjs/solid/issues/2339
84-
const el = props.ref as unknown as HTMLElement;
85-
const _parts = el.getAttribute('attr:parts');
151+
const _parts = el?.getAttribute('attr:parts');
86152
if (_parts) {
87-
el.removeAttribute('attr:parts');
88-
el.setAttribute('parts', _parts);
153+
el?.removeAttribute('attr:parts');
154+
el?.setAttribute('parts', _parts);
89155
}
90156
});
91157

92158
return (
93159
<Dynamic
94-
ref={props.ref}
160+
ref={handleRef}
95161
component="number-flow"
96162
class={props.class}
97163
// https://docs.solidjs.com/reference/jsx-attributes/attr
98164
attr:data-will-change={props.willChange ? '' : undefined}
99165
{...others}
100-
attr:parts={JSON.stringify(parts())}
166+
prop:parts={props.parts()}
167+
attr:parts={JSON.stringify(props.parts())}
101168
>
102169
<Dynamic component={SlottedTag} style={slottedStyles({ willChange: props.willChange })}>
103-
{parts().formatted}
170+
{props.parts().formatted}
104171
</Dynamic>
105172
</Dynamic>
106173
);
107174
}
108175

176+
// ===========================================================================
177+
// ROOT
178+
// ===========================================================================
179+
export default function NumberFlow(props: VoidProps<NumberFlowProps>) {
180+
const localesString = createMemo(
181+
() => (props.locales ? JSON.stringify(props.locales) : ''),
182+
[props.locales],
183+
);
184+
const [_, others] = splitProps(props, ['value', 'format', 'locales']);
185+
186+
const formatString = createMemo(() => (props.format ? JSON.stringify(props.format) : ''));
187+
const parts = createMemo(() => {
188+
const formatter = (formatters[`${localesString()}:${formatString()}`] ??= new Intl.NumberFormat(
189+
props.locales,
190+
props.format,
191+
));
192+
193+
return partitionParts(props.value, formatter);
194+
});
195+
196+
let innerRef: NumberFlowElement | undefined;
197+
198+
return <NumberFlowImpl {...others} innerRef={innerRef} parts={parts} />;
199+
}
200+
109201
// SSR-safe canAnimate
110202
/** Unfinished and untested. */
111203
export function useCanAnimate(

0 commit comments

Comments
 (0)