Skip to content

Commit

Permalink
feat: add conditional render helper and border highlighting (#727)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Norris <jonathan@taplytics.com>
  • Loading branch information
ajwootto and jonathannorris authored May 16, 2024
1 parent e5cd2f3 commit 70c90f2
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 4 deletions.
26 changes: 25 additions & 1 deletion examples/react/src/DevCycleExample.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { useVariable } from '@devcycle/react-client-sdk'
import {
RenderIfEnabled,
SwapComponents,
useVariable,
} from '@devcycle/react-client-sdk'
import React from 'react'

const OldComponent = ({ text }: { text: string }) => {
return <div>Old Component: {text}</div>
}

const RefactoredComponent = ({ text }: { text: string }) => {
return <div>Refactored Component: {text}</div>
}

const MyComponent = SwapComponents(
OldComponent,
RefactoredComponent,
'test-featre',
)

export default function DevCycleExample(): React.ReactElement {
const variableKey = 'feature-release'
const variableKeyString = 'variable-key-string'
Expand All @@ -26,6 +44,12 @@ export default function DevCycleExample(): React.ReactElement {
variable feature-release = {JSON.stringify(variable.value)}{' '}
</span>
</div>
<RenderIfEnabled variableKey={'test-featre'}>
<div>
<span>This content is conditionally rendered.</span>
</div>
</RenderIfEnabled>
<MyComponent text={'Hello'} />
<div>
<span>
variable variable-key-string ={' '}
Expand Down
17 changes: 15 additions & 2 deletions sdk/react/src/DevCycleProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import { ProviderConfig } from './types'
import React, { ReactNode, useEffect, useRef, useState } from 'react'
import initializeDevCycleClient from './initializeDevCycleClient'
import { Provider, initializedContext } from './context'
import {
Provider,
initializedContext,
debugContext,
debugContextDefaults,
} from './context'
import { DevCycleClient } from '@devcycle/js-client-sdk'

type Props = {
Expand Down Expand Up @@ -59,6 +64,12 @@ export function DevCycleProvider(props: Props): React.ReactElement {
}
}, [sdkKey, user, options])

const mergedDebugOptions = Object.assign(
{},
debugContextDefaults,
props.config.options?.reactDebug ?? {},
)

return (
<Provider value={{ client: clientRef.current }}>
<initializedContext.Provider
Expand All @@ -67,7 +78,9 @@ export function DevCycleProvider(props: Props): React.ReactElement {
isInitialized || clientRef.current.isInitialized,
}}
>
{props.children}
<debugContext.Provider value={mergedDebugOptions}>
{props.children}
</debugContext.Provider>
</initializedContext.Provider>
</Provider>
)
Expand Down
84 changes: 84 additions & 0 deletions sdk/react/src/RenderIfEnabled.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { RenderIfEnabled } from './RenderIfEnabled'
import { render } from '@testing-library/react'
import { useVariableValue } from './useVariableValue'
import '@testing-library/jest-dom'

const mockedUseVariable = jest.mocked(useVariableValue)

jest.mock('./useVariableValue')

describe('RenderIfEnabled', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render children when variable is set to target value', () => {
const variableKey = 'test-key'
const targetValue = true
const defaultValue = false
const children = <div>Test</div>
mockedUseVariable.mockReturnValue(targetValue)
const { getByText } = render(
<RenderIfEnabled
variableKey={variableKey}
targetValue={targetValue}
defaultValue={defaultValue}
>
{children}
</RenderIfEnabled>,
)
expect(mockedUseVariable).toHaveBeenCalledWith(
variableKey,
defaultValue,
)
expect(getByText('Test')).toBeInTheDocument()
})

it('should render children when variable is set to target value (string)', () => {
const variableKey = 'test-key'
const targetValue = 'test-value'
const defaultValue = 'not-test-value'
const children = <div>Test</div>
mockedUseVariable.mockReturnValue(targetValue)
const { getByText } = render(
<RenderIfEnabled
variableKey={variableKey}
targetValue={targetValue}
defaultValue={defaultValue}
>
{children}
</RenderIfEnabled>,
)
expect(getByText('Test')).toBeInTheDocument()
})

it('should not render children when variable is set to something else', () => {
const variableKey = 'test-key'
const targetValue = 'test-value'
const defaultValue = 'not-test-value'
const children = <div>Test</div>
mockedUseVariable.mockReturnValue('something else')
const { queryByText } = render(
<RenderIfEnabled
variableKey={variableKey}
targetValue={targetValue}
defaultValue={defaultValue}
>
{children}
</RenderIfEnabled>,
)
expect(queryByText('Test')).toBeNull()
})

it('should render children when variable is boolean enabled and target not specified', () => {
const variableKey = 'test-key'
const children = <div>Test</div>
mockedUseVariable.mockReturnValue(true)
const { getByText } = render(
<RenderIfEnabled variableKey={variableKey}>
{children}
</RenderIfEnabled>,
)
expect(getByText('Test')).toBeInTheDocument()
expect(mockedUseVariable).toHaveBeenCalledWith(variableKey, false)
})
})
68 changes: 68 additions & 0 deletions sdk/react/src/RenderIfEnabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import useVariableValue from './useVariableValue'
import { DVCVariableValue } from '@devcycle/js-client-sdk'
import { useContext } from 'react'
import { debugContext } from './context'

