Complete toast implementation

This commit is contained in:
Daniel J. Summers 2021-08-07 17:36:29 -04:00
parent 0679fe04ef
commit 058c5e5b77
4 changed files with 94 additions and 49 deletions

View File

@ -9,21 +9,18 @@
<component :is="Component" /> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>
<button @click.prevent="showToast('howdy', 'danger')">Show toast</button>
</main> </main>
<app-footer /> <app-footer />
<div aria-live="polite" aria-atomic="true"> <app-toaster />
<div class="toast-container position-absolute p-3 bottom-0 start-50 translate-middle-x" id="toasts"></div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { Toast } from 'bootstrap'
import AppFooter from './components/layout/AppFooter.vue' import AppFooter from './components/layout/AppFooter.vue'
import AppNav from './components/layout/AppNav.vue' import AppNav from './components/layout/AppNav.vue'
import AppToaster from './components/layout/AppToaster.vue'
import TitleBar from './components/layout/TitleBar.vue' import TitleBar from './components/layout/TitleBar.vue'
import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/css/bootstrap.min.css'
@ -34,12 +31,8 @@ export default defineComponent({
components: { components: {
AppFooter, AppFooter,
AppNav, AppNav,
AppToaster,
TitleBar TitleBar
},
setup () {
return {
showToast
}
} }
}) })
@ -52,33 +45,6 @@ export default defineComponent({
export function yesOrNo (cond : boolean) : string { export function yesOrNo (cond : boolean) : string {
return cond ? 'Yes' : 'No' return cond ? 'Yes' : 'No'
} }
/**
* Show a toast notification
*
* @param text The text of the notification
* @param type The type of notification to show (defaults to 'success')
* @param heading The optional text to use for the heading
*/
export function showToast (text : string, type = 'success', heading : string | undefined) : void {
let header : HTMLDivElement | undefined
if (heading) {
header = document.createElement('div')
header.className = 'toast-header'
header.innerHTML = 'The Header'
}
const body = document.createElement('div')
body.className = 'toast-body'
body.innerHTML = text
const toast = document.createElement('div')
if (header) toast.appendChild(header)
toast.appendChild(body)
toast.className = `toast bg-${type} text-white`
;(document.getElementById('toasts') as HTMLDivElement).appendChild(toast)
new Toast(toast).show()
}
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -0,0 +1,83 @@
<template>
<div aria-live="polite" aria-atomic="true">
<div class="toast-container position-absolute p-3 bottom-0 start-50 translate-middle-x" id="toasts"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Toast } from 'bootstrap'
/** Remove a toast once it's hidden */
const removeToast = (event : Event) => (event.target as HTMLDivElement).remove()
/** Create a toast, add it to the DOM, and show it */
const createToast = (level : 'success' | 'warning' | 'danger', message : string, process : string | undefined) => {
let header : HTMLDivElement | undefined
if (level !== 'success') {
// Create a heading, optionally including the process that generated the message
const heading = (typ : string) : string => {
const proc = process ? ` (${process})` : ''
return `<span class="me-auto"><strong>${typ.toUpperCase()}</strong>${proc}</span>`
}
header = document.createElement('div')
header.className = 'toast-header'
header.innerHTML = heading(level === 'warning' ? level : 'error')
// Include a close button, as these will not auto-close
const close = document.createElement('button')
close.type = 'button'
close.className = 'btn-close'
close.setAttribute('data-bs-dismiss', 'toast')
close.setAttribute('aria-label', 'Close')
header.appendChild(close)
}
const body = document.createElement('div')
body.className = 'toast-body'
body.innerHTML = message
const toastEl = document.createElement('div')
toastEl.className = `toast bg-${level} text-white`
toastEl.setAttribute('role', 'alert')
toastEl.setAttribute('aria-live', 'assertlive')
toastEl.setAttribute('aria-atomic', 'true')
toastEl.addEventListener('hidden.bs.toast', removeToast)
if (header) toastEl.appendChild(header)
toastEl.appendChild(body)
;(document.getElementById('toasts') as HTMLDivElement).appendChild(toastEl)
new Toast(toastEl, { autohide: level === 'success' }).show()
}
/**
* Generate a success toast
*
* @param message The message to be displayed
*/
export function toastSuccess (message : string) : void {
createToast('success', message, undefined)
}
/**
* Generate a warning toast
*
* @param message The message to be displayed
* @param process The process which generated the warning (optional)
*/
export function toastWarning (message : string, process : string | undefined) : void {
createToast('warning', message, process)
}
/**
* Generate an error toast
*
* @param message The message to be displayed
* @param process The process which generated the error (optional)
*/
export function toastError (message : string, process : string | undefined) : void {
createToast('danger', message, process)
}
export default defineComponent({
name: 'AppToaster'
})
</script>

View File

@ -9,6 +9,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted } from 'vue' import { defineComponent, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toastSuccess } from '@/components/layout/AppToaster.vue'
import { useStore } from '@/store' import { useStore } from '@/store'
export default defineComponent({ export default defineComponent({
@ -19,8 +20,8 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
store.commit('clearUser') store.commit('clearUser')
toastSuccess('Log Off Successful &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>')
router.push('/') router.push('/')
// TODO: toast
}) })
return { } return { }

View File

@ -3,8 +3,6 @@
<page-title title="Account Deletion Options" /> <page-title title="Account Deletion Options" />
<h3>Account Deletion Options</h3> <h3>Account Deletion Options</h3>
<p v-if="error !== ''">{{error}}</p>
<h4>Option 1 &ndash; Delete Your Profile</h4> <h4>Option 1 &ndash; Delete Your Profile</h4>
<p> <p>
Utilizing this option will remove your current employment profile and skills. This will preserve any success Utilizing this option will remove your current employment profile and skills. This will preserve any success
@ -38,9 +36,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue' import { defineComponent } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import api, { LogOnSuccess } from '@/api' import api, { LogOnSuccess } from '@/api'
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
import { useStore } from '@/store' import { useStore } from '@/store'
export default defineComponent({ export default defineComponent({
@ -49,16 +48,13 @@ export default defineComponent({
const store = useStore() const store = useStore()
const router = useRouter() const router = useRouter()
/** Error message encountered during actions */
const error = ref('')
/** Delete the profile only; redirect to home page on success */ /** Delete the profile only; redirect to home page on success */
const deleteProfile = async () => { const deleteProfile = async () => {
const resp = await api.profile.delete(store.state.user as LogOnSuccess) const resp = await api.profile.delete(store.state.user as LogOnSuccess)
if (typeof resp === 'string') { if (typeof resp === 'string') {
error.value = resp toastError(resp, 'Deleting Profile')
} else { } else {
// TODO: notify toastSuccess('Profile Deleted Successfully')
router.push('/citizen/dashboard') router.push('/citizen/dashboard')
} }
} }
@ -67,16 +63,15 @@ export default defineComponent({
const deleteAccount = async () => { const deleteAccount = async () => {
const resp = await api.citizen.delete(store.state.user as LogOnSuccess) const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
if (typeof resp === 'string') { if (typeof resp === 'string') {
error.value = resp toastError(resp, 'Deleting Account')
} else { } else {
store.commit('clearUser') store.commit('clearUser')
// TODO: notify toastSuccess('Account Deleted Successfully')
router.push('/so-long/success') router.push('/so-long/success')
} }
} }
return { return {
error,
deleteProfile, deleteProfile,
deleteAccount deleteAccount
} }