11import {
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' ;
1121import { JSX } from 'solid-js/jsx-runtime' ;
12- import { createEffect , createMemo , createSignal , onMount , splitProps } from 'solid-js' ;
1322import { 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:
1726const 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
4857NumberFlowElement . 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. */
111203export function useCanAnimate (
0 commit comments