Skip to content

Commit

Permalink
feat: Add portable Image component
Browse files Browse the repository at this point in the history
  • Loading branch information
codinsonn committed Apr 9, 2024
1 parent 17eb937 commit 7ba2858
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 86 deletions.
2 changes: 2 additions & 0 deletions apps/expo/app/ExpoRootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Stack } from 'expo-router'
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
import UniversalRootLayout from '@app/core/screens/UniversalRootLayout'
import { Image as ExpoContextImage } from '@app/core/components/Image.expo'
import { Link as ExpoContextLink } from '@app/core/navigation/Link.expo'
import { useRouter as useExpoContextRouter } from '@app/core/navigation/useRouter.expo'
import { useRouteParams as useExpoRouteParams } from '@app/core/navigation/useRouteParams.expo'
Expand All @@ -20,6 +21,7 @@ export default function ExpoRootLayout() {

return (
<UniversalAppProviders
contextImage={ExpoContextImage}
contextLink={ExpoContextLink}
contextRouter={expoContextRouter}
useContextRouteParams={useExpoRouteParams}
Expand Down
2 changes: 2 additions & 0 deletions apps/next/app/NextClientRootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'
import React from 'react'
import UniversalAppProviders from '@app/core/screens/UniversalAppProviders'
import { Image as NextContextImage } from '@app/core/components/Image.next'
import { Link as NextContextLink } from '@app/core/navigation/Link.next'
import { useRouter as useNextContextRouter } from '@app/core/navigation/useRouter.next'
import { useRouteParams as useNextRouteParams } from '@app/core/navigation/useRouteParams.next'
Expand All @@ -26,6 +27,7 @@ const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => {

return (
<UniversalAppProviders
contextImage={NextContextImage}
contextLink={NextContextLink}
contextRouter={nextContextRouter}
useContextRouteParams={useNextRouteParams}
Expand Down
97 changes: 97 additions & 0 deletions features/app-core/components/Image.expo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Image as ExpoImage } from 'expo-image'
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
import { Platform } from 'react-native'

/* --- <Image/> -------------------------------------------------------------------------------- */

const Image = (props: UniversalImageProps): JSX.Element => {
// Props
const {
/* - Universal - */
src,
alt,
width,
height,
style,
priority,
onError,
onLoadEnd,
/* - Split - */
expoPlaceholder,
/* - Next.js - */
onLoad,
fill,
/* - Expo - */
accessibilityLabel,
accessible,
allowDownscaling,
autoplay,
blurRadius,
cachePolicy,
contentFit,
contentPosition,
enableLiveTextInteraction,
focusable,
onLoadStart,
onProgress,
placeholderContentFit,
recyclingKey,
responsivePolicy,
} = props

// -- Overrides --

// @ts-ignore
const finalStyle = { width, height, ...style }
if (fill) finalStyle.height = '100%'
if (fill) finalStyle.width = '100%'

// -- Render --

return (
<ExpoImage
/* - Universal - */
source={src as any}
alt={alt || accessibilityLabel} // @ts-ignore
style={finalStyle}
priority={priority}
onError={onError}
onLoadEnd={onLoadEnd || onLoad as any}
/* - Split - */
placeholder={expoPlaceholder}
/* - Expo - */
accessibilityLabel={alt || accessibilityLabel}
accessible={accessible}
blurRadius={blurRadius}
cachePolicy={cachePolicy}
contentFit={contentFit}
contentPosition={contentPosition}
focusable={focusable}
onLoadStart={onLoadStart}
onProgress={onProgress}
placeholderContentFit={placeholderContentFit}
recyclingKey={recyclingKey}
responsivePolicy={responsivePolicy}
/* - Platform diffs - */
{...(Platform.select({
web: {},
native: {
autoplay,
enableLiveTextInteraction,
allowDownscaling,
},
}))}
/>
)
}

/* --- Static Methods -------------------------------------------------------------------------- */

Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache']
Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache']
Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync']
Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch']

/* --- Exports --------------------------------------------------------------------------------- */

export { Image }
File renamed without changes.
97 changes: 15 additions & 82 deletions features/app-core/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,22 @@
import { Image as ExpoImage } from 'expo-image'
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
import React from 'react'
import type { UniversalImageProps, UniversalImageMethods } from './Image.types'
import { CoreContext } from '../context/CoreContext'

/* --- <Image/> -------------------------------------------------------------------------------- */
/* --- <Image/> --------------------------------------------------------------------------------- */

const Image = (props: UniversalImageProps): JSX.Element => {
// Props
const {
/* - Universal - */
src,
alt,
width,
height,
style,
priority,
onError,
onLoadEnd,
/* - Split - */
expoPlaceholder,
/* - Next.js - */
onLoad,
fill,
/* - Expo - */
accessibilityLabel,
accessible,
allowDownscaling,
autoplay,
blurRadius,
cachePolicy,
contentFit,
contentPosition,
enableLiveTextInteraction,
focusable,
onLoadStart,
onProgress,
placeholderContentFit,
recyclingKey,
responsivePolicy,
} = props
const Image = ((props: UniversalImageProps) => {
// Context
const { contextImage: ContextImage } = React.useContext(CoreContext)

// -- Overrides --
// Static methods
if (!Image.clearDiskCache) Image.clearDiskCache = ContextImage.clearDiskCache
if (!Image.clearMemoryCache) Image.clearMemoryCache = ContextImage.clearMemoryCache
if (!Image.getCachePathAsync) Image.getCachePathAsync = ContextImage.getCachePathAsync
if (!Image.prefetch) Image.prefetch = ContextImage.prefetch

// @ts-ignore
const finalStyle = { width, height, ...style }
if (fill) finalStyle.height = '100%'
if (fill) finalStyle.width = '100%'

// -- Render --

return (
<ExpoImage
/* - Universal - */
source={src as any}
alt={alt || accessibilityLabel} // @ts-ignore
style={finalStyle}
priority={priority}
onError={onError}
onLoadEnd={onLoadEnd || onLoad as any}
/* - Split - */
placeholder={expoPlaceholder}
/* - Expo - */
accessibilityLabel={alt || accessibilityLabel}
accessible={accessible}
allowDownscaling={allowDownscaling}
autoplay={autoplay}
blurRadius={blurRadius}
cachePolicy={cachePolicy}
contentFit={contentFit}
contentPosition={contentPosition}
enableLiveTextInteraction={enableLiveTextInteraction}
focusable={focusable}
onLoadStart={onLoadStart}
onProgress={onProgress}
placeholderContentFit={placeholderContentFit}
recyclingKey={recyclingKey}
responsivePolicy={responsivePolicy}
/>
)
}

/* --- Static Methods -------------------------------------------------------------------------- */

Image.clearDiskCache = ExpoImage.clearDiskCache as UniversalImageMethods['clearDiskCache']
Image.clearMemoryCache = ExpoImage.clearMemoryCache as UniversalImageMethods['clearMemoryCache']
Image.getCachePathAsync = ExpoImage.getCachePathAsync as UniversalImageMethods['getCachePathAsync']
Image.prefetch = ExpoImage.prefetch as UniversalImageMethods['prefetch']
// Render
return <ContextImage {...props} />
}) as ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods

/* --- Exports --------------------------------------------------------------------------------- */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,33 @@ import { UniversalLinkProps } from '../navigation/Link.types'
import { UniversalRouterMethods } from '../navigation/useRouter.types'
import { UniversalRouteScreenProps } from '../navigation/useRouteParams.types'
import type { useLocalSearchParams } from 'expo-router'
import { UniversalImageMethods, UniversalImageProps } from '../components/Image.types'

// -i- This context's only aim is to provide React Portability & Framework Ejection patterns if required
// -i- By allowing you to provide your own custom Link and Router overrides, you could e.g.:
// -i- 1) Support Expo for Web by not defaulting to Next.js's Link and Router on web
// -i- 2) Eject from Next.js entirely and e.g. use another framework's Link component and Router
// -i- 2) Eject from Next.js entirely and e.g. use another framework's Image / Link / router

/* --- Types ----------------------------------------------------------------------------------- */

export type CoreContextType = {
contextImage: ((props: UniversalImageProps) => JSX.Element) & UniversalImageMethods
contextLink: (props: UniversalLinkProps) => JSX.Element
contextRouter: UniversalRouterMethods
useContextRouteParams: (routeScreenProps: UniversalRouteScreenProps) => ReturnType<typeof useLocalSearchParams>
}

/* --- Dummy ----------------------------------------------------------------------------------- */

const createDummyComponent = (contextComponentName: string) => (props: any) => {
throw new Error(`CoreContext was not provided with a ${contextComponentName}. Please provide one in UniversalAppProviders.`)
}

/* --- Context --------------------------------------------------------------------------------- */

export const CoreContext = React.createContext<CoreContextType>({
contextLink: null,
contextImage: createDummyComponent('contextImage') as any,
contextLink: createDummyComponent('contextLink'),
contextRouter: null,
useContextRouteParams: () => ({}),
})
4 changes: 2 additions & 2 deletions features/app-core/screens/UniversalAppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ type UniversalAppProvidersProps = CoreContextType & {

const UniversalAppProviders = (props: UniversalAppProvidersProps) => {
// Props
const { children, contextLink, contextRouter, useContextRouteParams } = props
const { children, contextImage, contextLink, contextRouter, useContextRouteParams } = props

// -- Render --

return (
<>
<CoreContext.Provider value={{ contextLink, contextRouter, useContextRouteParams }}>
<CoreContext.Provider value={{ contextImage, contextLink, contextRouter, useContextRouteParams }}>
{children}
</CoreContext.Provider>
</>
Expand Down

0 comments on commit 7ba2858

Please sign in to comment.