點燈坊

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

實作 Toast Alert

Sam Xiao's Avatar 2021-04-11

除了使用 Modal Dialog 通知 User 外,Toast Alert 亦為實務上常見的 UI。

Version

Vue 3.0.5
Tailwind CSS 2.1.1

Toast Alert

toast000

右側包含 dismiss button,且 5 秒鐘後也會自動 dismiss。

<template>
  <button class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" @click="onToggle">
    Toggle
  </button>
  <transition enter-from-class="opacity-0" enter-active-class="transition duration-1000" enter-to-class="opacity-100" leave-from-class="opacity-100" leave-active-class="transition duration-1000" leave-to-class="opacity-0">
    <div v-if="isShowAlert" class="rounded-md bg-green-50 p-4">
      <div class="flex">
        <div class="flex-shrink-0">
          <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
          </svg>
        </div>
        <div class="ml-3">
          <p class="text-sm font-medium text-green-800">
            Successfully uploaded
          </p>
        </div>
        <div class="ml-auto pl-3">
          <div class="-mx-1.5 -my-1.5">
            <button type="button" class="inline-flex bg-green-50 rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-green-50 focus:ring-green-600" @click="onDismiss">
              <span class="sr-only">Dismiss</span>
              <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
              </svg>
            </button>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
ref: isShowAlert = false
let timeoutId = 0

let onToggle = () => {
  isShowAlert = !isShowAlert

  if (isShowAlert) timeoutId = setTimeout(() => isShowAlert = false, 5000)
  else clearTimeout(timeoutId)
}

let onDismiss = () => {
  isShowAlert = false
  clearTimeout(timeoutId)
}
</script>

34 行

ref: isShowAlert = false
let timeoutId = 0
  • isShowAlert :須與 HTML 溝通,故使用 ref 宣告
  • timeoutId:只需與 JavaScript 溝通,故使用 let 宣告即可

37 行

let onToggle = () => {
  isShowAlert = !isShowAlert

  if (isShowAlert) timeoutId = setTimeout(() => isShowAlert = false, 5000)
  else clearTimeout(timeoutId)
}

當按下 Toggle button 將執行 onToggle

  • isShowAlert 做 toggle
  • isShowAlerttrue,使用 setTimeout()5 秒之後將 isShowAlert 設定為 false
  • isShowAlertfalse,則使用 clearTimeout() 取消

44 行

let onDismiss = () => {
  isShowAlert = false
  clearTimeout(timeoutId)
}

當按下 Toast alert 右側的 dismiss button 將執行 onDismiss

  • isShowAlert 設定為 false
  • 使用 clearTimeout() 取消

Function Pipeline

toast000

改用 Function Pipeline 方式實現。

<template>
  <button class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" @click="onToggle">
    Toggle
  </button>
  <transition enter-from-class="opacity-0" enter-active-class="transition duration-1000" enter-to-class="opacity-100" leave-from-class="opacity-100" leave-active-class="transition duration-1000" leave-to-class="opacity-0">
    <div v-if="isShowAlert" class="rounded-md bg-green-50 p-4">
      <div class="flex">
        <div class="flex-shrink-0">
          <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
          </svg>
        </div>
        <div class="ml-3">
          <p class="text-sm font-medium text-green-800">
            Successfully uploaded
          </p>
        </div>
        <div class="ml-auto pl-3">
          <div class="-mx-1.5 -my-1.5">
            <button type="button" class="inline-flex bg-green-50 rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-green-50 focus:ring-green-600" @click="onDismiss">
              <span class="sr-only">Dismiss</span>
              <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
              </svg>
            </button>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { ref } from 'vue'
import { pipe, not, ifElse, always, thunkify } from 'ramda'
import { read, write } from 'vue3-fp'

let isShowAlert = ref(false)
let timeoutId = 0

let hideAlert = pipe(
  always(false),
  write(isShowAlert)
)

let onToggle = pipe(
  read(isShowAlert),
  not,
  write(isShowAlert),
  ifElse(
    read(isShowAlert),
    () => timeoutId = setTimeout(hideAlert, 5000),
    thunkify(clearTimeout)(timeoutId)
  )
)

let onDismiss = pipe(
  always(false),
  write(isShowAlert),
  thunkify(clearTimeout)(timeoutId)
)
</script>

38 行

let isShowAlert = ref(false)
let timeoutId = 0
  • isShowAlert :須與 HTML 溝通,故使用 ref()
  • timeoutId:只需與 JavaScript 溝通,故使用 let 宣告即可

46 行

let onToggle = pipe(
  read(isShowAlert),
  not,
  write(isShowAlert),
  ifElse(
    read(isShowAlert),
    () => timeoutId = setTimeout(hideAlert, 5000),
    thunkify(clearTimeout)(timeoutId)
  )
)

當按下 Toggle button 將執行 onToggle

  • read(isShowAlert):讀取 isShowAlert state
  • not():對 isShowAlert toggle
  • write(isShowAlert):寫入 isShowAlert state
  • ifElse():若 isShowAlerttrue,使用 setTimeout()5 秒之後將 isShowAlert 設定為 false,反之則使用 clearTimeout() 取消

41 行

let hideAlert = pipe(
  always(false),
  write(isShowAlert)
)

isShowAlert state 設定為 false

57 行

let onDismiss = pipe(
  always(false),
  write(isShowAlert),
  thunkify(clearTimeout)(timeoutId)
)

當按下 toast alert 右側的 dismiss button 將執行 onDismiss

  • always(false):準備 false
  • write(isShowAlert):寫入 isShowAlert state
  • thunkify(clearTimeout)(timeoutId):使用 clearTimeout() 取消

Conclusion

  • Toast alert 的 UI 來自於 Tailwind UI,自行控制 isShowAlert state 顯示 toast alert