Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add componentDidCatch for error handling #60

Merged
merged 7 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading