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

Add support for error boundaries from React 16 #82

Merged
merged 2 commits into from
Dec 29, 2017
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
+ **BREAKING**: Stateless components that use the `@react` macro annotation must extend the `StatelessComponent` class instead of just `Component` [PR #69](https://github.com/shadaj/slinky/pull/69)
+ **BREAKING**: Callbacks passed to `setState` are now Scala functions, so there is no need to force implicit conversions [PR #71](https://github.com/shadaj/slinky/pull/71)
+ **BREAKING**: The tag construction flow now requires attributes to come before children. In addition, an empty list of attributes is no longer allowed [PR #73](https://github.com/shadaj/slinky/pull/73)
+ Add support for error boundaries, which were added in React 16 [PR #82](https://github.com/shadaj/slinky/pull/82)
+ Add a `*` tag for external components that can take any attribute [PR #81](https://github.com/shadaj/slinky/pull/81)
+ Fix bugs involving using companion object values from a `@react` annotated component [PR #80](https://github.com/shadaj/slinky/pull/80)
+ Add no-callback forceUpdate and make it available in annotated components [PR #78](https://github.com/shadaj/slinky/pull/78)
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/scala/me/shadaj/slinky/core/Component.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package me.shadaj.slinky.core

import me.shadaj.slinky.core.facade.{React, ReactElement}
import me.shadaj.slinky.core.facade.{ErrorBoundaryInfo, React, ReactElement}

import scala.scalajs.js

abstract class Component extends React.Component(null) {
type Props
Expand Down Expand Up @@ -38,6 +40,8 @@ abstract class Component extends React.Component(null) {

def componentWillUnmount(): Unit = {}

def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {}

def render(): ReactElement
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package me.shadaj.slinky.core

import me.shadaj.slinky.core.facade.{PrivateComponentClass, React, ReactElement}
import me.shadaj.slinky.core.facade.{ErrorBoundaryInfo, PrivateComponentClass, React, ReactElement}
import me.shadaj.slinky.readwrite.{Reader, Writer}

import scala.scalajs.js
Expand Down Expand Up @@ -167,7 +167,7 @@ object DefinitionBase {
}
}

@inline private[slinky] final def writeWithWrappingAdjustment[T](writer: Writer[T])(value: T): js.Object = {
@inline private[slinky] final def writeWithWrappingAdjustment[T](writer: Writer[T])(value: T): js.Object = {
val __value = writer.write(value)

if (js.typeOf(__value) == "object") {
Expand All @@ -177,3 +177,7 @@ object DefinitionBase {
}
}
}

trait ErrorBoundary extends React.Component {
def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit
}
23 changes: 18 additions & 5 deletions core/src/main/scala/me/shadaj/slinky/core/annotations/react.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,25 @@ class react extends scala.annotation.StaticAnnotation {
abort("There is no State type defined. If you want to create a stateless component, extend the StatelessComponent class instead.")
}

val isErrorBoundary = clazz.templ.stats.getOrElse(Nil).exists {
case d: Defn.Def => d.name.value == "componentDidCatch"
case _ => false
}

val newClazz =
q"""class ${clazz.name}(jsProps: scala.scalajs.js.Object) extends me.shadaj.slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) {
$propsAndStateImport
..${if (stateDefinition.isEmpty) Seq(q"override def initialState: State = ()") else Seq.empty}
..${clazz.templ.stats.getOrElse(Nil).filterNot(s => s == propsDefinition || s == stateDefinition.orNull)}
}"""
if (isErrorBoundary) {
q"""class ${clazz.name}(jsProps: scala.scalajs.js.Object) extends me.shadaj.slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) with me.shadaj.slinky.core.ErrorBoundary {
$propsAndStateImport
..${if (stateDefinition.isEmpty) Seq(q"override def initialState: State = ()") else Seq.empty}
..${clazz.templ.stats.getOrElse(Nil).filterNot(s => s == propsDefinition || s == stateDefinition.orNull)}
}"""
} else {
q"""class ${clazz.name}(jsProps: scala.scalajs.js.Object) extends me.shadaj.slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) {
$propsAndStateImport
..${if (stateDefinition.isEmpty) Seq(q"override def initialState: State = ()") else Seq.empty}
..${clazz.templ.stats.getOrElse(Nil).filterNot(s => s == propsDefinition || s == stateDefinition.orNull)}
}"""
}

(newClazz,
propsDefinition +:
Expand Down
33 changes: 19 additions & 14 deletions core/src/main/scala/me/shadaj/slinky/core/facade/React.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import scala.language.implicitConversions
@js.native
trait ReactElement extends js.Object

object ReactElement {
@inline implicit def stringToElement(s: String): ReactElement = {
s.asInstanceOf[ReactElement]
}

@inline implicit def optionToElement(s: Option[ReactElement]): ReactElement = {
s.getOrElse(null.asInstanceOf[ReactElement])
}

@inline implicit def seqElementToElement[T](s: Iterable[T])(implicit cv: T => ReactElement): ReactElement = {
s.map(cv).toJSArray.asInstanceOf[ReactElement]
}
}

@js.native
trait ReactInstance extends js.Object

Expand All @@ -29,6 +43,11 @@ object React extends js.Object {
val Fragment: js.Object = js.native
}

@js.native
trait ErrorBoundaryInfo extends js.Object {
val componentStack: String = js.native
}

@js.native
trait PrivateComponentClass extends js.Object {
@JSName("props")
Expand All @@ -55,17 +74,3 @@ trait PrivateComponentClass extends js.Object {
@JSName("setState")
def setStateR(fn: js.Function2[js.Object, js.Object, js.Object], callback: js.Function0[Unit]): Unit = js.native
}

object ReactElement {
@inline implicit def stringToElement(s: String): ReactElement = {
s.asInstanceOf[ReactElement]
}

@inline implicit def optionToElement(s: Option[ReactElement]): ReactElement = {
s.getOrElse(null.asInstanceOf[ReactElement])
}

@inline implicit def seqElementToElement[T](s: Iterable[T])(implicit cv: T => ReactElement): ReactElement = {
s.map(cv).toJSArray.asInstanceOf[ReactElement]
}
}
56 changes: 55 additions & 1 deletion tests/src/test/scala/me/shadaj/slinky/core/ComponentTest.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package me.shadaj.slinky.core

import me.shadaj.slinky.core.facade.ReactElement
import me.shadaj.slinky.core.facade.{ErrorBoundaryInfo, ReactElement}
import me.shadaj.slinky.web.ReactDOM
import org.scalajs.dom
import org.scalatest.{Assertion, AsyncFunSuite}
Expand Down Expand Up @@ -81,6 +81,34 @@ object TestForceUpdateComponent extends ComponentWrapper {
}
}

object BadComponent extends StatelessComponentWrapper {
type Props = Unit

class Def(jsProps: js.Object) extends Definition(jsProps) {
override def render(): ReactElement = {
throw new Exception("BOO")
}
}
}

object ErrorBoundaryComponent extends StatelessComponentWrapper {
case class Props(bad: Boolean, handler: (js.Error, ErrorBoundaryInfo) => Unit)

class Def(jsProps: js.Object) extends Definition(jsProps) with ErrorBoundary {
override def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {
props.handler.apply(error, info)
}

override def render(): ReactElement = {
if (props.bad) {
BadComponent()
} else {
null
}
}
}
}

class ComponentTest extends AsyncFunSuite {
test("setState given function is applied") {
val promise: Promise[Assertion] = Promise()
Expand Down Expand Up @@ -132,4 +160,30 @@ class ComponentTest extends AsyncFunSuite {

promise.future
}

test("Error boundary component catches an exception in its children") {
val promise: Promise[Assertion] = Promise()

ReactDOM.render(
ErrorBoundaryComponent(ErrorBoundaryComponent.Props(true, (error, info) => {
promise.success(assert(true))
})),
dom.document.createElement("div")
)

promise.future
}

test("Error boundary component works fine with no errors") {
var sawError = false

ReactDOM.render(
ErrorBoundaryComponent(ErrorBoundaryComponent.Props(false, (error, info) => {
sawError = true
})),
dom.document.createElement("div")
)

assert(!sawError)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package me.shadaj.slinky.core.annotations

import me.shadaj.slinky.core.Component
import me.shadaj.slinky.core.facade.ReactElement
import me.shadaj.slinky.core.{Component, StatelessComponent}
import me.shadaj.slinky.core.facade.{ErrorBoundaryInfo, ReactElement}
import me.shadaj.slinky.web.ReactDOM
import org.scalajs.dom
import org.scalatest.{Assertion, AsyncFunSuite}
Expand Down Expand Up @@ -117,6 +117,30 @@ object TakeValuesFromCompanionObject {
val foo = "hello"
}

@react class BadComponent extends StatelessComponent {
type Props = Unit

override def render(): ReactElement = {
throw new Exception("BOO")
}
}

@react class ErrorBoundaryComponent extends StatelessComponent {
case class Props(bad: Boolean, handler: (js.Error, ErrorBoundaryInfo) => Unit)

override def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {
props.handler.apply(error, info)
}

override def render(): ReactElement = {
if (props.bad) {
BadComponent()
} else {
null
}
}
}

class ReactAnnotatedComponentTest extends AsyncFunSuite {
test("setState given function is applied") {
val promise: Promise[Assertion] = Promise()
Expand Down Expand Up @@ -173,4 +197,30 @@ class ReactAnnotatedComponentTest extends AsyncFunSuite {

promise.future
}

test("Error boundary component catches an exception in its children") {
val promise: Promise[Assertion] = Promise()

ReactDOM.render(
ErrorBoundaryComponent(true, (error, info) => {
promise.success(assert(true))
}),
dom.document.createElement("div")
)

promise.future
}

test("Error boundary component works fine with no errors") {
var sawError = false

ReactDOM.render(
ErrorBoundaryComponent(false, (error, info) => {
sawError = true
}),
dom.document.createElement("div")
)

assert(!sawError)
}
}