TSS (tss-react)試用紀錄

React, CSS in JS, TypeScript, JSS, TSS

引言

簡單來說TSS就是JSS的TypeScript版本。JSS 主要有三個基本能力:

取得Theme

<ThemeProvider /> → useTheme()

自訂可共用性通用性的 CSS modules 與使用

makeStyles() → useStyles()

與自訂簡單 CSS 分隔特性的元件

withStyles() 或 styled()

簡單結論

  • styled()函式

    • 單一元件 CSS in JS 。適用單 Element 客製 CSS 分隔元件,如:h1 ~ h6, p, button等。

    • 自訂基礎元件開發階段。

  • withStyles() 函式

    • 複合元件 CSS in JS。適用複合 Element 客製 CSS 分隔元件,如:InputField, TextField等。

    • 自訂基礎元件開發階段。

  • makeStyles() → useStyles() 函式

    • 有共用性,然實際上CSS分隔的應用沒見過過有人共用。

    • 建立CSS modules再使用它(們)。

    • 應用開發階段。

原碼紀錄

styled() 函式範例。

適用單 Element 客製 CSS 分隔元件,如:h1 ~ h6, p, button等。 對高等元件也有效但這實用意義不大。

import React from 'react'
import { styled } from '@mui/material'

// styled with theme
export const H1 = styled('h1')(({ theme }) => ({
    color: theme.palette.primary.dark,
    border: 'solid 2px pink',
    borderRadius: '2px'
}));

// styled with theme & props
export const P1 = styled('p')<{
    bold?: string // 自訂 props
}>(props => {
    const { theme: { palette } } = props
    console.log('P1', { palette, props })
    return {
        borderStyle: 'solid',
        borderRadius: 3,
        padding: 3,
        color: palette.info.main,
        borderColor: palette.warning.light,
        borderWidth: props.bold === "true" ? 3 : 1,
        fontWeight: props.bold === "true" ? 900 : 300
    }
});

withStyles() 函式範例

適用複合 Element 客製 CSS 分隔元件,如:InputField, TextField等。

import type { FC } from 'react'
import React from 'react'
import { withStyles } from 'tss-react/mui'
import clsx from 'clsx'

const MyNote0: FC<{
    className?: string,
    children: React.ReactNode,
    primary: boolean
}> = (props) => (
    {/* ※ 注:外殼必需有 className={props.className} 屬性
      來銜接 withStyle 產生的 root css-class。 */}
    <div className={props.className}> 
        <p className={clsx(props.primary && 'primary')}>
            {props.children}
        </p>
    </div>
)

export const MyNote = withStyles(ANote0, (theme: Theme, props) => {
    const { palette, spacing } = theme
    return {
        root: {
            background: palette.info.light,
            color: palette.primary.dark,
            '& .primary': {
                padding: spacing(2),
                background: palette.warning.light
            }
        }
    }
})

withStyles函式對高階元件也有效。

import type { FC } from 'react'
import React from 'react';
import { Switch, FormControlLabel, Theme } from '@mui/material'
import { withStyles } from 'tss-react/mui';
import clsx from 'clsx';

export const MySwitch: FC<{
    label: string,
    checked?: boolean,
    onChange: (checked: boolean) => void,
    className?: string
}> = (props) => (
    <FormControlLabel className={props.className} label={props.label}
        control={<Switch checked={props.checked} onChange={(event, checked) => props.onChange(checked)} />}
    />
)

