├── .github
├── FUNDING.yml
└── workflows
│ └── sbt.yml
├── .sbtopts
├── .scala-steward.conf
├── project
├── build.properties
└── plugins.sbt
├── logo.png
├── docs
├── .eslintrc.json
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── docs
│ │ ├── electron.md
│ │ ├── hello-world.md
│ │ ├── exporting-components.md
│ │ ├── refs.md
│ │ ├── context.md
│ │ ├── custom-tags-and-attributes.md
│ │ ├── native-and-vr.md
│ │ ├── scalajs-react-interop.md
│ │ ├── abstracting-over-tags.md
│ │ ├── fragments-and-portals.md
│ │ ├── error-boundaries.md
│ │ ├── why-slinky.md
│ │ ├── resources.md
│ │ └── the-tag-api.md
├── pages
│ ├── index.js
│ ├── _app.js
│ └── docs
│ │ └── [id].js
├── jsconfig.json
├── next.config.js
├── src
│ └── main
│ │ ├── scala
│ │ └── slinky
│ │ │ ├── analytics
│ │ │ └── ReactGA.scala
│ │ │ ├── next
│ │ │ ├── Router.scala
│ │ │ ├── Head.scala
│ │ │ ├── Link.scala
│ │ │ └── Image.scala
│ │ │ ├── docs
│ │ │ ├── homepage
│ │ │ │ ├── HelloMessage.scala
│ │ │ │ ├── Timer.scala
│ │ │ │ ├── Jumbotron.scala
│ │ │ │ ├── TodoApp.scala
│ │ │ │ ├── Examples.scala
│ │ │ │ └── Homepage.scala
│ │ │ ├── MainPageContent.scala
│ │ │ ├── SyntaxHighlighter.scala
│ │ │ ├── App.scala
│ │ │ ├── DocsGroup.scala
│ │ │ ├── CodeExample.scala
│ │ │ └── Navbar.scala
│ │ │ ├── reacthelmet
│ │ │ └── Helmet.scala
│ │ │ └── remarkreact
│ │ │ └── Remark.scala
│ │ └── resources
│ │ ├── globals.css
│ │ └── index.css
├── build.sbt
├── .gitignore
└── package.json
├── hot
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── hot
│ ├── ReactProxy.scala
│ └── package.scala
├── secrets.tar.enc
├── reactrouter
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── reactrouter
│ └── ReactRouterDOM.scala
├── testRenderer
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── testrenderer
│ └── TestRenderer.scala
├── package.json
├── readWrite
└── src
│ └── main
│ ├── scala-3
│ └── slinky
│ │ └── readwrite
│ │ ├── UnionReaders.scala
│ │ ├── ObjectOrWrittenVersionSpecific.scala
│ │ ├── UnionWriters.scala
│ │ ├── TypeConstructorWriters.scala
│ │ ├── TypeConstructorReaders.scala
│ │ ├── CoreReadersMacro.scala
│ │ └── CoreWritersMacro.scala
│ ├── scala-2.13+
│ └── slinky
│ │ └── readwrite
│ │ └── CompatUtil.scala
│ ├── scala
│ └── slinky
│ │ └── readwrite
│ │ ├── WithRaw.scala
│ │ ├── ObjectOrWritten.scala
│ │ └── CoreWriters.scala
│ ├── scala-2
│ └── slinky
│ │ └── readwrite
│ │ ├── UnionReaders.scala
│ │ ├── ObjectOrWrittenVersionSpecific.scala
│ │ ├── UnionWriters.scala
│ │ ├── TypeConstructorWriters.scala
│ │ ├── TypeConstructorReaders.scala
│ │ ├── CoreWritersMacro.scala
│ │ └── CoreReadersMacro.scala
│ └── scala-2.13-
│ └── slinky
│ └── readwrite
│ └── CompatUtil.scala
├── history
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── history
│ └── History.scala
├── tests
├── package.json
├── src
│ └── test
│ │ ├── scala-2
│ │ └── slinky
│ │ │ └── core
│ │ │ ├── annotations
│ │ │ ├── LocalImportsComponent.scala
│ │ │ └── ReactAnnotatedFunctionalComponentTest.scala
│ │ │ └── ExternalComponentTest2.scala
│ │ └── scala
│ │ └── slinky
│ │ ├── core
│ │ ├── StrictModeTest.scala
│ │ ├── SuspenseTest.scala
│ │ ├── ProfilerTest.scala
│ │ ├── SVGTest.scala
│ │ ├── ComponentReturnTypeTests.scala
│ │ ├── ContextTest.scala
│ │ ├── ReactChildrenTest.scala
│ │ ├── ReactRefTest.scala
│ │ ├── ExportedComponentTest.scala
│ │ └── FunctionalComponentTest.scala
│ │ └── web
│ │ └── ReactDOMTest.scala
└── build.sbt
├── scalajsReactInterop
├── package.json
├── src
│ ├── main
│ │ ├── scala-2
│ │ │ └── slinky
│ │ │ │ └── scalajsreact
│ │ │ │ └── ScalaJSReactCompat.scala
│ │ ├── scala-3
│ │ │ └── slinky
│ │ │ │ └── scalajsreact
│ │ │ │ └── ScalaJSReactCompat.scala
│ │ └── scala
│ │ │ └── slinky
│ │ │ └── scalajsreact
│ │ │ └── Converters.scala
│ └── test
│ │ └── scala
│ │ └── slinky
│ │ └── scalajsreact
│ │ └── InteropTest.scala
└── build.sbt
├── vr
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── vr
│ ├── AppRegistry.scala
│ ├── Image.scala
│ ├── Environment.scala
│ ├── Text.scala
│ ├── NativeModules.scala
│ ├── ReactVR.scala
│ ├── VrButton.scala
│ └── View.scala
├── .gitignore
├── core
├── src
│ └── main
│ │ ├── scala
│ │ └── slinky
│ │ │ └── core
│ │ │ ├── facade
│ │ │ ├── Fragment.scala
│ │ │ ├── StrictMode.scala
│ │ │ ├── Suspense.scala
│ │ │ ├── Profiler.scala
│ │ │ └── ReactContext.scala
│ │ │ ├── StatelessComponentWrapper.scala
│ │ │ ├── SyntheticEvent.scala
│ │ │ ├── Component.scala
│ │ │ ├── ReactComponentClass.scala
│ │ │ └── ReactElementContainer.scala
│ │ ├── scala-2
│ │ └── slinky
│ │ │ └── core
│ │ │ ├── ComponentWrapper.scala
│ │ │ ├── StateReaderProvider.scala
│ │ │ ├── StateWriterProvider.scala
│ │ │ ├── ExternalPropsWriterProvider.scala
│ │ │ └── FunctionalComponentName.scala
│ │ └── scala-3
│ │ └── slinky
│ │ └── core
│ │ ├── ComponentWrapper.scala
│ │ ├── StateReaderProvider.scala
│ │ ├── StateWriterProvider.scala
│ │ ├── ExternalPropsWriterProvider.scala
│ │ └── FunctionalComponentName.scala
└── build.sbt
├── native
├── package.json
├── src
│ ├── main
│ │ └── scala
│ │ │ └── slinky
│ │ │ └── native
│ │ │ ├── Platform.scala
│ │ │ ├── AppRegistry.scala
│ │ │ ├── SafeAreaView.scala
│ │ │ ├── Keyboard.scala
│ │ │ ├── UseWindowsDimensions.scala
│ │ │ ├── TouchableOpacity.scala
│ │ │ ├── TouchableHighlight.scala
│ │ │ ├── Clipboard.scala
│ │ │ ├── NativeSyntheticEvent.scala
│ │ │ ├── Alert.scala
│ │ │ ├── ActivityIndicator.scala
│ │ │ ├── Button.scala
│ │ │ ├── Switch.scala
│ │ │ ├── Picker.scala
│ │ │ ├── Slider.scala
│ │ │ ├── Text.scala
│ │ │ ├── View.scala
│ │ │ └── Image.scala
│ └── test
│ │ └── scala
│ │ └── slinky
│ │ └── native
│ │ └── NativeStaticAPITest.scala
└── build.sbt
├── web
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── web
│ ├── SyntheticInputEvent.scala
│ ├── SyntheticCompositionEvent.scala
│ ├── SyntheticFocusEvent.scala
│ ├── SyntheticUIEvent.scala
│ ├── SyntheticClipboardEvent.scala
│ ├── SyntheticWheelEvent.scala
│ ├── SyntheticAnimationEvent.scala
│ ├── SyntheticTransitionEvent.scala
│ ├── SyntheticPointerEvent.scala
│ ├── SyntheticTouchEvent.scala
│ ├── SyntheticKeyboardEvent.scala
│ ├── SyntheticMouseEvent.scala
│ └── ReactDOM.scala
├── generator
├── build.sbt
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── generator
│ ├── TagsProvider.scala
│ └── Model.scala
├── .scalafmt.conf
├── publish.sh
├── .scalafix.conf
├── coreIntellijSupport
├── build.sbt
└── src
│ └── main
│ └── resources
│ └── META-INF
│ └── plugin.xml
├── .scalafix-scala3.conf
├── publish.sbt
├── LICENSE
├── docsMacros
└── src
│ └── main
│ └── scala
│ └── slinky
│ └── docs
│ └── CodeExampleImpl.scala
├── README.md
└── CODE_OF_CONDUCT.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [shadaj]
2 |
--------------------------------------------------------------------------------
/.sbtopts:
--------------------------------------------------------------------------------
1 | -J-Xmx2048M
2 | -J-XX:+UseG1GC
3 |
--------------------------------------------------------------------------------
/.scala-steward.conf:
--------------------------------------------------------------------------------
1 | updatePullRequests = "always"
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.10.10
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadaj/slinky/HEAD/logo.png
--------------------------------------------------------------------------------
/docs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/hot/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-hot"
4 |
--------------------------------------------------------------------------------
/secrets.tar.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadaj/slinky/HEAD/secrets.tar.enc
--------------------------------------------------------------------------------
/reactrouter/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-react-router"
4 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadaj/slinky/HEAD/docs/public/favicon.ico
--------------------------------------------------------------------------------
/testRenderer/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-testrenderer"
4 |
--------------------------------------------------------------------------------
/docs/pages/index.js:
--------------------------------------------------------------------------------
1 | import { component } from 'scala/index'
2 |
3 | export default component();
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "jsdom": "^22.1.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/UnionReaders.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | trait UnionReaders {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/ObjectOrWrittenVersionSpecific.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | trait ObjectOrWrittenVersionSpecific {}
--------------------------------------------------------------------------------
/history/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-history"
4 |
5 | libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0"
6 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/UnionWriters.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.reflect.ClassTag
4 |
5 | trait UnionWriters {
6 |
7 | }
--------------------------------------------------------------------------------
/docs/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "resources/globals.css"
2 | import "resources/index.css"
3 |
4 | import { component } from 'scala/_app'
5 |
6 | export default component();
7 |
--------------------------------------------------------------------------------
/tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "react": "18.2.0",
5 | "react-dom": "18.2.0",
6 | "text-enc": "0.7.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/pages/docs/[id].js:
--------------------------------------------------------------------------------
1 | import { component } from 'scala/docs-id'
2 |
3 | export default component();
4 |
5 | export { getStaticProps, getStaticPaths } from 'scala/docs-id-server';
6 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2.13+/slinky/readwrite/CompatUtil.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | object CompatUtil {
4 | type Factory[-A, +C] = scala.collection.Factory[A, C]
5 | }
6 |
--------------------------------------------------------------------------------
/scalajsReactInterop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "react": "18.2.0",
5 | "react-dom": "18.2.0",
6 | "text-enc": "0.7.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/scalajsReactInterop/src/main/scala-2/slinky/scalajsreact/ScalaJSReactCompat.scala:
--------------------------------------------------------------------------------
1 | package slinky.scalajsreact
2 |
3 | object ScalaJSReactCompat {
4 | type Element = japgolly.scalajs.react.raw.React.Element
5 | }
6 |
--------------------------------------------------------------------------------
/scalajsReactInterop/src/main/scala-3/slinky/scalajsreact/ScalaJSReactCompat.scala:
--------------------------------------------------------------------------------
1 | package slinky.scalajsreact
2 |
3 | object ScalaJSReactCompat {
4 | type Element = japgolly.scalajs.react.facade.React.Element
5 | }
--------------------------------------------------------------------------------
/docs/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "resources/*": ["src/main/resources/*"],
6 | "scala/*": ["target/next-modules/*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/vr/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-vr"
4 |
5 | libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test
6 |
7 | scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | target/
3 | **/.DS_Store
4 | core/src/gen
5 | node_modules/
6 | package-lock.json
7 | docs/build
8 | idea/
9 | .bloop/
10 | .metals/
11 | .vscode/
12 | project/.bloop/
13 | publishing-setup/
14 |
15 | .bsp/
16 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala/slinky/readwrite/WithRaw.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.scalajs.js
4 |
5 | trait WithRaw {
6 | def raw: js.Object = this.asInstanceOf[js.Dynamic].__slinky_raw.asInstanceOf[js.Object]
7 | }
8 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/facade/Fragment.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.facade
2 |
3 | import slinky.core.ExternalComponentNoProps
4 |
5 | object Fragment extends ExternalComponentNoProps {
6 | override val component = ReactRaw.Fragment
7 | }
8 |
--------------------------------------------------------------------------------
/docs/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | loader: "custom"
6 | },
7 | experimental: { images: { layoutRaw: true } }
8 | }
9 |
10 | module.exports = nextConfig
11 |
--------------------------------------------------------------------------------
/core/src/main/scala-2/slinky/core/ComponentWrapper.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | abstract class ComponentWrapper(implicit sr: StateReaderProvider, sw: StateWriterProvider)
4 | extends BaseComponentWrapper(sr, sw) {
5 | override type Definition = DefinitionBase[Props, State, Snapshot]
6 | }
7 |
--------------------------------------------------------------------------------
/native/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slinky-native-tests",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "16.6.3",
7 | "react-native": "0.58.4",
8 | "react-test-renderer": "16.6.3",
9 | "react-native-mock-render": "0.1.2"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/core/src/main/scala-3/slinky/core/ComponentWrapper.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | abstract class ComponentWrapper(implicit sr: => StateReaderProvider, sw: => StateWriterProvider)
4 | extends BaseComponentWrapper(sr, sw) {
5 | override type Definition = DefinitionBase[Props, State, Snapshot]
6 | }
7 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Platform.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @JSImport("react-native", "Platform")
7 | @js.native
8 | object Platform extends js.Object {
9 | val OS: String = js.native
10 | }
11 |
--------------------------------------------------------------------------------
/web/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-web"
4 |
5 | libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0"
6 |
7 | tpolecatDevModeOptions ~= { opts =>
8 | opts.filterNot(
9 | Set(
10 | ScalacOptions.privateKindProjector
11 | )
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/generator/build.sbt:
--------------------------------------------------------------------------------
1 | libraryDependencies += ("net.ruippeixotog" %% "scala-scraper" % "2.2.0").cross(CrossVersion.for3Use2_13)
2 |
3 | libraryDependencies += "io.circe" %% "circe-core" % "0.14.1"
4 | libraryDependencies += "io.circe" %% "circe-generic" % "0.14.1"
5 | libraryDependencies += "io.circe" %% "circe-parser" % "0.14.1"
6 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/facade/StrictMode.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.facade
2 |
3 | import slinky.core.ExternalComponentNoProps
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.|
7 |
8 | object StrictMode extends ExternalComponentNoProps {
9 | override val component: |[String, js.Object] = ReactRaw.StrictMode
10 | }
11 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/analytics/ReactGA.scala:
--------------------------------------------------------------------------------
1 | package slinky.analytics
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @JSImport("react-ga", JSImport.Default)
7 | @js.native
8 | object ReactGA extends js.Object {
9 | def initialize(id: String): Unit = js.native
10 | def pageview(path: String): Unit = js.native
11 | }
12 |
--------------------------------------------------------------------------------
/docs/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Slinky",
3 | "name": "Slinky",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/generator/src/main/scala/slinky/generator/TagsProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.generator
2 |
3 | import io.circe.generic.auto._, io.circe.syntax._
4 |
5 | trait TagsProvider {
6 | def extract: (Seq[Tag], Seq[Attribute])
7 |
8 | def main(args: Array[String]): Unit = {
9 | val extracted = extract
10 | println(TagsModel(extracted._1, extracted._2).asJson.toString())
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/src/test/scala-2/slinky/core/annotations/LocalImportsComponent.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.annotations
2 |
3 | // compile-only test, this used to crash the macro annotation
4 | object LocalImportsComponent {
5 | import slinky.core.StatelessComponent
6 |
7 | @react class LocalComponent extends StatelessComponent {
8 | type Props = Unit
9 | def render() = null
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/UnionReaders.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.scalajs.js.|
4 |
5 | trait UnionReaders {
6 | implicit def unionReader[A, B](implicit aReader: Reader[A], bReader: Reader[B]): Reader[A | B] = s => {
7 | try {
8 | aReader.read(s)
9 | } catch {
10 | case _: Throwable => bReader.read(s)
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = 2.4.2
2 | project.git = true
3 | maxColumn = 120
4 | align = more
5 | assumeStandardLibraryStripMargin = true
6 | rewrite.rules = [AvoidInfix, SortImports, RedundantBraces, RedundantParens, SortModifiers]
7 | rewrite.redundantBraces.stringInterpolation = true
8 | spaces.afterTripleEquals = true
9 | continuationIndent.defnSite = 2
10 | includeCurlyBraceInSelectChains = false
11 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/next/Router.scala:
--------------------------------------------------------------------------------
1 | package slinky.next
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @js.native trait Router extends js.Object {
7 | val query: js.Dynamic = js.native
8 | }
9 |
10 | @JSImport("next/router", JSImport.Namespace)
11 | @js.native
12 | object Router extends js.Object {
13 | def useRouter(): Router = js.native
14 | }
15 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/next/Head.scala:
--------------------------------------------------------------------------------
1 | package slinky.next
2 |
3 | import slinky.core.ExternalComponentNoProps
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | object Head extends ExternalComponentNoProps {
9 | @JSImport("next/head", JSImport.Default)
10 | @js.native
11 | object Component extends js.Object
12 |
13 | override val component = Component
14 | }
15 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticInputEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 | import scala.scalajs.js
5 | import org.scalajs.dom.InputEvent
6 |
7 | //https://react.dev/reference/react-dom/components/common#inputevent-handler
8 | @js.native
9 | trait SyntheticInputEvent[+TargetType] extends SyntheticEvent[TargetType, InputEvent] {
10 | val data: String = js.native
11 | }
12 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/AppRegistry.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.core.ReactComponentClass
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | @js.native
9 | @JSImport("react-360", "AppRegistry")
10 | object AppRegistry extends js.Object {
11 | def registerComponent(appKey: String, componentProvider: js.Function0[ReactComponentClass[_]]): Unit = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticCompositionEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.CompositionEvent
7 |
8 | // https://reactjs.org/docs/events.html?#composition-events
9 | @js.native
10 | trait SyntheticCompositionEvent[+TargetType] extends SyntheticEvent[TargetType, CompositionEvent] {
11 | val data: String = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticFocusEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.{EventTarget, FocusEvent}
7 |
8 | // https://reactjs.org/docs/events.html?#focus-events
9 | @js.native
10 | trait SyntheticFocusEvent[+TargetType] extends SyntheticEvent[TargetType, FocusEvent] {
11 | val relatedTarget: EventTarget = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticUIEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.{UIEvent, Window}
7 |
8 | // https://reactjs.org/docs/events.html?#ui-events
9 | @js.native
10 | trait SyntheticUIEvent[+TargetType] extends SyntheticEvent[TargetType, UIEvent] {
11 | val detail: Double = js.native
12 | val view: Window = js.native
13 | }
14 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/AppRegistry.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ReactComponentClass
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | @js.native
9 | @JSImport("react-native", "AppRegistry")
10 | object AppRegistry extends js.Object {
11 | def registerComponent(appKey: String, componentProvider: js.Function0[ReactComponentClass[_]]): Unit = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/hot/src/main/scala/slinky/hot/ReactProxy.scala:
--------------------------------------------------------------------------------
1 | package slinky.hot
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @JSImport("react-proxy", JSImport.Namespace, "ReactProxy")
7 | @js.native
8 | object ReactProxy extends js.Object {
9 | def createProxy(componentConstructor: js.Object): js.Object = js.native
10 | def getForceUpdate(react: js.Object): js.Function1[js.Object, Unit] = js.native
11 | }
12 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticClipboardEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.{ClipboardEvent, DataTransfer}
7 |
8 | // https://reactjs.org/docs/events.html#clipboard-events
9 | @js.native
10 | trait SyntheticClipboardEvent[+TargetType] extends SyntheticEvent[TargetType, ClipboardEvent] {
11 | val clipboardData: DataTransfer = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/docs/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | import org.scalajs.linker.interface.ModuleSplitStyle
4 |
5 | name := "slinky-docs-next"
6 |
7 | scalaJSLinkerConfig ~= {
8 | _.withModuleKind(ModuleKind.ESModule)
9 | .withModuleSplitStyle(ModuleSplitStyle.SmallestModules)
10 | }
11 |
12 | Compile / fastLinkJS / scalaJSLinkerOutputDirectory := target.value / "next-modules"
13 | Compile / fullLinkJS / scalaJSLinkerOutputDirectory := target.value / "next-modules"
14 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/next/Link.scala:
--------------------------------------------------------------------------------
1 | package slinky.next
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | import slinky.core.annotations.react
7 | import slinky.core.ExternalComponent
8 |
9 | @react object Link extends ExternalComponent {
10 | case class Props(href: String)
11 |
12 | @JSImport("next/link", JSImport.Default)
13 | @js.native
14 | object Component extends js.Object
15 |
16 | val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e # exit with nonzero exit code if anything fails
4 |
5 | openssl aes-256-cbc -K $encrypted_key -iv $encrypted_iv -in secrets.tar.enc -out secrets.tar -d
6 |
7 | tar xvf secrets.tar
8 |
9 | echo $PGP_PASSPHRASE | gpg --passphrase-fd 0 --batch --yes --import publishing-setup/private.key
10 |
11 | export GPG_TTY=/dev/ttys001
12 |
13 | cp publishing-setup/credentials.sbt credentials.sbt
14 |
15 | sbt publishSignedAll sonatypeBundleRelease
16 |
17 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/Image.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object Image extends ExternalComponent {
10 | case class Props(source: js.Object)
11 |
12 | @js.native
13 | @JSImport("react-360", "Image")
14 | object Component extends js.Object
15 |
16 | override val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
--------------------------------------------------------------------------------
/native/src/test/scala/slinky/native/NativeStaticAPITest.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import org.scalatest.funsuite.AnyFunSuite
4 |
5 | import scala.scalajs.js
6 |
7 | class NativeStaticAPITest extends AnyFunSuite {
8 | test("Can fire an alert") {
9 | Alert.alert("alert!")
10 | }
11 |
12 | test("Can read clipboard value") {
13 | assert(!js.isUndefined(Clipboard.getString))
14 | }
15 |
16 | test("Can write clipboard value") {
17 | Clipboard.setString("bar")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | name := "slinky-core"
4 |
5 | libraryDependencies ++= {
6 | CrossVersion.partialVersion(scalaVersion.value) match {
7 | case Some((2, _)) =>
8 | Seq(
9 | "org.scala-lang" % "scala-reflect" % scalaVersion.value
10 | )
11 | case _ => Seq.empty
12 | }
13 | }
14 |
15 | // Needed by useCallback due to false positive warning on implicit evidence
16 | scalacOptions -= "-Ywarn-unused:implicits"
17 | scalacOptions -= "-Wunused:implicits"
18 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticWheelEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.WheelEvent
7 |
8 | // https://reactjs.org/docs/events.html?#wheel-events
9 | @js.native
10 | trait SyntheticWheelEvent[+TargetType] extends SyntheticEvent[TargetType, WheelEvent] {
11 | val deltaMode: Int = js.native
12 | val deltaX: Double = js.native
13 | val deltaY: Double = js.native
14 | val deltaZ: Double = js.native
15 | }
16 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticAnimationEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.AnimationEvent
7 |
8 | // https://reactjs.org/docs/events.html?#animation-events
9 | @js.native
10 | trait SyntheticAnimationEvent[+TargetType] extends SyntheticEvent[TargetType, AnimationEvent] {
11 | val animationName: String = js.native
12 | val pseudoElement: String = js.native
13 | val elapsedTime: Float = js.native
14 | }
15 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticTransitionEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.TransitionEvent
7 |
8 | // https://reactjs.org/docs/events.html?#transition-events
9 | @js.native
10 | trait SyntheticTransitionEvent[+TargetType] extends SyntheticEvent[TargetType, TransitionEvent] {
11 | val propertyName: String = js.native
12 | val pseudoElement: String = js.native
13 | val elapsedTime: Float = js.native
14 | }
15 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala/slinky/readwrite/ObjectOrWritten.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.scalajs.js
4 |
5 | @js.native
6 | trait ObjectOrWritten[T] extends js.Object
7 |
8 | object ObjectOrWritten extends ObjectOrWrittenVersionSpecific {
9 | implicit def fromObject[T, O <: js.Object](obj: O): ObjectOrWritten[T] = obj.asInstanceOf[ObjectOrWritten[T]]
10 | implicit def fromWritten[T](v: T)(implicit writer: Writer[T]): ObjectOrWritten[T] =
11 | writer.write(v).asInstanceOf[ObjectOrWritten[T]]
12 | }
13 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/SafeAreaView.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object SafeAreaView extends ExternalComponent {
10 | case class Props(style: js.UndefOr[js.Object] = js.undefined)
11 |
12 | @js.native
13 | @JSImport("react-native", "SafeAreaView")
14 | object Component extends js.Object
15 |
16 | override val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/ObjectOrWrittenVersionSpecific.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.scalajs.js
4 |
5 | trait ObjectOrWrittenVersionSpecific {
6 | implicit def toUndefOrObject[T, O <: js.Object](value: js.Object): js.UndefOr[ObjectOrWritten[T]] =
7 | value.asInstanceOf[js.UndefOr[ObjectOrWritten[T]]]
8 |
9 | implicit def toUndefOrWritten[T](value: T)(implicit writer: Writer[T]): js.UndefOr[ObjectOrWritten[T]] =
10 | writer.write(value).asInstanceOf[js.UndefOr[ObjectOrWritten[T]]]
11 | }
12 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Keyboard.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @js.native
7 | @JSImport("react-native", "Keyboard")
8 | object Keyboard extends js.Object {
9 | def addListener(eventName: String, callBack: js.Function0[Unit]): Unit = js.native
10 |
11 | def removeListener(eventName: String, callBack: js.Function0[Unit]): Unit = js.native
12 |
13 | def removeAllListeners(eventName: String): Unit = js.native
14 |
15 | def dismiss(): Unit = js.native
16 | }
17 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/UseWindowsDimensions.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation._
5 |
6 | @js.native
7 | trait ScaledSize extends js.Object {
8 | var fontScale: Double = js.native
9 | var height: Double = js.native
10 | var scale: Double = js.native
11 | var width: Double = js.native
12 | }
13 |
14 | @JSImport("react-native", "useWindowDimensions")
15 | @js.native
16 | object useWindowDimensions extends js.Object {
17 | def apply(): ScaledSize = js.native
18 | }
19 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/Environment.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @js.native
7 | @JSImport("react-360", "Environment")
8 | object Environment extends js.Object {
9 | def clearBackground(): Unit = js.native
10 | def setBackgroundImage(url: js.Object, options: js.UndefOr[js.Object] = js.undefined): Unit = js.native
11 | def setBackgroundVideo(handle: String): Unit = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/TouchableOpacity.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object TouchableOpacity extends ExternalComponent {
10 | case class Props(onPress: js.UndefOr[() => Unit], style: js.UndefOr[js.Object] = js.undefined)
11 |
12 | @js.native
13 | @JSImport("react-native", "TouchableOpacity")
14 | object Component extends js.Object
15 |
16 | override val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/TouchableHighlight.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object TouchableHighlight extends ExternalComponent {
10 | case class Props(onPress: js.UndefOr[() => Unit], style: js.UndefOr[js.Object] = js.undefined)
11 |
12 | @js.native
13 | @JSImport("react-native", "TouchableHighlight")
14 | object Component extends js.Object
15 |
16 | override val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Clipboard.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import scala.concurrent.Future
4 | import scala.scalajs.js
5 | import scala.scalajs.js.annotation.JSImport
6 |
7 | @js.native
8 | @JSImport("react-native", "Clipboard")
9 | object RawClipboard extends js.Object {
10 | def getString(): js.Promise[String] = js.native
11 | def setString(content: String): Unit = js.native
12 | }
13 |
14 | object Clipboard {
15 | def getString: Future[String] = RawClipboard.getString().toFuture
16 | def setString(content: String): Unit = RawClipboard.setString(content)
17 | }
18 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/StrictModeTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.StrictMode
4 | import slinky.web.ReactDOM
5 | import slinky.web.html.div
6 |
7 | import org.scalajs.dom
8 |
9 | import org.scalatest.funsuite.AnyFunSuite
10 |
11 | class StrictModeTest extends AnyFunSuite {
12 | test("Can render a StrictMode component with children") {
13 | val target = dom.document.createElement("div")
14 | ReactDOM.render(
15 | StrictMode(
16 | div()
17 | ),
18 | target
19 | )
20 |
21 | assert(target.innerHTML == "
")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2.13-/slinky/readwrite/CompatUtil.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | object CompatUtil {
4 | // originally in scala-collection-compat
5 | type Factory[-A, +C] = scala.collection.generic.CanBuildFrom[Nothing, A, C]
6 |
7 | implicit class FactoryOps[-A, +C](private val factory: Factory[A, C]) {
8 |
9 | /**
10 | * @return A collection of type `C` containing the same elements
11 | * as the source collection `it`.
12 | * @param it Source collection
13 | */
14 | def fromSpecific(it: TraversableOnce[A]): C = (factory() ++= it).result()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs-next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "export": "next build && next export",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "next": "12.1.6",
14 | "react": "18.2.0",
15 | "react-dom": "18.2.0",
16 | "react-syntax-highlighter": "^15.5.0",
17 | "remark": "^12.0.1",
18 | "remark-react": "^7.0.1"
19 | },
20 | "devDependencies": {
21 | "eslint": "8.18.0",
22 | "eslint-config-next": "12.1.6"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/next/Image.scala:
--------------------------------------------------------------------------------
1 | package slinky.next
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | import slinky.core.annotations.react
7 | import slinky.core.ExternalComponent
8 |
9 | @react object Image extends ExternalComponent {
10 | case class Props(src: js.Object, layout: js.UndefOr[String] = js.undefined, priority: js.UndefOr[Boolean] = js.undefined, loader: js.UndefOr[js.Dynamic => js.Any] = js.undefined)
11 |
12 | @JSImport("next/image", JSImport.Default)
13 | @js.native
14 | object Component extends js.Object
15 |
16 | val component = Component
17 | }
18 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/SuspenseTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import org.scalajs.dom
4 | import slinky.core.facade.Suspense
5 | import slinky.web.ReactDOM
6 | import slinky.web.html.div
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class SuspenseTest extends AnyFunSuite {
11 | test("Can render a Suspense component with children") {
12 | val target = dom.document.createElement("div")
13 | ReactDOM.render(
14 | Suspense(fallback = div())(
15 | div("hello!")
16 | ),
17 | target
18 | )
19 |
20 | assert(target.innerHTML == "hello!
")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ProfilerTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import org.scalajs.dom
4 | import slinky.core.facade.Profiler
5 | import slinky.web.ReactDOM
6 | import slinky.web.html.div
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class ProfilerTest extends AnyFunSuite {
11 | test("Can render a Profiler component with children") {
12 | val target = dom.document.createElement("div")
13 | ReactDOM.render(
14 | Profiler(id = "profiler", onRender = (_, _, _, _, _, _, _) => {})(
15 | div("hello!")
16 | ),
17 | target
18 | )
19 |
20 | assert(target.innerHTML == "hello!
")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.scalafix.conf:
--------------------------------------------------------------------------------
1 | rules = [
2 | RemoveUnused
3 | DisableSyntax
4 | LeakingImplicitClassVal
5 | NoValInForComprehension
6 | ProcedureSyntax
7 | ]
8 |
9 | DisableSyntax.noVars = false
10 | DisableSyntax.noThrows = false
11 | DisableSyntax.noNulls = false
12 | DisableSyntax.noReturns = true
13 | DisableSyntax.noAsInstanceOf = false
14 | DisableSyntax.noIsInstanceOf = true
15 | DisableSyntax.noXml = true
16 | DisableSyntax.noFinalVal = true
17 | DisableSyntax.noFinalize = true
18 | DisableSyntax.noValPatterns = true
19 | DisableSyntax.regex = [
20 | {
21 | id = noJodaTime
22 | pattern = "org\\.joda\\.time"
23 | message = "Use java.time instead"
24 | }
25 | ]
26 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/HelloMessage.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage //nodisplay
2 |
3 | import slinky.core.StatelessComponent //nodisplay
4 | import slinky.core.annotations.react //nodisplay
5 | import slinky.core.facade.ReactElement //nodisplay
6 | import slinky.web.html._ //nodisplay
7 |
8 | @react class HelloMessage extends StatelessComponent {
9 | case class Props(name: String)
10 |
11 | override def render(): ReactElement = {
12 | div("Hello ", props.name)
13 | }
14 | }
15 |
16 | //display:ReactDOM.render(
17 | //display: HelloMessage(name = "Taylor"),
18 | //display: mountNode
19 | //display:)
20 |
21 | //run:HelloMessage(name = "Taylor") //nodisplay
22 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/NativeSyntheticEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.readwrite.{Reader, Writer}
4 |
5 | import scala.scalajs.js
6 |
7 | case class NativeSyntheticEvent[T](nativeEvent: T)
8 |
9 | object NativeSyntheticEvent {
10 | implicit def reader[T](implicit tReader: Reader[T]): Reader[NativeSyntheticEvent[T]] = { o =>
11 | NativeSyntheticEvent(tReader.read(o.asInstanceOf[js.Dynamic].nativeEvent.asInstanceOf[js.Object]))
12 | }
13 |
14 | implicit def writer[T](implicit tWriter: Writer[T]): Writer[NativeSyntheticEvent[T]] = { s =>
15 | js.Dynamic.literal(
16 | nativeEvent = tWriter.write(s.nativeEvent)
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/StatelessComponentWrapper.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.readwrite.{Reader, Writer}
4 |
5 | import scala.scalajs.js
6 |
7 | abstract class StatelessDefinition[Props, Snapshot](jsProps: js.Object)
8 | extends DefinitionBase[Props, Unit, Snapshot](jsProps) {
9 | override def initialState: Unit = ()
10 | }
11 |
12 | abstract class StatelessComponentWrapper
13 | extends BaseComponentWrapper(
14 | Reader.unitReader.asInstanceOf[StateReaderProvider],
15 | Writer.unitWriter.asInstanceOf[StateWriterProvider]
16 | ) {
17 | override type State = Unit
18 |
19 | override type Definition = StatelessDefinition[Props, Snapshot]
20 | }
21 |
--------------------------------------------------------------------------------
/coreIntellijSupport/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(SbtIdeaPlugin)
2 |
3 | name := "slinky-core-ijext"
4 |
5 | intellijPlugins += "org.intellij.scala".toPlugin
6 |
7 | intellijPlugins += "com.intellij.java".toPlugin
8 |
9 | packageMethod := PackagingMethod.Standalone()
10 |
11 | patchPluginXml := pluginXmlOptions { xml =>
12 | xml.version = version.value
13 | xml.sinceBuild = (intellijBuild in ThisBuild).value
14 | }
15 |
16 | val publishAutoChannel = taskKey[Unit]("publishAutoChannel")
17 | publishAutoChannel := Def.taskDyn {
18 | val isDev = version.value.contains("+")
19 | if (isDev) {
20 | publishPlugin.toTask(" develop")
21 | } else {
22 | publishPlugin.toTask(" Stable")
23 | }
24 | }.value
25 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/MainPageContent.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.web.html.{className, div, style}
4 | import slinky.core.facade.ReactElement
5 | import slinky.core.annotations.react
6 | import slinky.core.FunctionalComponent
7 |
8 | import scala.scalajs.js.Dynamic.literal
9 |
10 | @react object MainPageContent {
11 | val component = FunctionalComponent[Seq[ReactElement]] { children =>
12 | div(className := "article", style := literal(
13 | maxWidth = "calc(min(1400px, 100vw - 80px))",
14 | marginLeft = "auto",
15 | marginRight = "auto",
16 | marginBottom = "15px",
17 | padding = "5px"
18 | )).apply(
19 | children: _*
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/native/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 |
3 | import org.scalajs.jsenv.nodejs.NodeJSEnv
4 |
5 | import scala.util.Properties
6 |
7 | name := "slinky-native"
8 |
9 | libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test
10 |
11 | scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
12 | Test / scalaJSLinkerConfig ~= { _.withESFeatures(_.withUseECMAScript2015(false)) }
13 |
14 | Test / jsEnv := new NodeJSEnv(
15 | NodeJSEnv
16 | .Config()
17 | .withArgs(List("-r", baseDirectory.value.getAbsolutePath + "/node_modules/react-native-mock-render/mock.js"))
18 | )
19 |
20 | def escapeBackslashes(path: String): String =
21 | if (Properties.isWin)
22 | path.replace("\\", "\\\\")
23 | else
24 | path
25 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Alert.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.readwrite.ObjectOrWritten
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | case class AlertButton(text: String, onPress: () => Unit)
9 | case class AlertOptions(cancelable: js.UndefOr[Boolean] = js.undefined)
10 |
11 | @js.native
12 | @JSImport("react-native", "Alert")
13 | object Alert extends js.Object {
14 | def alert(
15 | title: String,
16 | message: js.UndefOr[String] = js.undefined,
17 | buttons: js.UndefOr[ObjectOrWritten[Seq[AlertButton]]] = js.undefined,
18 | options: js.UndefOr[ObjectOrWritten[AlertOptions]] = js.undefined,
19 | `type`: js.UndefOr[String] = js.undefined
20 | ): Unit = js.native
21 | }
22 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/facade/Suspense.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.facade
2 |
3 | import slinky.readwrite.Writer
4 | import slinky.core.{BuildingComponent, ExternalComponent, ExternalPropsWriterProvider}
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.|
8 |
9 | object Suspense
10 | extends ExternalComponent()(new Writer[Suspense.Props] {
11 | override def write(value: Suspense.Props): js.Object =
12 | js.Dynamic.literal(fallback = value.fallback)
13 | }.asInstanceOf[ExternalPropsWriterProvider]) {
14 | case class Props(fallback: ReactElement)
15 | override val component: |[String, js.Object] = ReactRaw.Suspense
16 |
17 | def apply(fallback: ReactElement): BuildingComponent[Nothing, js.Object] = apply(Props(fallback))
18 | }
19 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/ActivityIndicator.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 | import scala.scalajs.js.|
9 |
10 | @react object ActivityIndicator extends ExternalComponent {
11 | case class Props(
12 | animating: js.UndefOr[Boolean] = js.undefined,
13 | color: js.UndefOr[String] = js.undefined,
14 | size: js.UndefOr[String | Int] = js.undefined,
15 | hidesWhenStopped: js.UndefOr[Boolean] = js.undefined
16 | )
17 |
18 | @js.native
19 | @JSImport("react-native", "ActivityIndicator")
20 | object Component extends js.Object
21 |
22 | override val component = Component
23 | }
24 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/UnionWriters.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.reflect.ClassTag
4 | import scala.scalajs.js.|
5 |
6 | trait UnionWriters {
7 | implicit def unionWriter[A: ClassTag, B: ClassTag](implicit aWriter: Writer[A], bWriter: Writer[B]): Writer[A | B] = {
8 | v =>
9 | if (implicitly[ClassTag[A]].runtimeClass == v.getClass) {
10 | aWriter.write(v.asInstanceOf[A])
11 | } else if (implicitly[ClassTag[B]].runtimeClass == v.getClass) {
12 | bWriter.write(v.asInstanceOf[B])
13 | } else {
14 | try {
15 | aWriter.write(v.asInstanceOf[A])
16 | } catch {
17 | case _: Throwable =>
18 | bWriter.write(v.asInstanceOf[B])
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/generator/src/main/scala/slinky/generator/Model.scala:
--------------------------------------------------------------------------------
1 | package slinky.generator
2 |
3 | object Utils {
4 | val keywords = Set("var", "for", "object", "val", "type")
5 |
6 | def identifierFor(name: String): String = {
7 | if (Utils.keywords.contains(name)) {
8 | "`" + name + "`"
9 | } else name
10 | }
11 | }
12 |
13 | case class TagsModel(tags: Seq[Tag], attributes: Seq[Attribute])
14 |
15 | case class Tag(tagName: String, scalaJSType: String, docLines: Seq[String])
16 |
17 | case class Attribute(attributeName: String,
18 | attributeType: String,
19 | docLines: Seq[String],
20 | compatibleTags: Option[Seq[String]],
21 | withDash: Boolean,
22 | hasCaptureVariant: Boolean) /* tag, identifier, doc */
23 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Button.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object Button extends ExternalComponent {
10 | case class Props(
11 | onPress: () => Unit,
12 | title: String,
13 | accessibilityLabel: js.UndefOr[String] = js.undefined,
14 | color: js.UndefOr[String] = js.undefined,
15 | disabled: js.UndefOr[Boolean] = js.undefined,
16 | testID: js.UndefOr[String] = js.undefined,
17 | hasTVPreferredFocus: js.UndefOr[Boolean] = js.undefined
18 | )
19 |
20 | @js.native
21 | @JSImport("react-native", "Button")
22 | object Component extends js.Object
23 |
24 | override val component = Component
25 | }
26 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/Text.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object Text extends ExternalComponent {
10 | case class Props(
11 | numberOfLines: js.UndefOr[Int] = js.undefined,
12 | onLayout: js.UndefOr[NativeSyntheticEvent[LayoutEvent] => Unit] = js.undefined,
13 | onLongPress: js.UndefOr[() => Unit] = js.undefined,
14 | onPress: js.UndefOr[() => Unit] = js.undefined,
15 | style: js.UndefOr[js.Object] = js.undefined,
16 | testID: js.UndefOr[String] = js.undefined
17 | )
18 |
19 | @js.native
20 | @JSImport("react-360", "Text")
21 | object Component extends js.Object
22 |
23 | override val component = Component
24 | }
25 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/reacthelmet/Helmet.scala:
--------------------------------------------------------------------------------
1 | package slinky.reacthelmet
2 |
3 | import slinky.core.ExternalComponentNoProps
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | @JSImport("react-helmet", JSImport.Namespace)
9 | @js.native
10 | object ReactHelmet extends js.Object {
11 | val Helmet: HelmetStatic = js.native
12 | }
13 |
14 | @js.native
15 | trait HelmetStatic extends js.Object {
16 | def renderStatic(): HelmetRendered = js.native
17 | }
18 |
19 | @js.native trait HelmetRendered extends js.Object {
20 | val title: js.Object = js.native
21 | val meta: js.Object = js.native
22 | val link: js.Object = js.native
23 | val style: js.Object = js.native
24 | }
25 |
26 | object Helmet extends ExternalComponentNoProps {
27 | override val component = ReactHelmet.Helmet
28 | }
29 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticPointerEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.PointerEvent
7 |
8 | // https://reactjs.org/docs/events.html#pointer-events
9 | @js.native
10 | trait SyntheticPointerEvent[+TargetType] extends SyntheticEvent[TargetType, PointerEvent] {
11 | val pointerId: Int = js.native
12 | val width: Double = js.native
13 | val height: Double = js.native
14 | val pressure: Double = js.native
15 | val tangentialPressure: Double = js.native
16 | val tiltX: Double = js.native
17 | val tiltY: Double = js.native
18 | val twist: Double = js.native
19 | val pointerType: String = js.native
20 | val isPrimary: Boolean = js.native
21 | }
22 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticTouchEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.{TouchEvent, TouchList}
7 |
8 | // https://reactjs.org/docs/events.html?#touch-events
9 | @js.native
10 | trait SyntheticTouchEvent[+TargetType] extends SyntheticEvent[TargetType, TouchEvent] {
11 | val altKey: Boolean = js.native
12 | val changedTouches: TouchList = js.native
13 | val ctrlKey: Boolean = js.native
14 | def getModifierState(key: String): Boolean = js.native
15 | val metaKey: Boolean = js.native
16 | val shiftKey: Boolean = js.native
17 | val targetTouches: TouchList = js.native
18 | val touches: TouchList = js.native
19 | }
20 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Switch.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object Switch extends ExternalComponent {
10 | case class Props(
11 | disabled: js.UndefOr[Boolean] = js.undefined,
12 | onTintColor: js.UndefOr[String] = js.undefined,
13 | onValueChange: js.UndefOr[Boolean => Unit] = js.undefined,
14 | testID: js.UndefOr[String] = js.undefined,
15 | thumbTintColor: js.UndefOr[String] = js.undefined,
16 | tintColor: js.UndefOr[String] = js.undefined,
17 | value: js.UndefOr[Boolean] = js.undefined
18 | )
19 |
20 | @js.native
21 | @JSImport("react-native", "Switch")
22 | object Component extends js.Object
23 |
24 | override val component = Component
25 | }
26 |
--------------------------------------------------------------------------------
/tests/src/test/scala-2/slinky/core/ExternalComponentTest2.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.web.html.div
4 | import slinky.core.annotations.react
5 | import scala.scalajs.js
6 |
7 | import org.scalatest.funsuite.AnyFunSuite
8 |
9 | @react object ExternalSimpleWithProps2 extends ExternalComponent {
10 | case class Props(a: Int)
11 | override val component = "div"
12 | }
13 |
14 | @react object ExternalDivWithAllDefaulted2 extends ExternalComponent {
15 | case class Props(id: String = "foo")
16 | override val component = "div"
17 | }
18 |
19 | class ExternalComponentTest2 extends AnyFunSuite {
20 | test("Can construct an external component with generated apply") {
21 | div(ExternalSimpleWithProps2(a = 1))
22 | }
23 |
24 | test("Can construct an external component with default parameters") {
25 | div(ExternalDivWithAllDefaulted2())
26 | }
27 | }
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/SyntaxHighlighter.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @js.native
10 | @JSImport("react-syntax-highlighter", "Light")
11 | object SyntaxHighlighterComp extends js.Object {
12 | def registerLanguage(arg1: String, arg2: js.Object): Unit = js.native
13 | }
14 |
15 | @js.native
16 | @JSImport("react-syntax-highlighter/dist/cjs/languages/hljs/scala", JSImport.Default)
17 | object ScalaHighlightLanguage extends js.Object
18 |
19 | @react object SyntaxHighlighter extends ExternalComponent {
20 | SyntaxHighlighterComp.registerLanguage("scala", ScalaHighlightLanguage)
21 | val component = SyntaxHighlighterComp
22 | case class Props(language: String, style: js.Dictionary[js.Object])
23 | }
24 |
--------------------------------------------------------------------------------
/docs/public/docs/electron.md:
--------------------------------------------------------------------------------
1 | # Electron
2 | Slinky web projects can also be easily published as cross-platform desktop apps via [Electron](https://www.electronjs.org/).
3 |
4 | ## Creating a New Electron Project
5 | The easiest way to create a new Electron project is to use the [`electron` branch of Create React Scala App](https://github.com/shadaj/create-react-scala-app.g8/tree/electron). This template adds to the starter project the needed NPM packages and configuration files for your bundle to be made into an Electron app.
6 |
7 | You can use this template from the command line, having SBT and NPM:
8 | ```shell
9 | sbt new shadaj/create-react-scala-app.g8 --branch electron
10 | ```
11 |
12 | Then build the app, install the dependencies and make it with Electron.
13 | ```shell
14 | sbt build
15 | ```
16 |
17 | ```shell
18 | npm install
19 | ```
20 |
21 | ```shell
22 | npm run make
23 | ```
24 |
--------------------------------------------------------------------------------
/.scalafix-scala3.conf:
--------------------------------------------------------------------------------
1 | # This has to be maintained separately as Scala 3 does not currently support the rules:
2 | # - RemoveUnused (https://github.com/scalacenter/scalafix/issues/1682).
3 | # - ProcedureSyntax.
4 | # See https://github.com/scalacenter/scalafix/issues/1747.
5 | rules = [
6 | DisableSyntax
7 | LeakingImplicitClassVal
8 | NoValInForComprehension
9 | ]
10 |
11 | DisableSyntax.noVars = false
12 | DisableSyntax.noThrows = false
13 | DisableSyntax.noNulls = false
14 | DisableSyntax.noReturns = true
15 | DisableSyntax.noAsInstanceOf = false
16 | DisableSyntax.noIsInstanceOf = true
17 | DisableSyntax.noXml = true
18 | DisableSyntax.noFinalVal = true
19 | DisableSyntax.noFinalize = true
20 | DisableSyntax.noValPatterns = true
21 | DisableSyntax.regex = [
22 | {
23 | id = noJodaTime
24 | pattern = "org\\.joda\\.time"
25 | message = "Use java.time instead"
26 | }
27 | ]
28 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/NativeModules.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSImport
5 |
6 | @js.native
7 | @JSImport("react-360", "NativeModules")
8 | object NativeModules extends js.Object {
9 | @js.native
10 | object VideoModule extends js.Object {
11 | def createPlayer(name: String): Unit = js.native
12 | def destroyPlayer(name: String): Unit = js.native
13 | def play(name: String, options: js.Object): Unit = js.native
14 | def pause(name: String): Unit = js.native
15 | def resume(name: String): Unit = js.native
16 | def stop(name: String): Unit = js.native
17 | def seek(name: String, timeMs: Int): Unit = js.native
18 | def setParams(name: String, options: js.Object): Unit = js.native
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/ReactVR.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.readwrite.{Reader, Writer}
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | case class NativeSyntheticEvent[T](nativeEvent: T)
9 |
10 | object NativeSyntheticEvent {
11 | implicit def reader[T](implicit tReader: Reader[T]): Reader[NativeSyntheticEvent[T]] = { o =>
12 | NativeSyntheticEvent(tReader.read(o.asInstanceOf[js.Dynamic].nativeEvent.asInstanceOf[js.Object]))
13 | }
14 |
15 | implicit def writer[T](implicit tWriter: Writer[T]): Writer[NativeSyntheticEvent[T]] = { s =>
16 | js.Dynamic.literal(
17 | nativeEvent = tWriter.write(s.nativeEvent)
18 | )
19 | }
20 | }
21 |
22 | @js.native
23 | trait Asset extends js.Object
24 |
25 | @js.native
26 | @JSImport("react-360", JSImport.Namespace)
27 | object React360 extends js.Object {
28 | def asset(path: String): Asset = js.native
29 | }
30 |
--------------------------------------------------------------------------------
/core/src/main/scala-3/slinky/core/StateReaderProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 | import scala.quoted._
5 | import slinky.readwrite.Reader
6 | import scala.scalajs.LinkingInfo
7 |
8 | trait StateReaderProvider extends js.Object
9 | object StateReaderProvider {
10 | def impl(using q: Quotes): Expr[StateReaderProvider] = {
11 | import q.reflect._
12 | val module = Symbol.spliceOwner.owner.owner
13 | val stateType = TypeIdent(module.memberType("State")).tpe
14 | val instance = Implicits.search(TypeRepr.of[Reader].appliedTo(List(stateType)))
15 | instance match {
16 | case fail: ImplicitSearchFailure => report.throwError(fail.explanation)
17 | case s: ImplicitSearchSuccess =>
18 | '{
19 | if (LinkingInfo.productionMode) null
20 | else ${s.tree.asExpr}.asInstanceOf[StateReaderProvider]
21 | }
22 | }
23 | }
24 |
25 | implicit inline def get: StateReaderProvider = ${impl}
26 | }
27 |
--------------------------------------------------------------------------------
/core/src/main/scala-3/slinky/core/StateWriterProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 | import scala.quoted._
5 | import slinky.readwrite.Writer
6 | import scala.scalajs.LinkingInfo
7 |
8 | trait StateWriterProvider extends js.Object
9 | object StateWriterProvider {
10 | def impl(using q: Quotes): Expr[StateWriterProvider] = {
11 | import q.reflect._
12 | val module = Symbol.spliceOwner.owner.owner
13 | val stateType = TypeIdent(module.memberType("State")).tpe
14 | val instance = Implicits.search(TypeRepr.of[Writer].appliedTo(List(stateType)))
15 | instance match {
16 | case fail: ImplicitSearchFailure => report.throwError(fail.explanation)
17 | case s: ImplicitSearchSuccess =>
18 | '{
19 | if (LinkingInfo.productionMode) null
20 | else ${s.tree.asExpr}.asInstanceOf[StateWriterProvider]
21 | }
22 | }
23 | }
24 |
25 | implicit inline def get: StateWriterProvider = ${impl}
26 | }
27 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/SVGTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.ReactElement
4 | import slinky.web.svg._
5 |
6 | import scala.scalajs.js
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class SVGTest extends AnyFunSuite {
11 | test("Can specify key attribute for SVG element") {
12 | val instance: ReactElement = circle(key := "1")
13 |
14 | assert(instance.asInstanceOf[js.Dynamic].key.asInstanceOf[String] == "1")
15 | }
16 |
17 | test("Can specify className attribute for SVG element") {
18 | val instance: ReactElement = svg(className := "foo")
19 |
20 | assert(instance.asInstanceOf[js.Dynamic].props.className.asInstanceOf[String] == "foo")
21 | }
22 |
23 | test("Can specify role attribute for SVG element") {
24 | val instance: ReactElement = svg(role := "button")
25 |
26 | assert(instance.asInstanceOf[js.Dynamic].props.role.asInstanceOf[String] == "button")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/coreIntellijSupport/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 | slinky.core.intellij
3 | Slinky Library Support
4 | PATCH
5 | Shadaj Laddad and Slinky Contributors
6 |
7 | Typechecking support for the @react macro annotation in the Slinky framework for writing React apps with Scala.
8 |
9 |
10 |
11 | org.intellij.scala
12 | com.intellij.modules.java
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/core/src/main/scala-2/slinky/core/StateReaderProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 |
5 | import scala.reflect.macros.whitebox
6 |
7 | trait StateReaderProvider extends js.Object
8 | object StateReaderProvider {
9 | def impl(c: whitebox.Context): c.Expr[StateReaderProvider] = {
10 | import c.universe._
11 | val compName = c.internal.enclosingOwner.owner.asClass
12 | val q"$_; val x: $typedReaderType = null" = c.typecheck(
13 | q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Reader[comp.State] = null"
14 | ) // scalafix:ok
15 | val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type], silent = false)
16 | c.Expr(
17 | q"if (_root_.scala.scalajs.LinkingInfo.productionMode) null else $tpcls.asInstanceOf[_root_.slinky.core.StateReaderProvider]"
18 | )
19 | }
20 |
21 | implicit def get: StateReaderProvider = macro impl
22 | }
23 |
--------------------------------------------------------------------------------
/core/src/main/scala-2/slinky/core/StateWriterProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 |
5 | import scala.reflect.macros.whitebox
6 |
7 | trait StateWriterProvider extends js.Object
8 | object StateWriterProvider {
9 | def impl(c: whitebox.Context): c.Expr[StateWriterProvider] = {
10 | import c.universe._
11 | val compName = c.internal.enclosingOwner.owner.asClass
12 | val q"$_; val x: $typedReaderType = null" = c.typecheck(
13 | q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Writer[comp.State] = null"
14 | ) // scalafix:ok
15 | val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type], silent = false)
16 | c.Expr(
17 | q"if (_root_.scala.scalajs.LinkingInfo.productionMode) null else $tpcls.asInstanceOf[_root_.slinky.core.StateWriterProvider]"
18 | )
19 | }
20 |
21 | implicit def get: StateWriterProvider = macro impl
22 | }
23 |
--------------------------------------------------------------------------------
/docs/public/docs/hello-world.md:
--------------------------------------------------------------------------------
1 | # Hello World!
2 | Let's get started by writing a super simple Slinky app!
3 |
4 | If you want to test this locally, take a look at the [Installation](/docs/installation/) documentation on how to set up your own Slinky project.
5 |
6 | The simplest Slinky app, which renders "Hello, world!" to the screen, looks like this:
7 | ```scala
8 | import slinky.core._
9 | import slinky.web.ReactDOM
10 | import slinky.web.html._
11 |
12 | ReactDOM.render(
13 | h1("Hello, world!"),
14 | document.getElementById("root")
15 | )
16 | ```
17 |
18 | ## Slinky Modules
19 | Slinky is split up into many modules, which make it flexible to support a large variety of projects and environments. Here, we are using two modules: `core`, which contains wrappers around React and provides the base classes for creating components, and `web`, which has wrappers around ReactDOM and provides the tags API (covered in detailed [here](/docs/the-tag-api/)) for constructing HTML trees.
20 |
--------------------------------------------------------------------------------
/scalajsReactInterop/src/test/scala/slinky/scalajsreact/InteropTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.scalajsreact
2 |
3 | import org.scalatest.funsuite.AnyFunSuite
4 | import slinky.web.ReactDOM
5 | import org.scalajs.dom.document
6 | import japgolly.scalajs.react.vdom.html_<^._
7 | import Converters._
8 | import slinky.web.html.div
9 |
10 | class InteropTest extends AnyFunSuite {
11 | test("Can convert Scala.js React node to Slinky") {
12 | val target = document.createElement("div")
13 | ReactDOM.render(
14 | <.a().toSlinky,
15 | target
16 | )
17 |
18 | assert(target.innerHTML == "")
19 | }
20 |
21 | test("Can convert Slinky element to Scala.js React node and render through Slinky") {
22 | val target = document.createElement("div")
23 | ReactDOM.render(
24 | <.a(
25 | div("hello!").toScalaJSReact
26 | ).toSlinky,
27 | target
28 | )
29 |
30 | assert(target.innerHTML == "hello!
")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/publish.sbt:
--------------------------------------------------------------------------------
1 | ThisBuild / publishMavenStyle := true
2 |
3 | ThisBuild / pomIncludeRepository := { _ => false }
4 |
5 | ThisBuild / Test / publishArtifact := false
6 |
7 | ThisBuild / publishTo := sonatypePublishToBundle.value
8 |
9 | ThisBuild / sonatypeCredentialHost := "oss.sonatype.org"
10 |
11 | ThisBuild / pomExtra :=
12 | https://github.com/shadaj/slinky
13 |
14 |
15 | MIT
16 | https://opensource.org/licenses/MIT
17 | repo
18 |
19 |
20 |
21 | https://github.com/shadaj/slinky.git
22 | https://github.com/shadaj/slinky.git
23 |
24 |
25 |
26 | shadaj
27 | Shadaj Laddad
28 | http://shadaj.me
29 |
30 |
31 |
32 | Global / useGpgPinentry := true
33 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/SyntheticEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSName
5 |
6 | @js.native
7 | trait SyntheticEvent[+TargetType, +EventType] extends js.Object {
8 | val bubbles: Boolean = js.native
9 | val cancelable: Boolean = js.native
10 | val currentTarget: TargetType = js.native
11 | val defaultPrevented: Boolean = js.native
12 | val eventPhase: Int = js.native
13 | val isTrusted: Boolean = js.native
14 | val nativeEvent: EventType = js.native
15 | def preventDefault(): Unit = js.native
16 | def isDefaultPrevented(): Boolean = js.native
17 | def stopPropagation(): Unit = js.native
18 | def isPropagationStopped(): Boolean = js.native
19 | val target: TargetType = js.native
20 | val timeStamp: Int = js.native
21 | @JSName("type") val `type`: String = js.native
22 | }
23 |
--------------------------------------------------------------------------------
/core/src/main/scala-3/slinky/core/ExternalPropsWriterProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 | import scala.quoted._
5 | import slinky.readwrite.Writer
6 |
7 | // same as PropsWriterProvider except it always returns the typeclass instead of nulling it out in fullOpt mode
8 | trait ExternalPropsWriterProvider extends js.Object
9 | object ExternalPropsWriterProvider {
10 | def impl(using q: Quotes): Expr[ExternalPropsWriterProvider] = {
11 | import q.reflect._
12 | val module = Symbol.spliceOwner.owner.owner
13 | val stateType = TypeIdent(module.memberType("Props")).tpe
14 | val instance = Implicits.search(TypeRepr.of[Writer].appliedTo(List(stateType)))
15 | instance match {
16 | case fail: ImplicitSearchFailure => report.throwError(fail.explanation)
17 | case s: ImplicitSearchSuccess =>
18 | '{
19 | ${s.tree.asExpr}.asInstanceOf[ExternalPropsWriterProvider]
20 | }
21 | }
22 | }
23 |
24 | implicit inline def get: ExternalPropsWriterProvider = ${impl}
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/scala-2/slinky/core/ExternalPropsWriterProvider.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.scalajs.js
4 |
5 | import scala.reflect.macros.whitebox
6 |
7 | // same as PropsWriterProvider except it always returns the typeclass instead of nulling it out in fullOpt mode
8 | trait ExternalPropsWriterProvider extends js.Object
9 | object ExternalPropsWriterProvider {
10 | def impl(c: whitebox.Context): c.Expr[ExternalPropsWriterProvider] = {
11 | import c.universe._
12 | val compName = c.internal.enclosingOwner.owner.asClass
13 | val q"$_; val x: $typedReaderType = null" = c.typecheck(
14 | q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Writer[comp.Props] = null"
15 | ) // scalafix:ok
16 | val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type])
17 | c.Expr(q"$tpcls.asInstanceOf[_root_.slinky.core.ExternalPropsWriterProvider]")
18 | }
19 |
20 | implicit def get: ExternalPropsWriterProvider = macro impl
21 | }
22 |
--------------------------------------------------------------------------------
/docs/src/main/resources/globals.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
5 | line-height: 1;
6 | font-weight: 400;
7 | -webkit-font-smoothing: antialiased;
8 | }
9 |
10 | h3 {
11 | font-size: 25px;
12 | font-weight: 700;
13 | margin-top: 0;
14 | }
15 |
16 | code {
17 | font-family: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace;
18 | padding: 0;
19 | }
20 |
21 | code.code-block {
22 | background-color: #282c34;
23 | }
24 |
25 | p {
26 | font-size: 17px;
27 | line-height: 28.9px
28 | }
29 |
30 | pre {
31 | margin: 0 0 17px;
32 | }
33 |
34 | h1 {
35 | font-size: 60px;
36 | }
37 |
38 | h2 {
39 | font-size: 35px;
40 | }
41 |
42 | a {
43 | text-decoration: none;
44 | }
45 |
46 | table {
47 | border-collapse: collapse;
48 | width: 100%;
49 | }
50 |
51 | td, th {
52 | border-top: 1px solid #ddd;
53 | padding: 8px;
54 | text-align: left;
55 | }
56 |
--------------------------------------------------------------------------------
/history/src/main/scala/slinky/history/History.scala:
--------------------------------------------------------------------------------
1 | package slinky.history
2 |
3 | import org.scalajs.dom.History
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | @js.native
9 | trait RichHistory extends History {
10 | def action: String = js.native
11 | def block(prompt: Boolean = false): Unit = js.native
12 | def createHref(location: String): Unit = js.native
13 | def goBack(): Unit = js.native
14 | def goForward(): Unit = js.native
15 | def listen(listener: js.Function0[Unit]): Unit = js.native
16 | def location: String = js.native
17 | def push(route: String): Unit = js.native
18 | def replace(path: String, state: js.Object): Unit = js.native
19 | }
20 |
21 | @JSImport("history", JSImport.Default)
22 | @js.native
23 | object History extends js.Object {
24 | def createBrowserHistory(): RichHistory = js.native
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/facade/Profiler.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.facade
2 |
3 | import scala.scalajs.js
4 | import slinky.core.ExternalComponent
5 | import slinky.core.BuildingComponent
6 | import slinky.readwrite.Writer
7 | import slinky.core.ExternalPropsWriterProvider
8 |
9 | object Profiler
10 | extends ExternalComponent()(new Writer[Profiler.Props] {
11 | override def write(value: Profiler.Props): js.Object =
12 | js.Dynamic.literal(
13 | id = value.id,
14 | onRender = value.onRender: js.Function7[String, String, Double, Double, Double, Double, js.Object, Unit]
15 | )
16 | }.asInstanceOf[ExternalPropsWriterProvider]) {
17 | case class Props(id: String, onRender: (String, String, Double, Double, Double, Double, js.Object) => Unit)
18 | override val component = ReactRaw.Profiler
19 |
20 | def apply(
21 | id: String,
22 | onRender: (String, String, Double, Double, Double, Double, js.Object) => Unit
23 | ): BuildingComponent[Nothing, js.Object] = apply(Props(id, onRender))
24 | }
25 |
--------------------------------------------------------------------------------
/scalajsReactInterop/src/main/scala/slinky/scalajsreact/Converters.scala:
--------------------------------------------------------------------------------
1 | package slinky.scalajsreact
2 |
3 | import japgolly.scalajs.react.component.Generic.UnmountedRaw
4 | import japgolly.scalajs.react.vdom.{TagOf, VdomNode}
5 | import japgolly.scalajs.react.vdom.html_<^._
6 |
7 | import ScalaJSReactCompat._
8 |
9 | import slinky.core.facade.ReactElement
10 |
11 | object Converters {
12 | implicit class UnmountedToInstance(unmounted: UnmountedRaw) {
13 | def toSlinky: ReactElement =
14 | unmounted.raw.asInstanceOf[ReactElement]
15 | }
16 |
17 | implicit class TagToInstance(tag: TagOf[_]) {
18 | def toSlinky: ReactElement =
19 | tag.rawNode.asInstanceOf[ReactElement]
20 | }
21 |
22 | implicit class VdomToInstance(vdom: VdomElement) {
23 | def toSlinky: ReactElement =
24 | vdom.rawNode.asInstanceOf[ReactElement]
25 | }
26 |
27 | implicit class ComponentInstanceToVdom[T](component: T)(implicit ev: T => ReactElement) {
28 | def toScalaJSReact: VdomNode =
29 | VdomNode(ev(component).asInstanceOf[Element])
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/public/docs/exporting-components.md:
--------------------------------------------------------------------------------
1 | # Exporting Components
2 | If introducing Slinky to an existing JavaScript codebase, you can export Slinky components so that they are accessible from React JSX code. This functionality is available for both component classes and functional components, building on top of Slinky's readers to convert JavaScript props into Scala objects.
3 |
4 | To export a component, use the implicit conversion to `ReactComponentClass`. Instances of `ReactComponentClass` can be passed to JavaScript code and can be used as regular references to React components.
5 |
6 | ```scala
7 | @react class MyComponent extends Component {
8 | case class Props(name: String)
9 |
10 | ...
11 | }
12 |
13 | object SlinkyComponents {
14 | @JSExportTopLevel("SlinkyComponents") val components: js.Dictionary[ReactComponentClass] = js.Dictionary(
15 | "MyComponent" -> MyComponent
16 | )
17 | }
18 | ```
19 |
20 | ```jsx
21 | import { SlinkyComponents } from "scalajs";
22 | const { MyComponent } = SlinkyComponents;
23 |
24 | ...
25 |
26 |
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/Timer.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage //nodisplay
2 |
3 | import slinky.core.annotations.react //nodisplay
4 | import slinky.core.Component //nodisplay
5 | import slinky.core.facade.ReactElement //nodisplay
6 | import slinky.web.html._ //nodisplay
7 | import org.scalajs.dom.window._ //nodisplay
8 |
9 | @react class Timer extends Component {
10 | type Props = Unit
11 | case class State(seconds: Int)
12 |
13 | override def initialState = State(seconds = 0)
14 |
15 | def tick(): Unit = {
16 | setState(prevState =>
17 | State(prevState.seconds + 1))
18 | }
19 |
20 | private var interval = -1
21 |
22 | override def componentDidMount(): Unit = {
23 | interval = setInterval(() => tick(), 1000)
24 | }
25 |
26 | override def componentWillUnmount(): Unit = {
27 | clearInterval(interval)
28 | }
29 |
30 | override def render(): ReactElement = {
31 | div(
32 | "Seconds: ", state.seconds.toString
33 | )
34 | }
35 | }
36 |
37 | //display:ReactDOM.render(Timer(), mountNode)
38 | //run:Timer() //nodisplay
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/remarkreact/Remark.scala:
--------------------------------------------------------------------------------
1 | package slinky.remarkreact
2 |
3 | import slinky.core.facade.ReactElement
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 |
8 | @js.native
9 | @JSImport("remark", JSImport.Default)
10 | object Remark extends js.Function0[js.Object] {
11 | override def apply(): RemarkInstance = js.native
12 | }
13 |
14 | @js.native
15 | trait RemarkRenderer[T] extends js.Object
16 |
17 | @js.native
18 | trait RemarkInstance extends js.Object {
19 | def use[T](renderer: RemarkRenderer[T]): RemarkInstanceWithRenderer[T] = js.native
20 | def use[T](renderer: RemarkRenderer[T], options: js.Object): RemarkInstanceWithRenderer[T] = js.native
21 | }
22 |
23 | @js.native
24 | trait RemarkInstanceWithRenderer[T] extends js.Object {
25 | def processSync(text: String): RemarkResult[T] = js.native
26 | }
27 |
28 | @js.native
29 | trait RemarkResult[T] extends js.Object {
30 | val result: T = js.native
31 | }
32 |
33 | @js.native
34 | @JSImport("remark-react", JSImport.Default)
35 | object ReactRenderer extends RemarkRenderer[ReactElement]
36 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticKeyboardEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.KeyboardEvent
7 |
8 | // https://reactjs.org/docs/events.html?#keyboard-events
9 | @js.native
10 | trait SyntheticKeyboardEvent[+TargetType] extends SyntheticEvent[TargetType, KeyboardEvent] {
11 | val altKey: Boolean = js.native
12 | val charCode: Int = js.native
13 | val ctrlKey: Boolean = js.native
14 | def getModifierState(key: String): Boolean = js.native
15 | val key: String = js.native
16 | val keyCode: Int = js.native
17 | val locale: String = js.native
18 | val location: Int = js.native
19 | val metaKey: Boolean = js.native
20 | val repeat: Boolean = js.native
21 | val shiftKey: Boolean = js.native
22 | val which: Int = js.native
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/scala-3/slinky/core/FunctionalComponentName.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.quoted._
4 |
5 | final class FunctionalComponentName(val name: String) extends AnyVal
6 | object FunctionalComponentName {
7 | inline implicit def get: FunctionalComponentName = ${FunctionalComponentNameMacros.impl}
8 | }
9 |
10 | object FunctionalComponentNameMacros {
11 | def impl(using q: Quotes): Expr[FunctionalComponentName] = {
12 | import q.reflect._
13 |
14 | // from lihaoyi/sourcecode
15 | def isSyntheticName(name: String) =
16 | name == "" || (name.startsWith("")) || name == "component" || name == "macro" || name == "$anonfun"
17 |
18 | @scala.annotation.tailrec
19 | def findNonSyntheticOwnerName(current: Symbol): String =
20 | if (isSyntheticName(current.name.trim)) {
21 | findNonSyntheticOwnerName(current.owner)
22 | } else {
23 | current.name.trim.stripSuffix("$")
24 | }
25 |
26 | val name = Expr[String](findNonSyntheticOwnerName(Symbol.spliceOwner))
27 | '{
28 | new FunctionalComponentName(${name})
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/public/docs/refs.md:
--------------------------------------------------------------------------------
1 | # Refs
2 | Slinky supports the [new refs API](https://reactjs.org/docs/refs-and-the-dom.html) introduced in React 16.3.
3 |
4 | ## Creating a Ref Object
5 | To start using the new ref API, first create a ref object, which you can use as a ref property instead of a callback. The `createRef` method in Slinky takes a type parameter so that the ref type is statically typed.
6 |
7 | ## Refs on HTML Elements
8 | To create a ref for use with an HTML tag, type the ref to store the appropriate elemtn type from the `scala-js-dom` library.
9 |
10 | ```scala
11 | val myRef = React.createRef[html.Div]
12 |
13 | div(ref := myRef)
14 |
15 | // somewhere else...
16 | myRef.current.innerHTML
17 | ```
18 |
19 | ## Refs on Slinky Components
20 | If you want to place a ref on a Slinky component, type the ref to store the `Def` type inside your component.
21 |
22 | ```scala
23 | @react class MyComponent {
24 | def foo(): Unit = ...
25 | ...
26 | }
27 |
28 | val myRef = React.createRef[MyComponent.Def]
29 |
30 | MyComponent(...).withRef(myRef)
31 |
32 | // somewhere else...
33 | myRef.current.foo()
34 | ```
35 |
--------------------------------------------------------------------------------
/core/src/main/scala-2/slinky/core/FunctionalComponentName.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import scala.reflect.macros.whitebox
4 |
5 | final class FunctionalComponentName(val name: String) extends AnyVal
6 | object FunctionalComponentName {
7 | implicit def get: FunctionalComponentName = macro FunctionalComponentNameMacros.impl
8 | }
9 |
10 | object FunctionalComponentNameMacros {
11 | def impl(c: whitebox.Context): c.Expr[FunctionalComponentName] = {
12 | import c.universe._
13 |
14 | // from lihaoyi/sourcecode
15 | def isSyntheticName(name: String) =
16 | name == "" || (name.startsWith("")) || name == "component"
17 |
18 | @scala.annotation.tailrec
19 | def findNonSyntheticOwner(current: Symbol): Symbol =
20 | if (isSyntheticName(current.name.decodedName.toString.trim)) {
21 | findNonSyntheticOwner(current.owner)
22 | } else {
23 | current
24 | }
25 |
26 | c.Expr(
27 | q"new _root_.slinky.core.FunctionalComponentName(${findNonSyntheticOwner(c.internal.enclosingOwner).name.decodedName.toString})"
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Shadaj Laddad
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/public/docs/context.md:
--------------------------------------------------------------------------------
1 | # Context
2 | Slinky supports the [new context API](https://reactjs.org/docs/context.html) introduced in React 16.3.
3 |
4 | ## Creating a Context Object
5 | To start using the new context API, first create a context object, which contains the components needed to provide and consume context. The `createContext` method in Slinky takes a type parameter so that the context value is statically typed.
6 |
7 | ```scala
8 | val myContext = React.createContext[String]("default-context-value")
9 | ```
10 |
11 | ## Providing Context
12 | Now that you have a context object, you can use the `Provider` component to provide context in a React tree.
13 |
14 | ```scala
15 | div(
16 | myContext.Provider(value = "hello!")(
17 | ...
18 | )
19 | )
20 | ```
21 |
22 | By using the `Provider` component, all elements beneath it will now have access to the `hello!` value provided.
23 |
24 | ## Consuming Context
25 | To consume context, use the `Consumer` component and pass in a function that takes the context value and returns a React element.
26 |
27 | ```scala
28 | myContext.Consumer { value =>
29 | h1(s"the context value is $value")
30 | }
31 | ```
32 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | val scalaJSVersion =
2 | Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.16.0")
3 |
4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion)
5 |
6 | {
7 | if (scalaJSVersion.startsWith("0.6.")) Nil
8 | else Seq(addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2"))
9 | }
10 |
11 | libraryDependencies ++= {
12 | if (scalaJSVersion.startsWith("0.6.")) Nil
13 | else Seq("org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0")
14 | }
15 |
16 | libraryDependencies ++= {
17 | if (scalaJSVersion.startsWith("0.6.")) Nil
18 | else Seq("org.scala-js" %% "scalajs-linker" % "1.18.2")
19 | }
20 |
21 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2")
22 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.0")
23 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
24 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.4")
25 | addSbtPlugin("org.jetbrains" % "sbt-idea-plugin" % "3.24.0")
26 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5")
27 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2")
28 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Picker.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 | import scala.scalajs.js.|
9 |
10 | @react object Picker extends ExternalComponent {
11 | case class Props(
12 | onValueChange: js.UndefOr[(String | Int, Int) => Unit] = js.undefined,
13 | selectedValue: js.UndefOr[String | Int] = js.undefined,
14 | style: js.UndefOr[js.Object] = js.undefined,
15 | testID: js.UndefOr[String] = js.undefined,
16 | enabled: js.UndefOr[Boolean] = js.undefined,
17 | mode: js.UndefOr[String] = js.undefined,
18 | prompt: js.UndefOr[String] = js.undefined,
19 | itemStyle: js.UndefOr[js.Object] = js.undefined
20 | )
21 |
22 | @js.native
23 | @JSImport("react-native", "Picker")
24 | object Component extends js.Object {
25 | val Item: js.Object = js.native
26 | }
27 |
28 | override val component = Component
29 |
30 | @react object Item extends ExternalComponent {
31 | case class Props(label: String, value: String | Int)
32 |
33 | override val component = Component.Item
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/SyntheticMouseEvent.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.SyntheticEvent
4 |
5 | import scala.scalajs.js
6 | import org.scalajs.dom.{EventTarget, MouseEvent}
7 |
8 | // https://reactjs.org/docs/events.html#mouse-events
9 | @js.native
10 | trait SyntheticMouseEvent[+TargetType] extends SyntheticEvent[TargetType, MouseEvent] {
11 | val altKey: Boolean = js.native
12 | val button: Int = js.native
13 | val buttons: Int = js.native
14 | val clientX: Double = js.native
15 | val clientY: Double = js.native
16 | val ctrlKey: Boolean = js.native
17 | def getModifierState(key: String): Boolean = js.native
18 | val metaKey: Boolean = js.native
19 | val pageX: Double = js.native
20 | val pageY: Double = js.native
21 | val relatedTarget: EventTarget = js.native
22 | val screenX: Double = js.native
23 | val screenY: Double = js.native
24 | val shiftKey: Boolean = js.native
25 | }
26 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/VrButton.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @react object VrButton extends ExternalComponent {
10 | case class Props(
11 | disabled: js.UndefOr[Boolean] = js.undefined,
12 | ignoreLongClick: js.UndefOr[Boolean] = js.undefined,
13 | longClickDelayMS: js.UndefOr[Int] = js.undefined,
14 | onButtonPress: js.UndefOr[() => Unit] = js.undefined,
15 | onButtonRelease: js.UndefOr[() => Unit] = js.undefined,
16 | onClick: js.UndefOr[() => Unit] = js.undefined,
17 | onClickSound: js.UndefOr[js.Object] = js.undefined,
18 | onEnter: js.UndefOr[() => Unit] = js.undefined,
19 | onEnterSound: js.UndefOr[js.Object] = js.undefined,
20 | onExit: js.UndefOr[() => Unit] = js.undefined,
21 | onExitSound: js.UndefOr[js.Object] = js.undefined,
22 | onLongClick: js.UndefOr[() => Unit] = js.undefined,
23 | onLongClickSound: js.UndefOr[js.Object] = js.undefined,
24 | style: js.UndefOr[js.Object] = js.undefined
25 | )
26 |
27 | @js.native
28 | @JSImport("react-360", "VrButton")
29 | object Component extends js.Object
30 |
31 | override val component = Component
32 | }
33 |
--------------------------------------------------------------------------------
/docs/src/main/resources/index.css:
--------------------------------------------------------------------------------
1 | .docs-content li {
2 | font-size: 17px;
3 | line-height: 25px;
4 | }
5 |
6 | .article code {
7 | background-color: rgba(255, 229, 100, 0.3);
8 | }
9 |
10 | .article a {
11 | background-color: rgba(187, 239, 253, 0.5);
12 | border-bottom: 1px solid rgba(0,0,0,0.2);
13 | color: #1a1a1a;
14 | }
15 |
16 | .article a:hover {
17 | background-color: #bbeffd;
18 | border-bottom-color: #1a1a1a;
19 | }
20 |
21 | @media screen and (min-width: 1400px) {
22 | .article.fill-right {
23 | margin-left: calc(50vw - 700px);
24 | }
25 | }
26 |
27 | @media (max-width: 779px) {
28 | .sidebar-right {
29 | display: none !important;
30 | }
31 |
32 | .docs-page {
33 | display: block !important;
34 | }
35 |
36 | .docs-content {
37 | width: calc(100% - 15px) !important;
38 | margin-top: 90px;
39 | }
40 |
41 | .docs-sidebar {
42 | width: calc(100% - 15px) !important;
43 | margin-left: 0 !important;
44 | }
45 |
46 | .docs-sidebar-content {
47 | position: static !important;
48 | height: auto !important;
49 | padding-top: 0 !important;
50 | border: 1px solid #ececec !important;
51 | margin-left: auto !important;
52 | margin-right: auto !important;
53 | margin-bottom: 10px !important;
54 | padding-right: 0 !important;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docsMacros/src/main/scala/slinky/docs/CodeExampleImpl.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import java.io.File
4 |
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.io.Source
8 | import scala.reflect.macros.blackbox
9 |
10 | object CodeExampleImpl {
11 | def text(c: blackbox.Context)(exampleLocation: c.Expr[String]): c.Expr[ReactElement] = {
12 | import c.universe._
13 | val Literal(Constant(loc: String)) = exampleLocation.tree
14 | val inputFile = new File(s"docs/src/main/scala/${loc.split('.').mkString("/")}.scala")
15 | val enclosingPackage = loc.split('.').init.mkString(".")
16 |
17 | val fileContent = Source.fromFile(inputFile).mkString
18 |
19 | val innerCode = fileContent.split('\n')
20 |
21 | val textToDisplay = innerCode
22 | .map(_.replaceAllLiterally("//display:", ""))
23 | .filterNot(_.endsWith("//nodisplay"))
24 | .dropWhile(_.trim.isEmpty)
25 | .reverse.dropWhile(_.trim.isEmpty).reverse
26 | .mkString("\n")
27 |
28 | val codeToRun = innerCode.filter(_.startsWith("//run:")).map(_.replaceAllLiterally("//run:", "")).mkString("\n")
29 |
30 | c.Expr[ReactElement](
31 | q"""{
32 | import ${c.parse(enclosingPackage)}._
33 |
34 | _root_.slinky.docs.CodeExampleInternal(codeText = ${Literal(Constant(textToDisplay))}, demoElement = {${c.parse(codeToRun)}})
35 | }""")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/public/docs/custom-tags-and-attributes.md:
--------------------------------------------------------------------------------
1 | # Custom Tags and Attributes
2 | While Slinky's web module comes with a standard set of HTML and SVG tags and attributes, you may need to create custom tags and attributes for non-standard elements or web components.
3 |
4 | ## Custom Tags
5 | To create a custom tag, you can use the `CustomTag` class. Simply construct this class, and use the variable it's stored in as a regular Slinky tag. Custom tags are untyped in relation to attribute support, so you can use existing Slinky attributes with them.
6 |
7 | ```scala
8 | val myCustomTag = CustomTag("my-custom-element")
9 |
10 | div(
11 | myCustomTag(href := "foo")("hello!")
12 | )
13 | ```
14 |
15 | results in
16 |
17 | ```html
18 |
19 | hello!
20 |
21 | ```
22 |
23 | ## Custom Attributes
24 | To create a custom attribute, you can use the `CustomAttribute` class. Just like `CustomTag`, you can use the construct the class and use the variable it's stored in as a regular attribute. `CustomAttribute` takes a type parameter for statically typing the value of the attribute, but is untyped in relation to tag support so can be used with existing Slinky tags and custom tags.
25 |
26 | ```scala
27 | val myCustomAttr = CustomAttribute[String]("custom-href")
28 | div(myCustomAttr := "foo")
29 | ```
30 |
31 | results in
32 |
33 | ```html
34 |
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/public/docs/native-and-vr.md:
--------------------------------------------------------------------------------
1 | # React Native and VR
2 | Beginning with version 0.4.0, Slinky has official support for [React Native](http://facebook.github.io/react-native/) and [VR](http://facebook.github.io/react-360/) through modules providing external component definitions for each.
3 |
4 | ## React Native
5 | The `slinky-native` module contains component interfaces for React Native as well as Scala.js bindings to React Native APIs.
6 |
7 | The easiest way to create a new native project is to use [Expo Scala Template](https://github.com/shadaj/expo-template-scala). This template creates a starter project with a minimal React Native build configuration that enables hot reloading and bundling into a production app.
8 |
9 | You can use this template from the command line with NPM:
10 | ```bash
11 | $ npm install -g expo-cli
12 | $ expo init --template expo-template-scala
13 | ```
14 |
15 | ## React 360
16 | Similarly, the `slinky-vr` module contains interfaces for React 360 components and bindings for React 360 APIs.
17 |
18 | The easiest way to create a new VR project is to use [Create React VR Scala App](https://github.com/shadaj/create-react-vr-scala-app.g8). This template creates a starter project with a default React 360 build configuration that enables hot reloading and bundling into a production app.
19 |
20 | You can use this template from the command line with SBT:
21 | ```scala
22 | sbt new shadaj/create-react-vr-scala-app.g8
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/public/docs/scalajs-react-interop.md:
--------------------------------------------------------------------------------
1 | # Interop with scalajs-react
2 | If you're using Slinky in an application that's already using [scalajs-react](https://github.com/japgolly/scalajs-react), Slinky comes with the `slinky-scalajsreact-interop` module for crossing over between the two styles of writing React code.
3 |
4 | ```scala
5 | libraryDependencies += "me.shadaj" %%% "slinky-scalajsreact-interop" % "0.7.5"
6 | ```
7 |
8 | To use this module, simply import the implicit conversions between Slinky and scalajs-react types.
9 |
10 | ```scala
11 | import slinky.scalajsreact.Converters._
12 | ```
13 |
14 | Then use the converters `.toSlinky` and `.toScalaJSReact` to convert React elements from each library to the other.
15 |
16 | This makes it possible to use Slinky components from scalajs-react and vice-versa. For example, the following code can now work:
17 |
18 | ```scala
19 | val ScalajsReactComponent =
20 | ScalaComponent.builder[String]("Hello")
21 | .render_P(name => <.div( // a scalajs-react tag here
22 | "Hello, ", name,
23 | "This is a component from scalajs-react",
24 | div( // a Slinky tag being used here
25 | "and this is from Slinky inside the scalajs-react component!"
26 | ).toScalaJSReact
27 | ))
28 | .build
29 |
30 | @react class SlinkyComponent extends StatelessComponent {
31 | case class Props(name: String)
32 |
33 | def render(): ReactElement = {
34 | ScalajsReactComponent(props.name).toSlinky
35 | }
36 | }
37 |
38 | SlinkyComponent("Interop works!")
39 | ```
40 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ComponentReturnTypeTests.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.{Fragment, ReactElement}
4 | import slinky.web.ReactDOM
5 | import slinky.web.html._
6 | import org.scalajs.dom
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class ComponentReturnTypeTests extends AnyFunSuite {
11 | def testElement(elem: ReactElement): Unit = {
12 | assert((div(elem): ReactElement) != null) // test use in another element
13 | ReactDOM.render(div(elem), dom.document.createElement("div")) // test rendering to DOM
14 | ()
15 | }
16 |
17 | test("Components can return - arrays") {
18 | testElement(Seq(h1("a"), h1("b")))
19 | }
20 |
21 | test("Components can return - strings") {
22 | testElement("hello")
23 | }
24 |
25 | test("Components can return - numbers") {
26 | testElement(1)
27 | testElement(1D)
28 | testElement(1F)
29 | }
30 |
31 | test("Components can return - portals") {
32 | testElement(ReactDOM.createPortal(null, dom.document.createElement("div")))
33 | }
34 |
35 | test("Components can return - null") {
36 | testElement(null)
37 | }
38 |
39 | test("Components can return - booleans") {
40 | testElement(true)
41 | testElement(false)
42 | }
43 |
44 | test("Components can return - options") {
45 | testElement(Some(h1("hi")))
46 | testElement(Some(NoPropsComponent()))
47 | testElement(None)
48 | }
49 |
50 | test("Components can return - fragments") {
51 | testElement(Fragment(h1("hi")))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Slider.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 | import scala.scalajs.js.|
9 |
10 | @react object Slider extends ExternalComponent {
11 | case class Props(
12 | style: js.UndefOr[js.Object] = js.undefined,
13 | disabled: js.UndefOr[Boolean] = js.undefined,
14 | maximumValue: js.UndefOr[Double] = js.undefined,
15 | minimumTrackTintColor: js.UndefOr[String] = js.undefined,
16 | minimumValue: js.UndefOr[Double] = js.undefined,
17 | onSlidingComplete: js.UndefOr[Double => Unit] = js.undefined,
18 | onValueChange: js.UndefOr[Double => Unit] = js.undefined,
19 | step: js.UndefOr[Double] = js.undefined,
20 | maximumTrackTintColor: js.UndefOr[String] = js.undefined,
21 | testID: js.UndefOr[String] = js.undefined,
22 | value: js.UndefOr[Double] = js.undefined,
23 | thumbTintColor: js.UndefOr[String] = js.undefined,
24 | maximumTrackImage: js.UndefOr[ImageURISource | Int | Seq[ImageURISource]] = js.undefined,
25 | minimumTrackImage: js.UndefOr[ImageURISource | Int | Seq[ImageURISource]] = js.undefined,
26 | thumbImage: js.UndefOr[ImageURISource | Int | Seq[ImageURISource]] = js.undefined,
27 | trackImage: js.UndefOr[ImageURISource | Int | Seq[ImageURISource]] = js.undefined
28 | )
29 |
30 | @js.native
31 | @JSImport("react-native", "Slider")
32 | object Component extends js.Object
33 |
34 | override val component = Component
35 | }
36 |
--------------------------------------------------------------------------------
/vr/src/main/scala/slinky/vr/View.scala:
--------------------------------------------------------------------------------
1 | package slinky.vr
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 | import scala.scalajs.js.|
9 |
10 | case class EdgeInsets(top: Double, bottom: Double, left: Double, right: Double)
11 | case class Layout(x: Double, y: Double, width: Double, height: Double)
12 | case class LayoutEvent(layout: Layout)
13 |
14 | @react object View extends ExternalComponent {
15 | case class Props(
16 | billboarding: js.UndefOr[String] = js.undefined,
17 | cursorVisibilitySlop: js.UndefOr[Double | EdgeInsets] = js.undefined,
18 | hitSlop: js.UndefOr[Double | EdgeInsets] = js.undefined,
19 | onEnter: js.UndefOr[() => Unit] = js.undefined,
20 | onExit: js.UndefOr[() => Unit] = js.undefined,
21 | onHeadPose: js.UndefOr[NativeSyntheticEvent[js.Object] => Unit] = js.undefined,
22 | onHeadPoseCaptured: js.UndefOr[() => Unit] = js.undefined,
23 | onInput: js.UndefOr[() => Unit] = js.undefined,
24 | onInputCaptured: js.UndefOr[() => Unit] = js.undefined,
25 | onLayout: js.UndefOr[NativeSyntheticEvent[LayoutEvent] => Unit] = js.undefined,
26 | onMove: js.UndefOr[() => Unit] = js.undefined,
27 | pointerEvents: js.UndefOr[String] = js.undefined,
28 | style: js.UndefOr[js.Object] = js.undefined,
29 | testID: js.UndefOr[String] = js.undefined
30 | )
31 |
32 | @js.native
33 | @JSImport("react-360", "View")
34 | object Component extends js.Object
35 |
36 | override val component = Component
37 | }
38 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/TypeConstructorWriters.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.concurrent.ExecutionContext.Implicits.global
4 | import scala.concurrent.Future
5 | import scala.scalajs.js
6 |
7 | trait TypeConstructorWriters {
8 | implicit def optionWriter[T](implicit writer: Writer[T]): Writer[Option[T]] =
9 | _.map(v => writer.write(v)).orNull
10 |
11 | implicit def eitherWriter[A, B](implicit aWriter: Writer[A], bWriter: Writer[B]): Writer[Either[A, B]] = { v =>
12 | val written = v.fold(aWriter.write, bWriter.write)
13 | js.Dynamic.literal(
14 | isLeft = v.isLeft,
15 | value = written
16 | )
17 | }
18 |
19 | implicit def collectionWriter[T, C[_]](implicit writer: Writer[T], ev: C[T] <:< Iterable[T]): Writer[C[T]] = s => {
20 | val ret = js.Array[js.Object]()
21 | s.foreach(v => ret.push(writer.write(v)))
22 | ret.asInstanceOf[js.Object]
23 | }
24 |
25 | implicit def arrayWriter[T](implicit writer: Writer[T]): Writer[Array[T]] = s => {
26 | val ret = new js.Array[js.Object](s.length)
27 | (0 until s.length).foreach(i => ret(i) = (writer.write(s(i))))
28 | ret.asInstanceOf[js.Object]
29 | }
30 |
31 | implicit def mapWriter[A, B](implicit abWriter: Writer[(A, B)]): Writer[Map[A, B]] = s => {
32 | collectionWriter[(A, B), Iterable].write(s)
33 | }
34 |
35 | implicit def futureWriter[O](implicit oWriter: Writer[O]): Writer[Future[O]] = s => {
36 | import scala.scalajs.js.JSConverters._
37 | s.map(v => oWriter.write(v)).toJSPromise.asInstanceOf[js.Object]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/hot/src/main/scala/slinky/hot/package.scala:
--------------------------------------------------------------------------------
1 | package slinky
2 |
3 | import slinky.core.BaseComponentWrapper
4 | import slinky.core.facade.ReactRaw
5 |
6 | import scala.scalajs.js
7 |
8 | package object hot {
9 | def initialize(): Unit = {
10 | val dynamicReactProxyModule = ReactProxy.asInstanceOf[js.Dynamic]
11 | val proxyObject: js.Dynamic =
12 | if (js.isUndefined(dynamicReactProxyModule._proxies)) {
13 | val newProxyStore = js.Dynamic.literal()
14 | dynamicReactProxyModule._proxies = newProxyStore
15 | newProxyStore
16 | } else {
17 | dynamicReactProxyModule._proxies
18 | }
19 |
20 | BaseComponentWrapper.insertMiddleware { (constructor, component) =>
21 | val componentName = component.asInstanceOf[BaseComponentWrapper].getClass.getName
22 |
23 | if (js.isUndefined(component.asInstanceOf[js.Dynamic]._hot)) {
24 | component.asInstanceOf[js.Dynamic]._hot = true
25 |
26 | if (js.isUndefined(proxyObject.selectDynamic(componentName))) {
27 | proxyObject.updateDynamic(componentName)(ReactProxy.createProxy(constructor))
28 | } else {
29 | val forceUpdate = ReactProxy.getForceUpdate(ReactRaw)
30 | proxyObject
31 | .selectDynamic(componentName)
32 | .update(constructor)
33 | .asInstanceOf[js.Array[js.Object]]
34 | .foreach(o => forceUpdate(o))
35 | }
36 | }
37 |
38 | proxyObject.selectDynamic(componentName).get().asInstanceOf[js.Object]
39 | }
40 |
41 | BaseComponentWrapper.enableScalaComponentWriting()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ContextTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.React
4 | import slinky.web.ReactDOM
5 | import org.scalajs.dom.document
6 | import slinky.web.html.div
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class ContextTest extends AnyFunSuite {
11 | test("Can provide and read a simple context value") {
12 | val context = React.createContext(-1)
13 | var gotValue = 0
14 |
15 | ReactDOM.render(
16 | context.Provider(value = 2)(
17 | context.Consumer { value =>
18 | gotValue = value
19 | div()
20 | }
21 | ),
22 | document.createElement("div")
23 | )
24 |
25 | assert(gotValue == 2)
26 | }
27 |
28 | test("Can provide and read a case class context value") {
29 | case class Data(foo: Int)
30 | val context = React.createContext(Data(-1))
31 | var gotValue = 0
32 |
33 | ReactDOM.render(
34 | context.Provider(value = Data(3))(
35 | context.Consumer { value =>
36 | gotValue = value.foo
37 | div()
38 | }
39 | ),
40 | document.createElement("div")
41 | )
42 |
43 | assert(gotValue == 3)
44 | }
45 |
46 | test("Read a case class context value from default") {
47 | case class Data(foo: Int)
48 | val context = React.createContext(Data(3))
49 | var gotValue = 0
50 |
51 | ReactDOM.render(
52 | context.Consumer { value =>
53 | gotValue = value.foo
54 | div()
55 | },
56 | document.createElement("div")
57 | )
58 |
59 | assert(gotValue == 3)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/TypeConstructorWriters.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.concurrent.ExecutionContext.Implicits.global
4 | import scala.concurrent.Future
5 | import scala.scalajs.js
6 | import CompatUtil._
7 |
8 | trait TypeConstructorWriters {
9 | implicit def optionWriter[T](implicit writer: => Writer[T]): Writer[Option[T]] =
10 | _.map(v => writer.write(v)).orNull
11 |
12 | implicit def eitherWriter[A, B](implicit aWriter: => Writer[A], bWriter: => Writer[B]): Writer[Either[A, B]] = { v =>
13 | val written = v.fold(aWriter.write, bWriter.write)
14 | js.Dynamic.literal(
15 | isLeft = v.isLeft,
16 | value = written
17 | )
18 | }
19 |
20 | implicit def collectionWriter[T, C[_]](implicit writer: => Writer[T], ev: C[T] <:< Iterable[T]): Writer[C[T]] = s => {
21 | val ret = js.Array[js.Object]()
22 | s.foreach(v => ret.push(writer.write(v)))
23 | ret.asInstanceOf[js.Object]
24 | }
25 |
26 | implicit def arrayWriter[T](implicit writer: => Writer[T]): Writer[Array[T]] = s => {
27 | val ret = new js.Array[js.Object](s.length)
28 | (0 until s.length).foreach(i => ret(i) = (writer.write(s(i))))
29 | ret.asInstanceOf[js.Object]
30 | }
31 |
32 | implicit def mapWriter[A, B](implicit abWriter: => Writer[(A, B)]): Writer[Map[A, B]] = s => {
33 | collectionWriter[(A, B), Iterable].write(s)
34 | }
35 |
36 | implicit def futureWriter[O](implicit oWriter: => Writer[O]): Writer[Future[O]] = s => {
37 | import scala.scalajs.js.JSConverters._
38 | s.map(v => oWriter.write(v)).toJSPromise.asInstanceOf[js.Object]
39 | }
40 | }
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Text.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | case class BoundingBox(top: Double, left: Double, bottom: Double, right: Double)
10 |
11 | @react object Text extends ExternalComponent {
12 | case class Props(
13 | selectable: js.UndefOr[Boolean] = js.undefined,
14 | accessible: js.UndefOr[Boolean] = js.undefined,
15 | ellipsizeMode: js.UndefOr[String] = js.undefined,
16 | nativeID: js.UndefOr[String] = js.undefined,
17 | numberOfLines: js.UndefOr[Int] = js.undefined,
18 | onLayout: js.UndefOr[NativeSyntheticEvent[LayoutChangeEvent] => Unit] = js.undefined,
19 | onLongPress: js.UndefOr[() => Unit] = js.undefined,
20 | onPress: js.UndefOr[() => Unit] = js.undefined,
21 | pressRetentionOffset: js.UndefOr[BoundingBox] = js.undefined,
22 | allowFontScaling: js.UndefOr[Boolean] = js.undefined,
23 | style: js.UndefOr[js.Object] = js.undefined,
24 | testID: js.UndefOr[String] = js.undefined,
25 | disabled: js.UndefOr[Boolean] = js.undefined,
26 | selectionColor: js.UndefOr[String] = js.undefined,
27 | textBreakStrategy: js.UndefOr[String] = js.undefined,
28 | adjustsFontSizeToFit: js.UndefOr[Boolean] = js.undefined,
29 | minimumFontScale: js.UndefOr[Double] = js.undefined,
30 | suppressHighlighting: js.UndefOr[Boolean] = js.undefined
31 | )
32 |
33 | @js.native
34 | @JSImport("react-native", "Text")
35 | object Component extends js.Object
36 |
37 | override val component = Component
38 | }
39 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/App.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.core.annotations.react
4 | import slinky.web.html._
5 |
6 | import scala.scalajs.js
7 |
8 | import slinky.core.FunctionalComponent
9 | import slinky.core.ReactComponentClass
10 | import slinky.core.facade.Fragment
11 | import slinky.core.CustomAttribute
12 |
13 | import slinky.next.Head
14 |
15 | // @JSImport("resources/index.module.css", JSImport.Default)
16 | // @js.native
17 | // object AppCSS extends js.Object
18 |
19 | @react object App {
20 | // val css = AppCSS
21 |
22 | val charSet = CustomAttribute[String]("charSet")
23 |
24 | case class Props(Component: ReactComponentClass[js.Object], pageProps: js.Object)
25 | val component = FunctionalComponent[Props] { props =>
26 | Fragment(
27 | Head(
28 | meta(charSet := "utf-8"),
29 | meta(name := "viewport", content := "width=device-width, initial-scale=1, shrink-to-fit=no"),
30 | meta(name := "theme-color", content := "#000000"),
31 | link(rel := "manifest", href := "/manifest.json"),
32 | link(rel := "shortcut icon", href := "/favicon.ico"),
33 | title(s"Slinky - Write React apps in Scala just like ES6")
34 | ),
35 | Navbar(()),
36 | div(style := js.Dynamic.literal(
37 | marginTop = "60px"
38 | ))(
39 | props.Component(props.pageProps)
40 | )
41 | )
42 | }
43 |
44 | object Next {
45 | import slinky.core.ReactComponentClass
46 | import scala.scalajs.js.annotation.JSExportTopLevel
47 |
48 | @JSExportTopLevel(name = "component", moduleID = "_app")
49 | def component(): ReactComponentClass[_] = App.component
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/DocsGroup.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.core.FunctionalComponent
4 | import slinky.core.annotations.react
5 | import slinky.web.html._
6 |
7 | import slinky.next.Link
8 |
9 | import scala.scalajs.js.Dynamic.literal
10 |
11 | @react object DocsGroup {
12 | case class Props(name: String, curId: String, isOpen: Boolean, children: List[(String, String)])
13 |
14 | val component = FunctionalComponent[Props] { props =>
15 | div(style := literal(width = "100%"))(
16 | button(style := literal(
17 | backgroundColor = "transparent",
18 | marginTop = "10px",
19 | border = "none",
20 | fontSize = "18px",
21 | textTransform = "uppercase",
22 | fontWeight = "700",
23 | padding = "0",
24 | width = "100%",
25 | textAlign = "left",
26 | outline = "none"
27 | ))(
28 | div(style := literal(
29 | color = if (props.isOpen) "rgb(26, 26, 26)" else "rgb(109, 109, 109)"
30 | ))(props.name)
31 | ),
32 | ul(style := literal(display = "block", listStyle = "none", padding = "0"))(
33 | props.children.zipWithIndex.map { case ((name, link), index) =>
34 | li(key := index.toString, style := literal(marginTop = "5px", marginBottom = "10px"))(
35 | Link(s"/docs/$link")(
36 | a(style := literal(
37 | color = "rgb(26, 26, 26)",
38 | backgroundColor = "transparent",
39 | borderBottom = "none",
40 | fontWeight = if (props.curId == link) 700 else null
41 | ))(name)
42 | )
43 | )
44 | }
45 | )
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ReactChildrenTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.{React, ReactChildren, ReactElement}
4 | import slinky.web.html.div
5 |
6 | import scala.scalajs.js
7 |
8 | import org.scalatest.funsuite.AnyFunSuite
9 |
10 | class ReactChildrenTest extends AnyFunSuite {
11 | import React.Children._
12 |
13 | test("Can map over a single element") {
14 | assert(count(map((div(): ReactElement).asInstanceOf[ReactChildren], elem => elem)) == 1)
15 | }
16 |
17 | test("Can map over multiple elements") {
18 | assert(count(map(js.Array[ReactElement](div(), div()).asInstanceOf[ReactChildren], elem => elem)) == 2)
19 | }
20 |
21 | test("Can iterate with forEach over a single element") {
22 | var count = 0
23 | forEach((div(): ReactElement).asInstanceOf[ReactChildren], _ => count += 1)
24 | assert(count == 1)
25 | }
26 |
27 | test("Can iterate with forEach over multiple elements") {
28 | var count = 0
29 | forEach(js.Array[ReactElement](div(), div()).asInstanceOf[ReactChildren], _ => count += 1)
30 | assert(count == 2)
31 | }
32 |
33 | test("Can get count of a single element") {
34 | assert(count((div(): ReactElement).asInstanceOf[ReactChildren]) == 1)
35 | }
36 |
37 | test("Can get count of multiple elements") {
38 | assert(count(js.Array[ReactElement](div(), div()).asInstanceOf[ReactChildren]) == 2)
39 | }
40 |
41 | test("Can convert single element to array") {
42 | assert(toArray((div(): ReactElement).asInstanceOf[ReactChildren]).length == 1)
43 | }
44 |
45 | test("Can convert multiple elements to array") {
46 | assert(toArray(js.Array[ReactElement](div(), div()).asInstanceOf[ReactChildren]).length == 2)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/docs/public/docs/abstracting-over-tags.md:
--------------------------------------------------------------------------------
1 | # Abstracting Over Tags
2 | Slinky comes with a strongly-typed API for creating tag trees that not only checks attribute value types but also verifies that attributes are compatible with the tag they are assigned to. With Slinky 0.3.0, this API has been extended to make it possible to abstract over individual tag types while preserving the ability to assign attributes in a type-safe manner.
3 |
4 | To start, let's define a method that creates an instance of a specified tag with a single child "Hello":
5 | ```scala
6 | def createTag[T <: Tag](tag: T): ReactElement = {
7 | tag.apply("Hello")
8 | }
9 | ```
10 |
11 | Here we take a type parameter `T` to track the type of tag we are rendering, and then use the `tag` value's apply method to construct the tag. To use this method, we can call it and pass in the tag we want to render as a parameter (the type parameter will be inferred).
12 |
13 | ```scala
14 | div(
15 | createTag(h1)
16 | ) // renders Hello
17 | ```
18 |
19 | We can also assign attributes to the tag, but before we do that we first need to prove that the passed-in tag supports the attribute we want to assign. We can do this by adding `: insert_attribute_here.supports` at the end of the type parameters block to specify that we want the tag `T` to support the attribute we want to assign. If we wanted to assign `className`, for example, we can add `className.supports`:
20 | ```scala
21 | def createTag[T <: Tag : className.supports](tag: T): ReactElement = {
22 | tag.apply(className := "my-css-class")("Hello")
23 | }
24 | ```
25 |
26 | Using this version of `createTag` is the same as above; just pass in the tag to render and it will be rendered with the appropriate attributes and children.
27 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/TypeConstructorReaders.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.concurrent.ExecutionContext.Implicits.global
4 | import scala.concurrent.Future
5 | import scala.scalajs.js
6 | import scala.reflect.ClassTag
7 | import CompatUtil._
8 |
9 | trait TypeConstructorReaders {
10 | implicit def optionReader[T](implicit reader: Reader[T]): Reader[Option[T]] =
11 | (s => {
12 | if (js.isUndefined(s) || s == null) {
13 | None
14 | } else {
15 | Some(reader.read(s))
16 | }
17 | }): AlwaysReadReader[Option[T]]
18 |
19 | implicit def eitherReader[A, B](implicit aReader: Reader[A], bReader: Reader[B]): Reader[Either[A, B]] = o => {
20 | if (o.asInstanceOf[js.Dynamic].isLeft.asInstanceOf[Boolean]) {
21 | Left(aReader.read(o.asInstanceOf[js.Dynamic].value.asInstanceOf[js.Object]))
22 | } else {
23 | Right(bReader.read(o.asInstanceOf[js.Dynamic].value.asInstanceOf[js.Object]))
24 | }
25 | }
26 |
27 | implicit def collectionReader[T, C[A] <: Iterable[A]](
28 | implicit reader: Reader[T],
29 | bf: Factory[T, C[T]]
30 | ): Reader[C[T]] =
31 | c => bf.fromSpecific(c.asInstanceOf[js.Array[js.Object]].map(o => reader.read(o)))
32 |
33 | implicit def arrayReader[T](implicit reader: Reader[T], classTag: ClassTag[T]): Reader[Array[T]] = { c =>
34 | c.asInstanceOf[js.Array[js.Object]].map(o => reader.read(o)).toArray
35 | }
36 |
37 | implicit def mapReader[A, B](implicit abReader: Reader[(A, B)]): Reader[Map[A, B]] = o => {
38 | collectionReader[(A, B), Iterable].read(o).toMap
39 | }
40 |
41 | implicit def futureReader[O](implicit oReader: Reader[O]): Reader[Future[O]] =
42 | _.asInstanceOf[js.Promise[js.Object]].toFuture.map(v => oReader.read(v))
43 | }
44 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/Jumbotron.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage
2 |
3 | import slinky.core.FunctionalComponent
4 | import slinky.core.annotations.react
5 | import slinky.web.html._
6 | import slinky.next.Image
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.Dynamic.literal
10 | import slinky.next.Link
11 |
12 | @react object Jumbotron {
13 | val component = FunctionalComponent[Unit](_ => {
14 | div(style := literal(
15 | marginTop = "60px",
16 | width = "100%",
17 | backgroundColor = "#282c34",
18 | padding = "30px",
19 | boxSizing = "border-box",
20 | display = "flex",
21 | flexDirection = "column"
22 | ))(
23 | Image(src = SlinkyLogo, layout = "raw", priority = true, loader = (a: js.Dynamic) => a.src)(
24 | style := literal(
25 | maxWidth = "100%",
26 | maxHeight = "45vh",
27 | display = "block",
28 | marginLeft = "auto",
29 | marginRight = "auto"
30 | )
31 | ),
32 | h2(
33 | style := literal(
34 | color = "white",
35 | fontSize = "40px",
36 | display = "block",
37 | textAlign = "center",
38 | marginTop = "0px"
39 | )
40 | )("Write React apps in Scala just like you would in ES6"),
41 | div(style := literal(
42 | display = "flex",
43 | alignItems = "center",
44 | flexDirection = "row",
45 | alignSelf = "center"
46 | ))(
47 | Link(href = "/docs/installation/")(a(style := literal(
48 | padding = "15px",
49 | backgroundColor = "#DC322F",
50 | color = "white",
51 | fontSize = "30px"
52 | ))(
53 | "Get Started"
54 | )
55 | ))
56 | )
57 | })
58 | }
59 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/TypeConstructorReaders.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.concurrent.ExecutionContext.Implicits.global
4 | import scala.concurrent.Future
5 | import scala.scalajs.js
6 | import scala.reflect.ClassTag
7 | import CompatUtil._
8 |
9 | trait TypeConstructorReaders {
10 | implicit def optionReader[T](implicit reader: => Reader[T]): Reader[Option[T]] =
11 | (s => {
12 | if (js.isUndefined(s) || s == null) {
13 | None
14 | } else {
15 | Some(reader.read(s))
16 | }
17 | }): AlwaysReadReader[Option[T]]
18 |
19 | implicit def eitherReader[A, B](implicit aReader: => Reader[A], bReader: => Reader[B]): Reader[Either[A, B]] = o => {
20 | if (o.asInstanceOf[js.Dynamic].isLeft.asInstanceOf[Boolean]) {
21 | Left(aReader.read(o.asInstanceOf[js.Dynamic].value.asInstanceOf[js.Object]))
22 | } else {
23 | Right(bReader.read(o.asInstanceOf[js.Dynamic].value.asInstanceOf[js.Object]))
24 | }
25 | }
26 |
27 | implicit def collectionReader[T, C[A] <: Iterable[A]](
28 | implicit reader: => Reader[T],
29 | bf: Factory[T, C[T]]
30 | ): Reader[C[T]] =
31 | c => bf.fromSpecific(c.asInstanceOf[js.Array[js.Object]].map(o => reader.read(o)))
32 |
33 | implicit def arrayReader[T](implicit reader: => Reader[T], classTag: ClassTag[T]): Reader[Array[T]] = { c =>
34 | c.asInstanceOf[js.Array[js.Object]].map(o => reader.read(o)).toArray
35 | }
36 |
37 | implicit def mapReader[A, B](implicit abReader: => Reader[(A, B)]): Reader[Map[A, B]] = o => {
38 | collectionReader[(A, B), Iterable].read(o).toMap
39 | }
40 |
41 | implicit def futureReader[O](implicit oReader: => Reader[O]): Reader[Future[O]] =
42 | _.asInstanceOf[js.Promise[js.Object]].toFuture.map(v => oReader.read(v))
43 | }
44 |
--------------------------------------------------------------------------------
/testRenderer/src/main/scala/slinky/testrenderer/TestRenderer.scala:
--------------------------------------------------------------------------------
1 | package slinky.testrenderer
2 |
3 | import slinky.core.ReactComponentClass
4 | import slinky.core.facade.ReactElement
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.{JSImport, JSName}
8 |
9 | @js.native
10 | trait TestRenderer extends js.Object {
11 | def toJSON(): js.Object = js.native
12 | def toTree(): js.Object = js.native
13 | def update(element: ReactElement): Unit = js.native
14 | def unmount(): Unit = js.native
15 | def getInstance(): js.Object = js.native
16 | val root: TestInstance = js.native
17 | }
18 |
19 | @js.native
20 | @JSImport("react-test-renderer", JSImport.Default)
21 | object TestRenderer extends js.Object {
22 | def create(element: ReactElement): TestRenderer = js.native
23 | def act(callback: js.Function0[js.Any]): Unit = js.native
24 | }
25 |
26 | @js.native
27 | trait TestInstance extends js.Object {
28 | def find(test: js.Function1[TestInstance, Boolean]): TestInstance = js.native
29 | def findByType(`type`: ReactComponentClass[_]): TestInstance = js.native
30 | def findByProps(props: js.Object): TestInstance = js.native
31 |
32 | def findAll(test: js.Function1[TestInstance, Boolean]): js.Array[TestInstance] = js.native
33 | def findAllByType(`type`: ReactComponentClass[_]): js.Array[TestInstance] = js.native
34 | def findAllByProps(props: js.Object): js.Array[TestInstance] = js.native
35 |
36 | val instance: js.Object = js.native
37 | @JSName("type") val `type`: js.Object = js.native
38 | val props: js.Object = js.native
39 | val parent: TestInstance = js.native
40 | val children: js.Array[TestInstance] = js.native
41 | }
42 |
--------------------------------------------------------------------------------
/scalajsReactInterop/build.sbt:
--------------------------------------------------------------------------------
1 | enablePlugins(ScalaJSPlugin)
2 | enablePlugins(JSDependenciesPlugin)
3 |
4 | name := "slinky-scalajsreact-interop"
5 |
6 | libraryDependencies ++= {
7 | CrossVersion.partialVersion(scalaVersion.value) match {
8 | case Some((2, _)) =>
9 | Seq(
10 | "com.github.japgolly.scalajs-react" %%% "core" % "1.7.7"
11 | )
12 | case _ =>
13 | Seq(
14 | "com.github.japgolly.scalajs-react" %%% "core" % "2.0.0"
15 | )
16 | }
17 | }
18 |
19 | libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test
20 |
21 | Test / jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv()
22 |
23 | Test / unmanagedResourceDirectories += baseDirectory.value / "node_modules"
24 |
25 | jsDependencies ++= Seq(
26 | ((ProvidedJS / "text-enc/lib/encoding.js")
27 | .minified("text-enc/lib/encoding.js")
28 | .commonJSName("TextEnc")) % Test,
29 | ((ProvidedJS / "react/umd/react.development.js")
30 | .minified("react/umd/react.production.min.js")
31 | .dependsOn("text-enc/lib/encoding.js")
32 | .commonJSName("React")) % Test,
33 | ((ProvidedJS / "react-dom/umd/react-dom.development.js")
34 | .minified("react-dom/umd/react-dom.production.min.js")
35 | .dependsOn("react/umd/react.development.js")
36 | .commonJSName("ReactDOM")) % Test,
37 | ((ProvidedJS / "react-dom/umd/react-dom-test-utils.development.js")
38 | .minified("react-dom/umd/react-dom-test-utils.production.min.js")
39 | .dependsOn("react-dom/umd/react-dom.development.js")
40 | .commonJSName("ReactTestUtils")) % Test,
41 | ((ProvidedJS / "react-dom/umd/react-dom-server.browser.development.js")
42 | .minified("react-dom/umd/react-dom-server.browser.production.min.js")
43 | .dependsOn("react-dom/umd/react-dom.development.js")
44 | .commonJSName("ReactDOMServer")) % Test
45 | )
46 |
--------------------------------------------------------------------------------
/docs/public/docs/fragments-and-portals.md:
--------------------------------------------------------------------------------
1 | # Fragments and Portals
2 | Slinky supports the special fragment and portal element types that were introduced in React 16.
3 |
4 | ## Fragments
5 | [Fragments](https://reactjs.org/docs/fragments.html) make it possible to return multiple elements from a component. To create a fragment simply return a list of elements in your `render` method.
6 |
7 | ```scala
8 | @react class MyComponent extends StatelessComponent {
9 | trait Props = Unit
10 |
11 | def render = {
12 | List(
13 | h1("a"),
14 | h2("b"),
15 | h3("c")
16 | )
17 | }
18 | }
19 | ```
20 |
21 | Additionally, Slinky supports the `Fragment` component [introduced in React 16.2](https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html).
22 |
23 | ```scala
24 | import slinky.core.facade.Fragment
25 |
26 | @react class MyComponent extends StatelessComponent {
27 | trait Props = Unit
28 |
29 | def render = {
30 | Fragment(
31 | h1("a"),
32 | h2("b"),
33 | h3("c")
34 | )
35 | }
36 | }
37 | ```
38 |
39 | ## Portals
40 | [Portals](https://reactjs.org/docs/portals.html) are another special element type introduced in React 16 that make it possible to render React content in a different location in the DOM than it would normally go. This is useful for components like modals, which often need to be placed at a higher level in the DOM than where the component is placed.
41 |
42 | To construct a portal, use the method in `ReactDOM`:
43 |
44 | ```scala
45 | import org.scalajs.dom.document
46 |
47 | val containerDOMNode = document.createElement("button")
48 |
49 | // ...
50 |
51 | import slinky.web.ReactDOM
52 |
53 | div(
54 | // ...,
55 | ReactDOM.createPortal(
56 | h1("hello!"),
57 | containerDOMNode
58 | )
59 | )
60 | ```
61 |
62 | This will result in the `h1` tag being rendered into the containerDOMNode `button` tag instead of inside the parent `div` tag.
63 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/Component.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.{ErrorBoundaryInfo, React, ReactElement}
4 |
5 | import scala.scalajs.js
6 |
7 | abstract class Component extends React.Component(null) {
8 | type Props
9 | type State
10 | type Snapshot
11 |
12 | def initialState: State
13 |
14 | final def props: Props = ???
15 |
16 | final def state: State = ???
17 |
18 | final def setState(s: State): Unit = ???
19 |
20 | final def setState(fn: State => State): Unit = ???
21 |
22 | final def setState(fn: (State, Props) => State): Unit = ???
23 |
24 | final def setState(s: State, callback: () => Unit): Unit = ???
25 |
26 | final def setState(fn: State => State, callback: () => Unit): Unit = ???
27 |
28 | final def setState(fn: (State, Props) => State, callback: () => Unit): Unit = ???
29 |
30 | def componentWillMount(): Unit = {}
31 |
32 | def componentDidMount(): Unit = {}
33 |
34 | def componentWillReceiveProps(nextProps: Props): Unit = {}
35 |
36 | def shouldComponentUpdate(nextProps: Props, nextState: State): Boolean = true
37 |
38 | def componentWillUpdate(nextProps: Props, nextState: State): Unit = {}
39 |
40 | def getSnapshotBeforeUpdate(prevProps: Props, prevState: State): Snapshot = null.asInstanceOf[Snapshot]
41 |
42 | def componentDidUpdate(prevProps: Props, prevState: State): Unit = {}
43 |
44 | def componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot): Unit = {}
45 |
46 | def componentWillUnmount(): Unit = {}
47 |
48 | def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {}
49 |
50 | def render(): ReactElement
51 | }
52 |
53 | object Component {
54 | type Wrapper = ComponentWrapper
55 | }
56 |
57 | abstract class StatelessComponent extends Component {
58 | type State = Unit
59 | def initialState: State = ()
60 | }
61 |
62 | object StatelessComponent {
63 | type Wrapper = StatelessComponentWrapper
64 | }
65 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/TodoApp.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage //nodisplay
2 |
3 | import slinky.core.{Component, StatelessComponent, SyntheticEvent} //nodisplay
4 | import slinky.core.annotations.react //nodisplay
5 | import slinky.web.html._ //nodisplay
6 | import org.scalajs.dom.{html, Event} //nodisplay
7 |
8 | import scala.scalajs.js.Date //nodisplay
9 |
10 | case class TodoItem(text: String, id: Long)
11 |
12 | @react class TodoApp extends Component {
13 | type Props = Unit
14 | case class State(items: Seq[TodoItem], text: String)
15 |
16 | override def initialState = State(Seq.empty, "")
17 |
18 | def handleChange(e: SyntheticEvent[html.Input, Event]): Unit = {
19 | val eventValue = e.target.value
20 | setState(_.copy(text = eventValue))
21 | }
22 |
23 | def handleSubmit(e: SyntheticEvent[html.Form, Event]): Unit = {
24 | e.preventDefault()
25 |
26 | if (state.text.nonEmpty) {
27 | val newItem = TodoItem(
28 | text = state.text,
29 | id = Date.now().toLong
30 | )
31 |
32 | setState(prevState => {
33 | State(
34 | items = prevState.items :+ newItem,
35 | text = ""
36 | )
37 | })
38 | }
39 | }
40 |
41 | override def render() = {
42 | div(
43 | h3("TODO"),
44 | TodoList(items = state.items),
45 | form(onSubmit := (handleSubmit(_)))(
46 | input(
47 | onChange := (handleChange(_)),
48 | value := state.text
49 | ),
50 | button(s"Add #${state.items.size + 1}")
51 | )
52 | )
53 | }
54 | }
55 |
56 | @react class TodoList extends StatelessComponent {
57 | case class Props(items: Seq[TodoItem])
58 |
59 | override def render() = {
60 | ul(
61 | props.items.map { item =>
62 | li(key := item.id.toString)(item.text)
63 | }
64 | )
65 | }
66 | }
67 |
68 | //display:ReactDOM.render(TodoApp(), mountNode)
69 | //run:TodoApp() //nodisplay
--------------------------------------------------------------------------------
/docs/public/docs/error-boundaries.md:
--------------------------------------------------------------------------------
1 | # Error Boundaries
2 | Slinky supports writing error boundary components, a feature introduced in React 16 to make it easier to handle component exceptions. Error boundaries are components that can catch exceptions thrown by any of their children and display custom UI based on the error, such as a popup.
3 |
4 | To create an error boundary using the `@react` macro annotation, simply define the `componentDidCatch` method:
5 | ```scala
6 | @react class ErrorBoundaryComponent extends Component {
7 | type Props = ReactElement
8 | case class State(hasError: Boolean)
9 |
10 | def initialState = State(hasError = false)
11 |
12 | override def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {
13 | setState(State(hasError = true))
14 | println(s"got an error $error")
15 | }
16 |
17 | override def render(): ReactElement = {
18 | if (state.hasError) {
19 | h1("Something went wrong.")
20 | } else {
21 | props
22 | }
23 | }
24 | }
25 | ```
26 |
27 | If using the `ComponentWrapper` API, you similarly implement the `componentDidCatch` method.
28 | ```scala
29 | object ErrorBoundaryComponent extends ComponentWrapper {
30 | type Props = ReactElement
31 | case class State(hasError: Boolean)
32 |
33 | class Def(jsProps: js.Object) extends Definition(jsProps) {
34 | def initialState = State(hasError = false)
35 |
36 | override def componentDidCatch(error: js.Error, info: ErrorBoundaryInfo): Unit = {
37 | setState(State(hasError = true))
38 | println(s"got an error $error")
39 | }
40 |
41 | override def render(): ReactElement = {
42 | if (state.hasError) {
43 | h1("Something went wrong.")
44 | } else {
45 | props
46 | }
47 | }
48 | }
49 | }
50 | ```
51 |
52 | Using error boundary components is no different than using regular Slinky components. Simply render them to your tree and React will automatically set them up as error boundaries.
53 |
--------------------------------------------------------------------------------
/docs/public/docs/why-slinky.md:
--------------------------------------------------------------------------------
1 | # Why Slinky
2 | Slinky attempts to strike a balance between the JavaScript and Scala programming worlds, offering a complete API layer that mirrors the original JavaScript API for React and many extension points and accessory libraries for adding additional features on top of the core.
3 |
4 | Although JavaScript, and thus React, is dynamically typed, Slinky provides a statically typed API that makes it possible to catch issues at compile time. In React itself, the documentation includes descriptions of an internal type system that, although it does not exist at runtime, can be adapted to form an actual type system for Slinky. Where possible, Slinky also adds additional type safety beyond the core React types in places like HTML tree construction.
5 |
6 | To ensure the project succeeds in the short term and thrives in the long term, Slinky follows a set of core principles that guide its features:
7 |
8 | 1. **Modularity**: Allow developers to choose parts of Slinky that are appropriate for their apps
9 | 2. **Type safety for development experience**: Provide type safe facades whenever it can improve the coding experience, but not when it obscures the underlying concepts
10 | 3. **IDE Support**: Ensure that all Slinky features are well supported in major IDEs; place new features on hold if IDEs do not support their implementations
11 | 4. **Compatible with the Ecosystem**: Each feature must ensure that Slinky fits smoothly into the existing Scala and JavaScript ecosystems, integrating with existing tooling and community libraries
12 | 5. **Treat backward compatibility with utmost care**: Carefully design APIs to avoid breaking them in future. When breaking backwards compatibility outweighs the cost, document the breaking change clearly and follow the semantic versioning scheme so users aren’t caught off-guard.
13 | 6. **Quality**: Cover each feature with unit and integration tests to ensure that each new release is at least as good as the last one.
14 | 7. **Documentation**: Document each feature so that developers can learn about them and use them effectively.
15 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ReactRefTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import org.scalajs.dom
4 | import org.scalajs.dom.html
5 |
6 | import slinky.core.facade.React
7 | import slinky.web.ReactDOM
8 | import slinky.web.html.{div, ref}
9 |
10 | import scala.concurrent.Promise
11 |
12 | import org.scalatest.Assertion
13 | import org.scalatest.funsuite.AsyncFunSuite
14 |
15 | class ReactRefTest extends AsyncFunSuite {
16 | test("Can pass in a ref object to an HTML tag and use it") {
17 | val elemRef = React.createRef[html.Div]
18 | ReactDOM.render(
19 | div(ref := elemRef)("hello!"),
20 | dom.document.createElement("div")
21 | )
22 |
23 | assert(elemRef.current.innerHTML == "hello!")
24 | }
25 |
26 | test("Can pass in a ref object to a Slinky component and use it") {
27 | val promise: Promise[Assertion] = Promise()
28 | val ref = React.createRef[TestForceUpdateComponent.Def]
29 |
30 | ReactDOM.render(
31 | TestForceUpdateComponent(() => promise.success(assert(true))).withRef(ref),
32 | dom.document.createElement("div")
33 | )
34 |
35 | ref.current.forceUpdate()
36 |
37 | promise.future
38 | }
39 |
40 | test("Can use forwardRef to pass down a ref to a lower element") {
41 | val forwarded = React.forwardRef[String, html.Div](FunctionalComponent((props, rf) => {
42 | div(ref := rf)(props)
43 | }))
44 |
45 | val divRef = React.createRef[html.Div]
46 | ReactDOM.render(
47 | forwarded("hello").withRef(divRef),
48 | dom.document.createElement("div")
49 | )
50 |
51 | assert(divRef.current.innerHTML == "hello")
52 | }
53 |
54 | test("Can memo a functional component with forwarded ref") {
55 | val forwarded = React.memo(React.forwardRef[String, html.Div](FunctionalComponent((props, rf) => {
56 | div(ref := rf)(props)
57 | })))
58 |
59 | val divRef = React.createRef[html.Div]
60 | ReactDOM.render(
61 | forwarded("hello").withRef(divRef),
62 | dom.document.createElement("div")
63 | )
64 |
65 | assert(divRef.current.innerHTML == "hello")
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/web/src/main/scala/slinky/web/ReactDOM.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.facade.{React, ReactElement, ReactInstance}
4 | import org.scalajs.dom.Element
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.annotation.JSImport
8 |
9 | @js.native
10 | @JSImport("react-dom", JSImport.Namespace, "ReactDOM")
11 | object ReactDOM extends js.Object {
12 | def render(component: ReactElement, target: Element): ReactInstance = js.native
13 | def hydrate(component: ReactElement, target: Element): ReactInstance = js.native
14 | def findDOMNode(instance: React.Component): Element = js.native
15 |
16 | def flushSync[T](callback: js.Function0[T]): T = js.native
17 |
18 | def unmountComponentAtNode(container: Element): Unit = js.native
19 |
20 | /**
21 | * React Docs - Creates a portal. Portals provide a way to render children into a DOM node that exists outside the hierarchy of the DOM component.
22 | *
23 | * React 16 only
24 | * @param child the React node to render inside the selected container
25 | * @param container the DOM node to render the child node inside
26 | * @param key an optional key to distinguish this from other elements
27 | * @return a portal React element
28 | */
29 | def createPortal(child: ReactElement, container: Element, key: js.UndefOr[String] = js.undefined): ReactElement =
30 | js.native
31 | }
32 |
33 | @js.native
34 | @JSImport("react-dom/server", JSImport.Namespace, "ReactDOMServer")
35 | object ReactDOMServer extends js.Object {
36 | def renderToString(element: ReactElement): String = js.native
37 | def renderToStaticMarkup(element: ReactElement): String = js.native
38 |
39 | def renderToNodeStream(element: ReactElement): js.Object = js.native
40 | def renderToStaticNodeStream(element: ReactElement): js.Object = js.native
41 | }
42 |
43 | trait ReactRoot extends js.Object {
44 | def render(component: ReactElement): ReactInstance
45 | def unmount(): Unit
46 | }
47 |
48 | @js.native
49 | @JSImport("react-dom/client", JSImport.Namespace, "ReactDOM")
50 | object ReactDOMClient extends js.Object {
51 | def createRoot(target: Element): ReactRoot = js.native
52 | def hydrate(component: ReactElement, target: Element): ReactRoot = js.native
53 | }
54 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/ReactComponentClass.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.readwrite.Reader
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.ConstructorTag
7 | import slinky.readwrite.Writer
8 |
9 | @js.native
10 | trait ReactComponentClass[P] extends js.Object
11 |
12 | object ReactComponentClass {
13 | implicit class RichReactComponentClass[P: Writer](val c: ReactComponentClass[P]) {
14 | @inline def apply(props: P): BuildingComponent[Nothing, js.Object] =
15 | new BuildingComponent(
16 | js.Array(c.asInstanceOf[js.Any], implicitly[Writer[P]].write(props).asInstanceOf[js.Dictionary[js.Any]])
17 | )
18 | }
19 |
20 | implicit def wrapperToClass[T <: BaseComponentWrapper](wrapper: T)(
21 | implicit propsReader: Reader[wrapper.Props],
22 | ctag: ConstructorTag[wrapper.Def]
23 | ): ReactComponentClass[wrapper.Props] =
24 | wrapper
25 | .componentConstructor(propsReader, wrapper.hot_stateWriter, wrapper.hot_stateReader, ctag)
26 | .asInstanceOf[ReactComponentClass[wrapper.Props]]
27 |
28 | implicit def externalToClass(
29 | external: ExternalComponentWithAttributesWithRefType[_, _]
30 | ): ReactComponentClass[external.Props] =
31 | external.component
32 | .asInstanceOf[ReactComponentClass[external.Props]]
33 |
34 | implicit def externalNoPropsToClass(
35 | external: ExternalComponentNoPropsWithAttributesWithRefType[_, _]
36 | ): ReactComponentClass[Unit] =
37 | external.component.asInstanceOf[ReactComponentClass[Unit]]
38 |
39 | implicit def functionalComponentToClass[P](
40 | component: FunctionalComponent[P]
41 | )(implicit propsReader: Reader[P]): ReactComponentClass[P] =
42 | component.componentWithReader(propsReader).asInstanceOf[ReactComponentClass[P]]
43 |
44 | implicit def functionalComponentTakingRefToClass[P, R <: js.Any](
45 | component: FunctionalComponentTakingRef[P, R]
46 | )(implicit propsReader: Reader[P]): ReactComponentClass[P] =
47 | component.componentWithReader(propsReader).asInstanceOf[ReactComponentClass[P]]
48 |
49 | implicit def functionalComponentForwardedRefToClass[P, R <: js.Any](
50 | component: FunctionalComponentForwardedRef[P, R]
51 | )(implicit propsReader: Reader[P]): ReactComponentClass[P] =
52 | component.componentWithReader(propsReader).asInstanceOf[ReactComponentClass[P]]
53 | }
54 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/facade/ReactContext.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.facade
2 |
3 | import slinky.core.{BuildingComponent, ExternalComponent, ExternalPropsWriterProvider}
4 | import slinky.readwrite.Writer
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.|
8 |
9 | @js.native
10 | trait ReactContextRaw extends js.Object {
11 | val Provider: js.Object = js.native
12 | val Consumer: js.Object = js.native
13 | }
14 |
15 | case class ContextProviderProps[T](value: T)
16 | object ContextProviderProps {
17 | implicit def writer[T]: Writer[ContextProviderProps[T]] =
18 | v =>
19 | js.Dynamic.literal(
20 | value = v.value.asInstanceOf[js.Any]
21 | )
22 | }
23 |
24 | class ContextProvider[T](orig: ReactContext[T]) {
25 | private object External
26 | extends ExternalComponent()(ContextProviderProps.writer[T].asInstanceOf[ExternalPropsWriterProvider]) {
27 | override type Props = ContextProviderProps[T]
28 | override val component: |[String, js.Object] = orig.asInstanceOf[ReactContextRaw].Provider
29 | }
30 |
31 | def apply(value: T): BuildingComponent[Nothing, js.Object] = External(ContextProviderProps(value))
32 | }
33 |
34 | case class ContextConsumerProps[T](children: T => ReactElement)
35 | object ContextConsumerProps {
36 | implicit def writer[T]: Writer[ContextConsumerProps[T]] =
37 | v =>
38 | js.Dynamic.literal(
39 | children = Writer
40 | .function1[T, ReactElement](
41 | _.asInstanceOf[T],
42 | Writer.jsAnyWriter[ReactElement]
43 | )
44 | .write(v.children)
45 | )
46 | }
47 |
48 | class ContextConsumer[T](orig: ReactContext[T]) {
49 | private object External
50 | extends ExternalComponent()(ContextConsumerProps.writer[T].asInstanceOf[ExternalPropsWriterProvider]) {
51 | override type Props = ContextConsumerProps[T]
52 | override val component: |[String, js.Object] = orig.asInstanceOf[ReactContextRaw].Consumer
53 | }
54 |
55 | def apply(children: T => ReactElement): BuildingComponent[Nothing, js.Object] =
56 | External(ContextConsumerProps(children))
57 | }
58 |
59 | @js.native
60 | trait ReactContext[T] extends js.Object
61 | object ReactContext {
62 | implicit final class RichReactContext[T](private val orig: ReactContext[T]) extends AnyVal {
63 | def Provider: ContextProvider[T] = new ContextProvider[T](orig)
64 | def Consumer: ContextConsumer[T] = new ContextConsumer[T](orig)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala/slinky/readwrite/CoreWriters.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.annotation.compileTimeOnly
4 | import scala.scalajs.js
5 |
6 | @compileTimeOnly("Deferred writers are used to handle recursive structures")
7 | final class DeferredWriter[T, Term] extends Writer[T] {
8 | override def write(p: T): js.Object = null
9 | }
10 |
11 | trait FallbackWriters {
12 | def fallback[T]: Writer[T] = s => js.Dynamic.literal(__ = s.asInstanceOf[js.Any])
13 | }
14 |
15 | trait CoreWriters
16 | extends MacroWriters
17 | with UnionWriters
18 | with FallbackWriters
19 | with FunctionWriters
20 | with TypeConstructorWriters {
21 | implicit def jsAnyWriter[T <: js.Any]: Writer[T] = _.asInstanceOf[js.Object]
22 |
23 | implicit val unitWriter: Writer[Unit] = _ => js.Dynamic.literal()
24 |
25 | implicit val stringWriter: Writer[String] = _.asInstanceOf[js.Object]
26 |
27 | implicit val charWriter: Writer[Char] = _.toString.asInstanceOf[js.Object]
28 |
29 | implicit val byteWriter: Writer[Byte] = _.asInstanceOf[js.Object]
30 |
31 | implicit val shortWriter: Writer[Short] = _.asInstanceOf[js.Object]
32 |
33 | implicit val intWriter: Writer[Int] = _.asInstanceOf[js.Object]
34 |
35 | implicit val longWriter: Writer[Long] = _.toString.asInstanceOf[js.Object]
36 |
37 | implicit val booleanWriter: Writer[Boolean] = _.asInstanceOf[js.Object]
38 |
39 | implicit val doubleWriter: Writer[Double] = _.asInstanceOf[js.Object]
40 |
41 | implicit val floatWriter: Writer[Float] = _.asInstanceOf[js.Object]
42 |
43 | // This one deliberately doesn't have a by-name parameter since with Scala 3 unions, it manages to cause
44 | // infinite recursion, and there's no point in that (js.undefined | js.undefined | A is same as js.undefined | A,
45 | // while Option[Option[A]] is very different from Option[A]). Interestingly, if writer was a by-name parameter here,
46 | // scalac would resolve this as valid implicit for T = Any, making Any not writable
47 | implicit def undefOrWriter[T](implicit writer: Writer[T]): Writer[js.UndefOr[T]] =
48 | _.map(v => writer.write(v)).getOrElse(js.undefined.asInstanceOf[js.Object])
49 |
50 | implicit val rangeWriter: Writer[Range] = r => {
51 | js.Dynamic.literal(start = r.start, end = r.end, step = r.step, inclusive = r.isInclusive)
52 | }
53 |
54 | implicit val inclusiveRangeWriter: Writer[Range.Inclusive] =
55 | rangeWriter.asInstanceOf[Writer[Range.Inclusive]]
56 | }
57 |
--------------------------------------------------------------------------------
/reactrouter/src/main/scala/slinky/reactrouter/ReactRouterDOM.scala:
--------------------------------------------------------------------------------
1 | package slinky.reactrouter
2 |
3 | import slinky.core._
4 | import slinky.core.annotations.react
5 | import slinky.web.html.a
6 | import org.scalajs.dom.History
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.annotation.JSImport
10 |
11 | @JSImport("react-router", JSImport.Default)
12 | @js.native
13 | object ReactRouter extends js.Object {
14 | val StaticRouter: js.Object = js.native
15 | val MemoryRouter: js.Object = js.native
16 | }
17 |
18 | @JSImport("react-router-dom", JSImport.Default)
19 | @js.native
20 | object ReactRouterDOM extends js.Object {
21 | val Router: js.Object = js.native
22 | val BrowserRouter: js.Object = js.native
23 | val HashRouter: js.Object = js.native
24 | val Route: js.Object = js.native
25 | val Switch: js.Object = js.native
26 | val Link: js.Object = js.native
27 | val NavLink: js.Object = js.native
28 | val Redirect: js.Object = js.native
29 | val Prompt: js.Object = js.native
30 | }
31 |
32 | @react object StaticRouter extends ExternalComponent {
33 | case class Props(location: String, context: js.Object)
34 |
35 | override val component = ReactRouter.StaticRouter
36 | }
37 |
38 | @react object Router extends ExternalComponent {
39 | case class Props(history: History)
40 | override val component = ReactRouterDOM.Router
41 | }
42 |
43 | object BrowserRouter extends ExternalComponentNoProps {
44 | override val component = ReactRouterDOM.BrowserRouter
45 | }
46 |
47 | object Switch extends ExternalComponentNoProps {
48 | override val component = ReactRouterDOM.Switch
49 | }
50 |
51 | @react object Route extends ExternalComponent {
52 | case class Props(path: String, component: ReactComponentClass[_], exact: Boolean = false)
53 | override val component = ReactRouterDOM.Route
54 | }
55 |
56 | @react object Link extends ExternalComponentWithAttributes[a.tag.type] {
57 | case class Props(to: String)
58 | override val component = ReactRouterDOM.Link
59 | }
60 |
61 | @react object Redirect extends ExternalComponent {
62 | case class Props(to: String, push: Boolean = false)
63 | override val component = ReactRouterDOM.Redirect
64 | }
65 |
66 | @react object NavLink extends ExternalComponentWithAttributes[a.tag.type] {
67 | case class Props(to: String, activeStyle: Option[js.Dynamic] = None, activeClassName: Option[String] = None)
68 | override val component = ReactRouterDOM.NavLink
69 | }
70 |
--------------------------------------------------------------------------------
/tests/build.sbt:
--------------------------------------------------------------------------------
1 | import _root_.io.github.davidgregory084._
2 |
3 | enablePlugins(ScalaJSPlugin)
4 | enablePlugins(JSDependenciesPlugin)
5 |
6 | libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % Test
7 | libraryDependencies += ("org.scala-js" %%% "scalajs-fake-insecure-java-securerandom" % "1.0.0")
8 | .cross(CrossVersion.for3Use2_13)
9 |
10 | Test / jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv()
11 |
12 | Test / scalaJSLinkerConfig ~= {
13 | _.withESFeatures(
14 | _.withUseECMAScript2015(
15 | Option(System.getenv("ES2015_ENABLED")).map(_ == "true").getOrElse(false)
16 | )
17 | )
18 | }
19 |
20 | Test / unmanagedResourceDirectories += baseDirectory.value / "node_modules"
21 |
22 | jsDependencies ++= Seq(
23 | ((ProvidedJS / "text-enc/lib/encoding.js")
24 | .minified("text-enc/lib/encoding.js")
25 | .commonJSName("TextEnc")) % Test,
26 | ((ProvidedJS / "react/umd/react.development.js")
27 | .dependsOn("text-enc/lib/encoding.js")
28 | .minified("react/umd/react.production.min.js")
29 | .commonJSName("React")) % Test,
30 | ((ProvidedJS / "react-dom/umd/react-dom.development.js")
31 | .minified("react-dom/umd/react-dom.production.min.js")
32 | .dependsOn("react/umd/react.development.js")
33 | .commonJSName("ReactDOM")) % Test,
34 | ((ProvidedJS / "react-dom/umd/react-dom-test-utils.development.js")
35 | .minified("react-dom/umd/react-dom-test-utils.production.min.js")
36 | .dependsOn("react-dom/umd/react-dom.development.js")
37 | .commonJSName("ReactTestUtils")) % Test,
38 | ((ProvidedJS / "react-dom/umd/react-dom-server.browser.development.js")
39 | .minified("react-dom/umd/react-dom-server.browser.production.min.js")
40 | .dependsOn("react-dom/umd/react-dom.development.js")
41 | .commonJSName("ReactDOMServer")) % Test
42 | )
43 |
44 | // The Scala 3 tests still have a bunch of warnings that need fixing such as https://github.com/shadaj/slinky/issues/643
45 | // before CiMode can be used.
46 | tpolecatOptionsMode := (CrossVersion.partialVersion(scalaVersion.value) match {
47 | case Some((3, _)) => DevMode
48 | case _ => CiMode
49 | })
50 |
51 | scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
52 | case Some((2, _)) => Seq("-P:scalajs:nowarnGlobalExecutionContext")
53 | case _ => Seq.empty
54 | })
55 |
56 | // Unit statements are prevalent in the tests. There is no way to suppress them:
57 | // See https://github.com/typelevel/sbt-tpolecat/issues/134.
58 | Test / tpolecatExcludeOptions += ScalacOptions.warnNonUnitStatement
59 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/web/ReactDOMTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.web
2 |
3 | import slinky.core.ComponentWrapper
4 | import slinky.core.facade.ReactElement
5 | import slinky.web.ReactDOMClient.createRoot
6 | import org.scalajs.dom.{Element, document}
7 |
8 | import scala.scalajs.js
9 | import html._
10 |
11 | import org.scalatest.funsuite.AnyFunSuite
12 |
13 | object TestComponent extends ComponentWrapper {
14 | type Props = Unit
15 | type State = Unit
16 |
17 | class Def(jsProps: js.Object) extends Definition(jsProps) {
18 | override def initialState: Unit = ()
19 |
20 | override def render(): ReactElement = {
21 | a()
22 | }
23 | }
24 | }
25 |
26 | class ReactDOMTest extends AnyFunSuite {
27 | test("Renders a single element into the DOM using createRoot") {
28 | val target = document.createElement("div")
29 | ReactDOM.flushSync(() => createRoot(target).render(a()))
30 |
31 | assert(target.innerHTML == "")
32 | }
33 |
34 | test("Renders a single element into the DOM") {
35 | val target = document.createElement("div")
36 | ReactDOM.render(
37 | a(),
38 | target
39 | )
40 |
41 | assert(target.innerHTML == "")
42 | }
43 |
44 | test("Finds a dom node for a component") {
45 | val comp: ReactElement = TestComponent(())
46 | val target = document.createElement("div")
47 | val instance = ReactDOM.render(
48 | comp,
49 | target
50 | ).asInstanceOf[TestComponent.Def]
51 |
52 | assert(target.childNodes(0).asInstanceOf[Element] == ReactDOM.findDOMNode(instance))
53 | }
54 |
55 | test("Renders portals to the appropriate container DOM node") {
56 | val target = document.createElement("div")
57 | val container = document.createElement("div")
58 | ReactDOM.render(
59 | div(
60 | ReactDOM.createPortal(h1("hi"), container)
61 | ),
62 | target
63 | )
64 |
65 | assert(container.innerHTML == "hi
")
66 | assert(target.innerHTML == "")
67 | }
68 |
69 | test("unmount clears out the container") {
70 | val container = document.createElement("div")
71 | val root = createRoot(container)
72 |
73 | ReactDOM.flushSync(() => root.render(div("hello")))
74 |
75 | assert(container.innerHTML == "hello
")
76 |
77 | root.unmount()
78 |
79 | assert(container.innerHTML.length == 0)
80 | }
81 |
82 | test("unmountComponentAtNode clears out the container") {
83 | val container = document.createElement("div")
84 | ReactDOM.render(
85 | div("hello"),
86 | container
87 | )
88 |
89 | assert(container.innerHTML == "hello
")
90 |
91 | ReactDOM.unmountComponentAtNode(container)
92 |
93 | assert(container.innerHTML.length == 0)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/CoreWritersMacro.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.reflect.macros.whitebox
4 |
5 | trait MacroWriters {
6 | implicit def deriveWriter[T]: Writer[T] = macro MacroWritersImpl.derive[T]
7 | }
8 |
9 | class MacroWritersImpl(_c: whitebox.Context) extends GenericDeriveImpl(_c) {
10 | import c.universe._
11 |
12 | val typeclassType: c.universe.Type = typeOf[Writer[_]]
13 |
14 | def deferredInstance(forType: Type, constantType: Type) =
15 | q"new _root_.slinky.readwrite.DeferredWriter[$forType, $constantType]"
16 |
17 | def maybeExtractDeferred(tree: Tree): Option[Tree] =
18 | tree match {
19 | case q"new _root_.slinky.readwrite.DeferredWriter[$_, $t]()" =>
20 | Some(t)
21 | case q"new slinky.readwrite.DeferredWriter[$_, $t]()" =>
22 | Some(t)
23 | case _ => None
24 | }
25 |
26 | def createModuleTypeclass(tpe: Type, moduleReference: Tree): Tree =
27 | q"""new _root_.slinky.readwrite.Writer[$tpe] {
28 | def write(v: $tpe): _root_.scala.scalajs.js.Object = {
29 | _root_.scala.scalajs.js.Dynamic.literal()
30 | }
31 | }"""
32 |
33 | def createCaseClassTypeclass(clazz: Type, params: Seq[Seq[Param]]): Tree = {
34 | val paramsTrees = params.flatMap(_.map { p =>
35 | q"""{
36 | val writtenParam = ${getTypeclass(p.tpe)}.write(v.${p.name.toTermName})
37 | if (!_root_.scala.scalajs.js.isUndefined(writtenParam)) {
38 | ret.${TermName(p.name.encodedName.toString)} = writtenParam
39 | }
40 | }"""
41 | })
42 |
43 | q"""new _root_.slinky.readwrite.Writer[$clazz] {
44 | def write(v: $clazz): _root_.scala.scalajs.js.Object = {
45 | val ret = _root_.scala.scalajs.js.Dynamic.literal()
46 | ..$paramsTrees
47 | ret
48 | }
49 | }"""
50 | }
51 |
52 | def createValueClassTypeclass(clazz: Type, param: Param): Tree =
53 | q"""new _root_.slinky.readwrite.Writer[$clazz] {
54 | def write(v: $clazz): _root_.scala.scalajs.js.Object = {
55 | ${getTypeclass(param.tpe)}.write(v.${param.name.toTermName})
56 | }
57 | }"""
58 |
59 | def createSealedTraitTypeclass(traitType: Type, subclasses: Seq[Symbol]): Tree = {
60 | val cases = subclasses.map { sub =>
61 | cq"""(value: $sub) =>
62 | val ret = ${getTypeclass(sub.asType.toType)}.write(value)
63 | ret.asInstanceOf[_root_.scala.scalajs.js.Dynamic]._type = ${sub.name.toString}
64 | ret"""
65 | }
66 |
67 | q"""new _root_.slinky.readwrite.Writer[$traitType] {
68 | def write(v: $traitType): _root_.scala.scalajs.js.Object = {
69 | v match {
70 | case ..$cases
71 | case _ => _root_.slinky.readwrite.Writer.fallback[$traitType].write(v)
72 | }
73 | }
74 | }"""
75 | }
76 |
77 | def createFallback(forType: Type) = q"_root_.slinky.readwrite.Writer.fallback[$forType]"
78 | }
79 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/ExportedComponentTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.{React, ReactElement}
4 | import slinky.web.ReactDOM
5 |
6 | import scala.scalajs.js
7 | import org.scalajs.dom.document
8 |
9 | import org.scalatest.funsuite.AnyFunSuite
10 |
11 | object TestExportedComponentWithState extends ComponentWrapper {
12 | case class Props(name: String)
13 | type State = Int
14 |
15 | class Def(jsProps: js.Object) extends Definition(jsProps) {
16 | override def initialState: Int = 1
17 |
18 | override def render(): ReactElement = {
19 | s"${props.name} $state"
20 | }
21 | }
22 | }
23 |
24 | object TestExportedComponentStateless extends StatelessComponentWrapper {
25 | case class Props(name: String)
26 |
27 | class Def(jsProps: js.Object) extends Definition(jsProps) {
28 | override def render(): ReactElement = {
29 | s"${props.name}"
30 | }
31 | }
32 | }
33 |
34 | object TestExportedExternalComponent extends ExternalComponentNoProps {
35 | case class Props(children: Seq[ReactElement])
36 | val component = "div"
37 | }
38 |
39 | class ExportedComponentTest extends AnyFunSuite {
40 | test("Can construct an instance of an exported component with JS-provided props") {
41 | val container = document.createElement("div")
42 | ReactDOM.render(React.createElement(
43 | TestExportedComponentWithState: ReactComponentClass[_],
44 | js.Dictionary(
45 | "name" -> "lol"
46 | )
47 | ), container)
48 |
49 | assert(container.innerHTML == "lol 1")
50 | }
51 |
52 | test("Can construct an instance of a stateless exported component with JS-provided props") {
53 | val container = document.createElement("div")
54 | ReactDOM.render(React.createElement(
55 | TestExportedComponentStateless: ReactComponentClass[_],
56 | js.Dictionary(
57 | "name" -> "lol"
58 | )
59 | ), container)
60 |
61 | assert(container.innerHTML == "lol")
62 | }
63 |
64 | test("Can construct an instance of an exported functional component with JS-provided props") {
65 | case class FunctionalProps(name: String)
66 | val TestExportedFunctionalComponent = FunctionalComponent((p: FunctionalProps) => {
67 | p.name: ReactElement // FIXME - implicit conversion from string seems to not trigger in Scala 3
68 | })
69 |
70 | val container = document.createElement("div")
71 | ReactDOM.render(React.createElement(
72 | TestExportedFunctionalComponent: ReactComponentClass[_],
73 | js.Dictionary(
74 | "name" -> "lol"
75 | )
76 | ), container)
77 |
78 | assert(container.innerHTML == "lol")
79 | }
80 |
81 | test("Can construct an instance of an exported external component with JS-provided props") {
82 | val container = document.createElement("div")
83 | ReactDOM.render(React.createElement(
84 | TestExportedExternalComponent: ReactComponentClass[_],
85 | js.Dictionary(
86 | "children" -> js.Array("hello")
87 | )
88 | ), container)
89 |
90 | assert(container.innerHTML == "hello
")
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-2/slinky/readwrite/CoreReadersMacro.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.reflect.macros.whitebox
4 |
5 | trait MacroReaders {
6 | implicit def deriveReader[T]: Reader[T] = macro MacroReadersImpl.derive[T]
7 | }
8 |
9 | class MacroReadersImpl(_c: whitebox.Context) extends GenericDeriveImpl(_c) {
10 | import c.universe._
11 |
12 | val typeclassType: c.universe.Type = typeOf[Reader[_]]
13 |
14 | def deferredInstance(forType: c.universe.Type, constantType: c.universe.Type) =
15 | q"new _root_.slinky.readwrite.DeferredReader[$forType, $constantType]"
16 |
17 | def maybeExtractDeferred(tree: c.Tree): Option[c.Tree] =
18 | tree match {
19 | case q"new _root_.slinky.readwrite.DeferredReader[$_, $t]()" =>
20 | Some(t)
21 | case q"new slinky.readwrite.DeferredReader[$_, $t]()" =>
22 | Some(t)
23 | case _ => None
24 | }
25 |
26 | def createModuleTypeclass(tpe: c.universe.Type, moduleReference: c.Tree): c.Tree =
27 | q"""new _root_.slinky.readwrite.Reader[$tpe] {
28 | def forceRead(o: _root_.scala.scalajs.js.Object): $tpe = {
29 | $moduleReference
30 | }
31 | }"""
32 |
33 | def createCaseClassTypeclass(clazz: c.Type, params: Seq[Seq[Param]]): c.Tree = {
34 | val paramsTrees = params.map(_.map { p =>
35 | p.transformIfVarArg {
36 | p.default.map { d =>
37 | q"if (_root_.scala.scalajs.js.isUndefined(o.asInstanceOf[_root_.scala.scalajs.js.Dynamic].${p.name.toTermName})) $d else ${getTypeclass(p.tpe)}.read(o.asInstanceOf[_root_.scala.scalajs.js.Dynamic].${p.name.toTermName}.asInstanceOf[_root_.scala.scalajs.js.Object])"
38 | }.getOrElse {
39 | q"${getTypeclass(p.tpe)}.read(o.asInstanceOf[_root_.scala.scalajs.js.Dynamic].${p.name.toTermName}.asInstanceOf[_root_.scala.scalajs.js.Object])"
40 | }
41 | }
42 | })
43 |
44 | q"""new _root_.slinky.readwrite.Reader[$clazz] {
45 | def forceRead(o: _root_.scala.scalajs.js.Object): $clazz = {
46 | new $clazz(...$paramsTrees)
47 | }
48 | }"""
49 | }
50 |
51 | def createValueClassTypeclass(clazz: c.Type, param: Param): c.Tree =
52 | q"""new _root_.slinky.readwrite.Reader[$clazz] {
53 | def forceRead(o: _root_.scala.scalajs.js.Object): $clazz = {
54 | new $clazz(${getTypeclass(param.tpe)}.read(o))
55 | }
56 | }"""
57 |
58 | def createSealedTraitTypeclass(traitType: c.Type, subclasses: Seq[c.Symbol]): c.Tree = {
59 | val cases = subclasses.map(sub => cq"""${sub.name.toString} => ${getTypeclass(sub.asType.toType)}.read(o)""")
60 |
61 | q"""new _root_.slinky.readwrite.Reader[$traitType] {
62 | def forceRead(o: _root_.scala.scalajs.js.Object): $traitType = {
63 | o.asInstanceOf[_root_.scala.scalajs.js.Dynamic]._type.asInstanceOf[_root_.java.lang.String] match {
64 | case ..$cases
65 | case _ => _root_.slinky.readwrite.Reader.fallback[$traitType].read(o)
66 | }
67 | }
68 | }"""
69 | }
70 |
71 | def createFallback(forType: c.Type) = q"_root_.slinky.readwrite.Reader.fallback[$forType]"
72 | }
73 |
--------------------------------------------------------------------------------
/tests/src/test/scala-2/slinky/core/annotations/ReactAnnotatedFunctionalComponentTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core.annotations
2 |
3 | import slinky.core.FunctionalComponent
4 | import slinky.web.ReactDOM
5 |
6 | import org.scalajs.dom
7 | import org.scalatest.funsuite.AsyncFunSuite
8 |
9 |
10 | @react object SimpleFunctionalComponent {
11 | case class Props[T](in: Seq[T])
12 | val component = FunctionalComponent[Props[_]] { case Props(in) =>
13 | in.mkString(" ")
14 | }
15 | }
16 |
17 | @react object FunctionalComponentJustReExpose {
18 | val component = FunctionalComponent[Int] { in =>
19 | in.toString
20 | }
21 | }
22 |
23 | @react object FunctionalComponentWithPrivateValComponent {
24 | private val component = FunctionalComponent[Int] { in =>
25 | in.toString
26 | }
27 | }
28 |
29 | @react object FunctionalComponentWithProtectedValComponent {
30 | protected val component = FunctionalComponent[Int] { in =>
31 | in.toString
32 | }
33 | }
34 |
35 | @react object FunctionalComponentEmptyProps {
36 | case class Props()
37 | val component = FunctionalComponent[Props](_ => "test")
38 | }
39 |
40 | @react object FunctionalComponentUnitProps {
41 | type Props = Unit
42 | val component = FunctionalComponent[Props](_ => "test")
43 | }
44 |
45 | class ReactAnnotatedFunctionalComponentTest extends AsyncFunSuite {
46 | test("Simple component has generated apply") {
47 | val container = dom.document.createElement("div")
48 | ReactDOM.render(
49 | SimpleFunctionalComponent(in = Seq(1, 2, 3)),
50 | container
51 | )
52 |
53 | assert(container.innerHTML == "1 2 3")
54 | }
55 |
56 | test("Component without case class re-exports apply method") {
57 | val container = dom.document.createElement("div")
58 | ReactDOM.render(
59 | FunctionalComponentJustReExpose(1),
60 | container
61 | )
62 |
63 | assert(container.innerHTML == "1")
64 | }
65 |
66 | test("Component with private component definition works") {
67 | val container = dom.document.createElement("div")
68 | ReactDOM.render(
69 | FunctionalComponentWithPrivateValComponent(1),
70 | container
71 | )
72 |
73 | assert(container.innerHTML == "1")
74 | }
75 |
76 | test("Component with protected component definition works") {
77 | val container = dom.document.createElement("div")
78 | ReactDOM.render(
79 | FunctionalComponentWithProtectedValComponent(1),
80 | container
81 | )
82 |
83 | assert(container.innerHTML == "1")
84 | }
85 |
86 | test("Component with empty props has shortcut apply") {
87 | val container = dom.document.createElement("div")
88 | ReactDOM.render(
89 | FunctionalComponentEmptyProps(),
90 | container
91 | )
92 |
93 | assert(container.innerHTML == "test")
94 | }
95 |
96 | test("Component with unit props has shortcut apply") {
97 | val container = dom.document.createElement("div")
98 | ReactDOM.render(
99 | FunctionalComponentUnitProps(),
100 | container
101 | )
102 |
103 | assert(container.innerHTML == "test")
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/CoreReadersMacro.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.deriving._
4 | import scala.compiletime._
5 | import scalajs.js
6 | import scala.util.control.NonFatal
7 |
8 | trait MacroReaders {
9 | inline implicit def deriveReader[T]: Reader[T] = {
10 | summonFrom {
11 | case r: Reader[T] => r
12 | case vc: ExoticTypes.ValueClass[T] =>
13 | MacroReaders.ValueClassReader(vc, summonInline[Reader[vc.Repr]])
14 | case m: Mirror.ProductOf[T] => deriveProduct(m)
15 | case m: Mirror.SumOf[T] => deriveSum(m)
16 | case nu: ExoticTypes.NominalUnion[T] => MacroReaders.UnionReader(summonAll[Tuple.Map[nu.Constituents, Reader]])
17 | case _ => Reader.fallback[T]
18 | }
19 | }
20 |
21 | inline def deriveProduct[T](m: Mirror.ProductOf[T]): Reader[T] = {
22 | val labels = constValueTuple[m.MirroredElemLabels]
23 | val readers = summonAll[Tuple.Map[m.MirroredElemTypes, Reader]]
24 | val defaults = summonFrom {
25 | case d: ExoticTypes.DefaultConstructorParameters[T] => d.values
26 | case _ => null
27 | }
28 | MacroReaders.ProductReader(m, labels, readers, defaults)
29 | }
30 |
31 | inline def deriveSum[T](m: Mirror.SumOf[T]): Reader[T] = {
32 | val readers = summonAll[Tuple.Map[m.MirroredElemTypes, Reader]]
33 | MacroReaders.SumReader(readers)
34 | }
35 | }
36 |
37 | object MacroReaders {
38 | class ValueClassReader[T, R](vc: ExoticTypes.ValueClass[T] { type Repr = R }, reader: Reader[R]) extends Reader[T] {
39 | protected def forceRead(o: scala.scalajs.js.Object): T = vc.to(reader.read(o))
40 | }
41 |
42 | class UnionReader[T](readers: Tuple) extends Reader[T] {
43 | protected def forceRead(o: scala.scalajs.js.Object): T = {
44 | var lastEx: Throwable = null
45 | readers.productIterator.asInstanceOf[Iterator[Reader[T]]]
46 | .map { r => try { Some(r.read(o)) } catch { case NonFatal(ex) => lastEx = ex; None }}
47 | .collectFirst { case Some(a) => a }
48 | .getOrElse(throw lastEx)
49 | }
50 | }
51 |
52 | class ProductReader[T](m: Mirror.ProductOf[T], labels: Tuple, readers: Tuple, defaults: Array[Option[Any]]) extends Reader[T] {
53 | protected def forceRead(o: scala.scalajs.js.Object): T = {
54 | val dyn = o.asInstanceOf[js.Dictionary[js.Object]]
55 | m.fromProduct(new Product{
56 | def canEqual(that: Any) = this == that
57 | def productArity = readers.productArity
58 | def productElement(idx: Int): Any = {
59 | val key = labels.productElement(idx).asInstanceOf[String]
60 | def doRead = readers.productElement(idx).asInstanceOf[Reader[_]].read(dyn(key))
61 | if (!o.hasOwnProperty(key) && (defaults ne null)) {
62 | defaults(idx).getOrElse(doRead)
63 | } else {
64 | doRead
65 | }
66 | }
67 | })
68 | }
69 | }
70 |
71 | class SumReader[T](readers: Tuple) extends Reader[T] {
72 | protected def forceRead(o: scala.scalajs.js.Object): T = {
73 | val ord = o.asInstanceOf[js.Dynamic]._ord.asInstanceOf[Int]
74 | readers.productElement(ord).asInstanceOf[Reader[T]].read(o)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | Write Scala.js React apps just like you would in ES6
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Get started at [slinky.dev](https://slinky.dev)
16 |
17 | ## What is Slinky?
18 | Slinky is a framework for writing React apps in Scala with an experience just like using ES6.
19 |
20 | Slinky lets you:
21 | + Write React components in Scala with an API that mirrors vanilla React
22 | + Implement interfaces to other React libraries with automatic conversions between Scala and JS types
23 | + Write apps for React Native, React 360, and Electron, including the ability to share code with web apps
24 | + Develop apps iteratively with included hot-reloading support
25 |
26 | ## Contributing
27 | Slinky is split up into several submodules:
28 | + `core` contains the React.js facades and APIs for creating components and interfaces to external components
29 | + `web` contains bindings to React DOM and definitions for the HTML/SVG tag API
30 | + `reactrouter` contains bindings to React Router
31 | + `history` contains a facade for the HTML5 history API
32 | + `native` contains bindings to React Native and external component definitions for native UI elements
33 | + `vr` contains bindings to React 360 and external component definitions for VR UI elements
34 | + `readWrite` contains the `Reader` and `Writer` typeclasses used to persist state for hot reloading
35 | + `hot` contains the entrypoint for enabling hot-reloading
36 | + `scalajsReactInterop` implements automatic conversions between Slinky and Scala.js React types
37 | + `testRenderer` contains bindings to `react-test-renderer` for unit testing components
38 | + `coreIntellijSupport` contains IntelliJ-specific support for the `@react` macro annotation
39 | + `tests` contains the unit tests for the above modules (except native and vr which have local tests)
40 | + `docs` and `docsMacros` contains the documentation site, which is a Slinky app itself
41 |
42 | To run the main unit tests, first install the dependencies by running `npm install` inside the `tests` folder, then from the base folder run `sbt tests/test`. Similarly for React Native tests, run `npm install` inside the `native` folder, then from the base folder run `sbt native/test`.
43 |
44 | Note to IntelliJ IDEA users. When you try to import Slinky SBT definition in IDEA and encounter an exception like
45 | `java.nio.file.NoSuchFileException: /Users/someuser/.slinkyPluginIC/sdk/192.6817.14/plugins`, you should
46 | try to download required IntelliJ files for plugin subproject manually before importing:
47 |
48 | ```shell
49 | sbt coreIntellijSupport/updateIntellij
50 | ```
51 |
52 | And then import the project again.
53 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/Examples.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage
2 |
3 | import slinky.core.annotations.react
4 | import slinky.core.FunctionalComponent
5 | import slinky.docs.CodeExample
6 | import slinky.web.html._
7 |
8 | import scala.scalajs.js.Dynamic.literal
9 |
10 | @react object Examples {
11 | val component = FunctionalComponent[Unit] { _ =>
12 | div(
13 | div(style := literal(
14 | display = "flex",
15 | flexDirection = "row",
16 | justifyContent = "space-between",
17 | width = "100%",
18 | minHeight = "350px",
19 | marginTop = "35px"
20 | ))(
21 | div(style := literal(
22 | width = "30%"
23 | ))(
24 | h3("A Simple Component"),
25 | p(
26 | "Just like React, Slinky components implement a ",
27 | code("render()"),
28 | "method that returns what to display based on the input data, but also define a ", code("Props"), " type that defines the input data shape. ",
29 | "Slinky comes with a tags API for constructing HTML trees that gives a similar experience to other Scala libraries like ScalaTags but also includes additional type-safety requirements. ",
30 | "Input data that is passed into the component can be accessed by ", code("render()"), " via ", code("props")
31 | )
32 | ),
33 | div(style := literal(width = "65%", maxHeight = "450px"))(
34 | CodeExample("slinky.docs.homepage.HelloMessage")
35 | )
36 | ),
37 | div(style := literal(
38 | display = "flex",
39 | flexDirection = "row",
40 | justifyContent = "space-between",
41 | width = "100%",
42 | minHeight = "350px",
43 | marginTop = "35px"
44 | ))(
45 | div(style := literal(
46 | width = "30%"
47 | ))(
48 | h3("A Stateful Component"),
49 | p(
50 | "Slinky components, just like React components, can maintain internal state data (accessed with ", code("state"), ").",
51 | " When a component's state data changes after an invocation of ", code("setState"), ", the rendered markup will be update by re-invoking ", code("render()"), "."
52 | )
53 | ),
54 | div(style := literal(width = "65%", maxHeight = "450px"))(
55 | CodeExample("slinky.docs.homepage.Timer")
56 | )
57 | ),
58 | div(style := literal(
59 | display = "flex",
60 | flexDirection = "row",
61 | justifyContent = "space-between",
62 | width = "100%",
63 | minHeight = "350px",
64 | marginTop = "35px"
65 | ))(
66 | div(style := literal(
67 | width = "30%"
68 | ))(
69 | h3("An Application"),
70 | p(
71 | "Using ", code("props"), " and ", code("state"), ", we can put together a small Todo application. This example uses state to track the current list of items as well as the text that the user has entered."
72 | )
73 | ),
74 | div(style := literal(width = "65%", maxHeight = "450px"))(
75 | CodeExample("slinky.docs.homepage.TodoApp")
76 | )
77 | )
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/View.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.annotation.JSImport
9 | import scala.scalajs.js.|
10 |
11 | case class NativeTouchEvent(
12 | changedTouches: Seq[Any],
13 | identifier: String,
14 | locationX: Int,
15 | locationY: Int,
16 | pageX: Int,
17 | pageY: Int,
18 | target: String,
19 | timestamp: Int,
20 | touches: Seq[Any]
21 | )
22 |
23 | case class LayoutRectangle(x: Int, y: Int, width: Int, height: Int)
24 |
25 | case class LayoutChangeEvent(layout: LayoutRectangle)
26 |
27 | @react object View extends ExternalComponent {
28 | case class Props(
29 | onStartShouldSetResponder: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
30 | accessibilityLabel: js.UndefOr[ReactElement] = js.undefined,
31 | hitSlop: js.UndefOr[BoundingBox] = js.undefined,
32 | nativeID: js.UndefOr[String] = js.undefined,
33 | onLayout: js.UndefOr[NativeSyntheticEvent[LayoutChangeEvent] => Unit] = js.undefined,
34 | onMagicTap: js.UndefOr[() => Unit] = js.undefined,
35 | onMoveShouldSetResponder: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Boolean] = js.undefined,
36 | onMoveShouldSetResponderCapture: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Boolean] = js.undefined,
37 | onResponderGrant: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
38 | onResponderMove: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
39 | onResponderReject: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
40 | onResponderRelease: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
41 | onResponderTerminate: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Unit] = js.undefined,
42 | onResponderTerminationRequest: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Boolean] = js.undefined,
43 | accessible: js.UndefOr[Boolean] = js.undefined,
44 | onStartShouldSetResponderCapture: js.UndefOr[NativeSyntheticEvent[NativeTouchEvent] => Boolean] = js.undefined,
45 | pointerEvents: js.UndefOr[String] = js.undefined,
46 | removeClippedSubviews: js.UndefOr[Boolean] = js.undefined,
47 | style: js.UndefOr[js.Object] = js.undefined,
48 | testID: js.UndefOr[String] = js.undefined,
49 | accessibilityComponentType: js.UndefOr[String] = js.undefined,
50 | accessibilityLiveRegion: js.UndefOr[String] = js.undefined,
51 | collapsable: js.UndefOr[Boolean] = js.undefined,
52 | importantForAccessibility: js.UndefOr[String] = js.undefined,
53 | needsOffscreenAlphaCompositing: js.UndefOr[Boolean] = js.undefined,
54 | renderToHardwareTextureAndroid: js.UndefOr[Boolean] = js.undefined,
55 | accessibilityTraits: js.UndefOr[String | Seq[String]] = js.undefined,
56 | accessibilityViewIsModal: js.UndefOr[Boolean] = js.undefined,
57 | shouldRasterizeIOS: js.UndefOr[Boolean] = js.undefined
58 | )
59 |
60 | @js.native
61 | @JSImport("react-native", "View")
62 | object Component extends js.Object
63 |
64 | override val component = Component
65 | }
66 |
--------------------------------------------------------------------------------
/core/src/main/scala/slinky/core/ReactElementContainer.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import slinky.core.facade.ReactElement
4 |
5 | import scala.collection.immutable.{Iterable, Queue}
6 | import scala.concurrent.Future
7 | import scala.scalajs.js
8 | import scala.util.Try
9 |
10 | trait ReactElementContainer[F[_]] extends Any { self =>
11 | def map[A](fa: F[A])(f: A => ReactElement): F[ReactElement]
12 | }
13 |
14 | object ReactElementContainer {
15 | def apply[F[_]: ReactElementContainer]: ReactElementContainer[F] = implicitly[ReactElementContainer[F]]
16 |
17 | @inline implicit def function0Container: ReactElementContainer[Function0] = new ReactElementContainer[Function0] {
18 | override def map[A](fa: () => A)(f: A => ReactElement): () => ReactElement = () => f(fa())
19 | }
20 |
21 | @inline implicit def futureContainer: ReactElementContainer[Future] = new ReactElementContainer[Future] {
22 | import scala.concurrent.ExecutionContext.Implicits.global
23 | override def map[A](fa: Future[A])(f: A => ReactElement): Future[ReactElement] = fa.map(f)
24 | }
25 |
26 | @inline implicit def iterableContainer: ReactElementContainer[Iterable] = new ReactElementContainer[Iterable] {
27 | override def map[A](fa: Iterable[A])(f: A => ReactElement): Iterable[ReactElement] = fa.map(f)
28 | }
29 |
30 | @inline implicit def jsUndefOrContainer: ReactElementContainer[js.UndefOr] = new ReactElementContainer[js.UndefOr] {
31 | override def map[A](fa: js.UndefOr[A])(f: A => ReactElement): js.UndefOr[ReactElement] = fa.map(f)
32 | }
33 |
34 | @inline implicit def listContainer: ReactElementContainer[List] = new ReactElementContainer[List] {
35 | override def map[A](fa: List[A])(f: A => ReactElement): List[ReactElement] = fa.map(f)
36 | }
37 |
38 | @inline implicit def optionContainer: ReactElementContainer[Option] = new ReactElementContainer[Option] {
39 | override def map[A](fa: Option[A])(f: A => ReactElement): Option[ReactElement] = fa.map(f)
40 | }
41 |
42 | @inline implicit def queueContainer: ReactElementContainer[Queue] = new ReactElementContainer[Queue] {
43 | override def map[A](fa: Queue[A])(f: A => ReactElement): Queue[ReactElement] = fa.map(f)
44 | }
45 |
46 | @inline implicit def seqContainer: ReactElementContainer[Seq] = new ReactElementContainer[Seq] {
47 | override def map[A](fa: Seq[A])(f: A => ReactElement): Seq[ReactElement] = fa.map(f)
48 | }
49 |
50 | @inline implicit def setContainer: ReactElementContainer[Set] = new ReactElementContainer[Set] {
51 | override def map[A](fa: Set[A])(f: A => ReactElement): Set[ReactElement] = fa.map(f)
52 | }
53 |
54 | @inline implicit def someContainer: ReactElementContainer[Some] = new ReactElementContainer[Some] {
55 | override def map[A](fa: Some[A])(f: A => ReactElement): Some[ReactElement] = Some(fa.map(f).get)
56 | }
57 |
58 | @inline implicit def tryContainer: ReactElementContainer[Try] = new ReactElementContainer[Try] {
59 | override def map[A](fa: Try[A])(f: A => ReactElement): Try[ReactElement] = fa.map(f)
60 | }
61 |
62 | @inline implicit def vectorContainer: ReactElementContainer[Vector] = new ReactElementContainer[Vector] {
63 | override def map[A](fa: Vector[A])(f: A => ReactElement): Vector[ReactElement] = fa.map(f)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/CodeExample.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.core.FunctionalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 | import slinky.web.html._
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.Dynamic.literal
10 | import scala.language.experimental.macros
11 |
12 | @react object CodeExampleInternal {
13 | case class Props(codeText: String, demoElement: ReactElement)
14 |
15 | // from the reactjs.org theme
16 | val prismColors = js.Dictionary[js.Object](
17 | "hljs-comment" -> literal(color = "#999999"),
18 | "hljs-keyword" -> literal(color = "#c5a5c5"),
19 | "hljs-built_in" -> literal(color = "#5a9bcf"),
20 | "hljs-string" -> literal(color = "#8dc891"),
21 | "hljs-variable" -> literal(color = "#d7deea"),
22 | "hljs-title" -> literal(color = "#79b6f2"),
23 | "hljs-type" -> literal(color = "#FAC863"),
24 | "hljs-meta" -> literal(color = "#FAC863"),
25 | "hljs-strong" -> literal(fontWeight = 700),
26 | "hljs-emphasis" -> literal(fontStyle = "italic"),
27 | "hljs" -> literal(
28 | backgroundColor = "#282c34",
29 | color = "#ffffff",
30 | fontSize = "15px",
31 | lineHeight = "20px"
32 | ),
33 | "code[class*=\"language-\"]" -> literal(
34 | backgroundColor = "#282c34",
35 | color = "#ffffff"
36 | )
37 | )
38 |
39 | val component = FunctionalComponent[Props] { props =>
40 | div(style := literal(
41 | width = "100%",
42 | display = "flex",
43 | borderRadius = "10px",
44 | overflow = "hidden",
45 | border = "1px solid rgb(236, 236, 236)",
46 | height = "100%"
47 | ))(
48 | div(style := literal(
49 | width = "65%",
50 | height = "100%"
51 | ))(
52 | div(style := literal(
53 | width = "100%",
54 | display = "block",
55 | backgroundColor = "rgb(32, 35, 42)",
56 | padding = "10px",
57 | boxSizing = "border-box"
58 | ))(
59 | b(style := literal(color = "#999"))("SCALA CODE")
60 | ),
61 | div(style := literal(
62 | width = "100%",
63 | display = "block",
64 | padding = "10px",
65 | backgroundColor = "#282c34",
66 | boxSizing = "border-box",
67 | height = "calc(100% - 36px)",
68 | overflow = "auto"
69 | ))(
70 | SyntaxHighlighter(language = "scala", style = prismColors)(
71 | props.codeText
72 | )
73 | )
74 | ),
75 | div(style := literal(
76 | width = "35%",
77 | boxSizing = "border-box",
78 | borderLeft = "1px solid rgb(236, 236, 236)"
79 | ))(
80 | div(style := literal(
81 | backgroundColor = "rgb(236, 236, 236)",
82 | padding = "10px",
83 | boxSizing = "border-box"
84 | ))(
85 | b(style := literal(color = "rgb(109, 109, 109)"))("RESULT")
86 | ),
87 | div(style := literal(
88 | overflow = "auto",
89 | padding = "10px",
90 | boxSizing = "border-box"
91 | ))(props.demoElement)
92 | )
93 | )
94 | }
95 | }
96 |
97 | object CodeExample {
98 | def apply(exampleLocation: String): ReactElement = macro CodeExampleImpl.text
99 | }
100 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/homepage/Homepage.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs.homepage
2 |
3 | import slinky.core.StatelessComponent
4 | import slinky.core.annotations.react
5 | import slinky.docs.MainPageContent
6 | import slinky.web.html._
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.Dynamic.literal
10 | import scala.scalajs.js.annotation.JSImport
11 |
12 | import org.scalajs.dom
13 |
14 | @JSImport("resources/slinky-logo-horizontal.svg", JSImport.Default)
15 | @js.native
16 | object SlinkyHorizontalLogo extends js.Object
17 |
18 | @JSImport("resources/slinky-logo.svg", JSImport.Default)
19 | @js.native
20 | object SlinkyLogo extends js.Object
21 |
22 | @react class Homepage extends StatelessComponent {
23 | type Props = Unit
24 |
25 | override def componentDidMount(): Unit = {
26 | dom.window.scrollTo(0, 0)
27 | }
28 |
29 | def render() = {
30 | div(
31 | Jumbotron(()),
32 | MainPageContent(Seq(
33 | div(style := literal(
34 | width = "100%",
35 | overflow = "auto",
36 | marginTop = "40px"
37 | ))(
38 | div(style := literal(
39 | width = "100%",
40 | minWidth = "800px",
41 | display = "flex",
42 | flexDirection = "row",
43 | justifyContent = "space-around",
44 | ))(
45 | div(style := literal(width = "33%"))(
46 | h3(style := literal(fontWeight = 100))("Just like ES6"),
47 | p("Slinky has a strong focus on mirroring the ES6 API. This means that any documentation or examples for ES6 React can be easily applied to your Scala code."),
48 | p("There are no new patterns involved with using Slinky. Just write React apps like you would in any other language!")
49 | ),
50 | div(style := literal(width = "33%"))(
51 | h3(style := literal(fontWeight = 100))("Complete Interop"),
52 | p("Slinky provides straightforward APIs for using external components. Simply define the component's properties using standard Scala types and you're good to go!"),
53 | p("In addition, Slinky components can be used from JavaScript code, thanks to a built in Scala to JS mappings. This means that your favorite libraries like React Router work out of the box with Slinky!")
54 | ),
55 | div(style := literal(width = "33%"))(
56 | h3(style := literal(fontWeight = 100))("First-Class Dev Experience"),
57 | p("Writing web applications with Scala doesn't have to feel like a degraded development experience. Slinky comes ready with full integration with familiar tools like Webpack and React DevTools."),
58 | p("Slinky also comes with built-in support for hot-loading via Webpack, allowing you to make your code-test-repeat flow even faster!")
59 | )
60 | )
61 | ),
62 | hr(style := literal(
63 | height = "1px",
64 | marginBottom = "-1px",
65 | border = "none",
66 | borderBottom = "1px solid #ececec",
67 | marginTop = "40px"
68 | )),
69 | Examples(())
70 | ))
71 | )
72 | }
73 | }
74 |
75 | object Homepage {
76 | object Next {
77 | import slinky.core.ReactComponentClass
78 | import scala.scalajs.js.annotation.JSExportTopLevel
79 |
80 | @JSExportTopLevel(name = "component", moduleID = "index")
81 | def component(): ReactComponentClass[_] = Homepage
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/docs/public/docs/resources.md:
--------------------------------------------------------------------------------
1 | # Resources
2 | If you find a great article, talk, slides or project, related to Slinky,
3 | consider adding it to the list with your PR (`slinky/docs/public/docs/resources.md`).
4 |
5 | ## Tutorials
6 | Title | Author | Released | Slinky Version
7 | --- | --- | --- | ---
8 | [Slinky for React Part 1 (Play using Scala)](https://www.youtube.com/watch?v=oe14Hq_Uyv8) | Mark Lewis | Apr 2020 | 0.6.5
9 | [Slinky for React Part 2 (Play using Scala)](https://www.youtube.com/watch?v=Bv42oKOWJoI) | Mark Lewis | Apr 2020 | 0.6.5
10 | [React Task List with Slinky Part 1 (Play using Scala)](https://www.youtube.com/watch?v=VHyN-QPCNJo) | Mark Lewis | Apr 2020 | 0.6.5
11 | [React Task List with Slinky Part 2 (Play using Scala)](https://www.youtube.com/watch?v=f0DZ33k2kUQ) | Mark Lewis | Apr 2020 | 0.6.5
12 | [React Task List with Slinky Part 3 (Play using Scala](https://www.youtube.com/watch?v=PsWIqTGtZ0w) | Mark Lewis | Apr 2020 | 0.6.5
13 | [React Task List with Slinky Part 4 (Play using Scala)](https://www.youtube.com/watch?v=f1d2_gkLRCg) | Mark Lewis | Apr 2020 | 0.6.5
14 | [Livecoding: React Hooks Support in Slinky](https://www.youtube.com/watch?v=_VAxrJImF7E) | Shadaj Laddad | Feb 2019 | -
15 |
16 | ## Talks
17 | Title | Author | Released | Slinky Version
18 | --- | --- | --- | ---
19 | [Taming complex webapps with Scala and React](https://www.youtube.com/watch?v=pBtsgex2gUE) | Kavita Laddad | Nov 2019 | -
20 | [Slinky: a typesafe Scala interface to React](https://www.youtube.com/watch?v=AkMVfy_86HY) | Shadaj Laddad | Nov 2018 | -
21 | [Succeeding with Full Stack Scala](https://www.youtube.com/watch?v=G4GQIbzMfjU) | Ramnivas Laddad | Sep 2018 | -
22 | [Scala.js in production](https://www.youtube.com/watch?v=zU5_jXaM2Zs) | Ramnivas Laddad | Sep 2018 | -
23 | [Slinky A Modern Toolkit for Modern Apps](https://www.youtube.com/watch?v=8QK00wfQDGg) | Shadaj Laddad | Sep 2018 | -
24 |
25 | ## Blog Articles
26 | Title | Author | Released | Slinky Version
27 | --- | --- | --- | ---
28 | [Slinky doing React the Scala way](https://pme123.medium.com/slinky-doing-react-the-scala-way-f78ccf42bf8f) | Pascal Mengelt | Nov 2020 | 0.6.5
29 |
30 |
31 | ## Sample Projects
32 | Title | Author | Released | Slinky Version
33 | --- | --- | --- | ---
34 | [Demos for ScalablyTyped with Slinky flavour](https://github.com/ScalablyTyped/SlinkyDemos) | Øyvind Raddum Berg | Active | 0.6.5
35 | [Code for: Slinky doing React the Scala way](https://github.com/pme123/slinky-react-tutorial) | Pascal Mengelt | Nov 2020 | 0.6.5
36 | [A Simple Example using Slinky with Scalably Typed.](https://github.com/pme123/scalably-slinky-example) | Pascal Mengelt | Dec 2020 | 0.6.5
37 | [The famous TODO List as a client-server App](https://github.com/pme123/slinky-todos) | Pascal Mengelt | Dec 2020 | 0.6.6
38 |
39 |
40 | ## Projects using Slinky
41 | Title | Author | Released | Slinky Version
42 | --- | --- | --- | ---
43 | [Cazadescuentos App (pwa)](https://github.com/wiringbits/cazadescuentos/tree/master/pwa) | https://wiringbits.net | Active | 0.6.6
44 | [wiringbits scala-webapp-template](https://github.com/wiringbits/scala-webapp-template) | https://wiringbits.net | Active | 0.6.7
45 | [React / Binding.scala / html.scala Interoperability](https://github.com/Atry/ReactToBindingHtml.scala) | Bo Yang | Active | 0.6.8 (for Scala.js 0.6) / 0.7.3 (for Scala.js 1)
46 |
47 | ## Other
48 | Please add here stuff that does not fit any section of above. For example Cheat Sheets, Books etc.
49 |
50 | Title | Author | Released | Slinky Version
51 | --- | --- | --- | ---
52 | - | - | - | -
53 |
54 | ###
55 |
56 |
--------------------------------------------------------------------------------
/native/src/main/scala/slinky/native/Image.scala:
--------------------------------------------------------------------------------
1 | package slinky.native
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.concurrent.Future
8 | import scala.scalajs.js
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 | import scala.scalajs.js.JSConverters._
12 |
13 | case class ImageURISource(
14 | uri: js.UndefOr[String] = js.undefined,
15 | bundle: js.UndefOr[String] = js.undefined,
16 | method: js.UndefOr[String] = js.undefined,
17 | headers: js.UndefOr[js.Object] = js.undefined,
18 | body: js.UndefOr[String] = js.undefined,
19 | cache: js.UndefOr[String] = js.undefined,
20 | width: js.UndefOr[Int] = js.undefined,
21 | height: js.UndefOr[Int] = js.undefined,
22 | scale: js.UndefOr[Double] = js.undefined
23 | )
24 |
25 | @js.native
26 | trait ImageInterface extends js.Object
27 |
28 | case class ImageErrorEvent(error: js.Error)
29 |
30 | case class ImageProgressEvent(loaded: Int, total: Int)
31 |
32 | @react object Image extends ExternalComponent {
33 | case class Props(
34 | style: js.UndefOr[js.Object] = js.undefined,
35 | blurRadius: js.UndefOr[Int] = js.undefined,
36 | onLayout: js.UndefOr[NativeSyntheticEvent[LayoutChangeEvent] => Unit] = js.undefined,
37 | onLoad: js.UndefOr[() => Unit] = js.undefined,
38 | onLoadEnd: js.UndefOr[() => Unit] = js.undefined,
39 | onLoadStart: js.UndefOr[() => Unit] = js.undefined,
40 | resizeMode: js.UndefOr[String] = js.undefined,
41 | source: js.UndefOr[ImageURISource | js.Object | Seq[ImageURISource | js.Object]] = js.undefined,
42 | loadingIndicatorSource: js.UndefOr[Seq[ImageURISource | Int]] = js.undefined,
43 | onError: js.UndefOr[NativeSyntheticEvent[ImageErrorEvent] => Unit] = js.undefined,
44 | testID: js.UndefOr[String] = js.undefined,
45 | resizeMethod: js.UndefOr[String] = js.undefined,
46 | accessibilityLabel: js.UndefOr[ReactElement] = js.undefined,
47 | accessible: js.UndefOr[Boolean] = js.undefined,
48 | capInsets: js.UndefOr[BoundingBox] = js.undefined,
49 | defaultSource: js.UndefOr[js.Object | Int] = js.undefined,
50 | onPartialLoad: js.UndefOr[() => Unit] = js.undefined,
51 | onProgress: js.UndefOr[NativeSyntheticEvent[ImageProgressEvent] => Unit] = js.undefined
52 | )
53 |
54 | @js.native
55 | @JSImport("react-native", "Image")
56 | object Component extends js.Object {
57 | def getSize(
58 | uri: String,
59 | success: js.Function2[Int, Int, Unit],
60 | failure: js.UndefOr[js.Function1[js.Error, Unit]]
61 | ): Unit = js.native
62 | def prefetch(uri: String): Int | Unit = js.native
63 | def abortPrefetch(requestId: Int): Unit = js.native
64 | def queryCache(urls: js.Array[String]): js.Promise[js.Dictionary[String]] = js.native
65 | }
66 |
67 | override val component = Component
68 |
69 | def getSize(uri: String, success: (Int, Int) => Unit, failure: js.UndefOr[(js.Error) => Unit] = js.undefined): Unit =
70 | Component.getSize(uri, success, failure.map(v => v))
71 |
72 | def prefetch(uri: String): Int | Unit = Component.prefetch(uri)
73 |
74 | def abortPrefetch(requestId: Int): Unit = Component.abortPrefetch(requestId)
75 |
76 | def queryCache(urls: Seq[String]): Future[Map[String, String]] = {
77 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
78 | Component.queryCache(urls.toJSArray).toFuture.map(_.toMap)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contact@slinky.dev. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/docs/src/main/scala/slinky/docs/Navbar.scala:
--------------------------------------------------------------------------------
1 | package slinky.docs
2 |
3 | import slinky.core.FunctionalComponent
4 | import slinky.core.annotations.react
5 | import slinky.docs.homepage.SlinkyHorizontalLogo
6 | import slinky.next.{Image, Link}
7 | import slinky.web.html._
8 |
9 | import scala.scalajs.js
10 |
11 | @react object Navbar {
12 | val linkStyle = js.Dynamic.literal(
13 | color = "white",
14 | fontSize = "21px",
15 | fontWeight = 300,
16 | marginLeft = "30px",
17 | letterSpacing = "1.3px",
18 | textDecoration = "none"
19 | )
20 |
21 | val smallLinkStyle = js.Dynamic.literal(
22 | color = "white",
23 | fontSize = "15px",
24 | fontWeight = 200,
25 | marginLeft = "30px",
26 | textDecoration = "none"
27 | )
28 |
29 | val component = FunctionalComponent[Unit](_ => {
30 | header(style := js.Dynamic.literal(
31 | width = "100%",
32 | position = "fixed",
33 | top = 0,
34 | left = 0,
35 | backgroundColor = "#20232a"
36 | ))(
37 | div(
38 | style := js.Dynamic.literal(
39 | display = "flex",
40 | minHeight = "60px",
41 | flexDirection = "row",
42 | alignItems = "center",
43 | justifyContent = "space-between",
44 | maxWidth = "calc(min(1400px, 100vw - 80px))",
45 | marginLeft = "auto",
46 | marginRight = "auto",
47 | overflowX = "auto"
48 | )
49 | )(
50 | div(
51 | style := js.Dynamic.literal(
52 | display = "flex",
53 | height = "100%",
54 | alignItems = "center",
55 | minWidth = "150px",
56 | )
57 | )(
58 | Link(href = "/")(
59 | a(style := js.Dynamic.literal(
60 | marginRight = "50px"
61 | ))(
62 | Image(src = SlinkyHorizontalLogo, layout = "raw", priority = true, loader = (a: js.Dynamic) => a.src)(
63 | style := js.Dynamic.literal(
64 | height = "50px",
65 | width = "auto"
66 | )
67 | )
68 | )
69 | )
70 | ),
71 | div(
72 | style := js.Dynamic.literal(
73 | display = "flex",
74 | height = "100%",
75 | alignItems = "center",
76 | marginRight = "auto"
77 | )
78 | )(
79 | Link(href = "/docs/installation/")(a(style := linkStyle)(
80 | "Docs"
81 | ))
82 | ),
83 | div(
84 | style := js.Dynamic.literal(
85 | display = "flex",
86 | height = "100%",
87 | alignItems = "center",
88 | marginRight = "20px"
89 | ),
90 | className := "sidebar-right"
91 | )(
92 | a(
93 | href := "https://gitter.im/shadaj/slinky",
94 | style := smallLinkStyle
95 | )(
96 | "Community"
97 | ),
98 | Link(
99 | href = "/docs/resources/"
100 | )(a(style := smallLinkStyle)(
101 | "Resources"
102 | )),
103 | a(
104 | href := "https://github.com/shadaj/slinky/blob/main/CHANGELOG.md",
105 | style := smallLinkStyle
106 | )(
107 | "v0.7.5"
108 | ),
109 | a(
110 | href := "https://github.com/shadaj/slinky",
111 | style := smallLinkStyle
112 | )(
113 | "GitHub"
114 | )
115 | )
116 | )
117 | )
118 | })
119 | }
120 |
--------------------------------------------------------------------------------
/readWrite/src/main/scala-3/slinky/readwrite/CoreWritersMacro.scala:
--------------------------------------------------------------------------------
1 | package slinky.readwrite
2 |
3 | import scala.deriving._
4 | import scala.compiletime._
5 | import scalajs.js
6 | import scala.reflect.ClassTag
7 | import scala.util.control.NonFatal
8 |
9 | trait MacroWriters {
10 | inline implicit def deriveWriter[T]: Writer[T] = {
11 | summonFrom {
12 | case w: Writer[T] => w
13 | case vc: ExoticTypes.ValueClass[T] =>
14 | MacroWriters.ValueClassWriter(vc, summonInline[Writer[vc.Repr]])
15 | case m: Mirror.ProductOf[T] => deriveProduct(m)
16 | case m: Mirror.SumOf[T] => deriveSum(m)
17 | case nu: ExoticTypes.NominalUnion[T] =>
18 | MacroWriters.UnionWriter(
19 | summonAll[Tuple.Map[nu.Constituents, Writer]],
20 | summonAll[Tuple.Map[nu.Constituents, ClassTag]]
21 | )
22 | case _ => Writer.fallback[T]
23 | }
24 | }
25 |
26 | inline def deriveProduct[T](m: Mirror.ProductOf[T]): Writer[T] = {
27 | val labels = constValueTuple[m.MirroredElemLabels]
28 | val writers = summonAll[Tuple.Map[m.MirroredElemTypes, Writer]]
29 | MacroWriters.ProductWriter(labels, writers)
30 | }
31 |
32 | inline def deriveSum[T](m: Mirror.SumOf[T]): Writer[T] = {
33 | val labels = constValueTuple[m.MirroredElemLabels]
34 | val writers = summonAll[Tuple.Map[m.MirroredElemTypes, Writer]]
35 | MacroWriters.SumWriter(labels, writers, m.ordinal)
36 | }
37 | }
38 |
39 | object MacroWriters {
40 | class ValueClassWriter[T, R](vc: ExoticTypes.ValueClass[T] { type Repr = R }, w: Writer[R]) extends Writer[T] {
41 | def write(p: T): js.Object = w.write(vc.from(p))
42 | }
43 |
44 | class UnionWriter[T](writers: Tuple, classTags: Tuple) extends Writer[T] {
45 | def write(p: T): js.Object =
46 | classTags.productIterator.indexWhere(_.asInstanceOf[ClassTag[_]].runtimeClass == p.getClass) match {
47 | case -1 =>
48 | var lastEx: Throwable = null
49 | writers.productIterator.asInstanceOf[Iterator[Writer[T]]]
50 | .map { w => try { Some(w.write(p)) } catch { case NonFatal(e) => lastEx = e; None } }
51 | .collectFirst { case Some(obj) => obj }
52 | .getOrElse { throw lastEx }
53 | case other => writers.productElement(other).asInstanceOf[Writer[T]].write(p)
54 | }
55 | }
56 |
57 | class ProductWriter[T](labels: Tuple, writers: Tuple) extends Writer[T] {
58 | def write(p: T): js.Object = {
59 | val d = js.Dictionary[js.Object]()
60 | labels.productIterator
61 | .zip(writers.productIterator)
62 | .zip(p.asInstanceOf[Product].productIterator)
63 | .foreach { case ((label, writer), value) =>
64 | val written = writer.asInstanceOf[Writer[_]].write(value.asInstanceOf)
65 | if (!js.isUndefined(written)) {
66 | d(label.asInstanceOf[String]) = written
67 | }
68 | }
69 | d.asInstanceOf[js.Object]
70 | }
71 | }
72 |
73 | class SumWriter[T](labels: Tuple, writers: Tuple, ordinal: T => Int) extends Writer[T] {
74 | def write(p: T): js.Object = {
75 | // n.b. using function instead of full-fledged mirror b/c scala3-sjs somehow manages
76 | // to replace the path-dependent m.MirroredMonoType with garbage like org.scalatest.Exceptional
77 | // for no good reason
78 | val ord = ordinal(p)
79 | val typ = labels.productElement(ord)
80 | val base = writers.productElement(ord).asInstanceOf[Writer[T]].write(p)
81 | base.asInstanceOf[js.Dynamic]._type = typ.asInstanceOf[js.Any]
82 | base.asInstanceOf[js.Dynamic]._ord = ord
83 | base
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/src/test/scala/slinky/core/FunctionalComponentTest.scala:
--------------------------------------------------------------------------------
1 | package slinky.core
2 |
3 | import org.scalajs.dom.document
4 | import slinky.core.facade.{React, ReactElement}
5 | import slinky.web.ReactDOM
6 |
7 | import org.scalatest.funsuite.AnyFunSuite
8 |
9 | class FunctionalComponentTest extends AnyFunSuite {
10 | test("Can render a functional component") {
11 | val container = document.createElement("div")
12 | val component = FunctionalComponent[Int](_.toString)
13 | ReactDOM.render(component(1), container)
14 |
15 | assert(container.innerHTML == "1")
16 | }
17 |
18 | test("Re-rendering a memoed component with same props works") {
19 | val container = document.createElement("div")
20 | var renderCount = 0
21 | case class Props(a: Int)
22 | val component = React.memo(FunctionalComponent[Props] { props =>
23 | renderCount += 1
24 | props.a.toString
25 | })
26 |
27 | val inProps = Props(1)
28 | ReactDOM.render(component(inProps), container)
29 | assert(container.innerHTML == "1")
30 | assert(renderCount == 1)
31 |
32 | ReactDOM.render(component(inProps), container)
33 | assert(container.innerHTML == "1")
34 | assert(renderCount == 1)
35 | }
36 |
37 | test("Re-rendering a memoed component with different props works") {
38 | val container = document.createElement("div")
39 | var renderCount = 0
40 | case class Props(a: Int)
41 | val component = React.memo(FunctionalComponent[Props] { props =>
42 | renderCount += 1
43 | props.a.toString
44 | })
45 |
46 | val inProps = Props(1)
47 | ReactDOM.render(component(inProps), container)
48 | assert(container.innerHTML == "1")
49 | assert(renderCount == 1)
50 |
51 | ReactDOM.render(component(inProps.copy(a = 2)), container)
52 | assert(container.innerHTML == "2")
53 | assert(renderCount == 2)
54 | }
55 |
56 | test("Re-rendering a memoed component with matching comparison works") {
57 | val container = document.createElement("div")
58 | var renderCount = 0
59 | case class Props(a: Int, ignore: Int)
60 | val component = React.memo(FunctionalComponent[Props] { props =>
61 | renderCount += 1
62 | props.a.toString
63 | }, (oldProps: Props, newProps: Props) => oldProps.a == newProps.a)
64 |
65 | val inProps = Props(1, 2)
66 | ReactDOM.render(component(inProps), container)
67 | assert(container.innerHTML == "1")
68 | assert(renderCount == 1)
69 |
70 | ReactDOM.render(component(inProps.copy(ignore = 3)), container)
71 | assert(container.innerHTML == "1")
72 | assert(renderCount == 1)
73 | }
74 |
75 | test("Re-rendering a memoed component with non-matching comparison works") {
76 | val container = document.createElement("div")
77 | var renderCount = 0
78 | case class Props(a: Int)
79 | val component = React.memo(FunctionalComponent[Props] { props =>
80 | renderCount += 1
81 | props.a.toString
82 | }, (oldProps: Props, newProps: Props) => oldProps.a == newProps.a)
83 |
84 | val inProps = Props(1)
85 | ReactDOM.render(component(inProps), container)
86 | assert(container.innerHTML == "1")
87 | assert(renderCount == 1)
88 |
89 | ReactDOM.render(component(inProps.copy(a = 2)), container)
90 | assert(container.innerHTML == "2")
91 | assert(renderCount == 2)
92 | }
93 |
94 | test("Cannot reuse half-built functional component") {
95 | val component = FunctionalComponent[Int](_.toString)
96 | val halfBuilt = component(1)
97 | halfBuilt.withKey("abc"): ReactElement
98 |
99 | assertThrows[IllegalStateException] {
100 | halfBuilt.withKey("abc2"): ReactElement
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/docs/public/docs/the-tag-api.md:
--------------------------------------------------------------------------------
1 | # The Tag API
2 | When rendering HTML or SVG elements in Slinky, you're using the Slinky Tag API, which makes it possible to represent content trees in idiomatic Scala code.
3 |
4 | If you've used libraries like ScalaTags before, you'll find the Slinky API very familiar, as it follows the same general tree-building style as other Scala tags libraries.
5 |
6 | ## Rendering Elements
7 | Let's get started with rendering a simple HTML element!
8 |
9 | First, we import all HTML tags from the `web` module:
10 | ```scala
11 | import slinky.web.html._
12 | ```
13 |
14 | Now, we can render the element:
15 | ```scala
16 | div("hello")
17 | ```
18 |
19 | Slinky tags always take `ReactElement`s as children, but these element instances can created in various ways with built in implicit conversions.
20 | 1) Other tags: `h1("I am a child element!")`
21 | 2) Rendering a React component: `MyComponent()`
22 | 3) A string: `"hello"`
23 | 4) Scala collection types containing other React Elements: `List("hello", "world")`
24 |
25 | ## Adding Attributes
26 | In addition to containing other children, Slinky tags can be assigned attributes that will turn into HTML or SVG attributes at runtime
27 |
28 | For example, we can set the HTML class of an element using the `className` attribute:
29 | ```scala
30 | h1(className := "header") // turns into
31 | ```
32 |
33 | When combined with children, attributes generally come first and children follow in a separate argument list:
34 | ```scala
35 | h1(className := "header")(
36 | "Header child element 1",
37 | "Header child element 2"
38 | )
39 | ```
40 |
41 | When using the `data-` and `aria-` attributes, you can pass in the suffix as a string immediately following the `-`. For example, you could pass in a `data-columns` attribute as:
42 | ```scala
43 | div(data-"columns" := "3")
44 | ```
45 |
46 | ### Event Listeners
47 | To add event listeners to elements, you can pass in an attribute pair assigning an event to a handler function. In Slinky, the event value is a `SyntheticEvent` that wraps around a native [Scala.js DOM](https://github.com/scala-js/scala-js-dom) event and an element reference whose type matches the tag the handler is being attached to.
48 |
49 | ```scala
50 | input(onChange := (event => {
51 | println("the value of this input element was changed!")
52 | }))
53 | ```
54 |
55 | Scala.js even handles the process of binding functions to the appropriate scope, so there's no need to worry about where the event handler is implemented!
56 |
57 | ### Optional Attributes
58 |
59 | Slinky supports the use of the Option type to indicate where an attribute is optional. For example:
60 |
61 | ```scala
62 | h1(className := Some("header"))
63 | h2(className := None)
64 | ```
65 | Would be rendered as:
66 | ```html
67 |
68 |
69 | ```
70 |
71 | ### Styles
72 | When attaching CSS styles to an element, Slinky follows the React API of having the `style` be a JavaScript object and provides an attribute that can be assigned to a `js.Dynamic` value. This different from other Scala tags libraries, which usually provide individual attributes for assigning style values.
73 | ```scala
74 | h1(style := js.Dynamic.literal(
75 | fontSize = "30px"
76 | ))(
77 | "hello!"
78 | )
79 | ```
80 |
81 | ### Special Attributes
82 | Slinky supports the React special attributes `key` and `ref`.
83 |
84 | `key` gives a hint of matching components in an array to React and is always a string.
85 | ```scala
86 | div(key := "my-key")("hello")
87 | ```
88 |
89 | `ref` allows you to gain access to an instance of the rendered DOM element. Slinky only supports the functional ref style, where the value of `ref` is a function that takes the DOM node instance.
90 | ```scala
91 | div(ref := (elem => {
92 | // ...something with the DOM element elem
93 | }))("hello")
94 | ```
95 |
--------------------------------------------------------------------------------
/.github/workflows/sbt.yml:
--------------------------------------------------------------------------------
1 | name: Slinky CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | release:
9 | types: [published]
10 |
11 | jobs:
12 | test:
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | os: [ubuntu-latest, windows-latest]
18 | scalajs: ["1.16.0"]
19 | es2015_enabled: ["false", "true"]
20 | steps:
21 | - name: Configure git to disable Windows line feeds
22 | run: "git config --global core.autocrlf false"
23 | shell: bash
24 | - uses: actions/checkout@master
25 | - name: Set up JDK 1.8 and SBT
26 | uses: olafurpg/setup-scala@v10
27 | with:
28 | java-version: 1.8
29 | - name: Style checks
30 | run: sbt styleCheck
31 | - name: Install NPM Dependencies
32 | run: npm install; cd tests; npm install; cd ..; cd native; npm install; cd ..; cd scalajsReactInterop; npm install; cd ..
33 | shell: bash
34 | - name: Test core and native (fastopt + fullopt)
35 | run: sbt +tests/test +native/test "set scalaJSStage in Global := FullOptStage" +tests/test +native/test
36 | env:
37 | SCALAJS_VERSION: ${{ matrix.scalajs }}
38 | ES2015_ENABLED: ${{ matrix.es2015_enabled }}
39 | shell: bash
40 | - name: Test Scala.js React Interop (fastopt + fullopt)
41 | run: sbt scalajsReactInterop/test "set scalaJSStage in Global := FullOptStage" scalajsReactInterop/test
42 | env:
43 | SCALAJS_VERSION: ${{ matrix.scalajs }}
44 | ES2015_ENABLED: ${{ matrix.es2015_enabled }}
45 | shell: bash
46 | build-docs:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - uses: actions/checkout@master
50 | - name: Set up JDK 1.8 and SBT
51 | uses: olafurpg/setup-scala@v10
52 | with:
53 | java-version: 1.8
54 | - name: Build Docs Site
55 | run: sbt docs/fullLinkJS
56 | - name: Export Next.js
57 | run: cd docs && npm install && npm run export
58 | build-intellij-plugin:
59 | runs-on: ubuntu-latest
60 | steps:
61 | - uses: actions/checkout@master
62 | - name: Set up JDK 1.8 and SBT
63 | uses: olafurpg/setup-scala@v10
64 | with:
65 | java-version: 1.8
66 | - name: Build IntelliJ Plugin
67 | run: sbt coreIntellijSupport/updateIntellij coreIntellijSupport/compile
68 | publish:
69 | needs: [test, build-docs, build-intellij-plugin]
70 | runs-on: ubuntu-latest
71 | steps:
72 | - uses: actions/checkout@master
73 | - name: Set up JDK 1.8 and SBT
74 | uses: olafurpg/setup-scala@v10
75 | with:
76 | java-version: 1.8
77 | - run: git fetch --unshallow
78 | - name: Publish with SBT
79 | run: export JAVA_OPTS="-Xmx4g" && bash ./publish.sh
80 | if: github.ref == 'refs/heads/main' || github.event_name == 'release'
81 | env:
82 | encrypted_key: ${{ secrets.key }}
83 | encrypted_iv: ${{ secrets.iv }}
84 | PGP_PASSPHRASE: ${{ secrets.pgp_passphrase }}
85 | publish-intellij-plugin:
86 | needs: [test, build-docs, build-intellij-plugin]
87 | runs-on: ubuntu-latest
88 | steps:
89 | - uses: actions/checkout@master
90 | - name: Set up JDK 1.8 and SBT
91 | uses: olafurpg/setup-scala@v10
92 | with:
93 | java-version: 1.8
94 | - run: git fetch --unshallow
95 | - name: Publish to Marketplace
96 | run: sbt coreIntellijSupport/updateIntellij coreIntellijSupport/packageArtifact coreIntellijSupport/packageArtifact coreIntellijSupport/publishAutoChannel
97 | if: github.ref == 'refs/heads/main' || github.event_name == 'release'
98 | env:
99 | IJ_PLUGIN_REPO_TOKEN: ${{ secrets.ij_plugin_repo_token }}
100 |
--------------------------------------------------------------------------------