2017年8月25日

[筆記] 製作可拖曳的元素(HTML5 Drag and Drop API)

HTML5 中的 Drag & Drop API 可以讓我們在瀏覽器中做到拖曳元素、排序元素、或者是讓使用者透過拖拉的方式把要上傳的檔案拉到瀏覽器當中。
在學習 HTML5 Drag & Drop API 時,最重要的是去區分 Drag SourceDrop Target,因為它們會需要各自去監聽不同的事件。
假設我們要把下圖中藍色的圓從左邊的區域移到右邊的區域,那麼:
  • Drag Source 指的是被點擊要拖曳的物件,也就是藍色的圓,通常是一個 element
  • Drop Target 指的是拖曳的物件被放置的區域,也就是右邊的綠色區域,通常是一個 div container

前置動作

在開始實做前,有一些是需要知道和注意的前置動作。

事件(Event)

Drag & Drop 提供的間事件主要包含 dragstart, drag, dragend, dragenter, dragover, dragleave, drop,其中有些是針對 Drag Source 的,有些則是針對 Drop Target 的,整理如下表:
Drag Source Drop Target
dragstart
drag dragenter
dragover
dragleave
drop
dragend
  • drag:在 drag source 被拖曳時會持續被觸發。
  • dragover:當拖曳的 drag source 在 drop target 上方時會持續被觸發。
記得要針對被拖曳的物件取消預設行為(default)

CSS Style

針對要被拖曳的元素(Drag Source)可以透過 CSS 的屬性設定,避免讓使用者在拖曳該元素時選取到裡面的內容:
[draggable="true"] {
  /*
   To prevent user selecting inside the drag source
  */
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}

HTML Attribute

針對能夠被拖曳的元素,在其 HTML 標籤上添加屬性 draggable="true"
<div id="drag-source" draggable="true"></div>

Drag and Drop Basic

Demo & Source Code

See the Pen Drag and Drop Basic by PJCHEN (@PJCHENder) on CodePen.

