點燈坊

失くすものさえない今が強くなるチャンスよ

實作 Spinner

Sam Xiao's Avatar 2021-05-12

某些 API 可能需要較長回應時間,實務上會加上 Spinner 視覺效果,這在 Tailwind CSS 該如何實現呢 ?

Version

Tailwind CSS 2.1.1

Composition API

spinner000

按下 Get Books 後,會先顯示 spinner。

spinner001

等 API 完全回傳後才顯示資料。

<template lang='pug'>
div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
  svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
    path(fill='#ffffff',d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')
div
  button(@click='onClick', type='button', class='px-2.5 py-1.5').border.border-gray-300.shadow-sm.text-xs.font-medium.text-gray-700.rounded.bg-white.hover_bg-gray-50.focus_outline-none.focus_ring-2.focus_ring-offset-2.focus_ring-indigo-500 Get Books
div
  ul
    li(v-for='x in books') {{ x.title }} / {{ x.price }}
</template>
 
<script setup>
import getBooks from '/src/api/getBooks'

ref: isShowSpinner = false
ref: books = []

let onClick = () => {
  isShowSpinner = true
  getBooks()
    .then(x => (isShowSpinner = false, x))
    .then(x => x.data)
    .then(x => books = x)
}
</script>

第 2 行

div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
  svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
    path(fill='#ffffff',d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')

divsvg 構成 spinner。

第 2 行

div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
  • 使用 isShowSpinner state 控制 spinner 是否顯示
  • div 主要負責 overlay 部分

第 3 行

svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
    path(fill='#ffffff',d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')

使用 svg 為 spinner:

  • w-16:設定 spinner 寬度
  • h-16:設定 spinner 高度
  • animate-spin:使靜態 svg 能旋轉

18 行

let onClick = () => {
  isShowSpinner = true
  getBooks()
    .then(x => (isShowSpinner = false, x))
    .then(x => x.data)
    .then(x => books = x)
}
  • 一開始先設定 isShowSpinnertrue 顯示 spinner
  • 使用 getBooks() 呼叫 API,當 Promise 回傳成功立即設定 isShowSpinnerfalse 隱藏 spinner

Point-free

spinner000

結果不變,但使用 Point-free 改寫。

<template lang='pug'>
div(v-if='isShowSpinner').fixed.top-0.left-0.right-0.bottom-0.w-full.h-screen.z-50.overflow-hidden.bg-gray-700.opacity-75.flex.flex-col.items-center.justify-center
  svg(xmlns='http://www.w3.org/2000/svg', viewBox='0 0 16 16').w-16.h-16.animate-spin
    path(fill='#ffffff',d='M12.9 3.1c1.3 1.2 2.1 3 2.1 4.9 0 3.9-3.1 7-7 7s-7-3.1-7-7c0-1.9 0.8-3.7 2.1-4.9l-0.8-0.8c-1.4 1.5-2.3 3.5-2.3 5.7 0 4.4 3.6 8 8 8s8-3.6 8-8c0-2.2-0.9-4.2-2.3-5.7l-0.8 0.8z')
div
  button(@click='onClick', type='button', class='px-2.5 py-1.5').border.border-gray-300.shadow-sm.text-xs.font-medium.text-gray-700.rounded.bg-white.hover_bg-gray-50.focus_outline-none.focus_ring-2.focus_ring-offset-2.focus_ring-indigo-500 Get Books
div
  ul
    li(v-for='x in books') {{ x.title }} / {{ x.price }}
</template>

<script setup>
import { ref } from 'vue'
import { write } from 'vue3-fp'
import { pipe, andThen as then, prop, tap, T, F } from 'ramda'
import getBooks from '/src/api/getBooks'

let isShowSpinner = ref(false)
let books = ref([])

let showSpinner = pipe(
  T,
  write(isShowSpinner)
)

let hideSpinner = pipe(
  F, 
  write(isShowSpinner)
)

let onClick = pipe(
  showSpinner,
  getBooks,
  tap(then(hideSpinner)),
  then(prop('data')),
  then(write(books))
)
</script>

31 行

let onClick = pipe(
  showSpinner,
  getBooks,
  tap(then(hideSpinner)),
  then(prop('data')),
  then(write(books))
)

使用 pipe() 組合 onClick()

  • showSpinner:顯示 spinner
  • getBooks:呼叫 API
  • tap(then(hideSpinner)):回傳成功則顯示 spinner
  • then(prop('data')):只取 data property
  • then(write(books)):將結果寫入 books state

21 行

let showSpinner = pipe(
  T,
  write(isShowSpinner)
)

使用 pipe() 組合 showSpinner()

  • T:準備 true
  • write(isShowSpinner):寫入 isShowSpinner state

26 行

let hideSpinner = pipe(
  F, 
  write(isShowSpinner)
)

使用 pipe() 組合 hideSpinner()

  • F:準備 false
  • write(isShowSpinner):寫入 isShowSpinner state

Conclusion

  • Tailwind CSS 的 animate-spin 是關鍵,他使得靜態 svg 能旋轉達到動態效果,再搭配 isShowSpinner state 就能控制是否顯示 spinner