UNPKG

@number-flow/react

Version:

A component to transition and format numbers.

178 lines (175 loc) 6.73 kB
'use client'; import * as React from 'react'; import NumberFlowLite, { define, formatToData, renderInnerHTML } from 'number-flow/lite'; import { BROWSER } from 'esm-env'; const REACT_MAJOR = parseInt(React.version.match(/^(\d+)\./)?.[1]); const isReact19 = REACT_MAJOR >= 19; // Can't wait to not have to do this in React 19: const OBSERVED_ATTRIBUTES = [ 'data', 'digits' ]; class NumberFlowElement extends NumberFlowLite { attributeChangedCallback(attr, _oldValue, newValue) { this[attr] = JSON.parse(newValue); } } NumberFlowElement.observedAttributes = isReact19 ? [] : OBSERVED_ATTRIBUTES; define('number-flow-react', NumberFlowElement); // You're supposed to cache these between uses: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString // Serialize to strings b/c React: const formatters = {}; // Tiny workaround to support React 19 until it's released: const serialize = isReact19 ? (p)=>p : JSON.stringify; function splitProps(props) { const { transformTiming, spinTiming, opacityTiming, animated, respectMotionPreference, trend, plugins, ...rest } = props; return [ { transformTiming, spinTiming, opacityTiming, animated, respectMotionPreference, trend, plugins }, rest ]; } // We need a class component to use getSnapshotBeforeUpdate: class NumberFlowImpl extends React.Component { // Update the non-`data` props to avoid JSON serialization // Data needs to be set in render still: updateProperties(prevProps) { if (!this.el) return; this.el.batched = !this.props.isolate; const [nonData] = splitProps(this.props); Object.entries(nonData).forEach(([k, v])=>{ // @ts-ignore this.el[k] = v ?? NumberFlowElement.defaultProps[k]; }); if (prevProps?.onAnimationsStart) this.el.removeEventListener('animationsstart', prevProps.onAnimationsStart); if (this.props.onAnimationsStart) this.el.addEventListener('animationsstart', this.props.onAnimationsStart); if (prevProps?.onAnimationsFinish) this.el.removeEventListener('animationsfinish', prevProps.onAnimationsFinish); if (this.props.onAnimationsFinish) this.el.addEventListener('animationsfinish', this.props.onAnimationsFinish); } componentDidMount() { this.updateProperties(); if (isReact19 && this.el) { // React 19 needs this because the attributeChangedCallback isn't called: this.el.digits = this.props.digits; this.el.data = this.props.data; } } getSnapshotBeforeUpdate(prevProps) { this.updateProperties(prevProps); if (prevProps.data !== this.props.data) { if (this.props.group) { this.props.group.willUpdate(); return ()=>this.props.group?.didUpdate(); } if (!this.props.isolate) { this.el?.willUpdate(); return ()=>this.el?.didUpdate(); } } return null; } componentDidUpdate(_, __, didUpdate) { didUpdate?.(); } handleRef(el) { if (this.props.innerRef) this.props.innerRef.current = el; this.el = el; } render() { const [_, { innerRef, className, data, willChange, isolate, group, digits, onAnimationsStart, onAnimationsFinish, ...rest }] = splitProps(this.props); return(// @ts-expect-error missing types /*#__PURE__*/ React.createElement("number-flow-react", { ref: this.handleRef, "data-will-change": willChange ? '' : undefined, // Have to rename this: class: className, ...rest, dangerouslySetInnerHTML: { __html: BROWSER ? '' : renderInnerHTML(data) }, suppressHydrationWarning: true, digits: serialize(digits), // Make sure data is set last, everything else is updated: data: serialize(data) })); } constructor(props){ super(props); this.handleRef = this.handleRef.bind(this); } } const NumberFlow = /*#__PURE__*/ React.forwardRef(function NumberFlow({ value, locales, format, prefix, suffix, ...props }, _ref) { React.useImperativeHandle(_ref, ()=>ref.current, []); const ref = React.useRef(); const group = React.useContext(NumberFlowGroupContext); group?.useRegister(ref); const localesString = React.useMemo(()=>locales ? JSON.stringify(locales) : '', [ locales ]); const formatString = React.useMemo(()=>format ? JSON.stringify(format) : '', [ format ]); const data = React.useMemo(()=>{ const formatter = formatters[`${localesString}:${formatString}`] ??= new Intl.NumberFormat(locales, format); return formatToData(value, formatter, prefix, suffix); }, [ value, localesString, formatString, prefix, suffix ]); return /*#__PURE__*/ React.createElement(NumberFlowImpl, { ...props, group: group, data: data, innerRef: ref }); }); const NumberFlowGroupContext = /*#__PURE__*/ React.createContext(undefined); function NumberFlowGroup({ children }) { const flows = React.useRef(new Set()); const updating = React.useRef(false); const pending = React.useRef(new WeakMap()); const value = React.useMemo(()=>({ useRegister (ref) { React.useEffect(()=>{ flows.current.add(ref); return ()=>{ flows.current.delete(ref); }; }, []); }, willUpdate () { if (updating.current) return; updating.current = true; flows.current.forEach((ref)=>{ const f = ref.current; if (!f || !f.created) return; f.willUpdate(); pending.current.set(f, true); }); }, didUpdate () { flows.current.forEach((ref)=>{ const f = ref.current; if (!f || !pending.current.get(f)) return; f.didUpdate(); pending.current.delete(f); }); updating.current = false; } }), []); return /*#__PURE__*/ React.createElement(NumberFlowGroupContext.Provider, { value: value }, children); } export { NumberFlow as N, NumberFlowElement as a, NumberFlowGroup as b };