示範如何將一個物件從一個 container (#source-container) 內拖曳到另一個 container (#target-container)內:

HTML

針對可以被拖曳的元素加上 draggable="true"
<div id="drag-drop-basic">
  <div id="source-container">
    <div id="drag-source" draggable="true"></div>
  </div>
  <div id="target-container"></div>
</div> 

JavaScript

針對要被拖曳的元素(dragSource) 監聽 dragstart 事件,並且把要傳遞給 dropTarget 的資料透過 setData 加以設定:
let dragSource = document.querySelector('#drag-source')
dragSource.addEventListener('dragstart', dragStart)

function dragStart (e) {
  console.log('dragStart')
  e.dataTransfer.setData('text/plain', e.target.id)
}
針對要被置放的容器 dropTarget 監聽 drop 事件,來處理當使用者放掉的時候要執行的行為,並透過 getData 來取得傳遞的資料;監聽 dragenterdragover 事件來避免預設行為:
let dropTarget = document.querySelector('#target-container')
dropTarget.addEventListener('drop', dropped)
dropTarget.addEventListener('dragenter', cancelDefault)
dropTarget.addEventListener('dragover', cancelDefault)

function dropped (e) {
  console.log('dropped')
  cancelDefault(e)
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

Drag and Drop with multiple sources in multiple containers

Demo & Source Code

在這個範例中,我們可以把 drag sources 來回拖放於左右兩邊的 drop target:

See the Pen Drag and Drop in Multiple Container by PJCHEN (@PJCHENder) on CodePen.

HTML

在能夠被拖放的容器上加上 data-role="drag-drop-container" 屬性:
<div id="drag-drop-basic">
  <div id="source-container" data-role="drag-drop-container">
    <div id="drag-source" draggable="true"></div>
  </div>
  <div id="target-container" data-role="drag-drop-container"></div>
</div>

JavaScript

允許多個可拖曳的物件:
// Allow multiple draggable items
let dragSources = document.querySelectorAll('[draggable="true"]')
dragSources.forEach(dragSource => {
  dragSource.addEventListener('dragstart', dragStart)
})

function dragStart (e) {
  e.dataTransfer.setData('text/plain', e.target.id)
}
允許多個可置放的容器:
// Allow multiple dropped targets
let dropTargets = document.querySelectorAll('[data-role="drag-drop-container"]')
dropTargets.forEach(dropTarget => {
  dropTarget.addEventListener('drop', dropped)
  dropTarget.addEventListener('dragenter', cancelDefault)
  dropTarget.addEventListener('dragover', cancelDefault)
})

function dropped (e) {
  cancelDefault(e)
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

修改拖曳時的 CSS 樣式

Demo & Source Code

See the Pen Drag and Drop with CSS Style by PJCHEN (@PJCHENder) on CodePen.

我們新增兩個 CSS 樣式分別套用在被拖曳的物件和被放置的容器上:
// For drag sources
.dragging {
  opacity: .25;
}

// For drop target
.hover {
  background-color: rgba(0,191,165,.04);
}
針對物件本身,我們在開始拖曳時添加樣式,結束拖曳時移除樣式:
function dragStart (e) {
  this.classList.add('dragging')
  e.dataTransfer.setData('text/plain', e.target.id)
}

function dragEnd (e) {
  this.classList.remove('dragging')
}
針對容器,我們在進入容器時添加樣式,在離開或放置後移除樣式:
function dropped (e) {
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
  this.classList.remove('hover')
}

function dragOver (e) {
  this.classList.add('hover')
}

function dragLeave (e) {
  this.classList.remove('hover')
}

製作可拖拉排序的清單

Demo & Source Code

See the Pen Drag and Drop Sortable List by PJCHEN (@PJCHENder) on CodePen.

下面的範例中有用到 jQuery:

HTML

建立一個清單:
<ul id="items-list" class="moveable">
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
  <li>Four</li>
</ul>

JavaScript

先利用 document.querySelectorAll 將所有清單中的元素選取起來:
let items = document.querySelectorAll('#items-list > li')
將清單中的每一個元素都加上 draggable="true" 的屬性,並且監聽相關事件:
items.forEach(item => {
  $(item).prop('draggable', true)
  item.addEventListener('dragstart', dragStart)
  item.addEventListener('drop', dropped)
  item.addEventListener('dragenter', cancelDefault)
  item.addEventListener('dragover', cancelDefault)
})
取得被拖曳物件的 index 值:
function dragStart (e) {
  var index = $(e.target).index()
  e.dataTransfer.setData('text/plain', index)
}
放下(drop)的時候要把原本 index 的元素移除(remove)掉
function dropped (e) {
  cancelDefault(e)
  
  // get new and old index
  let oldIndex = e.dataTransfer.getData('text/plain')
  let target = $(e.target)
  let newIndex = target.index()
  
  // remove dropped items at old place
  let dropped = $(this).parent().children().eq(oldIndex).remove()

  // insert the dropped items at new place
  if (newIndex < oldIndex) {
    target.before(dropped)
  } else {
    target.after(dropped)
  }
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

其他

將檔案拖曳至瀏覽器

DataTransfer.files  

Drag and Drop Effect

DataTransfer 屬性 對象 事件
effectAllowed drag sources dragstart
dropEffect drag target dragover
effectAllowed 是針對 drag sources 的 dragstart 事件;dropEffect 則是針對 drop target 的 dragover 事件。
如果 effectAllowd 和 dropEffect 中設定的類型不同,則無法將該 source 拖曳到 target。
/**
 * copy, move, link, none
 **/
DataTransfer.effectAllowed  // 設定在 dragstart event,允許拖曳的效果
DataTransfer.dropEffect     // 設定在 dragover event,當檔案拖曳進來時顯示的效果

客制化拖曳圖片

套用在 dragStart 事件中:
DataTransfer.setDragImage(img, x-offset, y-offset)
function dragStart (e) {
  let img = new Image() 
  img.src = 'example.gif'
  e.dataTransfer.setDragImage(img, 10, 10)
}

NOTICE

在 FireFox 中的 dragstart 事件可能需要將 setData 設為空:
function dragstart (e) {
  e.dataTransfer.setData('text/plain', '')
}

其他

一個寫得很好的清單拖拉範例,裡面有用到 document.elementFromPoint(x, y) 這個 Web API,謝謝 @hannahpun 分享:

See the Pen Vanilla JavaScript - Drag Sort by Fitri Ali (@fitri) on CodePen.

參考

HTML Fundamentals @ Pluralsight
  1. Introduction to Drag and Drop Events
  2. Drag and Drop Events in Detail
  3. Safari Support
  4. Draggable CSS Style
  5. Drag and Drop Basics
  6. Using Role Selectors
  7. Events in Action
  8. Styling Drag Sources and Drop Targets
  9. Implementing Drag and Drop Sortable List
  10. Drag and Drop Data Transfer Types
  11. Dropping Files from The Client into The Web Browser
  12. Setting and Enforcing Drag and Drop Effects
  13. Customizing The Drag Cursor Image
  14. Implementing A Drag and Drop Module Demo and Markup
  15. Implementing A Drag and Drop Module JavaScript

2017年8月24日

[筆記] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)

本篇內容整理自 Philip Roberts 在 JS Conf 的演講影片 What the heck is the event loop anyway? 和 MDN Concurrency model and Event Loop
如果你對於 JavaScript 的執行環境還不夠清楚,建議可以參考以下文章:

單線程(single threaded)

首先,我們要知道 JavaScript 是單線程(single threaded runtime)的程式語言,所有的程式碼片段都會在堆疊中(stack)被執行,而且一次只會執行一個程式碼片段(one thing at a time)。

堆疊(stack)

在 JavaScript 中的執行堆疊(called stack)會記錄目前執行到程式的哪個部分,如果進入了某一個函式(step into),便把這個函式添加到堆疊(stack)當中的最上方;如果在函式中執行了 return ,則會將此函式從堆疊(stack)的最上方中抽離(pop off)。
以下面的程式碼為例:
function multiply(a, b) {
  return a * b
}

function square(n) {
  return multiply(n, n)
}

function printSquare(n) {
  let squared = square(n)
  console.log(squared)
}

printSquare(4)
當我們在執行 JavaScript 的函式時,首先進入 stack 的會是這個檔案中全域環境的程式(這裡稱作 main);接著 printSquare 會被呼叫(invoke)因此進入堆疊(stack)的最上方;在 printSquare 中會呼叫 square() 因此 square() 會進入堆疊(stack)的最上方;同樣的,square 中呼叫了 multiply(),因此 multiply 進入堆疊的最上方。
因此目前的執行的堆疊(call stack)會長的像這樣:
接著執行到每一個函式中的 return 或結尾時,這個函式便會跳離(pop off)堆疊。

無窮迴圈

如果他是一個無窮迴圈,例如:
function foo () {
  return foo()
}

foo()
那麼這個堆疊(Stack)將會不斷被疊加上去,直到瀏覽器出現錯誤:

阻塞(blocking)

當執行程式碼片段需要等待很長一段時間,或好像「卡住」的這種現象,被稱作 阻塞(blocking),假設請求資料的 AJAX Request 變成同步(Synchronous)處理的話,那麼每 request 一次,因為必需等這個函式執行完畢從堆疊(stack)中跳離開後才能往下繼續走,進而導致阻塞的情形產生,以下面的 pseudo code 為例:
// pseudo code
var foo = $.getSync('//foo.com')
var bar = $.getSync('//bar.com')
var qux = $.getSync('//qux.com')

console.log(foo)
console.log(bar)
console.log(qux)

阻塞的情形導致瀏覽器停滯

從上面的影片中可以看出當堆疊中有未處理完的函式導致阻塞產生時(以同步的方式模擬發出一個 request 但尚未回應前),我們沒辦法在瀏覽器執行其他任何動作,瀏覽器也無法重新渲染(click 的 button 一直處於被按壓的狀態);必須要等到 request 執行結束後瀏覽器才會繼續運作。堆疊被阻塞(stack blocked)的情況會導致我們的瀏覽器無法繼續渲染頁面,導致頁面「停滯」。

非同步處理與堆疊(Async Callback & Call Stack)

如果尚不清楚同步和非同步的差異,可參考 [筆記] 談談JavaScript中的asynchronous和event queue
為了要解決阻塞的問題,我們可以透過非同步(Asynchronous)的方式搭配 callback ,在這裡我們以 setTimeout 來模擬非同步請求的進行,以下面的程式碼為例:
console.log('hi')

setTimeout(function () {
  console.log('there')
}, 5000)

console.log('JSConfEU')
在執行這段程式的時候,執行堆疊(call stack)中會先執行 hi, 接著執行 setTimeout,但是在 setTimeout 中的這個回呼函式(callback function,簡稱 cb)並不會立即被執行(等等會說明它到哪去了),最後堆疊中(stack)會在執行 JSConfEU

Concurrency and Event Loop

為了要理解 JavaScript 之所以能夠透過非同步的方式(asynchronous)「看起來」一次處理很多事情,我們需要進一步瞭解 Event Loop。
我們之所以可以在瀏覽器中同時(concurrently)處理多個事情,是因為瀏覽器並非只是一個 JavaScript Runtime。
JavaScript 的執行時期(Runtime)一次只能做一件事,但瀏覽器提供了更多不同的 API 讓我們使用,進而讓我們可以透過 event loop 搭配非同步的方式同時處理多個事項。
在下面的影片中,我們可以看到當我們在堆疊中執行 setTimeout 這個 function 時,setTimeout 實際上是一個瀏覽器提供的 API ,而不是 JS 引擎本身的功能;於是瀏覽器提供一個計時器給我們使用, setTimeout 中的 callback function(簡稱 cb)會被放到 WebAPIs 中,這時候,setTimeout 這個 function 就已經執行結束,並從堆疊中脫離
當計時器的時間到時,會把要執行的 cb 放到一個叫做工作佇列(task queue)的地方。
這時候就輪到事件循環(event loop)的功能,它的作用很簡單—如果堆疊(stack)是空的,它便把佇列(queue)中的第一個項目放到堆疊當中;堆疊(stack)便會去執行這個項目。
event loop 的作用是去監控堆疊(call stack)和工作佇列(task queue),當堆疊當中沒有執行項目的時候,便把佇列中的內容拉到堆疊中去執行。

setTimeout 0 是什麼意思?

那麼如果我們使用 setTimeout 並且希望在 0 秒後馬上執行是什麼意思呢?
console.log('hi')

setTimeout(function () {
  console.log('there')
}, 0)

console.log('JSConfEU')
如同剛剛的邏輯,即使我們使用 0 秒,它一樣會先將 cb 放到 WebAPIs 的計時器中,當時間到時,把該 cb 放到工作佇列(task queue)內,「等到」所有堆疊的內容都被清空後才會「立即」執行這個 cb
也就是說會依序 console 出來的內容是 hi --> JSConfEU --> there , there 會是最後才輸入的。

把同樣的觀念套到 AJAX request

同樣的道理,我們把剛剛利用 setTimout 來模擬非同步處理的 code,改成發出 AJAX Request:
console.log('hi')

$.get('url', function cb(data) {
  console.log(data)
})

console.log('JSConfEU')
和 setTimeout 一樣,AJAX Request 的功能並不在 JavaScript 引擎中,而是屬於 Web APIs(XHR)。
當執行 AJAX 請求的時候,cb 的內容會被放在 Web APIs 中,因此原本的堆疊(stack)將可以繼續往下執行。
直到 AJAX Request 給予回應後(不論成功或失敗),便把 cb 放到工作佇列(queue)當中,當堆疊(stack)被清空的時候,event loop 便會把 cb 拉到堆疊中加以執行。

更多範例

講者 Philip Roberts 自己寫了一個方便視覺化瞭解 JavaScript Runtime, call stack, event loop, task queue 的工具 Loupe ,你可以把下面更多不同的例子,貼到他所提供的網站中執行:

Click Event

假設我們的程式碼長這樣,我們可以思考一下它會怎麼執行:
console.log('Started')

$.on('button', 'click', function onClick () {
  console.log('Clicked')
})

setTimeout(function onTimeout () {
  console.log('Timeout Finished')
}, 5000)

console.log('Done')
邏輯是一樣的。
首先 Started 會被放到堆疊中執行,再來 clicksetTimeout 這兩個都是屬於 WebAPIs,當他們被放到 WebAPIs 後,就會從堆疊中脫離,最後執行 Done
當 setTimeout 的 Timer 時間到時,或者是 click 事件被觸發時,WebAPIs 會將 cb 放到工作佇列中(task queue),當堆疊空掉的時候,event loop 就會把工作佇列中的內容搬到堆疊(stack)中加以執行。
因此,當我們點擊瀏覽器時,這個點擊事件的 cb 並不是立即被執行的,而是先被放到工作佇列(queue)中,直到堆疊(stack)空了之後,才會被執行:

Multiple setTimeout

一樣的邏輯,讓我們看下面這段程式碼,思考一下它會怎麼執行:
setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)

setTimeout(function timeout() {
console.log('hi')
}, 1000)
從運作的過程中我們便可以瞭解到,setTimeout 這個 timer 只能保證超過幾毫秒後 「即將會」 執行,但並不能保證在幾毫秒後會 「即刻」 執行。舉例來說,setTimeout(cb, 1000),表示在 1 秒後這個 function 「即將會」 被執行,但並不是說在 1 秒後這個 function 「立即會」 被執行。

寫一個非同步執行的 forEach callback

在這裡我們利用 setTimeout 的方式,可以寫一個非同步處理的 forEach。一樣的邏輯,讓我們看下面這段程式碼,思考一下它會怎麼執行:
// Synchronous
[1, 2, 3, 4].forEach(function (i) {
  console.log(i)
})

// Asynchronous
function asyncForEach(array, cb) {
  array.forEach(function () {
    setTimeout(cb, 0)
  })
}

asyncForEach([1, 2, 3, 4], function (i) {
  console.log(i)
})
一般來說 callback function(簡稱 cb)會有兩個情況:
  1. 當一個 function 執行完後要隨著執行的 function
  2. 當一個非同步事件觸發時要隨之執行的 function

模擬瀏覽器 render 的情況

在這個範例中,作者使用 delay() 這個內建的 function,目的是要模擬一個耗時的函式:
// Synchronous
[1, 2, 3, 4].forEach(function (i) {
  console.log(i)
  delay()
})

// Asynchronous
function asyncForEach(array, cb) {
  array.forEach(function () {
    setTimeout(cb, 0)
  })
}

asyncForEach([1, 2, 3, 4], function (i) {
  console.log(i)
  delay()
})
一般來說,瀏覽器會在每 16.6 毫秒(也就是每秒 60 個 frames)的時候重新渲染(render)畫面,而渲染的優先權高於回呼函數(callback function)。
從下面的影片中我們可以看出當我們在堆疊(stack)中執行一個耗時的函式時,畫面的渲染會被阻塞住,這時你並沒辦法選取瀏覽器上的文字、沒辦法點擊瀏覽器上的元件。
但是如果我們是透過非同步的方式執行這些函式的時候,在每一個 cb 從工作佇列(task queue)到堆疊(stack)的過程中,提供了瀏覽器重新渲染的機會。
而這也就是為什麼不要把耗時的程式碼放入堆疊(stack)當中,因為當堆疊在運作的時候,瀏覽器並沒有辦法重新渲染畫面。

滑動捲軸的情況

function animateSomething () {
  delay()
}

$.on('document', 'scroll', animateSomething)
從這個範例中我們可以看到雖然不會導致 stack 出現阻塞的情況,但是卻可能導致工作佇列(task queue)阻塞:

參考

2017年8月10日

[App] Color Palette 簡單儲存搜尋色票匯出 SASS/SCSS


個人非常喜歡蒐集一些有的沒的顏色,雖然已經有一些可以儲存色票網站,像是 coolors, Adobe Color 等等,但是一方面它是 5 個顏色 5 個顏色一組,而且載入速度都稍微有點慢,今天來和大家分享一個簡單好用可以儲存、搜尋自己的色票,並匯出成 SASS/SCSS 的 Web APP - Simple Color Palette

使用方式很簡單,其實好像也沒什麼好說明的...。

儲存色票

只要打入正確的色碼,並且可以幫這麼顏色取的名稱方便之後搜尋,在按下 「SAVE」:

搜尋顏色

匯出色彩

按下 「Export」後,可以將儲存的色票匯出成 SCSS/SASS 變數,會自動儲存在剪貼簿中:

儲存至雲端

新增的色票檔預設會自動儲存在本機,已經即使關掉瀏覽器色票檔還是會保留(但如果清除瀏覽器記錄的話,色票檔就會一併被清除喔!),如果你想要儲存在雲端的話,可以透過 Facebook 登入後,按下 「Save on Cloud」,如此即是你清除瀏覽器或者在不同電腦,將能夠同步你的色票檔:


趕快來試試 Simple Color Palette 吧!

2017年8月9日

如何在 Rails 中搭配 Turbolinks 使用 Vue (Use Vue in Rails with Turbolinks)

keywords: webpacker
說明如何在 Rails Turbolinks 中搭配使用 Vue。

Initialize the App

# initialize the app
rails new rails_sandbox_vue --database=postgresql --webpack=vue

# install package
bundle
yarn

Scaffold the app

# Scaffold the app
bin/rails g scaffold User name email

# Create database and migrate
bin/rails db:setup
bin/rails db:migrate

Create Vue Component

./app/javascript/ 中建立 vue component hello_vue.vue
<!--
./app/javascript/hello_vue.vue
-->

<template>
  <div>
    <h4>{{ message }}</h4>
    <ul>
      <li>Object: {{ obj }} </li>
      <li>Number: {{ num }} </li>
      <li>Array: {{ arr }} </li>
      <li>String: {{ str }} </li>
    </ul>
    </div>
</template>

<script>
export default {
  props: ['obj', 'arr', 'num', 'str'],
  data: function () {
    return {
      message: 'Hello, Vue and Turbolinks'
    }
  }
}
</script>

<style scoped>
h4 {
  font-size: 2em;
  text-align: center;
  color: steelblue;
}
</style>

Create Vue Adapter

./app/javascript/packs/
中建立 vue_adapter.js,在 import Vue 的地方要載入 vue.esm.js 可以 compile template 的版本。另外要把需要使用到的 Vue Component 在這裡執行註冊:
// ./app/javascript/packs/vue_adapter.js

// import Vue
import Vue from 'vue/dist/vue.esm.js'

// import your components
import HelloVue from 'hello-vue'

// register your components
Vue.component('hello-vue', HelloVue)

function VueConstructor () {
  let outlets = document.querySelectorAll('[data-vue-component-outlet]')
  outlets.forEach(function (outlet, index) {
    let id = outlet.getAttribute('data-vue-component-outlet')
    new Vue({
      el: '[data-vue-component-outlet=' + id + ']'
    })
  })
}

if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
  document.addEventListener('turbolinks:load', VueConstructor)
} else {
  document.addEventListener('DOMContentLoaded', VueConstructor)
}
Notice:
-import 的 Vue 要匯入的是 vue.esm.js
-記得註冊使用到的 Vue Component

add vue_adapter in head

在 layouts/application.html.erb 中的 head 中加入 <%= javascript_pack_tag 'vue_adapter', 'data-turbolinks-track': 'reload' %>,以及 <%= stylesheet_pack_tag 'vue_adapter', 'data-turbolinks-track': 'reload' %>
<!-- ./app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>RailsSandboxVue</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

    <%= stylesheet_pack_tag 'vue_adapter', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'vue_adapter', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
Notice:
-要把 javascript_pack_tag 放在 head 當中
-如果 Vue 中有使用 SCSS 則需在 head 中再放入 stylesheet_pack_tag

Import Vue component in template

我們把 Vue 的組件載入 index.html.erb 中,data-vue-components-outlet 這個屬性是關鍵字,後面放要載入的 Vue 組件名稱:
<!-- ./app/views/users/index.html.erb -->

<!--  ...  -->

<!--  假設這是透過 controller 傳過來的資料  -->
<% @hello_message = {num: 1, str: '2', arr: [1, 2, 3], obj: {name: 'foo', age: 12}} %>

<!-- Import Vue Component -->
<div data-vue-components-outlet="hello-turbolinks">
  <hello-turbolinks
  :obj="<%= @hello_message[:obj].to_json %>"
  :arr="<%= @hello_message[:arr] %>"
  :str="<%= @hello_message[:str] %>"
  :num="<%= @hello_message[:num] %>"
  ></hello-turbolinks>
</div>
<!-- End of Import Vue Component -->

<%= link_to 'New User', new_user_path %>

完成

分別開兩個 terminal 到 app 目錄底下,分別執行:
bin/webpack-dev-server
bin/rails s
就可以看到 Vue Component 正確運作了。

使用 inline-template

依照上面的邏輯,我們可以透過 inline-template 的方式來使用 Vue, 這樣就可以把 Vue Template 的部分寫在 erb 中而不用拉到 .vue 裡面,以下是所建立的檔案:
<!--  ./app/javascript/hello-vue-inline.vue  -->

<script>
export default {
  props: ['obj', 'arr', 'num', 'str'],
  data: function () {
    return {
      message: 'Hello, Vue in inline-template.'
    }
  }
}
</script>

<style scoped>
h4 {
  font-size: 2em;
  text-align: center;
  color: purple;
}
</style>
// ./app/javascript/packs/vue_adapter.js
/**
 * 新增兩行
 * "import HelloVueInline from 'hello-vue-inline'"
 * "Vue.component('hello-vue-inline', HelloVueInline)"
 **/

...

// import your components
import HelloVue from 'hello-vue'
import HelloVueInline from 'hello-vue-inline' // 新增這行

// register your components
Vue.component('hello-vue', HelloVue)
Vue.component('hello-vue-inline', HelloVueInline)

...

<!--  ./app/views/users/index.html.erb  -->
<!--
 - 留意 Use Vue Component by inline-template 的部分
-->

<!--  ...  -->

<% @hello_message = {num: 1, str: '2', arr: [1, 2, 3], obj: {name: 'foo', age: 12}} %>

<!-- Import Vue Component -->
<div data-vue-component-outlet="v-hello-vue">
  <hello-vue
  :obj="<%= @hello_message[:obj].to_json %>"
  :arr="<%= @hello_message[:arr] %>"
  :str="<%= @hello_message[:str] %>"
  :num="<%= @hello_message[:num] %>"
  ></hello-vue>
</div>
<!-- End of Import Vue Component -->

<!-- Use Vue Component by inline-template -->
<div data-vue-component-outlet="v-hello-vue-inline">
  <hello-vue-inline
  :obj="<%= @hello_message[:obj].to_json %>"
  :arr="<%= @hello_message[:arr] %>"
  :str="<%= @hello_message[:str] %>"
  :num="<%= @hello_message[:num] %>"
  inline-template
  >
    <div>
      <h4>{{ message }}</h4>
      <ul>
        <li>Object: {{ obj }} </li>
        <li>Number: {{ num }} </li>
        <li>Array: {{ arr }} </li>
        <li>String: {{ str }} </li>
      </ul>
    </div>
  </hello-vue-inline>
</div>
<!-- End of Vue Component by inline-template -->

<%= link_to 'New User', new_user_path %>

加入 View Helper

我們也可以寫一個 Rails View Helper 來方便我們使用 Vue 組件:
./app/helpers/ 中建立一支 vue_helper.rb
# ./app/helpers/vue_helper.rb
module VueHelper
  def vue_outlet(html_options = {})
    html_options = html_options.reverse_merge(data: {})
    html_options[:data].tap do |data|
      data[:vue_component_outlet] = "_v" + SecureRandom.hex(5)
    end
    html_tag = html_options[:tag] || :div
    html_options.except!(:tag)
    content_tag(html_tag, '', html_options) do
      yield
    end
  end
end
使用方式如下:
<!--
./app/views/users/index.html.erb
-->

<% @hello_message = {num: 1, str: '2', arr: [1, 2, 3], obj: {name: 'foo', age: 12}} %>

<!-- Import Vue Component by Helper -->
<%= vue_outlet do %>
  <hello-turbolinks
  :obj="<%= @hello_message[:obj].to_json %>"
  :arr="<%= @hello_message[:arr] %>"
  :str="<%= @hello_message[:str] %>"
  :num="<%= @hello_message[:num] %>"
  >
<% end %>
<!-- End of Import Vue Component by Helper -->
如果 tag 不想要使用 div 可以加上 options:
<!--
./app/views/users/index.html.erb
-->

<!-- With <p> -->
<%= vue_outlet tag: 'p' do %>
  <hello-turbolinks
  :obj="<%= @hello_message[:obj].to_json %>"
  :arr="<%= @hello_message[:arr] %>"
  :str="<%= @hello_message[:str] %>"
  :num="<%= @hello_message[:num] %>"
  >
<% end %>
<!-- End of With <p> -->

檔案範例

開發者

參考