Skip to content

Commit

Permalink
fix: type support for props supplied in .attrs() (#25)
Browse files Browse the repository at this point in the history
* feat: support .attrs

* fix: attrs used in factory config

* test: update

* refactor: simplify

* refactor: DP => A2
  • Loading branch information
sparten11740 authored Oct 23, 2024
1 parent 5855163 commit fe53111
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 25 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"microtime": "^3.1.1",
"react": "18.2.0",
"react-native": "^0.73.6",
"react-native-linear-gradient": "^2.8.3",
"react-native-reanimated": "^3.8.1",
"react-test-renderer": "18.2.0",
"rollup": "^4.13.2",
Expand Down
32 changes: 12 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import type {StyleProp} from "react-native";

export interface Config<P = any> {
export interface Config<P extends object, A extends object> {
name?: string;
props?: Partial<P>;
style?: any;
fixedStyle?: any;
attrs?: (props: P) => any;
attrs?: Attrs<A>;
comp?: React.ComponentType<any>;
child?: React.ComponentType<any>;
childProps?: ((props: any) => any) | any;
Expand All @@ -18,33 +18,25 @@ export type StyledProps<S extends object> = {
style?: StyleProp<S>;
};

type WithOptional<T, K extends keyof any> = Omit<T, K> & { [P in K]?: P extends keyof T ? T[P] : never }

type Attrs<A extends object> = ((props: Partial<A>) => Partial<A>) | Partial<A>
type ComponentStyle<P extends object, SP extends object, S extends object> = ((props: Partial<P> & SP) => S) | S

export type StyledComponent<P extends object, S extends object> = React.ForwardRefExoticComponent<
React.PropsWithoutRef<P & StyledProps<S> & { children?: React.ReactNode }> &
React.RefAttributes<any>
> & {
extend: <SP extends object>(more: ComponentStyle<P, SP, S>) => StyledComponent<P & SP, S>;
attrs: (attrs: any) => StyledComponent<P, S>;
attrs: <A extends P>(attrs: Attrs<A>) => StyledComponent<WithOptional<P, keyof A>, S>;
withComponent: (comp: React.ComponentType<any>) => StyledComponent<P, S>;
withChild: (child: React.ComponentType<any>, childProps?: any) => StyledComponent<P, S>;
};

type ComponentFactory<P extends object, S extends object> = <SP extends object>(
componentStyle: ComponentStyle<P, SP, S>
) => StyledComponent<P & SP, S> | (() => StyledComponent<P, S>);

export interface StyledFunction{
<P extends object, S extends object>(
Comp: React.ComponentType<P>,
config?: Config<P>
): ComponentFactory<P, S>;
}

const styled = <P extends object, S extends object = object>(
const styled = <P extends object, S extends object = object, A extends Partial<P> = {}>(
Comp: React.ComponentType<P>,
config: Config<P> = {}
) => <SP extends object>(componentStyle?: ComponentStyle<P, SP, S>): StyledComponent<P & SP, S> => {
config: Config<P, A> = {}
) => <SP extends object>(componentStyle?: ComponentStyle<P, SP, S>): StyledComponent<WithOptional<P & SP, keyof A>, S> => {
const {
name,
props: factoryProps = {},
Expand All @@ -67,7 +59,7 @@ const styled = <P extends object, S extends object = object>(

let style = {
...factoryStyle,
...attrsResult.style,
...(attrsResult as any).style,
...(typeof componentStyle === 'function'
? componentStyle(restProps as P & SP)
: componentStyle),
Expand Down Expand Up @@ -127,14 +119,14 @@ const styled = <P extends object, S extends object = object>(
// Extend the Styled component with custom methods
const StyledComponent = Object.assign(Styled, {
extend: <SP2 extends object>(more: ComponentStyle<P, SP & SP2, S>) => styled(StyledComponent, { name })(more),
attrs: (attrs: any) => styled(StyledComponent, { attrs })() as StyledComponent<P & SP, S>,
attrs: <A2 extends P>(attrs: Attrs<A2>) => styled(StyledComponent, { attrs })() as StyledComponent<WithOptional<P, keyof A2>, S>,
withComponent: (comp: React.ComponentType<any>) =>
styled(StyledComponent, { comp })(componentStyle) as StyledComponent<P & SP, S>,
withChild: (child: React.ComponentType<any>, childProps: any) =>
styled(StyledComponent, { child, childProps })() as StyledComponent<P & SP, S>,
});

return StyledComponent as StyledComponent<P & SP, S>;
return StyledComponent as StyledComponent<WithOptional<P & SP, keyof A>, S>;
};

export default styled;
32 changes: 27 additions & 5 deletions test/index.type-test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { Text } from 'react-native'
import React, {Text, ViewStyle} from 'react-native'
import styled from '../src/index'
import extendedStyled from '../src/rn'
import {useRef} from "react";
import LinearGradient, { LinearGradientProps } from 'react-native-linear-gradient'

const StyledText = styled(Text)(({ transparent }: { transparent?: boolean; color: string }) => ({ flex: 1, opacity: transparent ? 0.5 : 1 }))
const StyledTextWithAttrs = StyledText.attrs({ color: 'black' })
const StyledTextWithAttrsFromConfig = styled(StyledText, { attrs: { color: 'black' } })()
const StyledScalableText = StyledText.extend(({ big }: { big?: boolean }) => ({ fontSize: big ? 20 : 10 }))
const StyledTextWithObjectProps = styled(Text)({ flex: 1, opacity: 1 })
const ExtendedStyledText = extendedStyled.Text(({ transparent }: { transparent?: boolean; }) => ({ flex: 1, opacity: transparent ? 0.5 : 1 }))

const ExtenedStyledTextWithAttrs = extendedStyled.Text(({ transparent }: { transparent?: boolean; big?:boolean }) => ({ flex: 1, opacity: transparent ? 0.5 : 1 })).attrs(({big}: {big?: boolean}) => ({
const ExtendedStyledTextWithStyle = extendedStyled(Text)((props: {specificColor: string}) => ({color: props.specificColor}))
const ExtenedStyledTextWithAttrs = extendedStyled.Text(({ transparent }: { transparent?: boolean; big?:boolean }) => ({ flex: 1, opacity: transparent ? 0.5 : 1 })).attrs(({ big}) => ({
ellipsizeMode: big ? 'middle' : 'head',
}))
const ExtendedStyledTextWithStyle = extendedStyled(Text)((props: {specificColor: string}) => ({color: props.specificColor}))


const StyledView = extendedStyled.View({width: 100})
const StyledViewWithDynamicProps = extendedStyled.View((props: {active: boolean}) => ({width: props.active ? 100 : 50}))
Expand All @@ -31,12 +32,25 @@ const StyledTouchable = extendedStyled.Touchable({width: 100})
const StyledTouchableWithDynamicProps = extendedStyled.Image((props: {active: boolean}) => ({width: props.active ? 100 : 50}))
const ViewWithText = extendedStyled.View({}).withChild(StyledText)

const extendedWithLinear = Object.assign(extendedStyled, {
LinearGradient: styled<LinearGradientProps, ViewStyle>(LinearGradient, {
name: 'styled(LinearGradient)',
})
})

const DangerGradient = styled(LinearGradient)().attrs({ colors: ['orange', 'red']})
const DefaultGradient = extendedWithLinear.LinearGradient({ }).attrs({
colors: ['blue', 'green'],
})

const MyScreen = () => {
const ref = useRef()
return (
<>
<StyledText color="red" transparent>Hello Transparent World</StyledText>
<StyledText ref={ref} color="red">Hello Transparent World with ref</StyledText>
<StyledTextWithAttrs>Color set through .attrs() and therefore not required</StyledTextWithAttrs>
<StyledTextWithAttrsFromConfig>Color is set through config.attrs and therefore not required</StyledTextWithAttrsFromConfig>
{/* should also work without transparent prop */}
<StyledText color="red">Hello World</StyledText>
{/* @ts-expect-error -- should not work without required color prop */}
Expand Down Expand Up @@ -66,6 +80,14 @@ const MyScreen = () => {
<StyledTouchableWithDynamicProps active>Touchable with dynamic style</StyledTouchableWithDynamicProps>

<ViewWithText />

{/* @ts-expect-error -- missing "colors" */}
<LinearGradient/>
{/* doesn't require optional props */}
<DefaultGradient />
<DangerGradient />
{/* but they can be provided */}
<DefaultGradient colors={['blue', 'green']} />
</>
)
}

0 comments on commit fe53111

Please sign in to comment.