dillonchanis / vue-error-boundary

A reusable error boundary component for catching JavaScript errors and displaying fallback UIs.
MIT License
91 stars 9 forks source link

Composition UI support #7

Closed lancegliser closed 9 months ago

lancegliser commented 4 years ago

Hey there.

I had need of an ErrorBoundary on my project, but am trying to stay inside TypeScript. With the release of the Composition UI, it's a little more possible. I had a go at creating a version of what you've done using it. Thought you might care to use it as a starting point if you begin supporting it or TS:

https://gist.github.com/lancegliser/9a4d22dbab2113a45d5ddb04f5825071

The relevant bit is:

import {
  createComponent,
  createElement,
  onErrorCaptured,
  reactive,
  SetupContext
} from "@vue/composition-api";
import DefaultFallbackUI from "./ErrorBoundaryFallbackUI.vue";
import { IErrorBoundaryState } from "@/components/errorBoundary/IErrorBoundary";
import { Vue as VueType } from "vue/types/vue";
import { VNode } from "vue";

const isObjectEmpty = (obj: Object) =>
  Object.entries(obj).length === 0 && obj.constructor === Object;
// https://github.com/posva/vue-promised/blob/master/src/index.js
const convertVNodeArray = (h: Function, wrapperTag: string, nodes: VNode[]) => {
  // for arrays and single text nodes
  if (nodes.length > 1 || !nodes[0].tag) return h(wrapperTag, {}, nodes);
  return nodes[0];
};

interface IProps {
  stopPropagation: boolean;
  fallBack: Object;
  onError: Function;
  tag: string;
}

const defaultOnErrorFallback = () => {};
const ErrorBoundary = createComponent<IProps>({
  name: "ErrorBoundary",
  props: {
    stopPropagation: {
      type: Boolean,
      default: false
    },
    fallBack: {
      type: Object,
      default: () => DefaultFallbackUI
    },
    onError: {
      type: Function,
      default: defaultOnErrorFallback
    },
    tag: {
      type: String,
      default: "div"
    }
  },
  setup(props: IProps, context: SetupContext): () => VNode | null {
    const { state } = useState(props);
    return (): VNode | null => {
      const fallback = createElement(props.fallBack, {
        props: { state }
      });
      if (state.error) {
        return Array.isArray(fallback)
          ? convertVNodeArray(createElement, props.tag, fallback)
          : fallback;
      }
      if (isObjectEmpty(context.slots)) {
        console.warn("ErrorBoundary component must have child components.");
        return null;
      }
      const contents = context.slots.default();
      return Array.isArray(contents)
        ? convertVNodeArray(createElement, props.tag, contents)
        : contents;
    };
  }
});
export default ErrorBoundary;
function useState(props: IProps) {
  const state = reactive({
    error: undefined
  } as IErrorBoundaryState);
  onErrorCaptured((error: Error, vm: VueType, info: string = "") => {
    state.error = error;
    props.onError(error, vm, info);
    if (props.stopPropagation) {
      return false;
    }
  });
  return { state };
}
lancegliser commented 4 years ago

One issue I have thought of that isn't handled in this code:

How do I clear the error and display the component again?

dillonchanis commented 4 years ago

@lancegliser Hey, sorry for the late reply had a busy week. Haven't spent too much time in the Vue these days unfortunately, but I'll definitely consider updating to support TS. Thank you for the work you put in.

For the second issue, you should be able to "reset" the boundary by changing the key property. Check out this sandbox