type CommonProps = {
children: React.ReactNode
variableKey: string
}

type RenderIfEnabledProps<T extends DVCVariableValue> =
| CommonProps
| (CommonProps & {
targetValue: T
defaultValue: T
})

export const RenderIfEnabled = <T extends DVCVariableValue>(
props: RenderIfEnabledProps<T>,
): React.ReactNode => {
let targetValue: DVCVariableValue
let defaultValue: DVCVariableValue
if ('targetValue' in props) {
targetValue = props.targetValue
defaultValue = props.defaultValue
} else {
targetValue = true
defaultValue = false
}

const variableValue = useVariableValue(props.variableKey, defaultValue)
const debugSettings = useContext(debugContext)

if (variableValue === targetValue) {
if (debugSettings.showConditionalBorders) {
return (
<div
style={{
border: `2px solid ${debugSettings.borderColor}`,
position: 'relative',
}}
className={`devcycle-conditional-border devcycle-conditional-border-${props.variableKey}`}
>
<a
style={{
position: 'absolute',
cursor: 'pointer',
right: '-2px',
top: '-2.5rem',
color: 'white',
fontSize: '1.5rem',
padding: '2px 5px',
backgroundColor: `${debugSettings.borderColor}`,
}}
target={'_blank'}
href={`https://app.devcycle.com/r/variables/${props.variableKey}`}
rel="noreferrer"
>
{props.variableKey}: {JSON.stringify(variableValue)}
</a>
{props.children}
</div>
)
}
return <>{props.children}</>
}
return null
}
57 changes: 57 additions & 0 deletions sdk/react/src/SwapComponents.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useVariableValue } from './useVariableValue'
import { SwapComponents } from './SwapComponents'
import { render } from '@testing-library/react'
import '@testing-library/jest-dom'

const mockedUseVariable = jest.mocked(useVariableValue)

jest.mock('./useVariableValue')
const oldComponent = (props: { test: boolean }) => {
return <div>Old Component</div>
}

const newComponent = (props: { test: boolean }) => {
return <div>New Component</div>
}

const mismatchProps = (props: { something: boolean }) => {
return <div>Mismatch Props</div>
}

const RenderSwapped = () => {
const Swapped = SwapComponents(oldComponent, newComponent, 'test-key')
return (
<span>
<Swapped test={true} />
</span>
)
}

const MismatchPropsError = () => {
// @ts-expect-error should complain about non-matching props
const Swapped = SwapComponents(oldComponent, mismatchProps, 'test-key')
return <span></span>
}

const PropTypeError = () => {
const Swapped = SwapComponents(oldComponent, newComponent, 'test-key')
return (
<span>
{/*@ts-expect-error should complain test prop is missing*/}
<Swapped />
</span>
)
}

describe('SwapComponents', () => {
it('should render the old component if the variable is not enabled', () => {
mockedUseVariable.mockReturnValue(false)
const { getByText } = render(<RenderSwapped />)
expect(getByText('Old Component')).toBeInTheDocument()
})
it('should render the new component if the variable is enabled', () => {
mockedUseVariable.mockReturnValue(true)
const { getByText } = render(<RenderSwapped />)
expect(getByText('New Component')).toBeInTheDocument()
})
})
22 changes: 22 additions & 0 deletions sdk/react/src/SwapComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentProps, ComponentType } from 'react'
import useVariableValue from './useVariableValue'

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const SwapComponents = <T extends ComponentType<any>>(
OldComponent: T,
NewComponent: T,
variableKey: string,
) => {
const DevCycleConditionalComponent = (
props: ComponentProps<T>,
): React.ReactElement => {
const variableValue = useVariableValue(variableKey, false)
if (variableValue) {
return <NewComponent {...props} />
} else {
return <OldComponent {...props} />
}
}

return DevCycleConditionalComponent
}
12 changes: 12 additions & 0 deletions sdk/react/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ export type InitializedContext = {
export const initializedContext = createContext<InitializedContext>({
isInitialized: false,
})

export type DebugContext = {
showConditionalBorders: boolean
borderColor: string
}

export const debugContextDefaults: DebugContext = {
showConditionalBorders: false,
borderColor: '#ff6347',
}

export const debugContext = createContext<DebugContext>(debugContextDefaults)
3 changes: 3 additions & 0 deletions sdk/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type {
DVCEvent,
} from '@devcycle/js-client-sdk'

export * from './RenderIfEnabled'
export * from './SwapComponents'

export {
DevCycleProvider,
DVCProvider,
Expand Down
16 changes: 15 additions & 1 deletion sdk/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@ type WithEnvironmentKey = {
envKey: string
}

type OptionsWithDebug = DevCycleOptions & {
reactDebug?: {
/**
* Show borders around components that are conditionally rendered using the RenderIf helper
*/
showConditionalBorders?: boolean

/**
* Hex color string for the border color to show around conditional components
*/
borderColor?: string
}
}

export type ProviderConfig = (WithSDKKey | WithEnvironmentKey) & {
user?: DevCycleUser
options?: DevCycleOptions
options?: OptionsWithDebug
}

0 comments on commit 70c90f2

Please sign in to comment.