Skip to content

Commit

Permalink
feat!: add componentDidCatch for error handling (#60)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Add enableKopytkoComponentDidCatch bs_const to the manifest file
  • Loading branch information
bchelkowski committed Feb 16, 2024
1 parent 1442398 commit ea32b0e
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .kopytkorc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"baseManifest": "./node_modules/@dazn/kopytko-unit-testing-framework/manifest.js",
"baseManifest": "./manifest.js",
"sourceDir": "./src",
"pluginDefinitions": {
"generate-tests": "./node_modules/@dazn/kopytko-unit-testing-framework/plugins/generate-tests"
Expand Down
19 changes: 19 additions & 0 deletions docs/renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ these methods are called lifecycle methods:
end sub
```

- `componentDidCatch(error as Object, info as Object)` - called when a component method has thrown an error

IMPORTANT: This catch will only work with **bs_const** `enableKopytkoComponentDidCatch: true` in the **manifest** file.

```brightscript
sub componentDidCatch(error as Object, info as Object)
' The Roku exception object
' https://developer.roku.com/docs/references/brightscript/language/error-handling.md#the-exception-object
?error
' The info object containing
' componentMethod - component method where the error has been thrown
' componentName - node name that extends KopytkoGroup or KopytkoLayoutGroup
' componentProps - current component properties
' componentState - current component state
' componentVirtualDOM - current component virtual DOM
?info
end sub
```

Creating a tree of elements results in calling `constructor` method starting from the parent to children
and then `componentDidMount` in the opposite order - from children to the parent.

Expand Down
18 changes: 14 additions & 4 deletions docs/versions-migration-guide.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# Update Kopytko Framework to v2
# Update Kopytko Framework

## Highlighted breaking changes in Kopytko Framework v2
There were no interface changes making Kopytko Framework v2 a breaking change, but, because Kopytko Packager so far doesn't handle components and functions namespacing, the introduced new [`HttpRequest`](../src/components/http/request/Http.request.xml) component may cause name collision. It can happen if there already exist a HttpRequest component in the application the framework is used and it is very probable as Kopytko team was recommending creating own HttpRequest extending the [`Request`](../src/components/http/request/Request.xml) component. We came across Kopytko users' needs and created a helpful `HttpRequest` component implementing all necessary mechanisms to make an HTTP(S) call - we recommend switching over to Kopytko's `HttpRequest` component as soon as possible.
## Update from v2 to v3

Version 3 introduced the `componentDidCatch` lifecycle method. It is not needed to implement componentDidCatch, but there could be a scenario where it is implemented and a developer wants to disable it (for example, for the development time). Because of that there is a new **bs_const** that needs to be defined in the **manifest** file - `enableKopytkoComponentDidCatch`.

`enableKopytkoComponentDidCatch: true` - **enables** the `componentDidCatch` method

`enableKopytkoComponentDidCatch: false` - **disables** the `componentDidCatch` method

## Update from v1 to v2

### Highlighted breaking changes in Kopytko Framework v2

There were no interface changes making Kopytko Framework v2 a breaking change, but, because Kopytko Packager so far doesn't handle components and functions namespacing, the introduced new [`HttpRequest`](../src/components/http/request/Http.request.xml) component may cause name collision. It can happen if there already exist a HttpRequest component in the application the framework is used and it is very probable as Kopytko team was recommending creating own HttpRequest extending the [`Request`](../src/components/http/request/Request.xml) component. We came across Kopytko users' needs and created a helpful `HttpRequest` component implementing all necessary mechanisms to make an HTTP(S) call - we recommend switching over to Kopytko's `HttpRequest` component as soon as possible.

## Deprecations highlights in Kopytko Framework v2
### Deprecations highlights in Kopytko Framework v2

These APIs remain available in v2, but will be removed in future versions.

Expand Down
9 changes: 9 additions & 0 deletions manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const baseManifest = require('@dazn/kopytko-unit-testing-framework/manifest');

module.exports = {
...baseManifest,
bs_const: {
...baseManifest.bs_const,
enableKopytkoComponentDidCatch: false,
},
}
60 changes: 50 additions & 10 deletions src/components/renderer/Kopytko.brs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
' @import /components/functionCall.brs from @dazn/kopytko-utils

sub init()
m.state = {}
m.elementToFocus = Invalid

m._enabledErrorCatching = _isComponentDidCatchEnabled()
m._isInitialized = false
m._previousProps = {}
m._previousState = {}
Expand All @@ -21,7 +24,8 @@ sub initKopytko(dynamicProps = {} as Object, componentsMapping = {} as Object)
m.top.observeFieldScoped("focusedChild", "focusDidChange")
m.top.update(dynamicProps)

constructor()
_methodCall(constructor, "constructor")

m._previousState = _cloneObject(m.state) ' required because of setting default state in constructor()

_mountComponent()
Expand All @@ -32,7 +36,7 @@ end sub
sub destroyKopytko(data = {} as Object)
if (NOT m._isInitialized) then return

componentWillUnmount()
_methodCall(componentWillUnmount, "componentWillUnmount")

if (m["$$eventBus"] <> Invalid)
m["$$eventBus"].clear()
Expand All @@ -41,7 +45,8 @@ sub destroyKopytko(data = {} as Object)
m.state = {}
m._previousState = {}
m.top.unobserveFieldScoped("focusedChild")
m._kopytkoUpdater.destroy()

_methodCall(m._kopytkoUpdater.destroy, "destroyKopytko", [], m._kopytkoUpdater)

_clearDOM()

Expand Down Expand Up @@ -73,15 +78,15 @@ sub focusDidChange(event as Object)
end sub

sub setState(partialState as Object, callback = Invalid as Dynamic)
m._kopytkoUpdater.enqueueStateUpdate(partialState, callback)
_methodCall(m._kopytkoUpdater.enqueueStateUpdate, "setState", [partialState, callback], m._kopytkoUpdater)
end sub

sub forceUpdate()
m._kopytkoUpdater.forceStateUpdate()
_methodCall(m._kopytkoUpdater.forceStateUpdate, "forceUpdate", [], m._kopytkoUpdater)
end sub

sub enqueueUpdate()
m._kopytkoUpdater.enqueueStateUpdate()
_methodCall(m._kopytkoUpdater.enqueueStateUpdate, "enqueueUpdate", [], m._kopytkoUpdater)
end sub

sub updateProps(props = {} as Object)
Expand All @@ -92,10 +97,10 @@ end sub

sub _mountComponent()
m._virtualDOM = render()
m._kopytkoDOM.renderElement(m._virtualDOM, m.top)

m._kopytkoUpdater.setComponentMounted(m.state)
componentDidMount()
_methodCall(m._kopytkoDOM.renderElement, "renderElement", [m._virtualDOM, m.top], m._kopytkoDOM)
_methodCall(m._kopytkoUpdater.setComponentMounted, "setComponentMounted", [m.state], m._kopytkoUpdater)
_methodCall(componentDidMount, "componentDidMount")
end sub

sub _onStateUpdated()
Expand All @@ -114,7 +119,8 @@ sub _updateDOM()
m.top.setFocus(true)
end if

componentDidUpdate(m._previousProps, m._previousState)
_methodCall(componentDidUpdate, "componentDidUpdate", [m._previousProps, m._previousState])

m._previousState = _cloneObject(m.state)
end sub

Expand All @@ -131,3 +137,37 @@ function _cloneObject(obj as Object) as Object

return newObj
end function

function _isComponentDidCatchEnabled() as Boolean
isComponentDidCatchEnabled = false

#if enableKopytkoComponentDidCatch
isComponentDidCatchEnabled = true
#end if

return isComponentDidCatchEnabled AND Type(componentDidCatch) <> "<uninitialized>"
end function

sub _methodCall(func as Function, methodName as String, args = [] as Object, context = Invalid as Object)
if (m._enabledErrorCatching)
try
functionCall(func, args, context)
catch error
_throw(error, methodName)
end try

return
end if

functionCall(func, args, context)
end sub

sub _throw(error as Object, failingComponentMethod as String)
componentDidCatch(error, {
componentMethod: failingComponentMethod,
componentName: m.top.subtype(),
componentProps: m.top.getFields(),
componentState: m.state,
componentVirtualDOM: m._virtualDOM,
})
end sub

0 comments on commit ea32b0e

Please sign in to comment.