Skip to content

Commit

Permalink
TypeScript Rollout Tier 5 - Toast (#349)
Browse files Browse the repository at this point in the history
* feat(lib): rewrite Toast in TS

- Rewrites `Toast` in TypeScript:
  - Defines a new type `ToastProps` that represents the props type of
    `Toast`. `ToastOpenParams` extends this (see below).

  Trivial changes:
  - Adds `lang="ts"` to the script section
  - Wraps the entire component definition with `defineComponent` API

- Rewrites `component/toast/index.js` in TypeScript:
  - Defines a new type `ToastOpenParams` that represents the parameters
    type for `open`. `ToastOpenParams` accepts `VNode`(s) as `message`
    in addition to `string`(s).
  - Defines a new type `ToastProgrammaticInstance` that has the minimal
    interface of a programmatically opened `Toast` instance

  The following changes might not be trivial:
  - Renders `VNode` and an array of `VNode`(s) or `string`(s) through
    the slot. It used to render an array through the slot, but a single
    `VNode` also had to be rendered in the slot, otherwise `v-html`
    binding would try to stringify it.
  - Removes `params.parent` because it no longer has any effect
  - Replaces the `merge` helper function with spread syntax to provide
    the default value for `position`

  Trivial changes:
  - Replaces the extension: `.js` → `.ts`
  - Gives minimal types

* test(lib): rewrite Toast.spec in TS

- Rewrites `Toast.spec.js` in TypeScript:
  - Replaces `jest` API calls with those of `vi`: `fn`, `uesFakeTimers`,
    `useRealTimers`, and `advanceTimersByTime`
  - Replaces the extension: `.js` → `.ts`
  - Imports necessary functions and object from `vitest`
  - Gives minimal types

  Replaces the spec snapshot `Toast.spec.js.snap` with the one generated
  by `vitest` → `Toast.spec.ts.snap`.

* feat(lib): add `toast` to `$buefy`

Binds `$buefy.toast` to `ToastProgrammatic`.

* chore(lib): bundle Toast as TS

`rollup.config.mjs` removes "toast" from `JS_COMPONENTS`.

* feat(docs): rewrite Toast examples in TS

Rewrites example components in `pages/components/toast` in TypeScript.
Most of the changes are straightforward.

In `examples/ExSimple.vue`:
- Extraction of the type of a programmatically opened toast instance
  might look tricky (`ToastProgrammaticInstance`)

In `Toast.vue`:
- Separates the code snippet to an external file
  `outside-vue-instance.js` because dev build might treat it as the
  actual code and end up with an unresolved module error

Here is a TypeScript migration tip:
- Explicitly import and register components so that they are type
  checked. No type-checking is performed for globally registered
  components.
  • Loading branch information
kikuomax authored Jan 6, 2025
1 parent 8c74203 commit f982964
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 54 deletions.
1 change: 0 additions & 1 deletion packages/buefy-next/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ const JS_COMPONENTS = [
'tag',
'taginput',
'timepicker',
'toast',
'upload',
]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { transformVNodeArgs } from 'vue'
import { shallowMount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { BToast, ToastProgrammatic } from '@components/toast'

let wrapper
let wrapper: VueWrapper<InstanceType<typeof BToast>>

describe('BToast', () => {
HTMLElement.prototype.insertAdjacentElement = jest.fn()
HTMLElement.prototype.insertAdjacentElement = vi.fn()
beforeEach(() => {
wrapper = shallowMount(
BToast,
Expand Down Expand Up @@ -38,32 +40,32 @@ describe('BToast', () => {
})

afterEach(() => {
jest.useRealTimers()
vi.useRealTimers()
})

it('should close after the duration', () => {
jest.useFakeTimers()
vi.useFakeTimers()
const params = {
message: 'message',
duration: 1000,
onClose: jest.fn()
onClose: vi.fn()
}
new ToastProgrammatic().open(params)
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
expect(params.onClose).not.toHaveBeenCalled()
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
expect(params.onClose).toHaveBeenCalled()
})

it('indefinitely should be able to be manually closed', () => {
jest.useFakeTimers()
vi.useFakeTimers()
const params = {
message: 'message',
indefinite: true,
onClose: jest.fn()
onClose: vi.fn()
}
const toast = new ToastProgrammatic().open(params)
jest.advanceTimersByTime(10000)
vi.advanceTimersByTime(10000)
expect(params.onClose).not.toHaveBeenCalled()
toast.close()
expect(params.onClose).toHaveBeenCalled()
Expand Down
13 changes: 10 additions & 3 deletions packages/buefy-next/src/components/toast/Toast.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@
</transition>
</template>

<script>
<script lang="ts">
import { defineComponent } from 'vue'
import config from '../../utils/config'
import type { ExtractComponentProps } from '../../utils/helpers'
import NoticeMixin from '../../utils/NoticeMixin'
export default {
const Toast = defineComponent({
name: 'BToast',
mixins: [NoticeMixin],
data() {
return {
newDuration: this.duration || config.defaultToastDuration
}
}
}
})
export type ToastProps = ExtractComponentProps<typeof Toast>
export default Toast
</script>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BToast > render correctly 1`] = `
"<transition-stub enteractiveclass=\\"fadeInDown\\" leaveactiveclass=\\"fadeOut\\" appear=\\"false\\" persisted=\\"true\\" css=\\"true\\">
<div class=\\"toast is-dark is-top\\" aria-hidden=\\"false\\" role=\\"alert\\" style=\\"\\">
<!-- eslint-disable-next-line vue/no-v-html -->
<div></div>
</div>
</transition-stub>"
`;
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import { createApp, h as createElement } from 'vue'
import type { App, ComponentPublicInstance, VNode } from 'vue'

import Toast from './Toast.vue'
import type { ToastProps } from './Toast.vue'

import config from '../../utils/config'
import { merge, copyAppContext, getComponentFromVNode } from '../../utils/helpers'
import { copyAppContext, getComponentFromVNode } from '../../utils/helpers'
import { registerComponentProgrammatic } from '../../utils/plugins'

export type ToastOpenParams = Omit<ToastProps, 'message'> & {
// programmatically opened toast can have VNode(s) as the message
message?: string | VNode | (string | VNode)[],
onClose?: () => void
}

// Minimal definition of a programmatically opened toast.
//
// ESLint does not like `{}` as a type but allowed here to make them look
// similar to Vue's definition.
/* eslint-disable @typescript-eslint/ban-types */
type ToastProgrammaticInstance = ComponentPublicInstance<
{}, // P
{}, // B
{}, // D
{}, // C
{ close: () => void } // M
>
/* eslint-enable @typescript-eslint/ban-types */

class ToastProgrammatic {
constructor(app) {
private app: App | undefined

constructor(app?: App) {
this.app = app // may be undefined in the testing environment
}

open(params) {
open(params: string | ToastOpenParams) {
if (typeof params === 'string') {
params = {
message: params
}
}

const defaultParam = {
position: config.defaultToastPosition || 'is-top'
}
if (params.parent) {
delete params.parent
let slot: ToastOpenParams['message']
let { message, ...restParams } = params
if (typeof message !== 'string') {
slot = message
message = undefined
}
let slot
if (Array.isArray(params.message)) {
slot = params.message
delete params.message
const propsData: ToastProps = {
position: config.defaultToastPosition || 'is-top',
message,
...restParams
}
const propsData = merge(defaultParam, params)
const container = document.createElement('div')
// Vue 3 requires a new app to mount another component
const vueInstance = createApp({
Expand All @@ -42,7 +65,7 @@ class ToastProgrammatic {
close() {
const toast = getComponentFromVNode(this.toastVNode)
if (toast) {
toast.close()
(toast as InstanceType<typeof Toast>).close()
}
}
},
Expand Down Expand Up @@ -76,14 +99,15 @@ class ToastProgrammatic {
} else {
// adds $buefy global property
// so that $buefy.globalNoticeInterval is available on the new Vue app
vueInstance.config.globalProperties.$buefy = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vueInstance.config.globalProperties.$buefy = {} as any
}
return vueInstance.mount(container)
return vueInstance.mount(container) as ToastProgrammaticInstance
}
}

const Plugin = {
install(Vue) {
install(Vue: App) {
registerComponentProgrammatic(Vue, 'toast', new ToastProgrammatic(Vue))
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/buefy-next/src/utils/vue-augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'vue'
import type { LoadingProgrammatic } from '../components/loading'
import type { ModalProgrammatic } from '../components/modal'
import type { SnackbarProgrammatic } from '../components/snackbar'
import type { ToastProgrammatic } from '../components/toast'
import ConfigComponent from './ConfigComponent'

// Augments the global property with `$buefy`.
Expand All @@ -24,6 +25,7 @@ declare module '@vue/runtime-core' {
loading: LoadingProgrammatic,
modal: ModalProgrammatic,
snackbar: SnackbarProgrammatic,
toast: ToastProgrammatic,
// TODO: make key-values more specific
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
Expand Down
30 changes: 21 additions & 9 deletions packages/docs/src/pages/components/toast/Toast.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,30 @@
</div>
</template>

<script>
import { preformat } from '@/utils'
<script lang="ts">
import { defineComponent } from 'vue'
import { preformat, shallowFields } from '@/utils'
import ApiView from '@/components/ApiView.vue'
import CodeView from '@/components/CodeView.vue'
import Example from '@/components/Example.vue'
import VariablesView from '@/components/VariablesView.vue'
import api from './api/toast'
import variables from './variables/toast'
import { shallowFields } from '@/utils'
import ExSimple from './examples/ExSimple'
import ExSimple from './examples/ExSimple.vue'
import ExSimpleCode from './examples/ExSimple.vue?raw'
export default {
import outsideVueInstance from './outside-vue-instance.js?raw'
export default defineComponent({
components: {
ApiView,
CodeView,
Example,
VariablesView
},
data() {
return {
api,
Expand All @@ -35,13 +49,11 @@
ExSimple
}),
ExSimpleCode,
outsideVueInstance: `
import { ToastProgrammatic as Toast } from 'buefy'
Toast.open('Toasty!')`
outsideVueInstance,
}
},
methods: {
preformat
}
}
})
</script>
15 changes: 11 additions & 4 deletions packages/docs/src/pages/components/toast/examples/ExSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@
</section>
</template>

<script>
export default {
<script lang="ts">
import { defineComponent } from 'vue'
import { BButton } from '@ntohq/buefy-next'
import type { ToastProgrammatic } from '@ntohq/buefy-next'
type ToastProgrammaticInstance = ReturnType<ToastProgrammatic['open']>
export default defineComponent({
components: { BButton },
data() {
return {
indefinteToast: null
indefinteToast: null as ToastProgrammaticInstance | null
}
},
methods: {
Expand Down Expand Up @@ -85,5 +92,5 @@
}
}
}
}
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { ToastProgrammatic as Toast } from 'buefy'
Toast.open('Toasty!')

0 comments on commit f982964

Please sign in to comment.