├── .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 | --------------------------------------------------------------------------------