Skip to content

Commit

Permalink
feat: Parse NativeWind styles in custom Image & Link
Browse files Browse the repository at this point in the history
  • Loading branch information
codinsonn committed Apr 21, 2024
1 parent 3b275a7 commit 5cd3902
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 56 deletions.
8 changes: 6 additions & 2 deletions features/app-core/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Image as ExpoImage } from 'expo-image'
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
import { parseNativeWindStyles } from '../utils/parseNativeWindStyles'

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

Expand Down Expand Up @@ -38,10 +39,13 @@ const Image = (props: UniversalImageProps): JSX.Element => {
responsivePolicy,
} = props

// -- Nativewind --

const { nativeWindStyles, restStyle } = parseNativeWindStyles(style)
const finalStyle = { width, height, ...nativeWindStyles, ...restStyle }

// -- Overrides --

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

Expand Down
4 changes: 4 additions & 0 deletions features/app-core/components/Image.types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export type UniversalImageProps = {
* - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
style?: ExpoImageProps['style']

/** Universal, will affect both Expo & Next.js
* - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
className?: string

/** Universal, will affect both Expo & Next.js - Called on an image fetching error. */
onError?: ExpoImageProps['onError']

Expand Down
12 changes: 9 additions & 3 deletions features/app-core/components/Image.web.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import NextImage from 'next/image'
import { UniversalImageProps, UniversalImageMethods } from './Image.types'
import { parseNativeWindStyles } from '../utils/parseNativeWindStyles'

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

Expand All @@ -11,6 +12,7 @@ const Image = (props: UniversalImageProps): JSX.Element => {
alt,
width,
height,
className,
style = {},
priority = 'normal',
onError,
Expand All @@ -31,10 +33,13 @@ const Image = (props: UniversalImageProps): JSX.Element => {
contentFit,
} = props

// -- Nativewind --

const { nativeWindStyles, nativeWindClassName, restStyle } = parseNativeWindStyles(style)
const finalStyle = { width, height, ...nativeWindStyles, ...restStyle } as React.CSSProperties

// -- Overrides --

// @ts-ignore
const finalStyle = { width, height, ...style }
if (fill) finalStyle.height = '100%'
if (fill) finalStyle.width = '100%'
if (fill) finalStyle.objectFit = contentFit || 'cover'
Expand All @@ -47,7 +52,8 @@ const Image = (props: UniversalImageProps): JSX.Element => {
src={src as any}
alt={alt || accessibilityLabel}
width={width}
height={height} // @ts-ignore
height={height}
className={[className, nativeWindClassName].filter(Boolean).join(' ')}
style={finalStyle}
priority={priority === 'high'}
onError={onError as any}
Expand Down
3 changes: 3 additions & 0 deletions features/app-core/components/styled.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { styled } from 'nativewind'
import { Text as RNText, View as RNView } from 'react-native'
import { Link as UniversalLink } from '../navigation/Link'
import { Image as UniversalImage } from './Image'

/* --- Primitives ------------------------------------------------------------------------------ */

export const View = styled(RNView, '')
export const Text = styled(RNText, '')
export const Image = styled(UniversalImage, '')

/* --- Typography ------------------------------------------------------------------------------ */

Expand All @@ -17,6 +19,7 @@ export const P = styled(RNText, 'text-base')

/* --- Fix for Next Link ----------------------------------------------------------------------- */

export const Link = styled(UniversalLink, 'text-blue-500 underline')
export const LinkText = styled(RNText, 'text-blue-500 underline')
export const TextLink = (props: Omit<React.ComponentProps<typeof UniversalLink>, 'className'> & { className?: string }) => {
const { className, style, children, ...universalLinkProps } = props
Expand Down
8 changes: 7 additions & 1 deletion features/app-core/navigation/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Link as ExpoLink } from 'expo-router'
import type { UniversalLinkProps } from './Link.types'
import { parseNativeWindStyles } from '../utils/parseNativeWindStyles'

/* --- <Link/> --------------------------------------------------------------------------------- */

Expand All @@ -21,12 +22,17 @@ export const Link = (props: UniversalLinkProps) => {
maxFontSizeMultiplier
} = props

// -- Nativewind --

const { nativeWindStyles, restStyle } = parseNativeWindStyles(style)
const finalStyle = { ...nativeWindStyles, ...restStyle }

// -- Render --

return (
<ExpoLink
href={href}
style={style}
style={finalStyle}
onPress={onPress}
target={target}
asChild={asChild}
Expand Down
2 changes: 1 addition & 1 deletion features/app-core/navigation/Link.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type UniversalLinkProps = {
style?: ExpoLinkProps['style'];

/** -!- Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead */
className?: never;
className?: string; // never;

/** Universal - Should replace the current route without adding to the history - Default: false. */
replace?: boolean;
Expand Down
10 changes: 9 additions & 1 deletion features/app-core/navigation/Link.web.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import NextLink from 'next/link'
import type { ComponentProps } from 'react'
import type { UniversalLinkProps } from './Link.types'
import { parseNativeWindStyles } from '../utils/parseNativeWindStyles'

/* --- <Link/> --------------------------------------------------------------------------------- */

Expand All @@ -9,6 +10,7 @@ export const Link = (props: UniversalLinkProps) => {
const {
children,
href,
className,
style,
replace,
onPress,
Expand All @@ -21,12 +23,18 @@ export const Link = (props: UniversalLinkProps) => {
as,
} = props

// -- Nativewind --

const { nativeWindStyles, nativeWindClassName, restStyle } = parseNativeWindStyles(style)
const finalStyle = { ...nativeWindStyles, ...restStyle } as React.CSSProperties

// -- Render --

return (
<NextLink
href={href}
style={style as unknown as ComponentProps<typeof NextLink>['style']}
className={[className, nativeWindClassName].filter(Boolean).join(' ')}
style={finalStyle as unknown as ComponentProps<typeof NextLink>['style']}
onClick={onPress}
target={target}
replace={replace}
Expand Down
14 changes: 8 additions & 6 deletions features/app-core/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React from 'react'
import { Image } from '../components/Image'
import { View, H3, P, TextLink } from '../components/styled'
import { View, Image, H3, P, Link } from '../components/styled'

/* --- <HomeScreen/> --------------------------------------------------------------------------- */

const HomeScreen = () => {
return (
<View className="flex flex-1 justify-center items-center">
<Image src={require('../assets/aetherspaceLogo.png')} width={60} height={60} style={{ marginBottom: 12 }} />
<H3 className="text-center">Expo + Next.js app routing 👋</H3>
<Image src={require('../assets/aetherspaceLogo.png')} width={60} height={60} className="mb-3" />
<H3 className="text-center">Expo + Next.js app routing 🚀</H3>
<P className="mt-2 text-center">Open HomeScreen.tsx in features/app-core/screens to start working on your app</P>
<TextLink className="mt-4 text-center" href="/subpages/aetherspace">
<Link className="mt-4 text-center" href="/subpages/aetherspace">
Test navigation
</TextLink>
</Link>
<Link className="mt-4 text-center" href="/images">
Test images
</Link>
<Link href="https://universal-base-starter-docs.vercel.app/" target="_blank" style={styles.link}>Docs</Link>
</View>
)
Expand Down
48 changes: 9 additions & 39 deletions features/app-core/screens/ImagesScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,37 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Link } from '../navigation/Link'
import { Image } from '../components/Image'
import { View, Text, Image, Link } from '../components/styled'

/* --- <ImagesScreen/> --------------------------------------------------------------------------- */

const ImagesScreen = () => {
return (
<View style={styles.container}>
<View className="flex flex-1 justify-center items-center">
<Link
href="/"
style={{ ...styles.backButton, ...styles.link, textDecorationLine: 'none' }}
className="text-blue-500 absolute top-8 web:top-0 left-0 p-4"
>
{`< Back`}
</Link>
{/* - 1 - */}
<Image src={require('../assets/aetherspaceLogo.png')} width={60} height={60} />
<Text style={styles.subtitle}>src=static-require | width: 60 | height: 60</Text>
<Text className="mt-2 mb-4 text-center text-base">src=static-require | width: 60 | height: 60</Text>
{/* - 2 - */}
<Image src="https://codinsonn.dev/_next/image?url=%2Fimg%2FCodelyFansLogoPic160x160.jpeg&w=256&q=75" width={60} height={60} />
<Text style={styles.subtitle}>src=external-url | width: 60 | height: 60</Text>
<Text className="mt-2 mb-4 text-center text-base">src=external-url | width: 60 | height: 60</Text>
{/* - 3 - */}
<View style={{ width: 60, height: 80, position: 'relative', borderColor: 'black', borderStyle: 'dashed', borderWidth: 1 }}>
<View className="w-[60px] h-[80px] relative border-[1px] border-dashed border-black">
<Image src={require('../assets/aetherspaceLogo.png')} fill />
</View>
<Text style={styles.subtitle}>wrapper=50x80, relative | fill=true</Text>
<Text className="mt-2 mb-4 text-center text-base">wrapper=50x80, relative | fill=true</Text>
{/* - 4 - */}
<View style={{ width: 80, height: 60, position: 'relative', borderColor: 'black', borderStyle: 'dashed', borderWidth: 1 }}>
<View className="w-[80px] h-[60px] relative border-[1px] border-dashed border-black">
<Image src={require('../assets/aetherspaceLogo.png')} fill contentFit="contain" />
</View>
<Text style={styles.subtitle}>wrapper=80x60, relative | fill | contentFit=contain</Text>
<Text className="mt-2 mb-4 text-center text-base">wrapper=80x60, relative | fill | contentFit=contain</Text>
</View>
)
}

/* --- Styles ---------------------------------------------------------------------------------- */

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
backButton: {
position: 'absolute',
top: 16,
left: 16,
},
subtitle: {
marginTop: 8,
marginBottom: 16,
fontSize: 16,
textAlign: 'center',
},
link: {
marginTop: 16,
fontSize: 16,
color: 'blue',
textAlign: 'center',
textDecorationLine: 'underline',
},
})

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

export default ImagesScreen
6 changes: 3 additions & 3 deletions features/app-core/screens/SlugScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { useRouteParams } from '@app/core/navigation/useRouteParams'
import { View, Text, H3, TextLink } from '../components/styled'
import { View, Text, H3, Link } from '../components/styled'
import { useRouter } from '../navigation/useRouter'

/* --- <SlugScreen/> --------------------------------------------------------------------------- */
Expand Down Expand Up @@ -32,13 +32,13 @@ const SlugScreen = (props) => {
<Text className="mt-2 text-base text-center">
Need a more robust, Fully-Stacked, Full-Product, Universal App Setup?
</Text>
<TextLink
<Link
href="https://github.com/Aetherspace/green-stack-starter-demo#readme"
className="mt-4 text-center"
target="_blank"
>
Check out the GREEN Stack Starter
</TextLink>
</Link>
<Text className="mt-4 text-center text-blue-500 underline" onPress={() => push('/subpages/push')}>
{`router.push()`}
</Text>
Expand Down
31 changes: 31 additions & 0 deletions features/app-core/utils/parseNativeWindStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

/** --- parseNativeWindStyles() ---------------------------------------------------------------- */
/** -i- Util to extract Nativewind's style and/or className from a styled() components style prop */
export const parseNativeWindStyles = (style: any) => {
return Object.entries(style || {}).reduce(
(acc, [key, value]) => {
// If the key is unsupported, ignore it
if (['mask', 'childClassNames'].includes(key)) return acc
// If the key is a number, it's a Nativewind style
if (Number.isInteger(Number(key))) {
const isCSS = !!(value as Record<string, unknown>)['$$css']
if (isCSS) {
// If it's a CSS object, add as a Nativewind className
const { '$$css': _, ...classNameObjects } = value as Record<string, unknown>
const className = [acc.nativeWindClassName, ...Object.values(classNameObjects)].filter(Boolean).join(' ') // prettier-ignore
return { ...acc, nativeWindClassName: className }
} else if (Array.isArray(value)) {
// If it's an array, we should merge the arrays
const flattenedStyles = value.reduce((acc, val) => ({ ...acc, ...val }), {})
return { ...acc, nativeWindStyles: { ...acc.nativeWindStyles, ...flattenedStyles } } // prettier-ignore
} else {
// If it's a React-Native style object, check if we should merge arrays or objects
return { ...acc, nativeWindStyles: { ...acc.nativeWindStyles, ...(value as Record<string, unknown>) } } // prettier-ignore
}
}
// If the key is a string, it's a regular style
return { ...acc, restStyle: { ...acc.restStyle, [key]: value } }
},
{ nativeWindStyles: {}, nativeWindClassName: '', restStyle: {} }
)
}

0 comments on commit 5cd3902

Please sign in to comment.