用 React.js 實作拖曳與元件容器
「很久很久以前,有一個叫做 OwaBin (芋冰) 的食物,可以讓設計師用拖拉就做出 Launcher 這真是太神奇了!」
原本想說可以很歡樂的再 HanGee 幫忙設計跟網站,但是畢製的不可逆性質,讓我得把事情先推掉。 (也包括 SITCON 的任務,現在也在培養新人了⋯⋯)
半個月前討論這個計劃時,我非常有興趣,所以馬上做了一個簡易的測試版。 這篇文章會來說明這個功能。
預期完成的功能如下:
首先,我們要來了解 React.js Mixins 的神奇限制。 Mixin 的概念我認為跟 Ruby 的 module 非常接近,但是 Ruby 中可以 override module 的方法,但是 React.js 卻不行。
以下的範例都用CoffeeScript 解說。
1DefaultBlock = {
2 getDefaultProps: ->
3 {
4 width: 100
5 height: 100
6 }
7 render: ->
8 # Do something
9}
10
11Block = React.createClass {
12 mixins: [DefaultBlock]
13 getDefaultProps: ->
14 {
15 width: 100 # 發生錯誤,已經定義了預設屬性
16 }
17 render: -> # 發生錯誤,已經定義了 render
18 #Do customize render
19}
這是關於 Mixin 的神秘限制,也就是說無法進行 override 的動作,實作的時候並不會把 Mixin 的方法、預先定義的預設屬性覆蓋掉。
不過有例外,像是 componentWillMount
這類,在原始碼中有特殊定義,而不會發生錯誤(則是合併起來執行)
另一個要注意的點,就是所有的 Event 是無法直接 Bind 在 Component 上的,必須借助 Component 所 render
的 DOM 元素(但是 Component 依舊可以監聽事件,透過汽泡事件一樣可以拿到事件。)
下面的程式碼說明了事件的情況。
1OnDragBlock = React.createClass {
2 getDefaultProps: ->
3 {
4 onDrag: @handleDrag # 並不會產生反應
5 }
6
7 handleDrag: (event)->
8 # Some drag handler
9 event.stopPropagation() # 停止汽泡事件
10
11 render: ->
12 React.DOM.div {
13 onDrag: @handleDrag # 產生反應
14 }
15}
了解這兩個限制後,就可以開始來製作我們的拖曳元件功能。 (至於未來是否會修正,或者有更好的方法,就不得而知了。當然,如果我想到更好的方法,也會告訴大家。)
實作 Container Mixin
首先,我們需要一個可以放置任何元件進入的「容器」並且是可以被「重複產生」的。
1mixins = @mixins = @mixins ? {} # Register global variable mixins
2components = @components = @components ? {}
3
4ContainerMixin = {
5 getInitialState: ->
6 {
7 style: {} # 用於元件拖曳到上方時產生反饋
8 }
9 _defaultView: ->
10 @childComponents # 預設將子元件全部顯示
11 onDragOver: (event)->
12 event.preventDefault() # 阻止預設事件,讓原件可以被 "Drop"
13 event.stopPropagation() # 阻止汽泡事件,避免高亮狀態套用到上層容器
14 @setState {
15 style: {
16 "box-shadow": "inset 0 0 0 3px red" # 透過 box-shadow 製作高亮狀態
17 }
18 }
19 onDragLeave: ->
20 @setState {
21 style: {
22 "box-shadow": "none"
23 }
24 }
25 onDrop: ->
26 @onDragLeave()
27
28 componentToAdd = event.dataTransfer.getData("ComponentType") + "Component" # 取得元件
29 @childComponents.push components[componentToAdd](key: Date.now().toString(32)) # 產生元件
30
31 event.stopPropagation(); # 阻止汽泡事件,避免加到上層容器
32 componentWillMount: ->
33 @childComponents = [] # 初始化子元件容器
34 render: ->
35 @viewRender = @viewRender ? @_defaultView # 產生自定義的 "Render" 讓使用者仍可以改變 Render 方式
36 React.DOM.div {
37 onDragOver: @onDragOver
38 onDragLeave: @onDragLeave
39 onDrop: @onDrop
40 className: "component-container" # .component-container 定義了容器為 min-width: 100% 與 min-height: 100%
41 style: @state.style
42 }, @viewRender()
43}
44
45mixins.Container = ContainerMixin
那麼現在,我們可以用下面的語法產生各種類型的 Container (雖然範例還不完善)
1components = @components = @components ? {}
2mixins = @mixins = @mixins ? {}
3
4BasicContainer = React.createClass {
5 mixins: [mixins.Container]
6}
7
8components.BasicContainer = BasicContainer
實作 ToolIcon Mixin
接下來,我們需要一組可以拖曳的 Icon 並且依照 Icon 類型讓容器加入對應的原件。
1mixins = @mixins = @mixins ? {}
2
3ToolIcon = {
4 onDragStart: (event) ->
5 event.dataTransfer.setData "ComponentType", @componentType # 開始拖曳時儲存目前拖曳的元件類型
6 componentWillMount: ->
7 @icon = @icon ? "https://placehold.it/50" # ICON 圖檔
8 @componentType = @componentType ? "Unknown" # ICON 類型
9 render: ->
10 React.DOM.img {
11 src: @icon
12 draggable: true # 設定為可拖曳
13 onDragStart: @onDragStart
14 }
15}
16
17mixins.ToolIcon = ToolIcon
相較于容器簡單很多,現在可以透過以下的語法產生任何工具按鈕元件。
1components = @components = @components ? {}
2mixins = @mixins = @mixins ? {}
3
4ImageToolIcon = React.createClass {
5 mixins: [mixins.ToolIcon]
6 icon: "icon/image.png"
7 componentType: "Image"
8}
9
10components.ImageToolIcon = ImageToolIcon
實作 Image Component
接下來就是針對要放入容器的元件來實作,不過基本上跟一般的 React.js 實作大同小異。
1components = @components = @components ? {}
2
3ImageComponent = React.createClass {
4 render: ->
5 React.DOM.img {
6 src: "images/sample.jpg"
7 }
8}
Make it running!
最後,就是用 React.renderComponent
分別 render 容器與工具箱,就完成了!
(因為蒼時很想睡,所以偷懶沒寫這樣~~)
有任何問題歡迎討論,這一切都還有很大的改進空間,希望之後能順利完成 OwaBin 這套工具。 (雖然我可能幾乎沒機會參與了 QAQ)