Skip to content

Commit

Permalink
Added support for error boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
mcjazzyfunky committed Mar 7, 2019
1 parent 005d965 commit 8446dbf
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const HelloWorld = defineComponent({

defaultProps: {
name: 'world'
}
},

render(props) {
return div(`Hello, ${props.name}!`)
Expand Down
2 changes: 2 additions & 0 deletions src/demo/available-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import i18n from './demos/i18n'
import iterators from './demos/iterators'
import mousePositionHook from './demos/mouse-position-hook'
import mountUnmount from './demos/mount-unmount'
import errorBoundary from './demos/error-boundary'
import performanceTest1 from './demos/performance-test1'
import performanceTest2 from './demos/performance-test2'

Expand All @@ -27,6 +28,7 @@ const demos: [string, VirtualElement][] = [
['Iterators', iterators],
['Mouse position hook', mousePositionHook],
['Mount/Unmout', mountUnmount],
['Error boundary', errorBoundary],
['Performance test 1', performanceTest1],
['Performance test 2', performanceTest2]
]
Expand Down
2 changes: 1 addition & 1 deletion src/demo/demo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineComponent, mount, VirtualElement, useForceUpdate, useRef }
from '../modules/core/main/index'

import '../modules/adapt-dyo/main/index'
import '../modules/adapt-react/main/index'

import { div, h4, label, option, select } from '../modules/html/main/index'
import availableDemos from './available-demos'
Expand Down
61 changes: 61 additions & 0 deletions src/demo/demos/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
createElement,
defineComponent,
useCallback,
useState,
Boundary
} from 'js-surface';

const ErrorTrigger = defineComponent({
displayName: 'ErrorTrigger',

render() {
const
[errorMsg, setErrorMsg] = useState(null),
onButtonClick = useCallback(() => setErrorMsg('Simulated error!'))

if (errorMsg) {
throw new Error(errorMsg);
}

return (
<button onClick={onButtonClick}>
Click to trigger errror
</button>
)
}
})

const ErrorBoundary = defineComponent({
displayName: 'ErrorBoundary',

render(props) {
const
[error, setError] = useState(null),

handleReset = useCallback(() => {
setError(null)
}),

handleError = useCallback((error: any, info: any) => {
console.log(error)
console.log('Error info: ', info)
setError(error)
})

return (
<Boundary handle={handleError}>
{
error
? <div>
Catched error: <i>{error.message} </i>
<button onClick={handleReset}>Reset</button>
</div>
: <ErrorTrigger/>
}
</Boundary>
)
}
})

export default <ErrorBoundary/>
22 changes: 20 additions & 2 deletions src/modules/adapt-dyo/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
mount, unmount,
typeOf, propsOf, toChildArray, forEachChild,
useContext, useEffect, useMethods, useState,
Fragment, Props, Context,
Boundary, Fragment, Props, Context,
} from '../../core/main/index'

// TODO!!!
Expand Down Expand Up @@ -79,11 +79,19 @@ adapt(defineContext, (ctx: Context<any>, meta: any) => { // TODO
return [internalContext, Provider, Consumer]
})


adapt(useContext, (ctx: any) => {
return Dyo.useContext(ctx.Provider.__internal_context)[0]
})

adapt(Boundary, (props: any) => {
return (
Dyo.h(
DyoBoundary,
{ handle: props.handle },
props.children)
)
})

adapt(typeOf, (it: any) => it.type)
adapt(propsOf, (it: any) => it.type)
adapt(toChildArray, Dyo.Children.toArray)
Expand All @@ -99,3 +107,13 @@ adapt(unmount, Dyo.unmountComponentAtNode)
Object.defineProperty(Fragment, '__internal_type', {
value: Dyo.Fragment
})

function DyoBoundary(props: any) {
Dyo.useBoundary((e: any) => {
props.handle(e.message, null)
})

return props.children
}

(DyoBoundary as any).displayName = 'Boundary (inner)'
32 changes: 30 additions & 2 deletions src/modules/adapt-react/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
mount, unmount,
typeOf, propsOf, toChildArray, forEachChild,
useContext, useEffect, useMethods, useState,
Fragment, Props, Context,
Fragment, Boundary, Props, Context,
} from '../../core/main/index'