export const StyledMySwitch = withStyles(MySwitch, (theme: Theme, props) => {
    const { palette } = theme
    return {
        root: {
            background: props.checked ? palette.secondary.main : palette.error.light,
            color: palette.primary.dark,
        }
    }
}

應用

import { useReducer } from 'react'
import { Container } from '@mui/material'
import { H1, P1, MySwitch, StyledMySwitch as MySwitch2, ANote } from './widgets/StyledWidgets'

export default (props) => {
    const [bold, toggleBold] = useReducer((f) => !f, false)
    return (
        <Container>
            <H1>I am AppForm 0011</H1>

            <MySwitch label="blob" checked={bold} onChange={toggleBold} />
            <MySwitch2 label="blob2" checked={bold} onChange={toggleBold} />

            <ANote primary={bold}>
                this is foo.<br/>
                that is bar.
            </ANote> 

            <P1 bold={bold.toString()}>Show me the moeny. 111</P1>
        </Container>
    )
}

makeStyles → useStyels

makeStyels 建立CSS modules。

AppForm.styles.ts
import type { Theme } from "@mui/material";
import { makeStyles } from 'tss-react/mui';

export const useStyles = makeStyles<{ param0: boolean }>()(
  (theme: Theme, params) => {
    const { palette } = theme
    return {
      root: {
      },
      todoItem: {
        '&:nth-of-type(odd)': {
          backgroundColor: palette.grey[100], // stripe, 產生條紋// stripe, 產生條紋
        },
        '&:hover': {
          backgroundColor: palette.action.focus
        },
        '& button': {
          color: 'transparent'
        },
        '&:hover button': {
          color: palette.error.main
        },
        '&.completed span': {
          color: palette.grey[500],
          textDecoration: 'line-through'
        },
      },
    }
  }
);

再透過 useStyles 使用它(們)。

AppForm.tsx
// type
import type { TodoItem } from 'views/demo2/dm2030/todoListSlice'
//
import { Container, Stack, Paper, IconButton, Button, InputAdornment } from '@mui/material'
import { List, ListItem, ListItemText, ListItemIcon } from '@mui/material'
import { OutlinedInput } from '@mui/material'
import { H3, P1 } from 'components/highorder'
import RadioField from './RadioFiled'
// hooks
import { useState } from 'react'
import { useAppSelector, useAppDispatch } from 'hooks/hooks'
import * as act from 'views/demo2/dm2030/todoListSlice'
// icons
import DoneIcon from '@mui/icons-material/CheckCircle';
import UndoIcon from '@mui/icons-material/RadioButtonUnchecked';
import ClearIcon from '@mui/icons-material/Clear'
import CheckIcon from '@mui/icons-material/Check'
// CSS
import clsx from 'clsx'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { useStyles } from './AppForm.styles'
//import styels from './AppForm.module.css'

export default (props) => {
    const todoList = useAppSelector(store => store.todoList)
    const todoActiveCount = useAppSelector(act.activeCount)
    const dispatch = useAppDispatch()
    const [newText, setNewText] = useState('')
    const [filterCond, setFilterCond] = useState('all')

    const { classes: ss } = useStyles({ param0: false })

    const filterHandler = (todo: TodoItem) => (
        filterCond === 'all' ||
        filterCond === 'active' && !todo.completed ||
        filterCond === 'completed' && todo.completed
    );

    return (
        <Container className={clsx(ss.root, props.className)}>
            <H3>Todos</H3>
            <P1>新增(mount)與移除(unmount)加入過場動畫。<br />
                並條紋化與hover。<br />
                參考:<a href="http://chenglou.github.io/react-motion/demos/demo3-todomvc-list-transition/" target="_blank">RedoMVC</a>
            </P1>

            <Paper sx={{ p: 2 }}>
                <OutlinedInput placeholder="What needs to bo done?"
                    fullWidth
                    value={newText}
                    onChange={e => setNewText(e.target.value)}
                    onKeyUp={e => {
                        if (e.key === 'Enter') {
                            dispatch(act.addItem(newText))
                            setNewText('')
                        }
                    }}
                    startAdornment={
                        <InputAdornment position="start">
                            <CheckIcon sx={{ cursor: 'pointer' }} onClick={() => dispatch(act.checkAllItem())} />
                        </InputAdornment>
                    }
                />
                <List>
                    <TransitionGroup>
                        {todoList.filter(filterHandler)
                            .filter(c => c.text && c.text.includes(newText))
                            .map((todo) => (
                                <CSSTransition
                                    key={todo.id}
                                    timeout={400}
                                    classNames={{
                                        enter: "animate__animated",
                                        enterActive: "animate__fadeInUp",
                                        exit: "animate__animated",
                                        exitActive: "animate__backOutDown"
                                    }}
                                >
                                    <ListItem className={clsx(ss.todoItem, todo.completed && 'completed')}
                                        secondaryAction={
                                            <IconButton edge="end" onClick={() => dispatch(act.rmvItem(todo.id))}>
                                                <ClearIcon />
                                            </IconButton>
                                        }
                                    >
                                        <ListItemIcon onClick={() => dispatch(act.toggleItem(todo.id))}>
                                            {todo.completed ? <DoneIcon color="success" /> : <UndoIcon />}
                                        </ListItemIcon>
                                        <ListItemText>
                                            {todo.text}
                                        </ListItemText>
                                    </ListItem>
                                </CSSTransition>
                            ))}
                    </TransitionGroup>
                </List>

                <Stack direction="row" justifyContent="space-between" alignItems="baseline">
                    <P1>{todoActiveCount} items left</P1>
                    <RadioField value={filterCond} onChange={setFilterCond} />
                    <Button variant="text" onClick={() => dispatch(act.clearCompleted())}>Clear completed</Button>
                </Stack>
            </Paper>
        </Container >
    )
}

//=============================================================================
// const StyledListItem = styled(ListItem)(({ theme }) => ({
//     // stripe, 產生條紋
//     '&:nth-of-type(odd)': {
//         backgroundColor: theme.palette.grey[100],
//     },
//     '&:hover': {
//         backgroundColor: theme.palette.action.focus
//     },
// }));

Last updated