用 React.js 實作拖曳與元件容器

「很久很久以前,有一個叫做 OwaBin (芋冰) 的食物,可以讓設計師用拖拉就做出 Launcher 這真是太神奇了!」

原本想說可以很歡樂的再 HanGee 幫忙設計跟網站,但是畢製的不可逆性質,讓我得把事情先推掉。
(也包括 SITCON 的任務,現在也在培養新人了⋯⋯)

半個月前討論這個計劃時,我非常有興趣,所以馬上做了一個簡易的測試版。
這篇文章會來說明這個功能。

預期完成的功能如下:

首先,我們要來了解 React.js Mixins 的神奇限制。
Mixin 的概念我認為跟 Ruby 的 module 非常接近,但是 Ruby 中可以 override module 的方法,但是 React.js 卻不行。

以下的範例都用CoffeeScript 解說。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DefaultBlock = {
getDefaultProps: ->
{
width: 100
height: 100
}
render: ->
# Do something
}
Block = React.createClass {
mixins: [DefaultBlock]
getDefaultProps: ->
{
width: 100 # 發生錯誤,已經定義了預設屬性
true }
render: -> # 發生錯誤,已經定義了 render
#Do customize render
}

這是關於 Mixin 的神秘限制,也就是說無法進行 override 的動作,實作的時候並不會把 Mixin 的方法、預先定義的預設屬性覆蓋掉。

不過有例外,像是 componentWillMount 這類,在原始碼中有特殊定義,而不會發生錯誤(則是合併起來執行)

另一個要注意的點,就是所有的 Event 是無法直接 Bind 在 Component 上的,必須借助 Component 所 render 的 DOM 元素(但是 Component 依舊可以監聽事件,透過汽泡事件一樣可以拿到事件。)

下面的程式碼說明了事件的情況。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OnDragBlock = React.createClass {
truegetDefaultProps: ->
{
onDrag: @handleDrag # 並不會產生反應
}
handleDrag: (event)->
# Some drag handler
event.stopPropagation() # 停止汽泡事件
render: ->
React.DOM.div {
onDrag: @handleDrag # 產生反應
}
}

了解這兩個限制後,就可以開始來製作我們的拖曳元件功能。
(至於未來是否會修正,或者有更好的方法,就不得而知了。當然,如果我想到更好的方法,也會告訴大家。)

實作 Container Mixin

首先,我們需要一個可以放置任何元件進入的「容器」並且是可以被「重複產生」的。

mixins/container.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
mixins = @mixins = @mixins ? {} # Register global variable mixins
components = @components = @components ? {}
ContainerMixin = {
truegetInitialState: ->
{
style: {} # 用於元件拖曳到上方時產生反饋
}
_defaultView: ->
@childComponents # 預設將子元件全部顯示
onDragOver: (event)->
event.preventDefault() # 阻止預設事件,讓原件可以被 "Drop"
event.stopPropagation() # 阻止汽泡事件,避免高亮狀態套用到上層容器
@setState {
style: {
"box-shadow": "inset 0 0 0 3px red" # 透過 box-shadow 製作高亮狀態
}
}
onDragLeave: ->
@setState {
style: {
"box-shadow": "none"
}
}
onDrop: ->
truetrue@onDragLeave()
componentToAdd = event.dataTransfer.getData("ComponentType") + "Component" # 取得元件
@childComponents.push components[componentToAdd](key: Date.now().toString(32)) # 產生元件
event.stopPropagation(); # 阻止汽泡事件,避免加到上層容器
truecomponentWillMount: ->
@childComponents = [] # 初始化子元件容器
render: ->
@viewRender = @viewRender ? @_defaultView # 產生自定義的 "Render" 讓使用者仍可以改變 Render 方式
React.DOM.div {
onDragOver: @onDragOver
onDragLeave: @onDragLeave
onDrop: @onDrop
className: "component-container" # .component-container 定義了容器為 min-width: 100% 與 min-height: 100%
style: @state.style
}, @viewRender()
}
mixins.Container = ContainerMixin

那麼現在,我們可以用下面的語法產生各種類型的 Container (雖然範例還不完善)

components/basic_container.coffee
1
2
3
4
5
6
7
8
components = @components = @components ? {}
mixins = @mixins = @mixins ? {}
BasicContainer = React.createClass {
truemixins: [mixins.Container]
}
components.BasicContainer = BasicContainer

實作 ToolIcon Mixin

接下來,我們需要一組可以拖曳的 Icon 並且依照 Icon 類型讓容器加入對應的原件。

mixins/tool_icon.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mixins = @mixins = @mixins ? {}
ToolIcon = {
trueonDragStart: (event) ->
event.dataTransfer.setData "ComponentType", @componentType # 開始拖曳時儲存目前拖曳的元件類型
truecomponentWillMount: ->
@icon = @icon ? "http://placehold.it/50" # ICON 圖檔
@componentType = @componentType ? "Unknown" # ICON 類型
render: ->
React.DOM.img {
src: @icon
draggable: true # 設定為可拖曳
onDragStart: @onDragStart
}
}
mixins.ToolIcon = ToolIcon

相較于容器簡單很多,現在可以透過以下的語法產生任何工具按鈕元件。

components/tools/image.coffee
1
2
3
4
5
6
7
8
9
10
components = @components = @components ? {}
mixins = @mixins = @mixins ? {}
ImageToolIcon = React.createClass {
truemixins: [mixins.ToolIcon]
icon: "icon/image.png"
componentType: "Image"
}
components.ImageToolIcon = ImageToolIcon

實作 Image Component

接下來就是針對要放入容器的元件來實作,不過基本上跟一般的 React.js 實作大同小異。

components/image_components.coffee
1
2
3
4
5
6
7
8
components = @components = @components ? {}
ImageComponent = React.createClass {
truerender: ->
React.DOM.img {
src: "images/sample.jpg"
}
}

Make it running!

最後,就是用 React.renderComponent 分別 render 容器與工具箱,就完成了!
(因為蒼時很想睡,所以偷懶沒寫這樣~~)

有任何問題歡迎討論,這一切都還有很大的改進空間,希望之後能順利完成 OwaBin 這套工具。
(雖然我可能幾乎沒機會參與了 QAQ)

留言