diff --git a/.kopytkorc b/.kopytkorc index 555cb90..4c5ad68 100644 --- a/.kopytkorc +++ b/.kopytkorc @@ -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" diff --git a/docs/renderer.md b/docs/renderer.md index 8a642d1..d1dcdac 100644 --- a/docs/renderer.md +++ b/docs/renderer.md @@ -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. diff --git a/docs/versions-migration-guide.md b/docs/versions-migration-guide.md index 4d97968..bf3498b 100644 --- a/docs/versions-migration-guide.md +++ b/docs/versions-migration-guide.md @@ -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. diff --git a/manifest.js b/manifest.js new file mode 100644 index 0000000..5d3525c --- /dev/null +++ b/manifest.js @@ -0,0 +1,9 @@ +const baseManifest = require('@dazn/kopytko-unit-testing-framework/manifest'); + +module.exports = { + ...baseManifest, + bs_const: { + ...baseManifest.bs_const, + enableKopytkoComponentDidCatch: false, + }, +} diff --git a/src/components/renderer/Kopytko.brs b/src/components/renderer/Kopytko.brs index 2cd5e05..fba8a57 100644 --- a/src/components/renderer/Kopytko.brs +++ b/src/components/renderer/Kopytko.brs @@ -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 = {} @@ -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() @@ -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() @@ -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() @@ -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) @@ -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() @@ -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 @@ -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) <> "" +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