Skip to content

Commit 97101d0

Browse files
committed
feat(core): [styled] support infering ts type from props
1 parent 119f4b8 commit 97101d0

File tree

4 files changed

+40
-180
lines changed

4 files changed

+40
-180
lines changed

packages/core/src/styled.ts

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,48 @@
11
import {
2+
ComponentObjectPropsOptions,
3+
ExtractPropTypes,
24
defineComponent,
3-
DefineSetupFnComponent,
45
h,
56
inject,
67
onMounted,
78
onUnmounted,
8-
PropType,
9-
PublicProps,
109
reactive,
1110
ref,
12-
SlotsType,
1311
watch,
1412
} from 'vue'
1513
import domElements, { type SupportedHTMLElements } from '@/src/constants/domElements'
1614
import { type ExpressionType, generateClassName, generateComponentName, insertExpressions, injectStyle, removeStyle } from '@/src/utils'
1715
import { isStyledComponent, isValidElementType, isVueComponent } from '@/src/helper'
1816
import { DefaultTheme } from './providers/theme'
1917

20-
interface IProps {
21-
as?: PropType<SupportedHTMLElements>
22-
}
23-
24-
type ComponentCustomProps = PublicProps & {
25-
styled: boolean
26-
}
27-
28-
export type StyledComponentType<P = any> = DefineSetupFnComponent<IProps & P, any, SlotsType, IProps & P, ComponentCustomProps>
29-
30-
type StyledFactory = <T = Record<string, any>>(
31-
styles: TemplateStringsArray,
32-
...expressions: (ExpressionType<T & { theme: DefaultTheme }> | ExpressionType<T & { theme: DefaultTheme }>[])[]
33-
) => StyledComponentType
34-
type StyledComponent = StyledFactory & {
35-
attrs: <T extends Record<string, unknown>>(attrs: T) => StyledFactory
36-
}
3718
type Attrs = Record<string, any>
3819

