# 第一步
npx create-react-app --template typescript trello-clone
# 第二步
yarn && yarn build
# 第三步, 查看效果
yarn start
src/styles.ts
具体内容如下:
import React from 'react';
import styled from 'styled-components';
// 采用react的方式来定义CSS
export const buttonStyles :React.CSSProperties = {
backgroundColor: "#5aac44",
borderRadius: "3px",
border: "none",
boxShadow: "none"
}
// 下面采用styled-components定义
export const AppContainer = styled.div`
align-items: flex-start;
background-color: #3179ba;
display: flex;
flex-direction: row;
height: 100%;
padding: 20px;
width: 100%;
`
export const Button = styled.button`
background-color: #5aac44;
border-radius: 3px;
border: none;
box-shadow: none;
`
export const ColumnContainer = styled.div`
background-color: #ebecf0;
width: 300px;
min-height: 40px;
margin-right: 20px;
border-radius: 3px;
padding: 8px 8px;
flex-grow: 0;
`
export const ColumnTitle = styled.div`
padding: 6px 16px 12px;
font-weight: bold;
`
export const CardContainer = styled.div`
background-color: #fff;
cursor: pointer;
margin-bottom: 0.5rem;
padding: 0.5rem 1rem;
max-width: 300px;
border-radius: 3px;
box-shadow: #091e4240 0px 1px 0px 0px;
`
通过styled-components模块进行样式组建的编写需要下面的语句进行安装依赖:
yarn add styled-components
# 安装ts需要的对应的types
yarn add @types/styled-components
src/App.tsx
文件原来的文件如下:
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
修改为:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { AppContainer } from './styles'
const App = () => {
return (
<AppContainer>
可以拖动的列
</AppContainer>
);
}
export default App;
通过 yarn start
测试修改后的节目
先看一下React的使用类组件的方式:
import React from 'react';
interface CounterProps {
message: string;
}
interface CounterState {
count: number;
}
class Counter extends React.Component<CounterProps, CounterState> {
state : CounterState = {
count:0
};
render(): React.ReactNode {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
export default Counter;
React.Component需要接收两个参数,一个props和state,如果你i定义的是函数组件就不需要这么麻烦,如下:
export const Example = () => {
return <div>函数组件</div>
}
Typescript会自动推断出类型为JSX,如果你想明确是React函数组件,可以这样定义:
export const Example:React.FC = () => {
return <div>React函数组件</div>
}
需要的串联的组件有, 详细内容见 src/styles.ts
:
src/Column.tsx
文件,并以函数组件方式定义,内容如下:import React from "react";
export const Column = () => {
return <div>列容器的标题</div>
}
import React from "react";
import {ColumnContainer, ColumnTitle} from './styles';
export const Column = () => {
// return <div>列容器的标题</div>
return (
<ColumnContainer>
<ColumnTitle>列容器的标题</ColumnTitle>
</ColumnContainer>
)
}
import React from "react";
import {ColumnContainer, ColumnTitle} from './styles';
type ColumnProps = {
text: string
}
export const Column = ({text}: ColumnProps) => {
// return <div>列容器的标题</div>
return (
<ColumnContainer>
<ColumnTitle>列容器的标题</ColumnTitle>
</ColumnContainer>
)
}
import React from "react";
import {ColumnContainer, ColumnTitle} from './styles';
type ColumnProps = {
text?: string | undefined
}
//children == type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
export const Column = ({text, children}: React.PropsWithChildren<ColumnProps>) => {
// return <div>列容器的标题</div>
return (
<ColumnContainer>
<ColumnTitle>{text}</ColumnTitle>
{ /**这里children是由react负责去传递*/}
{children}
</ColumnContainer>
)
}
import React from "react";
import {CardContainer} from './styles';
interface CardProps {
text?: string| undefined
}
export const Card = ({text}: CardProps) => {
return <CardContainer>{text}</CardContainer>
}
src/App.tsx
import React from 'react';
import './App.css';
import { AppContainer } from './styles'
import {Card} from './Card'
import {Column} from './Column'
const App = () => {
return (
<AppContainer>
<Column text='To Do'>
<Card text="去买一杯咖啡" />
</Column>
<Column text='修改Bug'>
<Card text='需要修改React中出现的问题'/>
</Column>
<Column text='已经编写完成'>
<Card text='工作计划已将编写完成'/>
</Column>
</AppContainer>
);
}
export default App;
修改 src/styles.ts
文件
interface AddItemButtonProps {
dark?: boolean
}
export const AddItemButton = styled.button<AddItemButtonProps>`
background-color: #ffffff3d;
border-radius: 3px;
border: none;
color: ${props = > (props.dark ? "#000" : "#fff")};
cursor: pointer;
max-width: 300px;
padding: 10px 12px;
text-align: left;
transition: background 85ms ease-in;
width: 100%;
&:hover {
background-color: #ffffff52;
}
`
export const NewItemFormContainer = styled.div`
max-width: 300px;
display: flex;
flex-direction: column;
width: 100%;
align-items: flex-start;
`
export const NewItemButton = styled.button`
background-color: #5aac44;
border-radius: 3px;
border: none;
box-shadow: none;
color: #fff;
padding: 6px 12px;
text-align: center;
`
export const NewItemInput = styled.input`
border-radius:3px;
border: none;
box-shadow: #091e4240 0px 1px 0px 0px;
margin-bottom: 0.5rem;
padding: 0.5rem 1rem;
width: 100%;
`
import React,{useState} from "react";
import { NewItemForm } from "./NewItemForm";
import { AddItemButton } from './styles';
interface AddNewItemProps {
onAdd(text:string) :void
toggleButtonText:string
dark?: boolean | undefined
}
export const AddNewItem = (props: AddNewItemProps) => {
const [showForm, setShowForm] = useState(false);
const {onAdd, toggleButtonText, dark} = props;
if (showForm) {
return (
<NewItemForm
onAdd={text => {
onAdd(text)
setShowForm(false)
}} />
)
}
return (
<AddItemButton dark={dark} onClick={() => setShowForm(true)}>
{toggleButtonText}
</AddItemButton>
)
}
将组建添加到 src/Column.tsx
组件中去,代码如下:
<AppContainer>
<Column text='To Do'>
<Card text="去买一杯咖啡" />
</Column>
<Column text='修改Bug'>
<Card text='需要修改React中出现的问题'/>
</Column>
<Column text='已经编写完成'>
<Card text='工作计划已将编写完成'/>
</Column>
<AddNewItem toggleButtonText="新增一列" onAdd={console.log} />
</AppContainer>
定义自己的Hooks,在 src
文件夹中创建一个新的 utils
文件夹,并新建文件 useFocus.ts
import {useRef, useEffect} from 'react';
// Refs提供了一种被React渲染的实际DOM节点的方法
export const useFocus = () => {
const ref = useRef<HTMLInputElement>(null)
useEffect(() => {
ref.current?.focus()
})
return ref;
}
回到之前的 NewItemForm.tsx
文件中,并引入上面的 useFocus.ts
, 修改部分如下:
const inputRef = useFocus();
return (
<NewItemFormContainer>
<NewItemInput
value={text}
onChange={e => setText(e.target.value)}
ref={inputRef}
/>
<NewItemButton onClick={() => onAdd(text)}>创建新的表单</NewItemButton>
</NewItemFormContainer>
)
在 App.tsx
中创建Reducer的hooks,测试代码如下:
type State = {count:number}
type Action = {type: "increment"} | {type: "decrement"}
const App = () => {
const counterReducer = (state:State, action:Action) => {
switch(action.type) {
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count : state.count - 1}
default:
throw new Error()
}
}
const [state, dispatch] = useReducer(counterReducer, {count: 0})
return (
<>
<p>Count: {state.count}</p>
<button onClick={()=> dispatch({type: "decrement"})}>-</button>
<button onClick={()=> dispatch({type: "increment"})}>+</button>
</>
)
通常React应用程序中,数据通过props自上而下(父到子)传递,但对于应用程序中许多组件需要在组件之间共享一些数据,而不必在每个层级显示传递一个prop,这时我们采用Context进行共享一个组件树的Prop。让我们在 src
文件夹内部创建 AppStateContext.tsx
import React, {createContext} from "react";
type Task = {
id:string
text:string
}
type List = {
id: string
text: string
tasks: Task[]
}
export type AppState = {
lists: List[]
}
type AppStateContextProps = {
state: AppState
}
const AppStateContext = createContext<AppStateContextProps>({} as AppStateContextProps)
export const AppStateProvider = ({children}: React.PropsWithChildren<{}>) => {
return (
<AppStateContext.Provider value={{state: appData}}>
{children}
</AppStateContext.Provider>
)
}
const appData: AppState = {
lists: [
{id:"0", text:"To Do", tasks: [{id: "c0", text: "去买一杯咖啡"}]},
{id:"1", text:"修改Bug", tasks: [{id: "c2", text: "需要修改React中出现的问题"}]},
{id:"2", text:"已经编写完成", tasks: [{id: "c3", text: "工作计划已将编写完成"}]}
]
}
在 src
文件夹中创建 index.tsx
,内容如下:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AppStateProvider } from './AppStateContext'
ReactDOM.render(
<AppStateProvider>
<App />
</AppStateProvider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
继续修改 AppStateContext.tsx
,使用全局的上下文数据
import React, {createContext, useReducer, useContext} from "react";
export const useAppState = () => {
return useContext(AppStateContext)
}
修改 App.tsx
,替换已经常量为 AppStateContext.tsx
的数据,并修改 Column.tsx
// AppStateContext.tsx
import { useAppState } from './AppStateContext';
const { state } = useAppState()
return (
{state.lists.map((list, i) => (
<Column text={list.text} key={list.id} index={i}/>
))}
<AddNewItem toggleButtonText="新增一列" onAdd={console.log} />
<p>Count: {state2.count}</p>
<button onClick={()=> dispatch({type: "decrement"})}>-</button>
<button onClick={()=> dispatch({type: "increment"})}>+</button>
)
// Column.tsx
import { AddNewItem } from "./AddNewItem";
import { useAppState } from "./AppStateContext";
import { Card } from "./Card";
import {ColumnContainer, ColumnTitle} from './styles';
type ColumnProps = {
text?: string | undefined
index: number
}
export const Column = ({text,index}: ColumnProps) => {
// return <div>列容器的标题</div>
const {state} = useAppState()
return (
<ColumnContainer>
<ColumnTitle>{text}</ColumnTitle>
{state.lists[index].tasks.map(task => (
<Card text={task.text} key={task.id} />
))}
<AddNewItem toggleButtonText="增加新的任务" onAdd={console.log} dark />
</ColumnContainer>
)
}
修改 AppStateContext.tsx
文件,并修改 AppStateContextProps
的类型定义
// 定义Action
type Action =
{
type: "ADD_LIST",
payload: string,
} |
{
type: "ADD_TASK",
payload: { text: string; taskId: string }
} |
{
type: "MOVE_LIST",
payload: {
dragIndex: number
hoverIndex: number
}
}
type AppStateContextProps = {
state: AppState,
dispatch: React.Dispatch<Action> // new
// 手工定义假数据
const appData: AppState = {
lists: [
{ id: "0", text: "To Do", tasks: [{ id: "c0", text: "去买一杯咖啡" }] },
{ id: "1", text: "修改Bug", tasks: [{ id: "c2", text: "需要修改React中出现的问题" }] },
{ id: "2", text: "已经编写完成", tasks: [{ id: "c3", text: "工作计划已将编写完成" }] }
]
}
/**
* 用Reducer代替,上面定义的AppState,让组件按照Action来进行调用
*/
const appStateReducer = (state: AppState, action: Action): AppState => {
switch (action.type) {
case "ADD_LIST": {
const visibilityExample = "可见的"
return {
...state,
lists: [
...state.lists, {
id: uuidv4(),
text: action.payload,
tasks: []
}
]
}
}
case "ADD_TASK": {
const visibilityExample = "可见的"
return { ...state }
}
case "MOVE_LIST": {
}
default: {
return state
}
}
}
export const AppStateProvider = ({ children }: React.PropsWithChildren<{}>) => {
const [state, dispatch] = useReducer(appStateReducer, appData)
return (
<AppStateContext.Provider value={{ state, dispatch }}>
{children}
</AppStateContext.Provider>
)
}
yarn add react-dnd react-dnd-html5-backend
修改index.tsx
文件,增加Dnd组件
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AppStateProvider } from './AppStateContext'
import { DndProvider, } from 'react-dnd';
import { HTML5Backend as Backend } from 'react-dnd-html5-backend'
ReactDOM.render(
<DndProvider backend={Backend}>
<AppStateProvider>
<App />
</AppStateProvider>
</DndProvider>,
document.getElementById('root')
);
要实现这个功能,要添加Action,所以要修改AppStateContext.tsx
文件中的Action部分
注意: react-dnd的hooks只能回调当前拖动的项目数据,如果我们创建一个拖动预览,看起来就像是在复制一个组件,为了修复这个问题我们需要隐藏当前的组件,所有我们需要知道我们拖动的是什么内容(组件),它到底是卡片还是列。我能需要通过ID来确定。这里我们将使用useDrag
和useDrop
两个hooks来完成组件的拖放操作,创建一个新的文件ColumnDragItem.ts
来实现拖的动作
export interface ColumnDragItem {
index: number
id: string
text: string
type: "COLUMN"
}
export type DragItem = ColumnDragItem;
import { DragItem } from "./ColumnDragItem";
type Action = |
{
type: "SET_DRAGGED_ITEM"
payload: DragItem | undefined
}
useItemDrag
Hook这里可以新建一个自定义的Hook,它将返回一个可以接受拖动元素的拖动方法,每当开始拖动的时候都会发送SET_DRAG_ITEM操作,当停止拖动时,将在未定义的有效payload的情况下再次发送此操作。这里创建一个新文件useItemDrag.ts
import { useDrag } from 'react-dnd';
import { useAppState } from './AppStateContext';
import { DragItem } from './DragItem';
export const useItemDrag = (item: DragItem) => {
const { dispatch } = useAppState()
const [, drag] = useDrag({
type: "SET_DRAGGED_ITEM",
item,
end: () => dispatch({ type: "SET_DRAGGED_ITEM", payload: undefined }),
})
return { drag }
}
**注意:**新版本的react-dnd
是没有begin属性的
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。