Skip to content

Commit eef7117

Browse files
committed
feat(core): supports CSS objects and style functions
1 parent d0d6911 commit eef7117

File tree

8 files changed

+718
-9
lines changed

8 files changed

+718
-9
lines changed

packages/core/src/styled.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@ export type PropsDefinition<T> = {
4444
[K in keyof T]: T[K]
4545
}
4646

47+
// CSS样式对象类型
48+
type CSSStyleObject = Record<string, string | number>
49+
50+
// 样式函数类型
51+
type StyleFunction<T> = (props: BaseContext<T>) => CSSStyleObject
52+
4753
// 定义 styledComponent 类型
4854
interface StyledComponent<T extends object> {
55+
// 支持模板字符串
4956
<P>(
5057
styles: TemplateStringsArray,
5158
...expressions: (
@@ -54,12 +61,20 @@ interface StyledComponent<T extends object> {
5461
)[]
5562
): DefineSetupFnComponent<{ as?: string, props?: P } & TransformProps<T> & P & HTMLAttributes>
5663

64+
// 支持CSS对象
65+
<P>(
66+
styles: CSSStyleObject | StyleFunction<P & TransformProps<T>>
67+
): DefineSetupFnComponent<{ as?: string, props?: P } & TransformProps<T> & P & HTMLAttributes>
68+
5769
attrs: <A = object>(
5870
attrs: A | ((props: TransformProps<T> & A) => A),
5971
) => StyledComponent<A & T>
6072

6173
// 支持泛型参数的类型定义
6274
<P extends object>(props: PropsDefinition<P>): StyledComponent<P & T>
75+
76+
// 支持单独的props函数链式调用
77+
props: <P extends object>(propsDefinition: PropsDefinition<P>) => StyledComponent<P & T>
6378
}
6479

6580
// 类型辅助函数,用于在编译时转换 props 类型
@@ -91,15 +106,34 @@ function baseStyled<T extends object>(target: string | InstanceType<any>, propsD
91106
}
92107
let defaultAttrs: unknown
93108
function styledComponent<P>(
94-
stylesOrProps: TemplateStringsArray | PropsDefinition<P>,
109+
stylesOrProps: TemplateStringsArray | PropsDefinition<P> | CSSStyleObject | StyleFunction<P & TransformProps<T>>,
95110
...expressions: (
96111
| ExpressionType<BaseContext<P & TransformProps<T>>>
97112
| ExpressionType<BaseContext<P & TransformProps<T>>>[]
98113
)[]
99114
): any {
100-
// 处理泛型参数的情况,如 styled.div<Props>
115+
// 处理样式函数
116+
if (typeof stylesOrProps === 'function') {
117+
const styleFunction = stylesOrProps as StyleFunction<BaseContext<P & TransformProps<T>>>
118+
return createStyledComponentFromFunction<P>(styleFunction)
119+
}
120+
121+
// 处理CSS对象或props定义
101122
if (!Array.isArray(stylesOrProps)) {
102-
return baseStyled(target, { ...propsDefinition, ...stylesOrProps } as PropsDefinition<T & P>) as StyledComponent<T & P>
123+
// 检查是否为props定义(包含type、required、default等属性)
124+
const hasPropsDefinitionKeys = Object.values(stylesOrProps).some(value =>
125+
value && typeof value === 'object' && ('type' in value || 'required' in value || 'default' in value),
126+
)
127+
128+
if (!hasPropsDefinitionKeys) {
129+
// 是CSS对象
130+
const cssObject = stylesOrProps as CSSStyleObject
131+
return createStyledComponentFromObject<P>(cssObject)
132+
}
133+
else {
134+
// 是props定义
135+
return baseStyled(target, { ...propsDefinition, ...stylesOrProps } as PropsDefinition<T & P>) as StyledComponent<T & P>
136+
}
103137
}
104138

105139
// 正常的样式模板字符串处理
@@ -114,6 +148,39 @@ function baseStyled<T extends object>(target: string | InstanceType<any>, propsD
114148
return styledComponent
115149
}
116150

151+
// 添加props方法支持链式调用
152+
styledComponent.props = function <P extends object>(newPropsDefinition: PropsDefinition<P>) {
153+
return baseStyled(target, { ...propsDefinition, ...newPropsDefinition } as PropsDefinition<T & P>) as StyledComponent<T & P>
154+
}
155+
156+
// 将CSS对象转换为CSS字符串
157+
function cssObjectToString(cssObject: CSSStyleObject): string {
158+
return Object.entries(cssObject)
159+
.map(([key, value]) => {
160+
// 将驼峰命名转换为kebab-case
161+
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
162+
return `${kebabKey}: ${value};`
163+
})
164+
.join(' ')
165+
}
166+
167+
// 从CSS对象创建组件
168+
function createStyledComponentFromObject<P>(cssObject: CSSStyleObject) {
169+
const cssString = cssObjectToString(cssObject)
170+
const cssWithExpression = [cssString] as ExpressionType<any>[]
171+
return createStyledComponent<P>(cssWithExpression)
172+
}
173+
174+
// 从样式函数创建组件
175+
function createStyledComponentFromFunction<P>(styleFunction: StyleFunction<BaseContext<P & TransformProps<T>>>) {
176+
// 创建一个表达式函数,在运行时调用样式函数
177+
const cssWithExpression = [(props: BaseContext<P & TransformProps<T>>) => {
178+
const cssObject = styleFunction(props)
179+
return cssObjectToString(cssObject)
180+
}] as ExpressionType<any>[]
181+
return createStyledComponent<P>(cssWithExpression)
182+
}
183+
117184
function createStyledComponent<P>(cssWithExpression: ExpressionType<any>[]) {
118185
let type: string = target
119186
if (isVueComponent(target)) {

packages/docs/.vitepress/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ function sidebarGuide() {
5757
text: 'Advances',
5858
base: '/guide/advances/',
5959
items: [
60+
{ text: 'CSS Objects and Functions', link: 'css-objects-and-functions' },
6061
{ text: 'Theming', link: 'theming' },
6162
{ text: 'Global Styles', link: 'global-style' },
6263
{ text: 'CSS Mixin', link: 'css-mixin' },

packages/docs/.vitepress/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function sidebarGuide() {
8181
text: '进阶',
8282
base: '/zh/guide/advances/',
8383
items: [
84+
{ text: 'CSS对象和样式函数', link: 'css-objects-and-functions' },
8485
{ text: '主题', link: 'theming' },
8586
{ text: '全局样式', link: 'global-style' },
8687
{ text: '样式复用', link: 'css-mixin' },
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# CSS Objects and Style Functions
6+
7+
Starting from version v1.12.0, `@vue-styled-components/core` supports CSS objects and style functions, providing more flexible ways to define component styles.
8+
9+
## CSS Object Support
10+
11+
You can now pass CSS objects directly to styled components instead of template literals.
12+
13+
:::demo
14+
15+
```vue
16+
<script setup lang="ts">
17+
import { styled } from '@vue-styled-components/core'
18+
19+
const StyledDiv = styled.div({
20+
color: 'red',
21+
fontSize: '16px',
22+
backgroundColor: 'blue',
23+
padding: '10px',
24+
borderRadius: '8px',
25+
textAlign: 'center'
26+
})
27+
</script>
28+
29+
<template>
30+
<StyledDiv>CSS Object Styled Component</StyledDiv>
31+
</template>
32+
```
33+
34+
:::
35+
36+
### Benefits
37+
38+
- **Type Safety**: Better TypeScript support with autocomplete for CSS properties
39+
- **Dynamic Values**: Easier to work with computed values and variables
40+
- **Readability**: More structured and readable for complex styles
41+
42+
## Style Functions
43+
44+
Style functions allow you to create dynamic styles based on component props.
45+
46+
:::demo
47+
48+
```vue
49+
<script setup lang="ts">
50+
import { styled } from '@vue-styled-components/core'
51+
import { ref } from 'vue'
52+
53+
const buttonProps = {
54+
disabled: Boolean,
55+
size: String
56+
}
57+
58+
const StyledButton = styled.button.props(buttonProps)(({ disabled, size }) => ({
59+
color: disabled ? '#fff' : '#007bff',
60+
padding: size === 'large' ? '12px 24px' : size === 'small' ? '4px 8px' : '8px 16px',
61+
fontSize: size === 'large' ? '16px' : size === 'small' ? '12px' : '14px',
62+
border: 'none',
63+
borderRadius: '4px',
64+
backgroundColor: disabled ? '#ccc' : '#f8f9fa',
65+
cursor: disabled ? 'not-allowed' : 'pointer',
66+
transition: 'all 0.2s ease'
67+
}))
68+
69+
const disabled = ref(false)
70+
const size = ref<'small' | 'medium' | 'large'>('medium')
71+
</script>
72+
73+
<template>
74+
<div style="display: flex; flex-direction: column; gap: 16px; align-items: flex-start;">
75+
<StyledButton :disabled="disabled" :size="size">
76+
Dynamic Button ({{ size }})
77+
</StyledButton>
78+
79+
<div style="display: flex; gap: 8px;">
80+
<button @click="size = 'small'">Small</button>
81+
<button @click="size = 'medium'">Medium</button>
82+
<button @click="size = 'large'">Large</button>
83+
<button @click="disabled = !disabled">
84+
{{ disabled ? 'Enable' : 'Disable' }}
85+
</button>
86+
</div>
87+
</div>
88+
</template>
89+
```
90+
91+
:::
92+
93+
## Props Chain Calling
94+
95+
You can use the `.props()` method to define component props separately, then chain with element methods.
96+
97+
:::demo
98+
99+
```vue
100+
<script setup lang="ts">
101+
import { styled } from '@vue-styled-components/core'
102+
import { ref } from 'vue'
103+
104+
const StyledInput = styled
105+
.input
106+
.props({
107+
borderColor: { type: String, default: '#ccc' },
108+
size: { type: String, default: 'medium' }
109+
})(({ borderColor, size }) => ({
110+
border: `1px solid ${borderColor}`,
111+
padding: size === 'large' ? '12px' : size === 'small' ? '4px' : '8px',
112+
fontSize: size === 'large' ? '16px' : size === 'small' ? '12px' : '14px',
113+
borderRadius: '4px',
114+
outline: 'none',
115+
transition: 'border-color 0.2s ease',
116+
width: '100%'
117+
}))
118+
119+
const borderColor = ref('#ccc')
120+
const size = ref('medium')
121+
</script>
122+
123+
<template>
124+
<div style="display: flex; flex-direction: column; gap: 16px;">
125+
<StyledInput
126+
:borderColor="borderColor"
127+
:size="size"
128+
placeholder="Type something..."
129+
@focus="borderColor = '#007bff'"
130+
@blur="borderColor = '#ccc'"
131+
/>
132+
133+
<div style="display: flex; gap: 8px;">
134+
<button @click="size = 'small'">Small</button>
135+
<button @click="size = 'medium'">Medium</button>
136+
<button @click="size = 'large'">Large</button>
137+
</div>
138+
</div>
139+
</template>
140+
```
141+
142+
:::
143+
144+
## Combined Usage
145+
146+
You can combine props definition with style functions for more complex components.
147+
148+
:::demo
149+
150+
```vue
151+
<script setup lang="ts">
152+
import { styled } from '@vue-styled-components/core'
153+
import { ref } from 'vue'
154+
155+
const StyledCard = styled
156+
.div
157+
.props({
158+
elevation: { type: Number, default: 1 },
159+
rounded: { type: Boolean, default: true },
160+
variant: { type: String, default: 'default' }
161+
})(({ elevation, rounded, variant }) => ({
162+
boxShadow: `0 ${elevation * 2}px ${elevation * 4}px rgba(0,0,0,0.1)`,
163+
borderRadius: rounded ? '8px' : '0',
164+
padding: '16px',
165+
backgroundColor: variant === 'primary' ? '#007bff' : variant === 'success' ? '#28a745' : 'white',
166+
color: variant === 'default' ? '#333' : 'white',
167+
border: variant === 'default' ? '1px solid #e9ecef' : 'none',
168+
transition: 'all 0.2s ease',
169+
cursor: 'pointer'
170+
}))
171+
172+
const elevation = ref(1)
173+
const rounded = ref(true)
174+
const variant = ref('default')
175+
</script>
176+
177+
<template>
178+
<div style="display: flex; flex-direction: column; gap: 16px;">
179+
<StyledCard
180+
:elevation="elevation"
181+
:rounded="rounded"
182+
:variant="variant"
183+
>
184+
This is a {{ variant }} card, shadow level {{ elevation }}, rounded: {{ rounded }}
185+
</StyledCard>
186+
187+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
188+
<div>
189+
<label>阴影级别: </label>
190+
<input
191+
type="range"
192+
min="0"
193+
max="5"
194+
v-model.number="elevation"
195+
/>
196+
{{ elevation }}
197+
</div>
198+
199+
<label>
200+
<input type="checkbox" v-model="rounded" />
201+
圆角
202+
</label>
203+
204+
<select v-model="variant">
205+
<option value="default">默认</option>
206+
<option value="primary">主要</option>
207+
<option value="success">成功</option>
208+
</select>
209+
</div>
210+
</div>
211+
</template>
212+
```
213+
214+
:::
215+
216+
## Key Features
217+
218+
1. **CSS Object Conversion**: Automatically converts camelCase CSS properties to kebab-case
219+
2. **Style Functions**: Support functions that receive props and return CSS objects
220+
3. **Props Chain Calling**: Define component props through `.props()` method, then chain with element methods
221+
4. **Type Safety**: Full TypeScript support ensuring type safety for props and styles
222+
5. **Backward Compatibility**: Maintains full compatibility with existing template string syntax

0 commit comments

Comments
 (0)