39-
function baseStyled<P extends Record<string, any>>(target: string | InstanceType<any>, propsDefinition?: P & IProps): StyledComponent {
20+
type BaseContext<T> = T & { theme: DefaultTheme }
21+
type PropsDefinition<T> = {
22+
[K in keyof T]: T[K]
23+
}
24+
function baseStyled<T extends object>(target: string | InstanceType<any>, propsDefinition?: PropsDefinition<T>) {
4025
if (!isValidElementType(target)) {
4126
throw Error('The element is invalid.')
4227
}
4328
let attributes: Attrs = {}
44-
function styledComponent<T>(
29+
function styledComponent<P>(
4530
styles: TemplateStringsArray,
46-
...expressions: (ExpressionType<T> | ExpressionType<T>[])[]
47-
): StyledComponentType {
48-
const cssStringsWithExpression = insertExpressions<T>(styles, expressions)
49-
return createStyledComponent<T>(cssStringsWithExpression)
31+
...expressions: (
32+
| ExpressionType<BaseContext<P & ExtractPropTypes<PropsDefinition<T>>>>
33+
| ExpressionType<BaseContext<P & ExtractPropTypes<PropsDefinition<T>>>>[]
34+
)[]
35+
) {
36+
const cssStringsWithExpression = insertExpressions(styles, expressions)
37+
return createStyledComponent<P>(cssStringsWithExpression)
5038
}
5139

52-
styledComponent.attrs = function <T extends Record<string, any>>(attrs: T): StyledComponent {
40+
styledComponent.attrs = function <A extends Attrs = Record<string, any>>(attrs: A) {
5341
attributes = attrs
5442
return styledComponent
5543
}
5644

57-
function createStyledComponent<T>(cssWithExpression: ExpressionType<T & { theme: DefaultTheme }>[]): StyledComponentType {
45+
function createStyledComponent<P>(cssWithExpression: ExpressionType<any>[]) {
5846
let type: string = target
5947
if (isVueComponent(target)) {
6048
type = 'vue-component'
@@ -96,15 +84,15 @@ function baseStyled<P extends Record<string, any>>(target: string | InstanceType
9684
...props,
9785
...props.props,
9886
}
99-
tailwindClasses.value = injectStyle<T & { theme: DefaultTheme }>(defaultClassName, cssWithExpression, context)
87+
tailwindClasses.value = injectStyle(defaultClassName, cssWithExpression, context)
10088
},
10189
{
10290
deep: true,
10391
},
10492
)
10593

10694
onMounted(() => {
107-
tailwindClasses.value = injectStyle<T & { theme: DefaultTheme }>(defaultClassName, cssWithExpression, context)
95+
tailwindClasses.value = injectStyle(defaultClassName, cssWithExpression, context)
10896
})
10997

11098
onUnmounted(() => {
@@ -127,13 +115,15 @@ function baseStyled<P extends Record<string, any>>(target: string | InstanceType
127115
name: componentName,
128116
props: {
129117
as: {
130-
type: String as PropType<SupportedHTMLElements>,
118+
type: String,
119+
required: false,
131120
},
132121
props: {
133-
type: Object as PropType<P>,
122+
type: Object,
123+
required: false,
134124
},
135125
...propsDefinition,
136-
},
126+
} as ComponentObjectPropsOptions<{ as?: string; props?: P } & ExtractPropTypes<PropsDefinition<T>>>,
137127
inheritAttrs: true,
138128
},
139129
)
@@ -144,7 +134,7 @@ function baseStyled<P extends Record<string, any>>(target: string | InstanceType
144134

145135
/** Append all the supported HTML elements to the styled properties */
146136
const styled = baseStyled as typeof baseStyled & {
147-
[E in SupportedHTMLElements]: StyledComponent
137+
[E in SupportedHTMLElements]: ReturnType<typeof baseStyled>
148138
}
149139

150140
domElements.forEach((domElement: SupportedHTMLElements) => {

packages/core/src/utils/insertExpressions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export type ExpressionType<T = Record<string, any>> =
55
| string
66
| number
77
| TailwindObject
8+
| undefined
9+
| null
810

911
export function insertExpressions<T>(
1012
strings: TemplateStringsArray,

packages/playground/src/App.vue

Lines changed: 12 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,27 @@
11
<script setup lang="ts">
2-
import { styled, ThemeProvider, keyframes, withAttrs, css, cssClass, createGlobalStyle, tw } from '@/index'
3-
import Component from './Component.vue'
4-
import { ref } from 'vue'
2+
import { styled } from '@/index'
53
6-
const theme = ref({ primary: 'green', error: 'red' })
7-
8-
const color = ref('red')
9-
10-
const kf = keyframes`
11-
from {
12-
margin-left: 100%;
13-
width: 300%;
14-
}
15-
16-
to {
17-
margin-left: 0%;
18-
width: 100%;
19-
}
20-
`
21-
22-
const update = () => {
23-
theme.value.primary = theme.value.primary === 'red' ? 'green' : 'red'
24-
color.value = color.value === 'red' ? 'green' : 'red'
25-
}
26-
27-
const StyledComp3 = styled(Component)`
28-
position: sticky;
29-
background: ${(props) => props.theme.primary};
30-
`
31-
const StyledComp4 = styled.div`
32-
background: ${(props) => props.theme.error};
33-
`
34-
const StyledComp5 = styled.div`
35-
width: 40px;
36-
height: 40px;
37-
background: ${(props) => props.theme.error};
38-
animation-duration: 3s;
39-
animation-name: ${kf};
40-
animation-iteration-count: infinite;
41-
`
42-
43-
const StyledComp6 = styled('button', { color: String })<{
4+
const TestProps = styled.div<{
445
color: string
456
}>`
46-
width: 40px;
47-
height: 40px;
487
color: ${(props) => props.color};
498
`
509
51-
const WithAttrsComp = withAttrs(StyledComp6, { disabled: true })
52-
53-
const mixin = css<{
54-
color: string
55-
}>`
10+
const Test = styled('div', { color: String })`
5611
color: ${(props) => props.color};
5712
`
58-
const cr = styled('button', { color: String })
59-
const StyledComp7 = cr`
60-
${mixin}
61-
`
62-
const BlueButton = styled.button`
63-
width: 120px;
64-
height: 40px;
65-
margin-right: 8px;
66-
padding: 4px 8px;
67-
border-radius: 9999px;
68-
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
69-
background-color: skyblue;
70-
font-weight: bold;
71-
color: #fff;
72-
73-
.abc {
74-
color: red;
75-
}
76-
`
77-
const LinkButton = styled(BlueButton)`
78-
border: none;
79-
`
80-
81-
const StyledLink = styled.a`
82-
color: #ccc;
83-
`
84-
85-
const StyledBlueLink = styled(StyledLink, { color: String })`
86-
color: ${(props: any) => props.color};
87-
`
88-
89-
const commonCSS = css`
90-
padding: 10px 20px;
91-
border-radius: 8px;
92-
`
93-
94-
const commonClass = cssClass`
95-
${commonCSS}
96-
color: #fff;
97-
background-color: red;
98-
`
99-
100-
const testEmbedCss1 = css`
101-
margin: 40px;
102-
background: white;
103-
padding: 10px 20px;
104-
border-radius: 8px;
105-
`
106-
const testEmbedCss2 = css`
107-
margin: 40px;
108-
background: blue;
109-
padding: 10px 20px;
110-
border-radius: 8px;
111-
color: white;
112-
`
113-
114-
const show = ref(true)
115-
const TestEmbedComponent = styled('div', { show: Boolean })`
116-
${(props) => {
117-
return props.show ? testEmbedCss1 : testEmbedCss2
118-
}}
119-
`
120-
121-
// console.log(testEmbedCss1, testEmbedCss2)
122-
const visible = ref(true)
123-
124-
const Global = createGlobalStyle`
125-
body {
126-
display: flex
127-
}
128-
`
129-
const tt = 'bg-white'
130-
const TwComponent = styled.div`
131-
${tw`text-2xl text-gray-100 text-lg ${tt}`}
132-
`
133-
134-
const className = cssClass`
135-
color: red;
136-
`
13713
</script>
13814

13915
<template>
140-
<ThemeProvider :theme="theme">
141-
<TwComponent :class="className">Test</TwComponent>
142-
<Global />
143-
<div @click="visible = !visible">Test Remove</div>
144-
<StyledComp3 class="text-white" @click="update">12345</StyledComp3>
145-
<StyledComp4>12345</StyledComp4>
146-
<StyledComp5>12345</StyledComp5>
147-
<WithAttrsComp color="red">123</WithAttrsComp>
148-
<StyledComp7 color="blue">123</StyledComp7>
149-
<LinkButton as="a" href="#">Link Button</LinkButton>
150-
<StyledBlueLink :color="color" href="#" @click="update">Styled Link</StyledBlueLink>
151-
<div :class="commonClass">test common class</div>
152-
153-
<TestEmbedComponent :show="show"> White </TestEmbedComponent>
154-
<TestEmbedComponent :show="!show" @click="show = !show"> Blue </TestEmbedComponent>
155-
156-
<BlueButton v-if="visible" />
157-
</ThemeProvider>
16+
<Test color="orange">666</Test>
17+
18+
<TestProps
19+
:props="{
20+
color: 'red',
21+
}"
22+
>
23+
tttttttt
24+
</TestProps>
15825
</template>
15926

16027
<style>

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
33
"jsx": "preserve",
4+
"jsxImportSource": "vue",
45
// 目标 ECMAScript 版本
56
"target": "esnext",
67
// 模块解析策略

0 commit comments

Comments
 (0)