- 原文地址:Build a Drag and Drop layout builder with React and ImmutableJS
- 原文做者:Chris Kitson
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:fireairforce
- 校對者:Eternaldeath, portandbridge
『拖放』這一類的行爲存在着巨大的用戶需求,例如構建網站(Wix)或交互式應用程序(Trello)。毫無疑問,這種類型的交互創造了很是酷的用戶體驗。若是再加上一些最新的 UI 技術,咱們能夠建立一些很是好的軟件。javascript
我想構建一個能讓用戶使用一系列可定製 UI 組件來構建佈局的拖放佈局構建器,最終能構建出一個網站或者是 web 應用程序。 css
下面花一點時間來解釋它們在構建這個項目時所起的做用。html
React 基於聲明式編程,這意味着它根據狀態來進行渲染。狀態(State)實際上只是一個 JSON 對象,它具備告訴 React 應該怎麼去渲染(樣式和功能)的屬性。與操做 DOM 的庫(例如 jQuery)不一樣,咱們不直接改變 DOM,而是經過修改狀態(state)而後讓 React 去負責 DOM(稍後會介紹 DOM)。前端
在這個項目中,假設有一個父組件來保存佈局的狀態(JSON 對象),而且這個狀態將被傳遞給其餘的組件,這些組件都是 React 中的無狀態組件。java
這些組件的做用是從父組件中獲取狀態,而後根據其屬性來渲染自己。react
如下是一個具備三個 link 對象的狀態的簡單示例:android
{
links: [{
name: "Link 1",
url: "http://link.one",
selected: false
}, {
name: "Link 2",
url: "http://link.two",
selected: true
}, {
name: "Link 3",
url: "http://link.three",
selected: false
}]
}
複製代碼
經過上面的例子,咱們能夠遍歷 links 數組來爲每一個元素建立一個無狀態組件:ios
interface ILink {
name: string;
url: string;
selected: boolean;
}
const LinkComponent = ({ name, url, selected }: ILink) =>
<a href={url} className={selected ? 'selected': ''}>{name}</a>;
複製代碼
你能夠看到咱們如何根據狀態中保存的選定屬性將 css 類『selected』應用到 links 數組組件。下面是呈現給瀏覽器的內容:git
<a href="http://link.two" class="selected">Link 2</a>
複製代碼
咱們已經瞭解了狀態在咱們項目中的重要性,它是使 React 組件如何渲染的惟一真實的數據來源。React 中的狀態保存在不可變的數據結構中。github
簡而言之,這意味着一旦建立了數據對象,就不能直接去修改它。除非咱們建立一個具備更改狀態的新對象。
讓咱們用另一個簡單的例子來講明不變性:
interface ILink {
name: string;
url: string;
selected: boolean;
}
const link: ILink = {
name: "Link 1",
url: "http://link.one",
selected: false
}
複製代碼
在傳統的 JavaScript 中,你能夠經過下面的操做來更新 link 對象:
link.name = 'New name';
複製代碼
若是咱們的狀態是不可變的,那麼上面操做不可能完成的,那麼咱們必需要建立一個 name 屬性已經被修改的新對象。
link = {...link, name: 'New name' };
複製代碼
注意:爲了支持不變性,React 爲咱們提供了一個方法 this.setState()
,咱們可使用它來告訴組件狀態已經改變,而且組件還須要從新進行渲染若是狀態發生任何改變。
上面只是基本示例,可是若是想要在複雜的 JSON 狀態結構中更改嵌套了多層的屬性應該怎麼作?
ECMA Script 6 爲咱們提供了一些方便的操做符和方法來改變對象,但它們並不適用於複雜的數據結構,這就是咱們須要 ImmutableJS 來簡化任務的緣由。
稍後咱們會使用 ImmutableJS,可是如今你只須要知道它具備給咱們提供簡便的方法用來改變複雜的狀態方面的做用。
因此咱們知道咱們的狀態是一個不可變的 JSON 對象,而 React 來負責處理組件,但咱們須要有趣的用戶交互體驗,對吧?
幸好有了 HTML5 使得這實際上很是簡單,由於它提供了咱們能夠用來檢測拖動組件的時間和放置它們的位置的方法。因爲 React 將原生 HTML 元素暴露給瀏覽器,所以咱們可使用原生的事件方法使咱們的實現更加簡單。
注意:我得知使用 HTML5 實現的 DnD 可能存在一些問題但若是沒有其它的問題,這多是一個探究課程,若是發現有問題的話,咱們以後能夠換掉它。
在這個項目中,咱們擁有用戶能夠拖動的組件(HTML divs),我稱他們爲可拖動組件。
同時咱們也擁有容許用戶放置組件的區域, 我稱他們爲可放置組件。
使用原生 HTML5 事件如 onDragStart
、onDragOver
和 onDragDrop
,咱們也應該擁有基於 DnD 交互更改應用程序狀態所須要的東西。
如下是一個可拖動組件的實例:
export interface IDraggableComponent {
name: string;
type: string;
draggable?: boolean;
onDragStart: (ev: React.DragEvent<HTMLDivElement>, name: string, type: string) => void;
}
export const DraggableComponent = ({
name,
type,
onDragStart,
draggable = true
}: IDraggableComponent) =>
<div className='draggable-component' draggable={draggable} onDragStart={(ev) => onDragStart(ev, name, type)}>{name}</div>;
複製代碼
在上面的代碼片斷中,咱們渲染了一個 React 組件,該組件使用 onDragStart
事件告訴父組件咱們正開始拖動組件。咱們還能夠經過傳遞 draggable
屬性來切換拖動它的能力。
如下是一個可放置組件的實例:
export interface IDroppableComponent {
name: string;
onDragOver: (ev: React.DragEvent<HTMLDivElement>) => void;
onDrop: (ev: React.DragEvent<HTMLDivElement>, componentName: string) => void;
}
export const DroppableComponent = ({
name,
onDragOver,
onDrop
}: IDroppableComponent) =>
<div className='droppable-component'
onDragOver={(ev: React.DragEvent<HTMLDivElement>) => onDragOver(ev)}
onDrop={(ev: React.DragEvent<HTMLDivElement>) => onDrop(ev, name)}>
<span>Drop components here!</span>
</div>;
複製代碼
在上面的組件中,咱們正在監聽 onDrop
事件,所以咱們能夠根據放進可放置組件的新組件來更新狀態。
好的,是時候快速回顧一下,而後將他們所有放到一塊兒:
咱們將使用 React 中基於狀態對象的少許解耦無狀態組件來渲染整個佈局。用戶交互將由 HTML5 DnD 事件來處理,而時間會使用 ImmutableJS 來觸發對狀態對象的更改。
如今咱們已經對要作的事情以及如何處理它們有了深入的瞭解,讓咱們考慮一下這個難題中的一些最重要的部分:
爲了使咱們的組件能表示無限的佈局組合,狀態須要靈活且可拓展。咱們還須要記住的是,若是想要表示任何給定佈局的 DOM 樹,意味着須要不少使人愉快的遞歸來支持嵌套結構!
咱們的狀態須要存儲大量組件,能夠經過如下接口表示:
若是你不熟悉 JavaScript 中的接口,你應該看看 TypeScript — 你大概能看出我是它的粉絲。它很適用於 React。
export interface IComponent {
name: string;
type: string;
renderProps?: {
size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
};
children?: IComponent[];
}
複製代碼
我會使組件的定義最小化,可是你能夠根據須要拓展它。我在 renderProps
這裏定義一個對象,因此咱們能夠爲組件提供狀態來告訴它如何渲染,children
的屬性爲咱們提供了遞歸。
對於更高層次,我會建立一個對象數組來保存組件,它們將出如今狀態的根部。
爲了說明這一點,咱們建議將如下內容做爲 HTML 中標記的有效佈局:
<div class="content-panel-1">
<div class="component">
Component 1
</div>
<div class="component">
Component 2
</div>
</div>
<div class="content-panel-2">
<div class="component">
Component 3
</div>
</div>
複製代碼
爲了在狀態中表示這一點,咱們能夠爲內容面板定義以下所示的接口:
export interface IContent {
id: string;
cssClass: string;
components: IComponent[];
}
複製代碼
而後咱們的狀態將會成爲一個像以下 IContent
數組:
const state: IContent[] = [
{
id: 'content-panel-1',
cssClass: 'content-panel-1',
components: [{
type: 'component1',
renderProps: {},
children: []
},
{
type: 'component2',
renderProps: {},
children: []
}]
},
{
id: 'content-panel-2',
cssClass: 'content-panel-2',
components: [{
type: 'component3',
renderProps: {},
children: []
}]
}
];
複製代碼
經過在 children
數組屬性中推送其餘組件,咱們能夠定義其餘組件來建立嵌套的相似 DOM 的樹結構:
[0]
components:
[0]
children:
[0]
children:
[0]
...
複製代碼
佈局構建器組件將執行一系列功能,例如:
代碼大概是這樣的:
export class BuilderLayout extends React.Component {
public state: IBuilderState = {
dashboardState: []
};
constructor(props: {}) {
super(props);
this.onDragStart = this.onDragStart.bind(this);
this.onDragDrop = this.onDragDrop.bind(this);
}
public render() {
}
private onDragStart(event: React.DragEvent <HTMLDivElement>, name: string, type: string): void {
event.dataTransfer.setData('id', name);
event.dataTransfer.setData('type', type);
}
private onDragOver(event: React.DragEvent<HTMLDivElement>): void {
event.preventDefault();
}
private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string): void {
}
}
複製代碼
咱們先暫時不用管 render()
函數,後面很快會再見到它。
咱們有三個事件,咱們將綁定它們到咱們的『可拖動組件』和『可放置組件』上。
onDragStart()
——這個事件這裏咱們設置一些關於 event
對象中組件的細節,即 name
和 type
。
onDragOver()
——咱們如今不會對這個事件作任何事情,事實上咱們經過 .preventDefault()
函數禁用瀏覽器的默認行爲。
這就留下了 onDragDrop()
事件,這就是修改不可變狀態的神奇之處。爲了改變狀態,咱們須要幾條信息:
name
在 event
對象中設置 onDragStart()
。type
在 event
對象中設置 onDragStart()
。containerId
從可放置的組件中傳入這個方法。在 containerId
中必須告訴咱們,新的組件具體要放在狀態裏的什麼位置。可能有一種更簡潔的方法能夠作到這一點,但爲了描述這個位置,我將使用一個下劃線分隔的索引列表。
回顧咱們的狀態模型:
[index]
components:
[index]
children:
[index]
children:
[index]
...
複製代碼
用字符串格式表示爲 cb_index_index_index_index
。
此處的索引數描述了應該刪除組件的嵌套結構中的深度級別。
如今咱們須要調用 immutableJS 中的強大功能來幫助咱們改變應用程序的狀態。咱們將在 onDragDrop()
方法中執行此操做,改方法可能以下所示:
private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string) {
const name = event.dataTransfer.getData('id');
const type = event.dataTransfer.getData('type');
const newComponent: IComponent = this.generateComponent(name, type);
const containerArray: string[] = containerId.split('_');
containerArray.shift(); // 忽略第一個參數,它是字符串前綴
const componentsPath: Array<number | string> = [] containerArray.forEach((id: string, index: number) => {
componentsPath.push(parseInt(id, INT_LENGTH));
componentsPath.push(index === 0 ? 'components' : 'children');
});
const { dashboardState } = this.state;
let componentState = fromJS(dashboardState);
componentState = componentState.setIn(componentsPath, componentState.getIn(componentsPath).push(newComponent));
this.setState({ dashboardState: componentState.toJS() });
}
複製代碼
這裏的功能來自於 ImmutableJS 提供給咱們的 .setIn()
和 .getIn()
方法。
它們採用一組字符串/值以肯定要在嵌套狀態模型中獲取或設置值的位置。這與咱們生成可放置的 ids 方式很吻合。很酷吧?
fromJS()
和 toJS()
方法轉變 JSON 對象到 ImmutableJS 對象,而後再返回。
關於 ImmutableJS 有不少東西,我可能會在將來寫一篇關於它的專門的帖子。很抱歉,此次只是一次簡單的介紹!
最後讓咱們快速看一下前面提到的渲染方法。我想支持一個 CSS 網格系統相似於 Material responsive grid 來使咱們的佈局更加靈活。它使用 12 列網格來規定 HTML 佈局,以下所示:
<div class="mdc-layout-grid">
<div class="mdc-layout-grid__inner">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
Left column
</div>
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
Right column
</div>
</div>
</div>
複製代碼
將它與咱們的狀態所表明的嵌套結構相組合,咱們能夠獲得一個很是強大的佈局構建器。
如今,我只是將網格的大小固定爲兩列布局(即單個可放置組件中的兩列具備的遞歸)。
爲了實現這一點,咱們有一個可拖動組件的網格,它將包含兩個可放置的(每列一個)。
這是我以前建立的一個:
上面我有一個Grid,第一列中有一個Card,第二列中有一個Heading。
如今我在第一列中放置了另外一個Grid,第一列中有一個Heading,第二列中有一個Card。
你明白了嗎?
舉個例子來講明如何使用 React 僞代碼實現這個目的:
循環遍歷內容項(咱們狀態的根)而且渲染一個 ContentBuilderDraggableComponent
和一個 DroppableComponent
。
肯定組件是否爲 Grid 類型,而後渲染 ContentBuilderGridComponent
,不然渲染一個常規的 DraggableComponent
。
渲染被 X 個子項目標記的 Grid 組件,每一個子項目中都有一個 ContentBuilderDraggableComponent
和一個 DroppableComponent
。
class ContentBuilderComponent... {
render() {
return (
<ContentComponent>
components.map(...) {
<ContentBuilderDraggableComponent... />
}
<DroppableComponent... />
</ContentComponent>
)
}
}
class ContentBuilderDraggableComponent... {
render() {
if (type === GRID) {
return <ContentBuilderGridComponent... />
} else {
return <DraggableComponent ... />
}
}
}
class ContentBuilderGridComponent... {
render() {
<GridComponent...>
children.map(...) {
<GridItemComponent...>
gridItemChildren.map(...) {
<ContentBuilderDraggableComponent... />
<DroppableComponent... />
}
</GridItemComponent>
}
</GridComponent>
}
}
複製代碼
咱們已經完成了這篇文章,但我未來會對此進行一些拓展。這是一些想法:
但願你能 follow 我,若是你沒有,這是我在 GitHub 上的一個工做示例,但願你能欣賞它。 chriskitson/react拖放佈局構建器 使用React和ImmutableJS拖放(DnD)UI佈局構建器 - chriskitson/react拖放佈局構建器github.com
感謝您抽出寶貴時間閱讀個人文章。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。