Skip to content

Commit

Permalink
add callback based props to all types
Browse files Browse the repository at this point in the history
  • Loading branch information
KurtGokhan committed Oct 1, 2024
1 parent b14ac41 commit 0999093
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 85 deletions.
12 changes: 6 additions & 6 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { ConsoleTrackingHandler, TrackCallback, TrackEvent, Tracking } from 'react-clarify';
import { ConsoleTrackingHandler, TrackCallback, TrackEvent, TrackingProvider } from 'react-clarify';
import './App.css';

function App() {
Expand All @@ -8,7 +8,7 @@ function App() {
return (
<>
<ConsoleTrackingHandler>
<Tracking data={{ test: '5' }}>
<TrackingProvider data={{ test: '5' }}>
<h1>Vite + React</h1>
<div className="card">
<section>
Expand All @@ -30,8 +30,8 @@ function App() {
</section>

<section>
Track with <code>Tracking</code>:&nbsp;
<Tracking>
Track with <code>TrackingProvider</code>:&nbsp;
<TrackingProvider>
{({ track }) => (
<button
type="button"
Expand All @@ -43,7 +43,7 @@ function App() {
Clicked {count} times
</button>
)}
</Tracking>
</TrackingProvider>
</section>

<section>
Expand All @@ -53,7 +53,7 @@ function App() {
</button>
</section>
</div>
</Tracking>
</TrackingProvider>
</ConsoleTrackingHandler>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/react-clarify/src/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createTrackingContext } from './lib/creator';
export type * from './types';

export const {
Tracking,
TrackingProvider,
useTrackingContext,
TrackingHandler,
ConsoleTrackingHandler,
Expand Down
8 changes: 4 additions & 4 deletions packages/react-clarify/src/lib/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export function createTrackingContext<TBase extends ReactClarifyBase = ReactClar
const useTrackingHandler = () => useContext(handlerCtx);

const { useTrack, TrackingHandler, ConsoleTrackingHandler } = createTrackingHandlerProvider<TBase>(ctx, handlerCtx);
const { Tracking } = createTrackingProvider<TBase>(ctx, useTrack);
const { TrackCallback } = createTrackCallback<TBase>(useTrack);
const { TrackEvent } = createTrackEvent<TBase>(useTrack);
const { TrackingProvider, useResolvedTracking } = createTrackingProvider<TBase>(ctx, useTrack);
const { TrackCallback } = createTrackCallback<TBase>(useResolvedTracking);
const { TrackEvent } = createTrackEvent<TBase>(useResolvedTracking);

return {
Tracking,
TrackingProvider,
useTrackingContext,
useTrackingHandler,
useTrack,
Expand Down
49 changes: 27 additions & 22 deletions packages/react-clarify/src/lib/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
TrackFn,
TrackingContext,
TrackingData,
TrackingProps,
TrackingProviderProps,
TrackingRef,
} from '../types';

Expand All @@ -18,51 +18,56 @@ export function createTrackingProvider<TBase extends ReactClarifyBase = ReactCla
) {
type TData = TrackingData<TBase>;
type TRef = TrackingRef<TBase>;
type TProps = TrackingProps<TBase>;
type TProps = TrackingProviderProps<TBase>;
type TTrackFn = TrackFn<TBase>;

const Tracking = forwardRef<TRef, TProps>(function _Tracking({ children, enabled, skip, root, data }, ref) {
function useResolvedTracking({ children, enabled, skip, root, data }: TProps) {
const parentCtx = useContext(ctx);

const baseData: Partial<TData> = root ? {} : parentCtx.data;
const resolvedRoot = typeof root === 'function' ? root(parentCtx.data) : root;

const baseData = resolvedRoot ? ({} as TData) : parentCtx.data;
const newData =
typeof data === 'function'
? data(parentCtx.data)
: {
: ({
...baseData,
...data,
};
} as TData);

const resolvedSkip = typeof skip === 'function' ? skip(newData) : skip;

const currentData = skip ? baseData : newData;
const currentData = resolvedSkip ? baseData : newData;
const dataRef = useStable(currentData);
const newEnabled = typeof enabled === 'boolean' ? enabled : parentCtx.enabled;
const resolvedEnabled = typeof enabled === 'function' ? enabled(currentData) : enabled;
const newEnabled = typeof resolvedEnabled === 'boolean' ? resolvedEnabled : parentCtx.enabled;

const ctxValue = useMemo<TrackingContext>(
const track = useTrack();
const trackFn = useStableCallback<TTrackFn>(({ args, data }) => track({ data: { ...currentData, ...data }, args }));

const result = useMemo<TRef>(
() => ({
track: trackFn,
enabled: newEnabled,
get data() {
return dataRef.current;
},
}),
[newEnabled, dataRef],
[newEnabled, dataRef, trackFn],
);

const track = useTrack();
const trackFn = useStableCallback<TTrackFn>(({ args, data }) => track({ data: { ...currentData, ...data }, args }));
const content = typeof children === 'function' ? children(result) : children;

const refImpl = useStable<TRef>({
getData() {
return dataRef.current;
},
track: trackFn,
});
return { result, content };
}

useImperativeHandle(ref, () => refImpl.current, [refImpl]);
const TrackingProvider = forwardRef<TRef, TProps>(function _Tracking(props, ref) {
const { result, content } = useResolvedTracking(props);

const content = typeof children === 'function' ? children(refImpl.current) : children;
useImperativeHandle(ref, () => result, [result]);

return <ctx.Provider value={ctxValue}>{content}</ctx.Provider>;
return <ctx.Provider value={result}>{content}</ctx.Provider>;
});

return { Tracking };
return { TrackingProvider, useResolvedTracking };
}
25 changes: 14 additions & 11 deletions packages/react-clarify/src/lib/track-callback.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import type { DOMAttributes, ReactElement } from 'react';
import { Children, cloneElement, useCallback } from 'react';
import { useStableCallback } from '../hooks/use-stable-callback';
import type { ReactClarify, ReactClarifyBase, TrackCallbackProps, TrackFn } from '../types';
import type { ReactClarify, ReactClarifyBase, TrackCallbackProps, TrackingResolver } from '../types';

type CallbackNames<Props> = keyof { [key in keyof Props as key extends `on${string}` ? key : never]: true };

export function createTrackCallback<TBase extends ReactClarifyBase = ReactClarify>(useTrack: () => TrackFn<TBase>) {
export function createTrackCallback<TBase extends ReactClarifyBase = ReactClarify>(
useResolver: TrackingResolver<TBase>,
) {
type TProps = TrackCallbackProps<TBase>;

function TrackCallback<
ComponentType = DOMAttributes<any>,
CallbackName extends PropertyKey = CallbackNames<ComponentType> | (string & {}),
>({ children, callback, name, disabled, data, ...props }: TProps & { callback: CallbackName }) {
children = Children.only(children) as ReactElement;
>({ callback, name, disabled, ...props }: TProps & { callback: CallbackName }) {
if (!callback) throw new Error('Callback name must be provided');

const {
content,
result: { track },
} = useResolver(props);

const children = Children.only(content) as ReactElement;
if (!children) throw new Error('Children passed to track directive must be an element with ref');
if (!callback) throw new Error('Callback name must be provided');

const resolvedName = name ?? String(callback);
const track = useTrack();
const trackFn = useStableCallback((...args: any[]) => track({ data, args: [resolvedName, ...args] }));

const originalCallback = children.props[callback];
const handle = useCallback(
(...args: any[]) => {
trackFn(...args);
track({ args: [resolvedName, ...args], data: {} });
return originalCallback?.(args);
},
[originalCallback, trackFn],
[originalCallback, track, resolvedName],
);

return cloneElement(children, {
Expand Down
22 changes: 11 additions & 11 deletions packages/react-clarify/src/lib/track-event.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import type { ReactElement } from 'react';
import { Children, cloneElement, useCallback, useRef } from 'react';
import { useCombinedRefs } from '../hooks/use-combined-refs';
import { useStableCallback } from '../hooks/use-stable-callback';
import type { ReactClarify, ReactClarifyBase, TrackEventProps, TrackFn } from '../types';
import type { ReactClarify, ReactClarifyBase, TrackEventProps, TrackingResolver } from '../types';

export function createTrackEvent<TBase extends ReactClarifyBase = ReactClarify>(useTrack: () => TrackFn<TBase>) {
export function createTrackEvent<TBase extends ReactClarifyBase = ReactClarify>(useResolver: TrackingResolver<TBase>) {
type TProps = TrackEventProps<TBase>;

function TrackEvent<EventName extends keyof HTMLElementEventMap>({
children,
refProp = 'ref',
event,
eventOptions,
name,
disabled,
stopPropagation,
preventDefault,
data,
...props
}: TProps & { event: EventName }) {
children = Children.only(children) as ReactElement;
const {
content,
result: { track },
} = useResolver(props);

const children = Children.only(content) as ReactElement;
if (!children) throw new Error('Children passed to track directive must be an element with ref');

const resolvedName = name ?? event;
const track = useTrack();
const trackFn = useStableCallback((...args: any[]) => track({ data, args: [resolvedName, ...args] }));

const handle = useCallback(
(ev: HTMLElementEventMap[EventName], ...args: any[]) => {
trackFn(ev, ...args);
track({ args: [resolvedName, ev, ...args], data: {} });
if (stopPropagation) ev.stopPropagation();
if (preventDefault) ev.preventDefault();
},
[trackFn, stopPropagation, preventDefault],
[track, stopPropagation, preventDefault, resolvedName],
);

const cleanupRef = useRef<() => void>();
Expand Down
6 changes: 3 additions & 3 deletions packages/react-clarify/src/middlewares/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addMiddlewares } from 'jsx-middlewares/react';
import { TrackCallback, TrackEvent, Tracking } from '..';
import { TrackCallback, TrackEvent, TrackingProvider } from '..';
import type { TrackingData } from '../types';

export interface TrackingAttributes {
Expand Down Expand Up @@ -39,9 +39,9 @@ function register() {
function trackingDirective(next, type, { $tracking, ...props }, key) {
if ($tracking) {
return (
<Tracking data={$tracking} key={key}>
<TrackingProvider data={$tracking} key={key}>
{next(type, props, undefined)}
</Tracking>
</TrackingProvider>
);
}

Expand Down
37 changes: 21 additions & 16 deletions packages/react-clarify/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,32 @@ export type TrackFn<TBase extends ReactClarifyBase = ReactClarify> = (
options: TrackingHandlerObject<TBase, Partial<TrackingData<TBase>> | undefined>,
) => void;

export type TrackingProps<TBase extends ReactClarifyBase = ReactClarify> = {
enabled?: boolean;
skip?: boolean;
root?: boolean;
data?: Partial<TrackingData<TBase>> | ((parentData?: Readonly<TrackingData<TBase>>) => TrackingData<TBase>);
children?: ReactNode | ((ref: TrackingRef<TBase>) => ReactNode);
export type TrackingResolveFn<TBase extends ReactClarifyBase = ReactClarify, TRes = unknown> = (
data: Readonly<TrackingData<TBase>>,
) => TRes;

export type TrackingResolver<TBase extends ReactClarifyBase = ReactClarify> = (props: TrackingProviderProps<TBase>) => {
result: TrackingRef<TBase>;
content: ReactNode;
};

export interface TrackingRef<TBase extends ReactClarifyBase = ReactClarify> {
getData: () => Partial<TrackingData<TBase>>;
track: TrackFn<TBase>;
}
export type TrackingProviderProps<TBase extends ReactClarifyBase = ReactClarify> = {
enabled?: boolean | TrackingResolveFn<TBase, boolean>;
skip?: boolean | TrackingResolveFn<TBase, boolean>;
root?: boolean | TrackingResolveFn<TBase, boolean>;
data?: Partial<TrackingData<TBase>> | TrackingResolveFn<TBase, TrackingData<TBase>>;
children?: ReactNode | ((ref: TrackingRef<TBase>) => ReactNode);
};

export interface TrackingContext<TBase extends ReactClarifyBase = ReactClarify> {
readonly enabled: boolean;
readonly data: Readonly<TrackingData<TBase>>;
}

export interface TrackingRef<TBase extends ReactClarifyBase = ReactClarify> extends TrackingContext<TBase> {
readonly track: TrackFn<TBase>;
}

export interface TrackingHandlerContext<TBase extends ReactClarifyBase = ReactClarify> {
handle: TrackingHandlerFn<TBase>;
}
Expand All @@ -62,19 +70,16 @@ export interface TrackingHandlerProps<TBase extends ReactClarifyBase = ReactClar
onHandle?: TrackingHandlerFn<TBase>;
}

export interface TrackCallbackProps<TBase extends ReactClarifyBase = ReactClarify> {
export interface TrackCallbackProps<TBase extends ReactClarifyBase = ReactClarify>
extends TrackingProviderProps<TBase> {
name?: string;
data?: Partial<TrackingData<TBase>>;
children?: ReactNode;
disabled?: boolean;
}

export interface TrackEventProps<TBase extends ReactClarifyBase = ReactClarify> {
export interface TrackEventProps<TBase extends ReactClarifyBase = ReactClarify> extends TrackingProviderProps<TBase> {
name?: string;
stopPropagation?: boolean;
preventDefault?: boolean;
data?: Partial<TrackingData<TBase>>;
children?: ReactNode;
disabled?: boolean;
eventOptions?: boolean | AddEventListenerOptions;
refProp?: string;
Expand Down
10 changes: 5 additions & 5 deletions packages/react-clarify/tests/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Tracking, TrackingHandler, type TrackingHandlerFn, type TrackingProps } from 'react-clarify';
import { TrackingHandler, type TrackingHandlerFn, TrackingProvider, type TrackingProviderProps } from 'react-clarify';
export * as userEvent from '@testing-library/user-event';

export function createTrackingWrapper(defaultProps: TrackingProps = {}) {
export function createTrackingWrapper(defaultProps: TrackingProviderProps = {}) {
const spy = vi.fn<Parameters<TrackingHandlerFn>>();

const wrapper = (baseProps: TrackingProps) => {
const wrapper = (baseProps: TrackingProviderProps) => {
const { children, ...props } = { ...defaultProps, ...baseProps };

return (
<Tracking {...props}>
<TrackingProvider {...props}>
{(ctx) => (
<TrackingHandler onHandle={spy}>{typeof children === 'function' ? children(ctx) : children}</TrackingHandler>
)}
</Tracking>
</TrackingProvider>
);
};

Expand Down
2 changes: 1 addition & 1 deletion website/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ React Clarify works by defining the tracking context in a cascading manner. Let'

In summary, there are 3 group of components you need to know about:

- `Tracking` is a context provider for defining the data that will be passed to the tracking handler. data for the nested `Tracking` will be merged together.
- `TrackingProvider` is a context provider for defining the data that will be passed to the tracking handler. data for the nested `TrackingProvider` will be merged together.
- `TrackingHandler` is for defining a callback function that will be called when a tracking event is fired. `ConsoleTrackingHandler` is a predefined handler for simply logging the tracked data to the console. Handlers can be nested to call them all when a tacking event occurs.
- `TrackEvent` and is used to fire a tracking event when a DOM event occurs. It's children must be a single React component that takes a ref.
- As an alternative to `TrackEvent`, `TrackCallback` can be used to fire tracking events using not just DOM events, but any arbitrary callback function. It can be used like `<TrackCallback callback="onClick">`.
Expand Down
10 changes: 5 additions & 5 deletions website/src/examples/intro.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from 'react';
import { ConsoleTrackingHandler, TrackEvent, Tracking } from 'react-clarify';
import { ConsoleTrackingHandler, TrackEvent, TrackingProvider } from 'react-clarify';

export default function Example() {
return (
<Tracking data={{ user: { name: 'johndoe', id: '123456' }, company: 'acme' }}>
<TrackingProvider data={{ user: { name: 'johndoe', id: '123456' }, company: 'acme' }}>
<ConsoleTrackingHandler level="info" transform={JSON.stringify}>
<Tracking data={{ page: 'Home Page' }}>
<TrackingProvider data={{ page: 'Home Page' }}>
<p>Clicking the button will log a message to the console and send a tracking event.</p>

<TrackEvent event="click" data={{ element: 'My button' }}>
<button type="button">Click me</button>
</TrackEvent>
</Tracking>
</TrackingProvider>
</ConsoleTrackingHandler>
</Tracking>
</TrackingProvider>
);
}

0 comments on commit 0999093

Please sign in to comment.