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

Initial React Native support #119

Merged
merged 22 commits into from
Mar 18, 2018
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
8 changes: 3 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ branches:
install:
- . $HOME/.nvm/nvm.sh
- nvm install stable
- nvm use stable
- npm install
- npm install jsdom
- cd native; npm install; cd ..
script:
- sbt tests/test
- sbt tests/test native/test
- sbt docs/compile example/compile
- sbt ";set scalaJSStage in Global := FullOptStage; tests/test"
- sbt ";set scalaJSStage in Global := FullOptStage; tests/test; native/test"
after_success:
- 'if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./publish.sh; fi'
- 'if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ -n "$TRAVIS_TAG" ]; then bash ./publish.sh; fi'
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Changelog

## vNEXT
### Highlights :tada:
+ **Slinky now has support for React Native**, available in the `slinky-native` module. Try it out with [create-react-native-scala-app](https://github.com/shadaj/create-react-native-scala-app.g8)
+ Want to write fancier unit tests for your Slinky app? Slinky now comes with an interface for `react-test-renderer`, available under the `slinky-testrenderer` module.

### Details
+ The `@react` macro now produces nicer APIs for external components that have default values for all props parameters.
+ Add more variations for `ExternalComponent` that support providing a statically-typed interface for the component instance: `ExternalComponentWithRefType`, `ExternalComponentWithAttributesWithRefType`, `ExternalComponentNoPropsWithRefType`, `ExternalComponentNoPropsWithAttributesWithRefType`
+ Fix exceptions when declaring custom tags and attributes in a component class [PR #118](https://github.com/shadaj/slinky/pull/118)

## v0.3.2
Expand Down
10 changes: 7 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
enablePlugins(ScalaJSPlugin)

organization in ThisBuild := "me.shadaj"

scalaVersion in ThisBuild := "2.12.4"
Expand All @@ -10,6 +8,8 @@ lazy val slinky = project.in(file(".")).aggregate(
readWrite,
core,
web,
testRenderer,
native,
hot,
scalajsReactInterop
).settings(publishArtifact := false)
Expand Down Expand Up @@ -65,6 +65,10 @@ lazy val web = project.settings(
}
).dependsOn(core)

lazy val testRenderer = project.settings(macroAnnotationSettings).dependsOn(core)

lazy val native = project.settings(macroAnnotationSettings).dependsOn(core, testRenderer % Test)

lazy val hot = project.settings(macroAnnotationSettings).dependsOn(core)

lazy val scalajsReactInterop = project.settings(macroAnnotationSettings).dependsOn(core)
Expand All @@ -75,4 +79,4 @@ lazy val example = project.settings(macroAnnotationSettings).dependsOn(web, hot,

lazy val docsMacros = project.settings(macroAnnotationSettings).dependsOn(web, hot, scalajsReactInterop)

lazy val docs = project.settings(macroAnnotationSettings).dependsOn(web, hot, scalajsReactInterop, docsMacros)
lazy val docs = project.settings(macroAnnotationSettings).dependsOn(web, hot, scalajsReactInterop, docsMacros)
42 changes: 27 additions & 15 deletions core/src/main/scala/slinky/core/ExternalComponent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import scala.scalajs.js.|
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

case class BuildingComponent[E](c: String | js.Object, props: js.Object, key: String = null, ref: js.Object => Unit = null, mods: Seq[AttrPair[E]] = Seq.empty) {
def apply(tagMod: AttrPair[E], tagMods: AttrPair[E]*): BuildingComponent[E] = copy(mods = mods ++ (tagMod +: tagMods))
case class BuildingComponent[E, R <: js.Object](c: String | js.Object, props: js.Object, key: String = null, ref: R => Unit = null, mods: Seq[AttrPair[E]] = Seq.empty) {
def apply(tagMod: AttrPair[E], tagMods: AttrPair[E]*): BuildingComponent[E, R] = copy(mods = mods ++ (tagMod +: tagMods))

def withKey(key: String): BuildingComponent[E] = copy(key = key)
def withRef(ref: js.Object => Unit): BuildingComponent[E] = copy(ref = ref)
def withKey(key: String): BuildingComponent[E, R] = copy(key = key)
def withRef(ref: R => Unit): BuildingComponent[E, R] = copy(ref = ref)

def apply(children: ReactElement*): ReactElement = {
val written = props.asInstanceOf[js.Dictionary[js.Any]]
Expand All @@ -24,7 +24,7 @@ case class BuildingComponent[E](c: String | js.Object, props: js.Object, key: St
}

if (ref != null) {
written("ref") = ref: js.Function1[js.Object, Unit]
written("ref") = ref: js.Function1[R, Unit]
}

mods.foreach { m =>
Expand All @@ -36,40 +36,52 @@ case class BuildingComponent[E](c: String | js.Object, props: js.Object, key: St
}

object BuildingComponent {
implicit def make[E]: BuildingComponent[E] => ReactElement = _.apply(Seq.empty: _*)
implicit def make[E, R <: js.Object]: BuildingComponent[E, R] => ReactElement = _.apply(Seq.empty: _*)
}

abstract class ExternalComponent(implicit pw: ExternalPropsWriterProvider) extends ExternalComponentWithAttributes[Nothing]()(pw)

abstract class ExternalComponentWithAttributes[E <: TagElement](implicit pw: ExternalPropsWriterProvider) {
abstract class ExternalComponentWithAttributesWithRefType[E <: TagElement, R <: js.Object](implicit pw: ExternalPropsWriterProvider) {
type Props
type Element = E
type RefType = R

private[this] final val writer = pw.asInstanceOf[Writer[Props]]

val component: String | js.Object

def apply(p: Props): BuildingComponent[E] = {
def apply(p: Props): BuildingComponent[E, R] = {
// no need to take key or ref here because those can be passed in through attributes
new BuildingComponent(component, writer.write(p), null, null, Seq.empty)
}
}

abstract class ExternalComponentNoProps extends ExternalComponentNoPropsWithAttributes[Nothing]
abstract class ExternalComponentWithAttributes[E <: TagElement](implicit pw: ExternalPropsWriterProvider)
extends ExternalComponentWithAttributesWithRefType[E, js.Object]()(pw)

abstract class ExternalComponentWithRefType[R <: js.Object](implicit pw: ExternalPropsWriterProvider) extends ExternalComponentWithAttributesWithRefType[Nothing, R]()(pw)

abstract class ExternalComponent(implicit pw: ExternalPropsWriterProvider) extends ExternalComponentWithAttributes[Nothing]()(pw)

abstract class ExternalComponentNoPropsWithAttributes[E <: TagElement] {
abstract class ExternalComponentNoPropsWithAttributesWithRefType[E <: TagElement, R <: js.Object] {
val component: String | js.Object

def apply(mod: AttrPair[E], tagMods: AttrPair[E]*): BuildingComponent[E] = BuildingComponent(component, js.Dynamic.literal(), mods = mod +: tagMods)
def apply(mod: AttrPair[E], tagMods: AttrPair[E]*): BuildingComponent[E, R] = BuildingComponent(component, js.Dynamic.literal(), mods = mod +: tagMods)

def withKey(key: String): BuildingComponent[E] = BuildingComponent(component, js.Dynamic.literal(), key = key)
def withRef(ref: js.Object => Unit): BuildingComponent[E] = BuildingComponent(component, js.Dynamic.literal(), ref = ref)
def withKey(key: String): BuildingComponent[E, R] = BuildingComponent(component, js.Dynamic.literal(), key = key)
def withRef(ref: R => Unit): BuildingComponent[E, R] = BuildingComponent(component, js.Dynamic.literal(), ref = ref)

def apply(children: ReactElement*): ReactElement = {
React.createElement(component, js.Dynamic.literal().asInstanceOf[js.Dictionary[js.Any]], children: _*)
}
}

abstract class ExternalComponentNoPropsWithAttributes[T <: TagElement]
extends ExternalComponentNoPropsWithAttributesWithRefType[T, js.Object]

abstract class ExternalComponentNoPropsWithRefType[R <: js.Object]
extends ExternalComponentNoPropsWithAttributesWithRefType[Nothing, R]

abstract class ExternalComponentNoProps extends ExternalComponentNoPropsWithAttributes[Nothing]

trait ExternalPropsWriterProvider extends js.Object
object ExternalPropsWriterProvider {
def impl(c: blackbox.Context): c.Expr[ExternalPropsWriterProvider] = {
Expand Down
40 changes: 31 additions & 9 deletions core/src/main/scala/slinky/core/annotations/react.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ class react extends scala.annotation.StaticAnnotation {
val applyTypes = tparams.map(t => Type.Name(t.name.value))
val applyValues = caseClassparamss.map(ps => ps.map(p => Term.Name(p.name.value)))
val caseClassApply = if (applyTypes.isEmpty) {
q"""def apply[..$tparams](...$caseClassparamss): slinky.core.KeyAndRefAddingStage[Def] =
q"""def apply[..$tparams](...$caseClassparamss): _root_.slinky.core.KeyAndRefAddingStage[Def] =
this.apply(${Term.Name("Props")}.apply(...$applyValues))"""
} else {
q"""def apply[..$tparams](...$caseClassparamss): slinky.core.KeyAndRefAddingStage[Def] =
q"""def apply[..$tparams](...$caseClassparamss): _root_.slinky.core.KeyAndRefAddingStage[Def] =
this.apply(${Term.Name("Props")}.apply[..$applyTypes](...$applyValues))"""
}

Expand Down Expand Up @@ -61,15 +61,15 @@ class react extends scala.annotation.StaticAnnotation {

val newClazz =
if (isErrorBoundary) {
q"""class ${clazz.name}(jsProps: scala.scalajs.js.Object) extends slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) with slinky.core.ErrorBoundary {
q"""class ${clazz.name}(jsProps: _root_.scala.scalajs.js.Object) extends _root_.slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) with slinky.core.ErrorBoundary {
$propsAndStateImport
null.asInstanceOf[${Type.Name("Props")}]
null.asInstanceOf[${Type.Name("State")}]
..${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 slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) {
q"""class ${clazz.name}(jsProps: _root_.scala.scalajs.js.Object) extends _root_.slinky.core.DefinitionBase[$propsSelect, $stateSelect](jsProps) {
$propsAndStateImport
null.asInstanceOf[${Type.Name("Props")}]
null.asInstanceOf[${Type.Name("State")}]
Expand All @@ -95,14 +95,28 @@ class react extends scala.annotation.StaticAnnotation {
val applyTypes = tparams.map(t => Type.Name(t.name.value))
val applyValues = caseClassparamss.map(ps => ps.map(p => Term.Name(p.name.value)))
val caseClassApply = if (applyTypes.isEmpty) {
q"""def apply[..$tparams](...$caseClassparamss): slinky.core.BuildingComponent[Element] =
q"""def apply[..$tparams](...$caseClassparamss): _root_.slinky.core.BuildingComponent[Element, RefType] =
this.apply(${Term.Name(tname.value)}.apply(...$applyValues))"""
} else {
q"""def apply[..$tparams](...$caseClassparamss): slinky.core.BuildingComponent[Element] =
q"""def apply[..$tparams](...$caseClassparamss): _root_.slinky.core.BuildingComponent[Element, RefType] =
this.apply(${Term.Name(tname.value)}.apply[..$applyTypes](...$applyValues))"""
}

Seq(caseClassApply)
if (caseClassparamss.flatten.forall(_.default.isDefined) || caseClassparamss.flatten.isEmpty) {
Seq(
caseClassApply,
q"""def apply(mod: _root_.slinky.core.AttrPair[Element], tagMods: _root_.slinky.core.AttrPair[Element]*): _root_.slinky.core.BuildingComponent[Element, RefType] = {
_root_.slinky.core.BuildingComponent[Element, RefType](component, _root_.scala.scalajs.js.Dynamic.literal(), mods = (mod +: tagMods).asInstanceOf[_root_.scala.collection.immutable.Seq[_root_.slinky.core.AttrPair[Element]]])
}""",
q"""def withKey(key: String): _root_.slinky.core.BuildingComponent[Element, RefType] = _root_.slinky.core.BuildingComponent(component, _root_.scala.scalajs.js.Dynamic.literal(), key = key)""",
q"""def withRef(ref: RefType => Unit): _root_.slinky.core.BuildingComponent[Element, RefType] = _root_.slinky.core.BuildingComponent(component, _root_.scala.scalajs.js.Dynamic.literal(), ref = ref)""",
q"""def apply(children: _root_.slinky.core.facade.ReactElement*): _root_.slinky.core.facade.ReactElement = {
_root_.slinky.core.facade.React.createElement(component, _root_.scala.scalajs.js.Dynamic.literal().asInstanceOf[_root_.scala.scalajs.js.Dictionary[js.Any]], children: _*)
}"""
)
} else {
Seq(caseClassApply)
}
case _ => Seq.empty
}
}
Expand Down Expand Up @@ -130,7 +144,7 @@ class react extends scala.annotation.StaticAnnotation {
// companion object does not exists
case cls @ Defn.Class(_, name, _, ctor, Template(_, Seq(Term.Apply(Ctor.Ref.Name(sc), _)), _, _)) if sc == "Component" || sc == "StatelessComponent" =>
val (newCls, companionStats) = createBody(cls, name, ctor.paramss, sc == "StatelessComponent")
val companion = q"object ${Term.Name(name.value)} extends slinky.core.ComponentWrapper { ..$companionStats }"
val companion = q"object ${Term.Name(name.value)} extends _root_.slinky.core.ComponentWrapper { ..$companionStats }"

if (isIntellij) {
Term.Block(Seq(q"val ${Pat.Var.Term(Term.Name("_" + cls.name.value))} = null", companion))
Expand All @@ -146,11 +160,19 @@ class react extends scala.annotation.StaticAnnotation {
val objStats = createExternalBody(obj) ++ obj.templ.stats.getOrElse(Nil)
obj.copy(templ = obj.templ.copy(stats = Some(objStats)))

case obj@Defn.Object(_, _, Template(_, Seq(Term.Apply(Term.ApplyType(Ctor.Ref.Name("ExternalComponentWithRefType"), _), _)), _, _)) =>
val objStats = createExternalBody(obj) ++ obj.templ.stats.getOrElse(Nil)
obj.copy(templ = obj.templ.copy(stats = Some(objStats)))

case obj@Defn.Object(_, _, Template(_, Seq(Term.Apply(Term.ApplyType(Ctor.Ref.Name("ExternalComponentWithAttributesWithRefType"), _), _)), _, _)) =>
val objStats = createExternalBody(obj) ++ obj.templ.stats.getOrElse(Nil)
obj.copy(templ = obj.templ.copy(stats = Some(objStats)))

case Defn.Object(_, _, Template(_, Seq(Term.Apply(Ctor.Ref.Name("ExternalComponentWithAttributes"), _)), _, _)) =>
abort("ExternalComponentWithAttributes must take a type argument of the target tag type but found none")

case _ =>
abort(s"@react must annotate a class that extends Component or an object that extends ExternalComponent(WithAttributes), got ${defn.structure}")
abort(s"@react must annotate a class that extends Component or an object that extends ExternalComponent(WithAttributes)(WithRefType), got ${defn.structure}")
}
}
}
19 changes: 19 additions & 0 deletions native/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
enablePlugins(ScalaJSPlugin)

import org.scalajs.core.tools.io.{MemVirtualJSFile, VirtualJSFile}
import org.scalajs.jsenv.nodejs.NodeJSEnv

name := "slinky-native"

libraryDependencies += "org.scalatest" %%% "scalatest" % "3.0.3" % Test

scalacOptions += "-P:scalajs:sjsDefinedByDefault"
scalacOptions += "-Ywarn-unused-import"

scalaJSModuleKind in Test := ModuleKind.CommonJSModule

jsEnv in Test := new NodeJSEnv() {
override def customInitFiles(): Seq[VirtualJSFile] = super.customInitFiles() :+ new MemVirtualJSFile("addReactNativeMock.js").withContent(
s"""require("${(baseDirectory.value / "node_modules/react-native-mock-render/mock.js").getAbsolutePath}");"""
)
}
11 changes: 11 additions & 0 deletions native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "slinky-native-tests",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "16.2.0",
"react-native": "0.52.0",
"react-test-renderer": "16.2.0",
"react-native-mock-render": "0.0.21"
}
}
21 changes: 21 additions & 0 deletions native/src/main/scala/slinky/native/ActivityIndicator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package slinky.native

import slinky.core.ExternalComponent
import slinky.core.annotations.react

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.|

@react object ActivityIndicator extends ExternalComponent {
case class Props(animating: js.UndefOr[Boolean] = js.undefined,
color: js.UndefOr[String] = js.undefined,
size: js.UndefOr[String | Int] = js.undefined,
hidesWhenStopped: js.UndefOr[Boolean] = js.undefined)

@js.native
@JSImport("react-native", "ActivityIndicator")
object Component extends js.Object

override val component = Component
}
19 changes: 19 additions & 0 deletions native/src/main/scala/slinky/native/Alert.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package slinky.native

import slinky.readwrite.ObjectOrWritten

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

case class AlertButton(text: String, onPress: () => Unit)
case class AlertOptions(cancelable: js.UndefOr[Boolean] = js.undefined)

@js.native
@JSImport("react-native", "Alert")
object Alert extends js.Object {
def alert(title: String,
message: js.UndefOr[String] = js.undefined,
buttons: js.UndefOr[ObjectOrWritten[Seq[AlertButton]]] = js.undefined,
options: js.UndefOr[ObjectOrWritten[AlertOptions]] = js.undefined,
`type`: js.UndefOr[String] = js.undefined): Unit = js.native
}
12 changes: 12 additions & 0 deletions native/src/main/scala/slinky/native/AppRegistry.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package slinky.native

import slinky.core.ReactComponentClass

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

@js.native
@JSImport("react-native", "AppRegistry")
object AppRegistry extends js.Object {
def registerComponent(appKey: String, componentProvider: js.Function0[ReactComponentClass]): Unit = js.native
}
23 changes: 23 additions & 0 deletions native/src/main/scala/slinky/native/Button.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package slinky.native

import slinky.core.ExternalComponent
import slinky.core.annotations.react

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

@react object Button extends ExternalComponent {
case class Props(onPress: () => Unit,
title: String,
accessibilityLabel: js.UndefOr[String] = js.undefined,
color: js.UndefOr[String] = js.undefined,
disabled: js.UndefOr[Boolean] = js.undefined,
testID: js.UndefOr[String] = js.undefined,
hasTVPreferredFocus: js.UndefOr[Boolean] = js.undefined)

@js.native
@JSImport("react-native", "Button")
object Component extends js.Object

override val component = Component
}
17 changes: 17 additions & 0 deletions native/src/main/scala/slinky/native/Clipboard.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package slinky.native

import scala.concurrent.Future
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

@js.native
@JSImport("react-native", "Clipboard")
object RawClipboard extends js.Object {
def getString(): js.Promise[String] = js.native
def setString(content: String): Unit = js.native
}

object Clipboard {
def getString: Future[String] = RawClipboard.getString().toFuture
def setString(content: String): Unit = RawClipboard.setString(content)
}
Loading