diff --git a/src/components/ErrorBoundary/PdapErrorBoundary.vue b/src/components/ErrorBoundary/PdapErrorBoundary.vue index 1a52795..afcd75b 100644 --- a/src/components/ErrorBoundary/PdapErrorBoundary.vue +++ b/src/components/ErrorBoundary/PdapErrorBoundary.vue @@ -1,8 +1,10 @@ + Oops, something went wrong! @@ -10,16 +12,57 @@ contact@pdap.io for assistance. - - + + diff --git a/src/components/ErrorBoundary/README.md b/src/components/ErrorBoundary/README.md index dfd13de..6346f43 100644 --- a/src/components/ErrorBoundary/README.md +++ b/src/components/ErrorBoundary/README.md @@ -2,9 +2,11 @@ Intercepts uncaught errors from its children and renders an error UI in place of its children. ## Props -| name | required? | types | description | default | -| ----------- | --------- | -------- | ------------------------------- | ------- | -| `component` | no | `string` | component to render as fallback | `'div'` | +| name | required? | types | description | default | +| ----------- | --------- | ------------------------------------------------------------------------------- | ------------------------------- | ----------- | +| `component` | no | `string` | component to render as fallback | `'div'` | +| `onError` | no | `(error: Error, target?: ComponentPublicInstance \| null \| undefined) => void` | callback to run on error | `undefined` | +| `params` | no | `Record` | params to forward to fallback | `undefined` | ## Example _From data-sources `App.vue`: This will catch any uncaught error at the route level and render the error fallback_ diff --git a/src/components/ErrorBoundary/error-boundary.spec.ts b/src/components/ErrorBoundary/error-boundary.spec.ts index 562f106..e8cf676 100644 --- a/src/components/ErrorBoundary/error-boundary.spec.ts +++ b/src/components/ErrorBoundary/error-boundary.spec.ts @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import ErrorBoundary from './PdapErrorBoundary.vue'; import { nextTick } from 'vue'; @@ -28,4 +28,23 @@ describe('ErrorBoundary', () => { ); expect(wrapper.html()).toMatchSnapshot(); }); + + it('calls the onError callback when an error occurs', async () => { + const onErrorSpy = vi.fn(); + + wrapper = mount(ErrorBoundary, { + props: { + onError: onErrorSpy, + }, + slots: { + default: 'Default Content', + }, + }); + + const testError = new Error('Test Error'); + wrapper.vm.interceptError(testError); + await nextTick(); + + expect(onErrorSpy).toHaveBeenCalledWith(testError, undefined, undefined); + }); }); diff --git a/src/components/ErrorBoundary/index.ts b/src/components/ErrorBoundary/index.ts new file mode 100644 index 0000000..0fac895 --- /dev/null +++ b/src/components/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default as ErrorBoundary } from './PdapErrorBoundary.vue'; diff --git a/src/components/ErrorBoundary/types.ts b/src/components/ErrorBoundary/types.ts new file mode 100644 index 0000000..6701e7b --- /dev/null +++ b/src/components/ErrorBoundary/types.ts @@ -0,0 +1,17 @@ +import { ComponentPublicInstance } from 'vue'; + +export interface PdapErrorEmitted { + error: Error; + vm: ComponentPublicInstance | null; + info?: string; +} + +export interface PdapErrorBoundaryProps { + component: string; + onError?: ( + error: Error, + target?: ComponentPublicInstance | null | undefined, + info?: string + ) => void; + params?: Record; +} diff --git a/src/components/FlexContainer/FlexContainer.vue b/src/components/FlexContainer/FlexContainer.vue deleted file mode 100644 index 6cb3ee9..0000000 --- a/src/components/FlexContainer/FlexContainer.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/FlexContainer/__snapshots__/flex-container.spec.ts.snap b/src/components/FlexContainer/__snapshots__/flex-container.spec.ts.snap deleted file mode 100644 index 5555488..0000000 --- a/src/components/FlexContainer/__snapshots__/flex-container.spec.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Renders container component > Renders a container 1`] = `Container Content`; diff --git a/src/components/FlexContainer/flex-container.spec.ts b/src/components/FlexContainer/flex-container.spec.ts deleted file mode 100644 index 8aefe5a..0000000 --- a/src/components/FlexContainer/flex-container.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Component -import FlexContainer from './FlexContainer.vue'; - -// Utils -import { mount } from '@vue/test-utils'; -import { describe, expect, test } from 'vitest'; - -// Test -describe('Renders container component', () => { - // Render - test('Renders a container', () => { - const wrapper = mount(FlexContainer, { - slots: { - default: 'Container Content', - }, - }); - - expect(wrapper.find('.pdap-flex-container').exists()).toBe(true); - expect(wrapper.classes()).toContain('pdap-flex-container'); - expect(wrapper.html()).toContain('Container Content'); - expect(wrapper.html()).toMatchSnapshot(); - }); - - // Props - // Props - align - test('Renders start aligned container', () => { - const wrapper = mount(FlexContainer, { props: { alignment: 'start' } }); - expect(wrapper.props().alignment).toBe('start'); - expect(wrapper.classes()).toContain('pdap-flex-container-start'); - }); - - test('Renders center aligned container', () => { - const wrapper = mount(FlexContainer, { props: { alignment: 'center' } }); - expect(wrapper.props().alignment).toBe('center'); - expect(wrapper.classes()).toContain('pdap-flex-container-center'); - }); -}); diff --git a/src/components/FlexContainer/index.ts b/src/components/FlexContainer/index.ts deleted file mode 100644 index 785e98b..0000000 --- a/src/components/FlexContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import FlexContainer from './FlexContainer.vue'; - -export { FlexContainer }; diff --git a/src/components/FlexContainer/types.ts b/src/components/FlexContainer/types.ts deleted file mode 100644 index e1d831c..0000000 --- a/src/components/FlexContainer/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface PdapFlexContainerProps { - alignment?: 'center' | 'start'; - component?: string; -} diff --git a/src/components/GridContainer/GridContainer.vue b/src/components/GridContainer/GridContainer.vue deleted file mode 100644 index 58865d7..0000000 --- a/src/components/GridContainer/GridContainer.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/GridContainer/__snapshots__/grid.spec.ts.snap b/src/components/GridContainer/__snapshots__/grid.spec.ts.snap deleted file mode 100644 index 7b991e4..0000000 --- a/src/components/GridContainer/__snapshots__/grid.spec.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Renders container component > Renders a container 1`] = ` - - - - - -`; diff --git a/src/components/GridContainer/grid.spec.ts b/src/components/GridContainer/grid.spec.ts deleted file mode 100644 index b8ee539..0000000 --- a/src/components/GridContainer/grid.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* eslint-disable vue/one-component-per-file */ -// Component -import GridContainer from './GridContainer.vue'; -import GridItem, { PdapGridItemProps } from '../GridItem/GridItem.vue'; - -// Utils -import { defineComponent } from 'vue'; -import { mount } from '@vue/test-utils'; -import { describe, expect, test, vi } from 'vitest'; - -type TestSlotProps = T; - -const data = [ - { component: 'li' }, - { component: 'img', src: 'https://mock.test.com' }, - { component: 'card', spanColumn: 3, spanRow: 2 }, -]; -const template = - ''; - -const expectComponents = new Set(['li', 'img', 'card']); - -const MultipleItems = defineComponent({ - components: { - GridItem, - }, - props: { - data: { - type: Array, - default: data, - }, - }, - template, -}); - -const SingleItem = defineComponent({ - components: { - GridItem, - }, - props: { - data: { - type: Array, - default: data, - }, - }, - template: '', -}); - -const getComputedStyle = vi.fn((el) => el.getComputedStyle()); -getComputedStyle.mockReturnValue({ - gridTemplateRows: 'repeat(3, minmax(20px, 1fr))', - gridTemplateColumns: 'repeat(2, auto)', - gridRow: 'span 2 / span 2', -}); -vi.stubGlobal('getComputedStyle', getComputedStyle); - -// Test -describe('Renders container component', () => { - // Render - test('Renders a container', () => { - const wrapper = mount(GridContainer, { - slots: { - default: MultipleItems, - }, - }); - - const container = wrapper.find('.pdap-grid-container'); - const items = container.findAll('.pdap-grid-item'); - - // Container - expect(container.exists()).toBe(true); - expect(wrapper.classes()).toContain('pdap-grid-container'); - - // Renders all items passed - expect(items.length).toBe(3); - - // Renders fall-through prop conditionally for img element - expect(container.find('[src="https://mock.test.com"]').exists()).toBe(true); - - // Renders each type of component passed - expect( - items.every((item) => - item.getRootNodes().some((node) => expectComponents.has(node.localName)) - ) - ).toBe(true); - - // Snapshot - expect(wrapper.html()).toMatchSnapshot(); - }); - - test('Renders a container with template props passed', () => { - const wrapper = mount(GridContainer, { - slots: { - default: MultipleItems, - }, - props: { - templateColumns: 'repeat(2, auto)', - templateRows: 'repeat(3, minmax(20px, 1fr))', - }, - }); - - expect(window.getComputedStyle(wrapper.vm.$el).gridTemplateColumns).toBe( - 'repeat(2, auto)' - ); - expect(window.getComputedStyle(wrapper.vm.$el).gridTemplateRows).toBe( - 'repeat(3, minmax(20px, 1fr))' - ); - }); - - test('Renders a container with columns and rows props passed', () => { - const wrapper = mount(GridContainer, { - slots: { - default: MultipleItems, - }, - props: { - rows: 2, - columns: 3, - }, - }); - - expect(wrapper.vm.$props.rows).toBe(2); - expect(wrapper.vm.$props.columns).toBe(3); - }); - - test('Renders a grid item with custom row span prop', () => { - const wrapper = mount(GridContainer, { - slots: { - default: SingleItem, - }, - }); - - expect( - window.getComputedStyle(wrapper.find('.pdap-grid-item').element).gridRow - ).toBe('span 2 / span 2'); - }); -}); diff --git a/src/components/GridContainer/index.ts b/src/components/GridContainer/index.ts deleted file mode 100644 index e407d59..0000000 --- a/src/components/GridContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import GridContainer from './GridContainer.vue'; - -export { GridContainer }; diff --git a/src/components/GridContainer/types.ts b/src/components/GridContainer/types.ts deleted file mode 100644 index 54eabca..0000000 --- a/src/components/GridContainer/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface PdapGridContainerProps { - columns?: 1 | 2 | 3 | 'auto'; - component?: string; - rows?: number | 'auto'; - templateColumns?: string; - templateRows?: string; -} diff --git a/src/components/GridItem/GridItem.vue b/src/components/GridItem/GridItem.vue deleted file mode 100644 index 79dd56a..0000000 --- a/src/components/GridItem/GridItem.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/GridItem/index.ts b/src/components/GridItem/index.ts deleted file mode 100644 index 2e0be61..0000000 --- a/src/components/GridItem/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import GridItem from './GridItem.vue'; - -export { GridItem }; diff --git a/src/components/GridItem/types.ts b/src/components/GridItem/types.ts deleted file mode 100644 index 70d1cd8..0000000 --- a/src/components/GridItem/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface PdapGridItemProps { - component?: string; - spanColumn?: 1 | 2 | 3; - spanRow?: number; -} diff --git a/src/components/index.ts b/src/components/index.ts index 4e6d2f6..4d15c5f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,9 +1,7 @@ export { Button } from './Button'; -export { FlexContainer } from './FlexContainer'; +export { ErrorBoundary } from './ErrorBoundary'; export { Footer } from './Footer'; export { Form } from './Form'; -export { GridContainer } from './GridContainer'; -export { GridItem } from './GridItem'; export { Input } from './Input'; export { Header } from './Header'; export { Nav } from './Nav'; diff --git a/src/demo/pages/ComponentDemo.vue b/src/demo/pages/ComponentDemo.vue index b98a3d2..3e4935d 100644 --- a/src/demo/pages/ComponentDemo.vue +++ b/src/demo/pages/ComponentDemo.vue @@ -144,7 +144,7 @@ - Here is a form using the Form component directly + Form Say hello - And here is the Quick Search Form component + Quick Search Form + + Error Boundary + console.debug({ error, vm, info })" + > + + + This is the content that will render inside the error boundary if + there is no error + + Click here to trigger error + + @@ -166,6 +181,7 @@ import { Breadcrumbs, Button, Dropdown, + ErrorBoundary, Form, QuickSearchForm, } from '../../components'; @@ -234,6 +250,10 @@ function buttonAlert(msg: string) { alert(msg); } +function triggerError() { + throw new Error('Trigger error fallback'); +} + function submit(values: Record<'firstName' | 'lastName' | 'iceCream', string>) { console.debug({ values }); const alertString = `Howdy, ${values.firstName} ${values.lastName}\n${ diff --git a/src/index.ts b/src/index.ts index eb83c74..cf2bcaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,15 @@ // Components export * from './components'; -// Styles +// Styles - compiled automatically import './styles/styles.css'; // Types export * from './components/Button/types'; export * from './components/Dropdown/types'; -export * from './components/FlexContainer/types'; +export * from './components/ErrorBoundary/types'; export * from './components/Footer/types'; export * from './components/Form/types'; -export * from './components/GridContainer/types'; -export * from './components/GridItem/types'; export * from './components/Header/types'; export * from './components/Input/types'; export * from './components/Nav/types';
@@ -10,16 +12,57 @@ contact@pdap.io for assistance.
Form
+ This is the content that will render inside the error boundary if + there is no error +