function adapt(base: any, delegate: any) {
Expand Down Expand Up @@ -40,7 +40,7 @@ adapt(defineComponent, (factory: any) => {
const
defaultProps = factory.meta.defaultProps

let ret = (props: Props, ref: any) => {
let ret: any = (props: Props, ref: any) => {
if (defaultProps) {
props = Object.assign({}, defaultProps, props) // TODO - performance
}
Expand All @@ -52,6 +52,8 @@ adapt(defineComponent, (factory: any) => {
ret = React.memo(ret)
}

ret.displayName = factory.meta.displayName

if (factory.meta.render.length > 1) {
ret = React.forwardRef(ret)
}
Expand Down Expand Up @@ -81,6 +83,32 @@ adapt(useState, React.useState)
adapt(mount, ReactDOM.render)
adapt(unmount, ReactDOM.unmountComponentAtNode)

adapt(Boundary, (props: any) => {
return (
React.createElement(
ReactBoundary,
{ handle: props.handle },
props.children)
)
})

Object.defineProperty(Fragment, '__internal_type', {
value: React.Fragment
})

class ReactBoundary extends React.Component {
static displayName = 'Boundary (inner)'

static getDerivedStateFromError() {
}

componentDidCatch(error: any, info: any) {
if (this.props.handle) {
this.props.handle(error, info)
}
}

render() {
return this.props.children
}
}
40 changes: 40 additions & 0 deletions src/modules/core/main/api/Boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import defineComponent from './defineComponent'
import isNode from './isNode'

type BoundaryProps = {
handle: (exception: any) => void,
children?: any // TODO
}

const Boundary = defineComponent<BoundaryProps>({
displayName: 'Boundary',

properties: {
handle: {
type: Function,
required: true
},

children: {
validate: isNode
}
},

render(props) {
delegate = delegate || (Boundary as any).__apply

if (!delegate) {
throw new Error('[Boundary] Adapter has not been initialized')
}

return delegate(props)
}
})

// --- locals -------------------------------------------------------

let delegate: any = null // TODO

//--- exports -------------------------------------------------------

export default Boundary
25 changes: 17 additions & 8 deletions src/modules/core/main/api/defineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ import ComponentConfig from './types/ComponentConfig'
import ComponentFactory from './types/ComponentFactory'
import h from './h'
import { Spec, SpecValidator } from 'js-spec'
import { metadata } from 'src/modules/svg/main';

function defineComponent<P extends Props = {}, M extends Methods = {}>(
config: ComponentConfig<P>): ComponentFactory<P, M>

function defineComponent(config: any): any {
const createInternalType = (defineComponent as any).__apply

if (!createInternalType) {
throw new Error('[defineComponent] Adapter has not been initialized')
}

if (process.env.NODE_ENV === 'development' as any) {
const error = validateComponentConfig(config)

Expand Down Expand Up @@ -49,7 +42,23 @@ function defineComponent(config: any): any {
})

Object.defineProperty(ret, '__internal_type', {
value: createInternalType(ret)
configurable: true,

get() {
const createInternalType = (defineComponent as any).__apply

if (!createInternalType) {
throw new Error('[defineComponent] Adapter has not been initialized')
}

const internalType = createInternalType(ret)

Object.defineProperty(ret, '__internal_type', {
value: internalType
})

return internalType
}
})

return ret
Expand Down
3 changes: 2 additions & 1 deletion src/modules/core/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export { default as useMethods } from './api/useMethods'
export { default as usePrevious } from './api/usePrevious'
export { default as useRef } from './api/useRef'
export { default as useState } from './api/useState'
export { default as Boundary } from './api/Boundary'
export { default as Fragment } from './api/Fragment'

// types
export { default as ComponentConfig } from './api/types/ComponentConfig' // TODO - splint into two config types?
export { default as ComponentConfig } from './api/types/ComponentConfig' // TODO - split into two config types?
export { default as ComponentFactory } from './api/types/ComponentFactory'
export { default as ComponentMeta } from './api/types/ComponentMeta'
export { default as Context } from './api/types/Context'
Expand Down

0 comments on commit 8446dbf

Please sign in to comment.