蒼時弦也
蒼時弦也
資深軟體工程師
發表於

用 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)