From 93b0138074799c46790ffe14e5df4fcd336f21f9 Mon Sep 17 00:00:00 2001 From: aappddeevv Date: Sun, 28 Aug 2022 07:34:58 -0400 Subject: [PATCH] scala3 ongoing updates --- .gitignore | 1 + .scalafmt.conf | 4 +- README.md | 20 ++-- build.sbt | 46 +++++---- .../src/main/scala/components/Label.scala | 9 +- .../mssql/src/main/scala/mssql/package.scala | 6 +- .../src/main/scala/node-fetch.scala | 4 +- .../node-fetch/src/main/scala/package.scala | 32 +++--- jshelpers/src/main/scala/any.scala | 11 ++- jshelpers/src/main/scala/dynamic.scala | 2 +- jshelpers/src/main/scala/instances.scala | 4 +- jshelpers/src/main/scala/misc.scala | 1 - jshelpers/src/main/scala/null.scala | 54 ++++++----- jshelpers/src/main/scala/object.scala | 3 +- jshelpers/src/main/scala/promise.scala | 18 ++-- jshelpers/src/main/scala/syntax.scala | 1 + jshelpers/src/main/scala/undefor.scala | 26 ++--- project/build.properties | 2 +- project/plugins.sbt | 14 +-- project/project/metals.sbt | 6 -- project/project/project/metals.sbt | 6 -- react-dom/src/main/scala/package.scala | 2 +- react/src/main/scala/When.scala | 16 +-- react/src/main/scala/extras.scala | 97 +++++++++---------- react/src/main/scala/package.scala | 62 +++++++----- react/src/main/scala/react.scala | 37 ++++--- react/src/main/scala/reactjs.scala | 44 ++++----- react/src/main/scala/syntax/conversions.scala | 2 +- react/src/test/scala/EffectArgTests.scala | 22 +++++ .../scalajs-reaction/docs/misc/bundling.md | 49 ++++++++++ 30 files changed, 352 insertions(+), 249 deletions(-) delete mode 100644 project/project/metals.sbt delete mode 100644 project/project/project/metals.sbt create mode 100644 react/src/test/scala/EffectArgTests.scala diff --git a/.gitignore b/.gitignore index ed889f12..4df52232 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ scalajs-reaction-docs /*.yml .vscode .ipy* +metals.sbt diff --git a/.scalafmt.conf b/.scalafmt.conf index 4271d229..c1235c69 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,9 +1,11 @@ -version = "3.0.0-RC3" +version = "3.0.6" maxColumn = 120 #align = most continuationIndent.defnSite = 2 assumeStandardLibraryStripMargin = true docstrings = ScalaDoc +docstrings.wrapMaxColumn=80 +docstrings.style = Asterisk lineEndings = preserve includeCurlyBraceInSelectChains = false danglingParentheses = false diff --git a/README.md b/README.md index 75164137..1088b4a9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@

Use react hooks and scala.js to catch the best user experience.

-![scalajs-reaction](https://img.shields.io/maven-central/v/org.ttgoss.js/react_sjs1_3?versionPrefix=1.0.0-RC1) ![scala](https://img.shields.io/maven-central/v/org.scala-lang/scala3-compiler_3?versionSuffix=3.1.0) +scalajs-reaction: ![scalajs-reaction](https://img.shields.io/maven-central/v/org.ttgoss.js/react_sjs1_3?versionPrefix=1.0.0-RC1), scala: ![scala](https://img.shields.io/maven-central/v/org.scala-lang/scala3-compiler_3?versionSuffix=3.1.0) -scalajs reaction focuses on scala3 and easier integration into the js world. +scalajs reaction focuses on scala3 and interop with the react js world. Use react version 18+ and/or the experimental to ensure that all hooks defined in this library are included in the underlying js source. Use the latest react-native. Get started with the [docs](http://aappddeevv.github.io/scalajs-reaction) -Tiny example to declare a react component: +Here's how easy it is to declare a function component. ```scala val HelloWorld: ReactFC0 = () => div("hello world") @@ -17,6 +17,8 @@ val HelloWorld: ReactFC0 = () => div("hello world") # What is scalajs-reaction? +An interop library with reactjs and the react ecosystem. + This library is small and focuses on simple js function components and hooks. Hooks are described on the [react](https://reactjs.org/docs/hooks-reference.html) page. @@ -35,14 +37,14 @@ scalajs-reaction emphasizes: `scalajs-reaction` allows you to build your entire interface in scalajs-reaction. As long as your front-end solution can manage the model of -scala.js's output, you should consider scalajs-reaction for your solution. By providing a thin veneer over standard scala functions and hooks, it eschews abstractions and avoids getting in your way. +compiler scala.js's output, you should consider scalajs-reaction for your solution. By providing a thin veneer over standard scala functions and hooks, it eschews abstractions and avoids getting in your way. - [Demo (WIP)](http://aappddeevv.github.io/scalajs-reaction/demo/index.html). - [Live Coding](https://www.youtube.com/watch?v=7on-oT2Naco): Uses the old API but still helpful. -The react-native use-case for scala.js may actually be more +The react-native use-case for scala.js may be more compelling than for web applications. -Support is provided for the experimental, concurrent API. +Support is provided for the experimental, reactjs concurrent API. A g8 template is available. Use `sbt new aappddeevv/scalajs-reaction-app.g8` (in transition to scala3) to create a new project. @@ -166,7 +168,7 @@ react function components are not pure functions and can never be pure functions like a "context" that is established for some part of the DOM that also returns HTML builder instructions each time it is called. If you come from the java Spring world, think of it as a bean factory that knows how to emit render instructions. -It is better to think of react components not as a function but as an object. +It is better to think of react function components as an object and not as a function. # Usage @@ -292,8 +294,8 @@ complicated. You are likely only to use one set of react libraries per application so you should not encounter any package namespace collisions. -In many cases, the full package label has been dramatically shortened -to make it easier to import the content you need. The package names +In many cases, the full package label has been shortened +to make it easier to import the content. The package names closely mirror the javascript libraries themselves. You will most likely also need DOM bindings. Here's a link to the api diff --git a/build.sbt b/build.sbt index 72c01e99..63801a8c 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges lazy val resolverSettings = Seq( resolvers ++= Seq( - Resolver.sonatypeRepo("releases"), + //Resolver.sonatypeRepo("releases"), //Resolver.jcenterRepo ) ) @@ -23,8 +23,11 @@ val commonScalacOptions = Seq( //"-Ywarn-value-discard", //"-Ywarn-unused:imports,locals", //,"-Ywarn-dead-code" - "source", - "future", + "-indent", + "-source", + "3.1", + "-new-syntax", + "-explain", "-Ysafe-init", "-Yexplicit-nulls", //"-language:unsafeNulls", @@ -36,7 +39,7 @@ lazy val jsSettings = Seq( scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, scalaModuleInfo ~= (_.map(_.withOverrideScalaVersion(true))), libraryDependencies ++= Seq( - ("org.scala-js" %%% "scalajs-dom" % "2.0.0"), + ("org.scala-js" %%% "scalajs-dom" % "2.2.0"), ), // testing libraryDependencies += "com.lihaoyi" %%% "utest" % "0.7.10" % "test", @@ -51,9 +54,9 @@ def buildinfo_settings(pkg: String) = ) lazy val compilerSettings = Seq( - scalacOptions in (Compile, doc) ++= Seq("-groups"), + // not sure what this does anymore so removed it + //(doc / Compile / scalacOptions) ++= Seq("-groups"), scalacOptions ++= commonScalacOptions, - //addCompilerPlugin(scalafixSemanticdb), autoAPIMappings := true, autoCompilerPlugins := true ) @@ -78,7 +81,7 @@ lazy val publishSettings = Seq( url("https://github.com/aappddeevv/scalajs-reaction"), "scm:git:git@github.com:aappddeevv/scalajs-reaction.git") ), - publishArtifact in Test := false, + Test / publishArtifact := false, // https://www.scala-sbt.org/1.x/docs/Publishing.html#Publishing // should go to snapshots if isSnapshot.value is true // nedeed to make sbt-sonatype: publishSigned work @@ -86,7 +89,7 @@ lazy val publishSettings = Seq( sonatypeCredentialHost := "s01.oss.sonatype.org", sonatypeProfileName := "org.ttgoss", publishMavenStyle := true, - useGpg := true, + //useGpg := true, credentials ++= (for { keyid <- sys.env.get("GPG_KEY") } yield Credentials("GnuPG Key ID", "gpg", keyid, "ignored")).toSeq, @@ -110,11 +113,11 @@ def std_settings(p: String, d: String) = inThisBuild( List( - scalaVersion := "3.1.0", + scalaVersion := "3.1.3", organization := "org.ttgoss.js", organizationName := "The Trapelo Group (TTG) Open Source Software (TTGOSS)", startYear := Some(2018), - scalafixDependencies += "com.nequissimus" %% "sort-imports" % "0.3.2", + //scalafixDependencies += "com.nequissimus" %% "sort-imports" % "0.3.2", //,scalafmtOnCompile := true, // should come from sbt-dynver //version := "0.1.0-M7" @@ -127,7 +130,7 @@ inThisBuild( lazy val root = project .in(file(".")) - .settings(skip in publish := true) + .settings(publish / skip := true) .aggregate( //apollo, apollo3, @@ -555,7 +558,7 @@ lazy val examples = project .settings(fpsettings) .settings(std_settings("examples", "Example web application")) .settings( - skip in publish := true, + publish / skip := true, // Watch non-scala assets. watchSources += baseDirectory.value / "examples/src/main/assets", libraryDependencies ++= Seq( @@ -576,21 +579,22 @@ lazy val examples = project ) .enablePlugins(ScalaJSPlugin, BuildInfoPlugin) .settings(buildinfo_settings("ttg.examples")) - .settings(artifactPath.in(Compile, fastOptJS) := crossTarget.in(Compile, fastOptJS).value / "Scala.js") - .settings(artifactPath.in(Compile, fullOptJS) := crossTarget.in(Compile, fullOptJS).value / "Scala.js") + .settings(Compile / fastOptJS / artifactPath := (Compile / fastOptJS / crossTarget).value / "Scala.js") + //.settings(Compile / fullOptJS / artifactPath := crossTarget.in(Compile, fullOptJS).value / "Scala.js") + .settings(Compile / fullOptJS / artifactPath := (Compile / fullOptJS / crossTarget).value / "Scala.js") lazy val docs = project .in(file("scalajs-reaction-docs")) .settings(std_settings("scalajs-reaction-docs", "docs fake project")) .settings( - skip.in(publish) := true + publish / skip := true //,mdocVariables := Map("VERSION" -> version.value) //scalacOptions -= -"Yno-imports", //scalacOptions -= "-Ydata-warnings", , - unidocProjectFilter in (ScalaUnidoc, unidoc) := inAnyProject -- inProjects(examples),// -- inProjects(apollo), - target in (ScalaUnidoc, unidoc) := (baseDirectory in LocalRootProject).value / "website" / "scalajs-reaction" / "static" / "api", - cleanFiles += (target in (ScalaUnidoc, unidoc)).value + ScalaUnidoc / unidoc / unidocProjectFilter := inAnyProject -- inProjects(examples),// -- inProjects(apollo), + ScalaUnidoc / unidoc / target := (LocalRootProject / baseDirectory).value / "website" / "scalajs-reaction" / "static" / "api", + cleanFiles += (ScalaUnidoc / unidoc / target).value ) .enablePlugins(ScalaJSPlugin, ScalaUnidocPlugin) // keep this list in sync with root, or filter the dependencies directly from root... @@ -653,19 +657,19 @@ addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck") val npmBuild = taskKey[Unit]("fullOptJS then webpack") npmBuild := { - (fullOptJS in (examples, Compile)).value + (examples / Compile / fullOptJS).value "npm run examples" ! } val npmBuildFast = taskKey[Unit]("fastOptJS then webpack") npmBuildFast := { - (fastOptJS in (examples, Compile)).value + (examples / Compile / fastOptJS).value "npm run examples:dev" ! } val npmRunDemo = taskKey[Unit]("fastOptJS then run webpack server") npmRunDemo := { - (fastOptJS in (examples, Compile)).value + (examples / Compile / fastOptJS).value "npm run examples:dev:start" ! } diff --git a/components/fabric/src/main/scala/components/Label.scala b/components/fabric/src/main/scala/components/Label.scala index d2560001..0d8b09cf 100644 --- a/components/fabric/src/main/scala/components/Label.scala +++ b/components/fabric/src/main/scala/components/Label.scala @@ -29,7 +29,7 @@ import react.* import vdom.* import fabric.styling.* -object Label { +object Label: @js.native @JSImport("office-ui-fabric-react/lib/Label", "Label") object JS extends ReactJSComponent @@ -50,10 +50,11 @@ object Label { with ComponentRef[js.Any] with Disabled with Theme - with ReactJSProps { + with MaybeHasStrKey + with ReactJSProps: var styles: js.UndefOr[IStyleFunctionOrObject[StyleProps, Styles]] = js.undefined + //var key: js.UndefOr[react.KeyType] = js.undefined var required: js.UndefOr[Boolean] = js.undefined - } trait StyleProps extends js.Object: var className: js.UndefOr[String] = js.undefined @@ -63,4 +64,4 @@ object Label { trait Styles extends IStyleSetTag: var root: js.UndefOr[IStyle] = js.undefined -} + diff --git a/components/mssql/src/main/scala/mssql/package.scala b/components/mssql/src/main/scala/mssql/package.scala index a1cc5690..a10354a5 100644 --- a/components/mssql/src/main/scala/mssql/package.scala +++ b/components/mssql/src/main/scala/mssql/package.scala @@ -19,9 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +package mssql import scala.scalajs.js -package object mssql { - import mssql.SchemaColumn - type Schema = js.Dictionary[SchemaColumn] -} +type Schema = js.Dictionary[mssql.SchemaColumn] diff --git a/components/node-fetch/src/main/scala/node-fetch.scala b/components/node-fetch/src/main/scala/node-fetch.scala index 880a290b..67819734 100644 --- a/components/node-fetch/src/main/scala/node-fetch.scala +++ b/components/node-fetch/src/main/scala/node-fetch.scala @@ -1,11 +1,9 @@ package node_fetch import scala.scalajs.js -import js.| -import scala.scalajs.js.annotation._ +import scala.scalajs.js.annotation.* import scala.scalajs.js.typedarray.{ArrayBuffer, Uint8Array} - @js.native @JSImport("node-fetch", "fetch") def fetch( diff --git a/components/node-fetch/src/main/scala/package.scala b/components/node-fetch/src/main/scala/package.scala index f5e4c2d7..b803797e 100644 --- a/components/node-fetch/src/main/scala/package.scala +++ b/components/node-fetch/src/main/scala/package.scala @@ -1,17 +1,21 @@ +package node_fetch + import scala.scalajs.js -import js.| -import scala.scalajs.js.annotation._ +import scala.scalajs.js.annotation.* import scala.scalajs.js.typedarray.{ArrayBuffer, Uint8Array} -package object node_fetch { - type RequestInfo = String | Request - type HeadersInit = - Headers | Sequence[Sequence[ByteString]] | OpenEndedDictionary[ByteString] - type ByteString = String - - /** Keep the type simple so we don't have to pull in Blob, BufferSource, FormData, URLSearchParams. */ - type BodyInit = String | js.Any - //Blob | BufferSource | FormData | String //todo: add URLSearchParams - type Sequence[T] = js.Array[T] - type OpenEndedDictionary[T] = js.Dictionary[T] -} +type RequestInfo = String | Request + +type HeadersInit = + Headers | Sequence[Sequence[ByteString]] | OpenEndedDictionary[ByteString] + +type ByteString = String + +/** Keep the type simple so we don't have to pull in Blob, BufferSource, FormData, URLSearchParams. */ +type BodyInit = String | js.Any + +//Blob | BufferSource | FormData | String //todo: add URLSearchParams +type Sequence[T] = js.Array[T] + +type OpenEndedDictionary[T] = js.Dictionary[T] + diff --git a/jshelpers/src/main/scala/any.scala b/jshelpers/src/main/scala/any.scala index e41d7ba6..389ac54b 100644 --- a/jshelpers/src/main/scala/any.scala +++ b/jshelpers/src/main/scala/any.scala @@ -30,6 +30,11 @@ object any: import scala.scalajs.js import scala.language.unsafeNulls + /** Totally unsafe. */ + extension (a: scala.Any) + /** Totally unsafe, use at your own risk! */ + def unsafeAsJsAny = a.asInstanceOf[js.Any] + /** All of these are unsafe. */ extension [T <: js.Any](a: T) /** Convert T => T|Null. */ @@ -100,13 +105,13 @@ object any: /** If value is null or undefined be undefined, otherwise defined. Could be called "filterNull". */ def toNonNullUndefOr: js.UndefOr[T] = // we keep this so that it works when needed - if (a == null || js.isUndefined(a)) js.undefined + if a == null || js.isUndefined(a) then js.undefined else js.defined(a) /** If value is null or undefined be None, else Some. */ def toNonNullOption: Option[T] = // also defined in react package, repeated here - if (js.isUndefined(a) || a == null) None + if js.isUndefined(a) || a == null then None else Option(a) /** Equivalent `!!x` for some javascript value x. */ @@ -128,7 +133,7 @@ object any: * }}} */ def toTruthyUndefOr: js.UndefOr[T] = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) js.defined(a) + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then js.defined(a) else js.undefined end extension diff --git a/jshelpers/src/main/scala/dynamic.scala b/jshelpers/src/main/scala/dynamic.scala index d5aade8d..66705059 100644 --- a/jshelpers/src/main/scala/dynamic.scala +++ b/jshelpers/src/main/scala/dynamic.scala @@ -88,7 +88,7 @@ object dynamic: /** Uses truthiness to determine None, you may not want this. */ def toOption[T <: js.Object]: Option[T] = - if (js.DynamicImplicits.truthValue(jsdyn)) Some(jsdyn.asInstanceOf[T]) + if js.DynamicImplicits.truthValue(jsdyn) then Some(jsdyn.asInstanceOf[T]) else None // /** Null and undefined => None, otherwise Some. The safest conversion. */ diff --git a/jshelpers/src/main/scala/instances.scala b/jshelpers/src/main/scala/instances.scala index cc8cf76f..6a75bc70 100644 --- a/jshelpers/src/main/scala/instances.scala +++ b/jshelpers/src/main/scala/instances.scala @@ -21,12 +21,12 @@ package jshelpers -trait AllInstances extends JSPromiseInstances +trait AllInstances extends syntax.JSPromiseInstances //with ToLocaleStringInstances /** Instances with everything but all so it can be subclassed elsewhere. */ trait InstancesTrait: - object jspromise extends JSPromiseInstances + object jspromise extends syntax.JSPromiseInstances //object anyval extends ToLocaleStringInstances /** Instances is the wrong concept here as these are not typeclass diff --git a/jshelpers/src/main/scala/misc.scala b/jshelpers/src/main/scala/misc.scala index fc0c9601..7465a484 100644 --- a/jshelpers/src/main/scala/misc.scala +++ b/jshelpers/src/main/scala/misc.scala @@ -23,7 +23,6 @@ package jshelpers package syntax import scala.scalajs.js -import js.| object misc: /** diff --git a/jshelpers/src/main/scala/null.scala b/jshelpers/src/main/scala/null.scala index a348c5ba..edf753d8 100644 --- a/jshelpers/src/main/scala/null.scala +++ b/jshelpers/src/main/scala/null.scala @@ -30,7 +30,7 @@ String | Unit => Option[String]: provides a string when Some and js.undefined w x.fold[String | Unit](())(x => x) */ - object jsnull: +object jsnull: /** * It is common in interop code to model a value as A or null but not undefined * even though null and undefined may both mean "absent value." See `|.merge` @@ -61,14 +61,14 @@ x.fold[String | Unit](())(x => x) /** Like `.toNonNullOption`. */ def toOption: Option[A] = { val g = forceGet - if (g != null) Option(g) + if g != null then Option(g) else None } /** If Null, then false, else true. */ @targetName("toTruthyOrNull") def toTruthy: Boolean = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) true + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then true else false /** Uh-oh, thought it was `A|Null` but you need to say its a @@ -79,22 +79,22 @@ x.fold[String | Unit](())(x => x) /** null => undefined, otherwise A. */ def toUndefOr: js.UndefOr[A] = - if (a == null) js.undefined + if a == null then js.undefined else js.defined(forceGet) def toTruthyUndefOr: js.UndefOr[A] = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then js.defined(forceGet) else js.undefined def toTruthyOption: Option[A] = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then Option(forceGet) else None @targetName("filterTruthOrNull") def filterTruthy: A | Null = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) a + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then a else null.asInstanceOf[A | Null] /** Absorb the null and change A|Null => A. Value could still be null, @@ -116,7 +116,7 @@ x.fold[String | Unit](())(x => x) /** getOrElse but less typing. */ @targetName("getOrElseOrNull2") def ??[B >: A](default: => B): B = - if (isEmpty) default else forceGet + if isEmpty then default else forceGet // /** getOrElse but less typing. */ // inline def !?[B >: A](default: => B): B = getOrElse[B](default) @@ -130,10 +130,10 @@ x.fold[String | Unit](())(x => x) // if (a != null) next(forceGet) else a def orElse[B >: A](other: B | Null): B | Null = - if (a == null) other else a + if a == null then other else a def collect[B](pf: PartialFunction[A, B]): B | Null = - if (a != null && pf.isDefinedAt(forceGet)) pf.apply(forceGet).asInstanceOf[B | Null] + if a != null && pf.isDefinedAt(forceGet) then pf.apply(forceGet).asInstanceOf[B | Null] else null.asInstanceOf[B | Null] def contains[A1 >: A](elem: A1): Boolean = !isEmpty && forceGet == elem @@ -142,21 +142,21 @@ x.fold[String | Unit](())(x => x) def forall(p: A => Boolean): Boolean = isEmpty || p(forceGet) - def foreach[U](f: A => U): Unit = if (a != null) f(forceGet) else () + def foreach[U](f: A => U): Unit = if a != null then f(forceGet) else () def isEmpty: Boolean = a == null def isDefined: Boolean = a != null def isNotDefined: Boolean = !isDefined - def knownSize: Int = if (isEmpty) 0 else 1 + def knownSize: Int = if isEmpty then 0 else 1 @targetName("nullGetOrElse") def getOrElse[B >: A](default: => B): B = - if (isEmpty) default else forceGet + if isEmpty then default else forceGet def orNull[A1 >: A]: A1 = - if (a == null) null.asInstanceOf[A1] else forceGet.asInstanceOf[A1] + if a == null then null.asInstanceOf[A1] else forceGet.asInstanceOf[A1] def fold[B](ifNull: => B)(f: A => B): B = if a == null then @@ -164,7 +164,7 @@ x.fold[String | Unit](())(x => x) else f(forceGet) def flatten[B](implicit ev: A <:< |[B, Null]): B | Null = - if (a == null) null.asInstanceOf[B | Null] else ev(forceGet) + if a == null then null.asInstanceOf[B | Null] else ev(forceGet) /** Collapse A|Null => A but the value may be null! You are on your own. * Should be called `unsafeMerge`. @@ -172,28 +172,28 @@ x.fold[String | Unit](())(x => x) def merge: A = forceGet def orElse[B >: A](alternative: => B | Null): B | Null = - if (isEmpty) alternative else a + if isEmpty then alternative else a def flatMap[B](f: A => B | Null): B | Null = - if (a != null) f(forceGet) else null.asInstanceOf[B | Null] + if a != null then f(forceGet) else null.asInstanceOf[B | Null] def map[B](f: A => B): B | Null = - if (a != null) f(forceGet).asInstanceOf[B | Null] else null.asInstanceOf[B | Null] + if a != null then f(forceGet).asInstanceOf[B | Null] else null.asInstanceOf[B | Null] def filter(p: A => Boolean): A | Null = - if (isEmpty || p(forceGet)) a else null.asInstanceOf[A | Null] + if isEmpty || p(forceGet) then a else null.asInstanceOf[A | Null] def withFilter(p: A => Boolean): OrNull.WithFilter[A] = new OrNull.WithFilter[A](a, p) def zip[A1 >: A, B](that: B | Null): (A1, B) | Null = - if (a == null || that == null) + if a == null || that == null then null.asInstanceOf[(A1, B) | Null] else (forceGet, that.asInstanceOf[B]).asInstanceOf[(A1, B) | Null] def unzip[A1, A2](implicit asPair: A <:< (A1, A2)): (A1 | Null, A2 | Null) = - if (isEmpty) + if isEmpty then (null.asInstanceOf[A1 | Null], null.asInstanceOf[A2 | Null]) else { val e = asPair(forceGet) @@ -201,14 +201,14 @@ x.fold[String | Unit](())(x => x) } def iterator: collection.Iterator[A] = - if (isEmpty) collection.Iterator.empty else collection.Iterator.single(forceGet) + if isEmpty then collection.Iterator.empty else collection.Iterator.single(forceGet) - def toList: List[A] = if (isEmpty) List() else new ::(forceGet, Nil) + def toList: List[A] = if isEmpty then List() else new ::(forceGet, Nil) object OrNull: class WithFilter[A](self: A | Null, p: A => Boolean): def localFilter(p: A => Boolean): A | Null = - if (self == null || p(self.asInstanceOf[A])) self else null.asInstanceOf[A | Null] + if self == null || p(self.asInstanceOf[A]) then self else null.asInstanceOf[A | Null] def map[B](f: A => B): B | Null = localFilter(p).map(f) def flatMap[B](f: A => B | Null): B | Null = localFilter(p).flatMap(f) def foreach[U](f: A => U): Unit = localFilter(p).foreach(f) @@ -239,9 +239,11 @@ x.fold[String | Unit](())(x => x) /** Swap `js.UndefOr[A|Null]` for `js.UndefOr[A]|Null`. */ @targetName("nullSwap") def swap: js.UndefOr[A | Null] = - if (a == null || a == js.undefined) ().asInstanceOf[js.UndefOr[A | Null]] + if a == null || a == js.undefined then ().asInstanceOf[js.UndefOr[A | Null]] else a.asInstanceOf[js.UndefOr[A | Null]] extension (a: String | Null) /** Return string's "zero" which is an empty string. Could be called orBlank. */ - def orEmpty: String = if (a == null) "" else a.asInstanceOf[String] + def orEmpty: String = if a == null then "" else a.asInstanceOf[String] + +end jsnull \ No newline at end of file diff --git a/jshelpers/src/main/scala/object.scala b/jshelpers/src/main/scala/object.scala index 9ae8ae7b..2e83ab5b 100644 --- a/jshelpers/src/main/scala/object.scala +++ b/jshelpers/src/main/scala/object.scala @@ -29,7 +29,8 @@ object jsobject: /** The "combine" methods are shallow, mutable merges, this may not be what you want. */ extension [A <: js.Object](o: A) // def asDict[B]: js.Dictionary[B] = o.asInstanceOf[js.Dictionary[B]] - // def asAnyDict: js.Dictionary[js.Any] = o.asInstanceOf[js.Dictionary[js.Any]] + /** Any js.Object is also a dictionary. */ + def asAnyDict: js.Dictionary[js.Any] = o.asInstanceOf[js.Dictionary[js.Any]] // def asDyn: js.Dynamic = o.asInstanceOf[js.Dynamic] // def asJSDyn: js.Dynamic = o.asInstanceOf[js.Dynamic] // def asUndefOr: js.UndefOr[A] = js.defined(o) diff --git a/jshelpers/src/main/scala/promise.scala b/jshelpers/src/main/scala/promise.scala index eee48985..f1789486 100644 --- a/jshelpers/src/main/scala/promise.scala +++ b/jshelpers/src/main/scala/promise.scala @@ -20,8 +20,8 @@ */ package jshelpers +package syntax -import scala.reflect.ClassTag import scala.scalajs.js import scala.annotation.targetName @@ -52,7 +52,7 @@ object promise: def map[B](f: A => B): js.Promise[B] = jsThen[B](f) /** Cast to S otherwise throw a ClassCastException. */ - def mapTo[S](implicit tag: ClassTag[S]): js.Promise[S] = + def mapTo[S](implicit tag: scala.reflect.ClassTag[S]): js.Promise[S] = self.`then`[S]((a: A) => tag.runtimeClass.cast(a).asInstanceOf[RVAL[S]], js.undefined).asInstanceOf[js.Promise[S]] /** flatMap */ @@ -128,7 +128,7 @@ object promise: /** Not sure this is semantically right... */ def collect[S](pf: PartialFunction[A, S]): js.Promise[S] = { val onf: RESOLVE[A, S] = (a: A) => { - if (pf.isDefinedAt(a)) js.Promise.resolve[S](pf.apply(a)) + if pf.isDefinedAt(a) then js.Promise.resolve[S](pf.apply(a)) else js.Promise.reject(new NoSuchElementException()) } self.`then`[S](onf, js.undefined).asInstanceOf[js.Promise[S]] @@ -138,7 +138,7 @@ object promise: * return failure from this. */ def fallbackTo[U >: A](that: js.Promise[U]): js.Promise[U] = - if (self == that) self.asInstanceOf[js.Promise[U]] + if self == that then self.asInstanceOf[js.Promise[U]] else { val onf: RESOLVE[A, U] = (a: A) => a val onr: REJECTED[U] = (erra: scala.Any) => { @@ -194,7 +194,7 @@ object promise: def filter(p: A => Boolean): js.Promise[A] = { val onf = js.Any.fromFunction1 { (a: A) => val result = p(a) - if (result) JSPromiseCreators[A](a) + if result then JSPromiseCreators[A](a) else JSPromiseCreators.fail(new NoSuchElementException()) }.asInstanceOf[RESOLVE[A, A]] self.`then`[A](onf, js.undefined).asInstanceOf[js.Promise[A]] @@ -210,7 +210,7 @@ object promise: def recover[U >: A](pf: PartialFunction[scala.Any, U]): js.Promise[U] = { val onf = ().asInstanceOf[RESOLVE[A, U]] val onr = js.Any - .fromFunction1((any: Any) => if (pf.isDefinedAt(any)) pf.apply(any) else js.Promise.reject(any)) + .fromFunction1((any: Any) => if pf.isDefinedAt(any) then pf.apply(any) else js.Promise.reject(any)) .asInstanceOf[REJECTED[U]] self.`then`[U](onf, onr).asInstanceOf[js.Promise[U]] } @@ -219,7 +219,11 @@ object promise: def recoverWith[U >: A](pf: PartialFunction[scala.Any, js.Thenable[U]]): js.Promise[U] = { val onf = ().asInstanceOf[RESOLVE[A, U]] val onr = js.Any - .fromFunction1((any: Any) => if (pf.isDefinedAt(any)) pf.apply(any) else js.Promise.reject(any)) + .fromFunction1((any: Any) => + if pf.isDefinedAt(any) then + pf.apply(any) + else + js.Promise.reject(any)) .asInstanceOf[REJECTED[U]] self.`then`[U](onf, onr).asInstanceOf[js.Promise[U]] } diff --git a/jshelpers/src/main/scala/syntax.scala b/jshelpers/src/main/scala/syntax.scala index cf62f8e1..889635ee 100644 --- a/jshelpers/src/main/scala/syntax.scala +++ b/jshelpers/src/main/scala/syntax.scala @@ -30,3 +30,4 @@ export jshelpers.syntax.jsnull.* export jshelpers.syntax.misc.* export jshelpers.syntax.dynamic.* export jshelpers.syntax.jsobject.* +export jshelpers.syntax.promise.* diff --git a/jshelpers/src/main/scala/undefor.scala b/jshelpers/src/main/scala/undefor.scala index f9a8d065..d2c8aee4 100644 --- a/jshelpers/src/main/scala/undefor.scala +++ b/jshelpers/src/main/scala/undefor.scala @@ -42,7 +42,7 @@ object undefor: /** This could also be `_.toOption.filter(_ != null)` but below is slightly faster. */ @targetName("toNonNullOptionA") def toNonNullOption = - if (_a.isEmpty || _a == null) None + if _a.isEmpty || _a == null then None else _a.toOption /** Calls toString. I'm not sure this is needed at all. */ @@ -56,7 +56,7 @@ object undefor: def as[B]: js.UndefOr[B] = _a.asInstanceOf[js.UndefOr[B]] // _a.map(_.asInstanceOf[B]) /** Convert UndefOr[A] => A|Null. Much like stdlib `orNull` but keeps type signature. */ - def toNull: A | Null = if (_a.isDefined) _a.asInstanceOf[A | Null] else null + def toNull: A | Null = if _a.isDefined then _a.asInstanceOf[A | Null] else null /** Same as `.getOrElse` just shorter. */ @targetName("getOrElseUndefOrNull2") @@ -93,7 +93,7 @@ object undefor: /** Keep type signature, but filter out non-truthy values to `js.undefined`. */ @targetName("filterTruthyUndefOrNull") def filterTruthy: js.UndefOr[T|Null] = - if (js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic])) a + if js.DynamicImplicits.truthValue(a.asInstanceOf[js.Dynamic]) then a else js.undefined end extension @@ -106,7 +106,7 @@ object undefor: /** Determine if is defined including the value not being null. */ @targetName("isDefinedUndefOrNull") - def isDefined: Boolean = if (!js.isUndefined(a) && a != null) true else false + def isDefined: Boolean = if !js.isUndefined(a) && a != null then true else false /** Convenience. */ @targetName("isEmptyUndefOrNull") @@ -117,7 +117,7 @@ object undefor: /** Treat null as undefined and change type from `js.UndefOr[T|Null]` to `js.UndefOr[T]`. */ @targetName("undefAbsorbUndefOrNull") def absorbNull: js.UndefOr[T] = - if (a == null) js.undefined + if a == null then js.undefined else a.asInstanceOf[js.UndefOr[T]] /** Collapse everything at once. */ @@ -130,7 +130,7 @@ object undefor: /** Absorb the `js.UndefOr` leaving `T|Null`. */ def absorbUndef: T | Null = - if (a.isEmpty) null.asInstanceOf[T | Null] else a.asInstanceOf[T | Null] + if a.isEmpty then null.asInstanceOf[T | Null] else a.asInstanceOf[T | Null] /** `flatten` but leave the UndefOr. Same as `absorbUndef`. */ def flattenUndefOr: T | Null = absorbUndef @@ -138,13 +138,13 @@ object undefor: /** Natural transformation. */ @targetName("swapUndefOrNull") def swap: js.UndefOr[T] | Null = - if (a.isDefined && a != null) a.asInstanceOf[js.UndefOr[T] | Null] + if a.isDefined && a != null then a.asInstanceOf[js.UndefOr[T] | Null] else ().asInstanceOf[js.UndefOr[T] | Null] /** Undestands UndefOr and Null to do the orElse. */ @targetName("getOrElseUndefOrNull") def getOrElse[B >: T](default: => T): T = - if (a.isEmpty || a == null) default else a.asInstanceOf[T] + if a.isEmpty || a == null then default else a.asInstanceOf[T] /** Alias for getOrElse. */ @targetName("getOrElse1") @@ -153,11 +153,11 @@ object undefor: /** May be undefined or null or something. Throws exception. */ @targetName("undefGet") inline def undefGet: T = - if (a == null || a.isEmpty) throw new NoSuchElementException("get on UndefOr[T|Null]") + if a == null || a.isEmpty then throw new NoSuchElementException("get on UndefOr[T|Null]") else forceGet /** Only works with another js.UndefOr[T|Null] and takes into account null. */ - def orDeepElse(that: js.UndefOr[T | Null]) = if (a.isDefined && a != null) a else that + def orDeepElse(that: js.UndefOr[T | Null]) = if a.isDefined && a != null then a else that end extension extension [A <: js.Object](a: js.UndefOr[A]) @@ -167,20 +167,20 @@ object undefor: extension [A, B](tuple: (js.UndefOr[A], js.UndefOr[B])) @targetName("undefMapX2") def mapX[T](f: (A, B) => T): js.UndefOr[T] = - if (tuple._1.isDefined && tuple._2.isDefined) js.defined(f(tuple._1.get, tuple._2.get)) + if tuple._1.isDefined && tuple._2.isDefined then js.defined(f(tuple._1.get, tuple._2.get)) else js.undefined extension [A, B, C](tuple: (js.UndefOr[A], js.UndefOr[B], js.UndefOr[C])) @targetName("undefMapX3") def mapX[T](f: (A, B, C) => T): js.UndefOr[T] = - if (tuple._1.isDefined && tuple._2.isDefined && tuple._3.isDefined) + if tuple._1.isDefined && tuple._2.isDefined && tuple._3.isDefined then js.defined(f(tuple._1.get, tuple._2.get, tuple._3.get)) else js.undefined extension [A, B, C, D](tuple: (js.UndefOr[A], js.UndefOr[B], js.UndefOr[C], js.UndefOr[D])) @targetName("undefMapX4") def mapX[T](f: (A, B, C, D) => T): js.UndefOr[T] = - if (tuple._1.isDefined && tuple._2.isDefined && tuple._3.isDefined && tuple._4.isDefined) + if tuple._1.isDefined && tuple._2.isDefined && tuple._3.isDefined && tuple._4.isDefined then js.defined(f(tuple._1.get, tuple._2.get, tuple._3.get, tuple._4.get)) else js.undefined diff --git a/project/build.properties b/project/build.properties index 13ecfea5..38c0109b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ -sbt.version=1.6.1 +sbt.version=1.7.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index 74e3f367..006029a9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,17 +1,17 @@ val scalaJSVersion = - Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.8.0") + Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.10.1") addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") -addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.6") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +//addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.6") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") -addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") +addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") //addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.1.1") //addSbtPlugin("com.thoughtworks.sbt-scala-js-map" % "sbt-scala-js-map" % "latest.release") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.28") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.8") +//addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.1") +//addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.8") //addSbtPlugin("com.thoughtworks.sbt-scala-js-map" % "sbt-api-mappings" % "4.0.0+21-b1f441da") //addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") diff --git a/project/project/metals.sbt b/project/project/metals.sbt deleted file mode 100644 index 24f47856..00000000 --- a/project/project/metals.sbt +++ /dev/null @@ -1,6 +0,0 @@ -// DO NOT EDIT! This file is auto-generated. - -// This file enables sbt-bloop to create bloop config files. - -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") - diff --git a/project/project/project/metals.sbt b/project/project/project/metals.sbt deleted file mode 100644 index 24f47856..00000000 --- a/project/project/project/metals.sbt +++ /dev/null @@ -1,6 +0,0 @@ -// DO NOT EDIT! This file is auto-generated. - -// This file enables sbt-bloop to create bloop config files. - -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") - diff --git a/react-dom/src/main/scala/package.scala b/react-dom/src/main/scala/package.scala index 27eab6ad..940b9be5 100644 --- a/react-dom/src/main/scala/package.scala +++ b/react-dom/src/main/scala/package.scala @@ -43,7 +43,7 @@ def renderToElementWithId( )(htmlel => Right(ReactDOMJS.render(el, htmlel, cb.orUndefined.map(js.Any.fromFunction0(_))))) } -/* Render into an elemen given by its id. Prefer this over `renderToElementWithId`. */ +/* Render into an element given by its id. Prefer this over `renderToElementWithId`. */ def renderToElement(id: String, cb: Option[() => Unit] = None): Either[String, ReactNode => Unit] = Option(dom.document.getElementById(id)) .fold[Either[String, ReactNode => Unit]]( diff --git a/react/src/main/scala/When.scala b/react/src/main/scala/When.scala index 3b280226..bfea86c4 100644 --- a/react/src/main/scala/When.scala +++ b/react/src/main/scala/When.scala @@ -37,17 +37,17 @@ trait When: /** Render something or return a null element. Render is by name. Could just use fold. */ def when(cond: Boolean)(render: => ReactNode): ReactNode = - if (cond) render else nullElement + if cond then render else nullElement /** Render something if notcond or return a null element. Render is by name. Could also use fold. */ def whenNot(cond: Boolean)(render: => ReactNode): ReactNode = - if (!cond) render else nullElement + if !cond then render else nullElement def when[T](cond: js.UndefOr[T])(render: => ReactNode): ReactNode = - if (cond.isDefined) render else nullElement + if cond.isDefined then render else nullElement def whenNot[T](cond: js.UndefOr[T])(render: => ReactNode): ReactNode = - if (cond.isEmpty) render else nullElement + if cond.isEmpty then render else nullElement // Had the Unwrap variants with the name when/whenNot but am trying this // api so you could have a wrapped Boolean but only test its wrappiness @@ -59,17 +59,17 @@ trait When: // /** Render something or return a null element. Render is by name. Could just use fold. */ def whenUnwrap[T <: Boolean](cond: js.UndefOr[T])(render: => ReactNode)(implicit ev: T =:= Boolean): ReactNode = - if (cond.getOrElse(false)) render else nullNode + if cond.getOrElse(false) then render else nullNode /** Render something or return a null element. Render is by name. Could just use fold. */ def whenUnwrap[T <: Boolean](cond: Option[T])(render: => ReactNode)(implicit ev: T =:= Boolean): ReactNode = - if (cond.getOrElse(false)) render else nullNode + if cond.getOrElse(false) then render else nullNode /** Render something if not cond or return a null element. Render is by name. Could also use fold. */ def whenNotUnwrap[T <: Boolean](cond: js.UndefOr[T])(render: => ReactNode)(implicit ev: T =:= Boolean): ReactNode = - if (!cond.getOrElse(false)) render else nullElement + if !cond.getOrElse(false) then render else nullElement /** Render something if not cond or return a null element. Render is by name. Could also use fold. */ def whenNotUnwrap[T <: Boolean](cond: Option[T])(render: => ReactNode)(implicit ev: T =:= Boolean): ReactNode = - if (!cond.getOrElse(false)) render else nullElement + if !cond.getOrElse(false) then render else nullElement diff --git a/react/src/main/scala/extras.scala b/react/src/main/scala/extras.scala index 4f9c6be9..7ea8cab5 100644 --- a/react/src/main/scala/extras.scala +++ b/react/src/main/scala/extras.scala @@ -20,71 +20,67 @@ */ package react +package extras import scala.scalajs.js import js.| import js.annotation._ /** Extra things not specifically core react but mentioned in the FAQ. */ -package object extras { - /** Create an expensive ref. - * - * @see https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily - */ - def useExpensiveRef[T](value: => T) = { - val instanceRef = useRef[T | Null](null) - () => { - val instance = instanceRef.current - if (instance != null) instance.asInstanceOf[T] - else { - val x = value - instanceRef.current = x - x - } +/** Create an expensive ref. + * + * @see https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily + */ +def useExpensiveRef[T](value: => T) = + val instanceRef = useRef[T | Null](null) + () => { + val instance = instanceRef.current + if instance != null then instance.asInstanceOf[T] + else { + val x = value + instanceRef.current = x + x } } - /** Force a render. */ - def useForceRender() = { - val (s, update) = useStateStrict[Boolean](true) - val fr = useCallbackMounting(() => update(!_)) - //() => update(!_) - fr - } +/** Force a render. */ +def useForceRender() = + val (s, update) = useStateStrict[Boolean](true) + val fr = useCallbackMounting(() => update(!_)) + //() => update(!_) + fr - /** Use previous value using `.hashCode` as the equality test. */ - def usePreviousScalaValue[T](value: T) = { - val ref = useRef[T](value) - useEffect(value.hashCode)(() => ref.current = value) - ref.current - } +/** Use previous value using `.hashCode` as the equality test. */ +def usePreviousScalaValue[T](value: T) = + val ref = useRef[T](value) + useEffect(value.hashCode)(() => ref.current = value) + ref.current - /** Use previous value using reference equality (I think). */ - def usePreviousValue[T <: js.Any](value: T) = { - val ref = useRef[T](value) - useEffect(value)(() => ref.current = value) - ref.current - } +/** Use previous value using reference equality (I think). */ +def usePreviousValue[T <: js.Any](value: T) = + val ref = useRef[T](value) + useEffect(value)(() => ref.current = value) + ref.current - /** http://usehooks.com. Use inside your hooks to - * memo a value so callers don't have to. Simplified via - * use-custom-compare-effect approach vs usehooks. - * - * Needs testing! - * - * @param value Value to memoize. - * @param compare (old,new)=>Boolean - */ - def useMemoCompare[T](value: T, compare: (T, T) => Boolean) = { - val previousRef = useRef[T](value) - val previous = previousRef.current - val isEqual = compare(previous, value) + +/** http://usehooks.com. Use inside your hooks to + * memo a value so callers don't have to. Simplified via + * use-custom-compare-effect approach vs usehooks. + * + * Needs testing! + * + * @param value Value to memoize. + * @param compare (old,new)=>Boolean + */ +def useMemoCompare[T](value: T, compare: (T, T) => Boolean) = + val previousRef = useRef[T](value) + val previous = previousRef.current + val isEqual = compare(previous, value) // useEffectAlways(() => if (!isEqual) previousRef.current = value) // if (isEqual) previous else value - if (!isEqual) previousRef.current = value - previousRef.current - } + if !isEqual then previousRef.current = value + previousRef.current //private useMemoCompareJS = js.Any.fromFunction2(useMemoCompare) @@ -95,4 +91,3 @@ package object extras { // def useCustomCompareEffect[T](create: EffectArg, input: T, compare: (T, T) => Boolean) = // useEffect(unsafeDeps(useMemoCompare(input, compare)))(create) -} diff --git a/react/src/main/scala/package.scala b/react/src/main/scala/package.scala index ac808674..cad19376 100644 --- a/react/src/main/scala/package.scala +++ b/react/src/main/scala/package.scala @@ -44,10 +44,14 @@ package object react extends react.React with When { /** Empty array which is different than undefinedDependencies. Typically * indicates mount/unmount hook processing. `=== []` + * + * Use `useEffectMounting` instead. */ val emptyDependencies: Dependencies = js.Array() - /** Undefined array typically indicates per render. Typically *not* what you want. */ + /** Undefined array typically indicates per render. Typically *not* what you want. + * Use `useEffectEveryRender` instead. + */ val undefinedDependencies: js.UndefOr[Dependencies] = js.undefined /** Create a dependencies array. With no args, its a zero-length array. @@ -58,7 +62,7 @@ package object react extends react.React with When { def deps(values: AllType*): Dependencies = values.toJSArray /** Escape hatch. Create a dependencies array from *any* list of scala - * objects. Make sure you think about each value type in the list of + * values. Make sure you think about each value type in the list of * dependencies. Use this inference helper to remind you to think about the * types and scala-js interop. */ @@ -66,7 +70,7 @@ package object react extends react.React with When { values.toJSArray.asInstanceOf[Dependencies] /** Noop for `useEffect`-like callbacks. */ - val noCleanUp = () => () + val noCleanUp: js.Function0[Unit] = () => () /** * Non-mutable ref. Object returned from `createRef()` and some variants of `useRef` hook. @@ -159,11 +163,9 @@ package object react extends react.React with When { /** Very un-ergonomic, but this is what FB has. */ type KeyType = String | Int - /** Use these to mix into your traits to ensure you have a a key and ref to - * set. For example, add this to a Props class so that you can specify a key - * when you create it. These are not special or used for tags, you can not - * use this trait and just define the key and/or ref in your Props trait - * directly. + /** Use these to mix into your prop traits to ensure you have a a key and ref to + * set. For example, add this as a supertrait to a Props class so that you can specify a key + * when you create it. */ @deprecated("Use ReactJSProps", "0.1.0") trait ReactPropsJs extends js.Object { @@ -171,8 +173,9 @@ package object react extends react.React with When { def ref[E]: js.UndefOr[RefCb[E]] = js.undefined } - /** Use this instead of ReactPropsJs. But! you should probably should be - * using `MaybeHasStrKey` + /** You should probably should be using `MaybeHasStrKey` but generally + * use this supertrait to allow callers to specify key and ref values + * and have them typed. */ trait ReactJSProps extends js.Object { var key: js.UndefOr[KeyType] = js.undefined @@ -184,7 +187,9 @@ package object react extends react.React with When { var key: js.UndefOr[String] = js.undefined } - /** Known to contain a key. */ + /** Known to contain a key. You can force callers to incuded a key + * using this supertrait so it has very limited use. + */ trait HasKey extends js.Object { val key: KeyType } @@ -205,9 +210,9 @@ package object react extends react.React with When { // } /** - * A standard HTML element that has been created using createElement. - * Props are optional of course. We use this tag it to show that it came - * from the standard DOM components vs a custom one. + * A standard HTML element that has been created using `createElement`. + * Props are optional of course. We use this tag it to show that it is + * associated from the standard DOM tagset. */ @js.native trait ReactDOMElement extends ReactElement { @@ -280,17 +285,20 @@ package object react extends react.React with When { /** * This type is used only as a target for imported javascript authored components to - * "tag" a component type or created via other js-interop mechanisms such as - * redux integration. By using a separate type, you must use then - * scalajs-react's API to create an element. You can use this to annotate a - * js function react component as well e.g. () => ReactNode. + * "tag" a component declare in scala-js land. By using a separate type, you must use the + * this library's API to create an element. Use can this to annotate a + * js function react component as well e.g. () => ReactNode if the specifi nature + * of the component is not important. + * + * Typical use is `@JSImport("some-lib", "BlahView") @js.native val BlahView: ReactJSComponent` + * where `BlahView` is a JS react component in js library `some-lib`. */ @js.native trait ReactJSComponent extends js.Object - /** Import type target from js land that are functions and imported as such. May or may - * not make a difference to have this typed separately. You could also import - * it as a jsFunctionN object. + /** Import type target from js land that are functions and it is important to import + * them as such. May or may not make a difference to have this typed separately. + * You could also import it as a jsFunctionN object. */ @js.native trait ReactJSFunctionComponent extends js.Object @@ -421,7 +429,7 @@ package object react extends react.React with When { // if (js.isUndefined(t) || t == null) None // else Option(t.asInstanceOf[T]) - /** Shorted version of `js.defined(blah)` */ + /** Short version of `js.defined(blah)` */ @inline def jsdef[A](a: A) = js.defined(a) /** Short version of `js.undefined`. */ @@ -491,7 +499,10 @@ package object react extends react.React with When { /** A component that takes a ref as the second argument. */ type RectFCWithRef[P <: js.Object, T <: js.Any] = js.Function2[P, ReactRef[T], ReactNode] - /** A type used to drive type inference when declaring your component. */ + /** A type used to drive type inference when declaring your component. + * + * Use this pattern, `val render: ReactFC0 = () => { ... }`. + */ type ReactFC0 = js.Function0[ReactNode] /** Type inference hepler. */ @@ -518,6 +529,9 @@ package object react extends react.React with When { /** Type inference helper. */ val nullDate = null.asInstanceOf[js.Date] - /** Empty array. Freshly allocated! */ + /** Empty array. Freshly allocated! + * + * @todo Add `()` to communicate it is side effecting. + */ def emptyArray[T] = js.Array[T]() } diff --git a/react/src/main/scala/react.scala b/react/src/main/scala/react.scala index ac98d002..a00dbb47 100644 --- a/react/src/main/scala/react.scala +++ b/react/src/main/scala/react.scala @@ -61,7 +61,7 @@ trait React: * @todo Seems like this is an expensive call. Can we do better? */ @inline def extractChildren(item: js.UndefOr[js.Object]|Null): js.Array[ReactNode] = - if (item == null) js.Array() // need this since could be a "defined" null + if item == null then js.Array() // need this since could be a "defined" null else item.toOption .flatMap(_.asInstanceOf[js.Dictionary[js.Array[ReactNode]]].get("children")) @@ -126,8 +126,9 @@ trait React: } /** - * @deprecated + * */ + @deprecated def unsafeCreateElement(component: js.Dynamic => ReactNode, props: js.Dynamic) = { val jsc = js.Any.fromFunction1(component).asInstanceOf[ReactType] createElement0(jsc, props) @@ -199,9 +200,14 @@ trait React: ): (S, Dispatch[A]) = ReactJS.useReducer(reducer, initialArg, init) /** Effect is always run on render, which is probably too much. */ + @deprecated("Use useEffectEveryRender") def useEffectAlways(didUpdate: EffectArg) = ReactJS.useEffect(didUpdate, undefinedDependencies) + /** Effect is always run on render, which is probably too much. */ + def useEffectEveryRender(didUpdate: EffectArg) = + ReactJS.useEffect(didUpdate, undefinedDependencies) + /** Effect is run when dependencies change. */ def useEffectA(dependencies: Dependencies)(didUpdate: EffectArg) = ReactJS.useEffect(didUpdate, dependencies) @@ -209,15 +215,17 @@ trait React: // def useEffect(didUpdate: EffectArg, dependencies: js.UndefOr[Dependencies]) = // ReactJS.useEffect(didUpdate, dependencies) - /** Effect is run when dependencies change. */ + /** Effect is run when dependencies change. Unlike the JS version, the dependencies + * come first. + */ def useEffect(dependencies: AllType*)(didUpdate: EffectArg) = ReactJS.useEffect( didUpdate, - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray ) - /** Effect is run at mount/unmount time with an implied `[]` per the react docs. */ + /** Effect is run at mount/unmount time via `useEffect` with an implied `[]` per the react docs. */ def useEffectMounting(didUpdate: EffectArg) = ReactJS.useEffect(didUpdate, emptyDependencies) @@ -240,7 +248,7 @@ trait React: def useMemo[T](dependencies: AllType*)(value: js.Function0[T]): T = ReactJS.useMemo[T]( value, - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray ) @@ -255,12 +263,17 @@ trait React: ): Unit = ReactJS.useDebugValue[T](value, js.Any.fromFunction1[T, String](format)) + // + // The useCallback functions have a few varieties. Only callbacks up to arity 5 + // are included here so if you need higher arity, write your own. + // + def useCallbackMounting[T](callback: js.Function0[T]): js.Function0[T] = ReactJS.useCallback(callback, emptyDependencies).asInstanceOf[js.Function0[T]] def useCallbackA[T](dependencies: Dependencies | Unit)(callback: js.Function0[T]): js.Function0[T] = ReactJS.useCallback(callback, dependencies).asInstanceOf[js.Function0[T]] def useCallback[T](dependencies: AllType*)(callback: js.Function0[T]): js.Function0[T] = - if (dependencies.length == 0) ReactJS.useCallback(callback).asInstanceOf[js.Function0[T]] + if dependencies.length == 0 then ReactJS.useCallback(callback).asInstanceOf[js.Function0[T]] else ReactJS.useCallback(callback, dependencies.toJSArray).asInstanceOf[js.Function0[T]] def useCallbackMounting1[A1, T](callback: js.Function1[A1, T]): js.Function1[A1, T] = @@ -271,7 +284,7 @@ trait React: ReactJS .useCallback( callback, - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray) .asInstanceOf[js.Function1[A1, T]] @@ -283,7 +296,7 @@ trait React: ReactJS .useCallback( callback, - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray) .asInstanceOf[js.Function2[A1, A2, T]] def useCallback2A[A1, A2, T](dependencies: Dependencies)(callback: js.Function2[A1, A2, T]): js.Function2[A1, A2, T] = @@ -305,7 +318,7 @@ trait React: ReactJS .useCallback( js.Any.fromFunction3[A1, A2, A3, T](callback), - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray ) .asInstanceOf[js.Function3[A1, A2, A3, T]] @@ -333,7 +346,7 @@ trait React: ReactJS .useCallback( js.Any.fromFunction4[A1, A2, A3, A4, T](callback), - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray ) .asInstanceOf[js.Function4[A1, A2, A3, A4, T]] @@ -362,7 +375,7 @@ trait React: ReactJS .useCallback( js.Any.fromFunction5[A1, A2, A3, A4, A5, T](callback), - if (dependencies.length == 0) undefinedDependencies + if dependencies.length == 0 then undefinedDependencies else dependencies.toJSArray ) .asInstanceOf[js.Function5[A1, A2, A3, A4, A5, T]] diff --git a/react/src/main/scala/reactjs.scala b/react/src/main/scala/reactjs.scala index a1808c20..f8ef3158 100644 --- a/react/src/main/scala/reactjs.scala +++ b/react/src/main/scala/reactjs.scala @@ -22,7 +22,7 @@ package react import scala.scalajs.js -import js.annotation._ +import js.annotation.* @js.native trait Children extends js.Object: @@ -40,7 +40,7 @@ trait TransitionConfig extends js.Object: trait DeferredValueConfig extends js.Object: var timeoutMs: js.UndefOr[Int] = js.undefined -/** Concurrent features targeted at v18, I recall. */ +/** Concurrent features targeted at v18+. */ @js.native trait Concurrent extends js.Object: def useTransition(config: TransitionConfig): js.Tuple2[js.Function1[js.Function0[Unit], Unit], Boolean] = js.native @@ -54,13 +54,14 @@ trait Concurrent extends js.Object: source: MutableSource[S], getSnapshot: js.Function1[S, A], subscribe: js.Function2[S, js.Function0[Unit], js.Function0[Unit]]): A = js.native +end Concurrent /** Opaque type. */ @js.native trait MutableSource[T] extends js.Object @js.native -trait ReactJS extends js.Object with Concurrent: +trait ReactJS extends js.Object: val Children: Children = js.native @@ -125,7 +126,9 @@ object DynamicImport: def apply(f: js.Function1[_ <: js.Object, ReactNode]) = new DynamicImport { val `default` = f } /** Magnet pattern to create a friendly arg converter for effect hooks. As much - * as possible these need to be casts vs allocations. + * as possible these need to be casts vs allocations and ideally these need to be + * stable values so that react change detection does not rerun the hook. It + * is better to declare the effect functions as `val`s of js functions. */ @js.native trait EffectArg extends js.Object @@ -139,31 +142,28 @@ object EffectArg: /** Convert a scala EffectCallbackArg to js using a proxy approach. * Use a general return of A vs unit to be more friendly. Requires - * 2 scala => js function conversions. Ugh! + * 2 scala => js function conversions. Ugh! Discards callback value. */ - @inline def convertEffectCallbackArg[A](arg: () => (() => A)): js.Any = { () => + inline private def convertEffectCallbackArg[A](arg: () => (() => A)): js.Any = { () => val rthunk = arg() js.Any.fromFunction0 { () => rthunk(); () } }: js.Function0[js.Function0[Unit]] - /** No final callback. - * - * @todo Not sure inner definition is needed. - */ - @inline implicit def fromThunkJS[U](f: js.Function0[U]): EffectArg = - js.Any.fromFunction0[Unit] { () => f(); () }.asInstanceOf[EffectArg] + /** Return value from callback is discarded. */ + given fromThunkJS: Conversion[js.Function0[?], EffectArg] = + f => js.Any.fromFunction0[Unit] { () => f(); () }.asInstanceOf[EffectArg] - /** No final callback. */ - @inline implicit def fromThunk[U](f: () => U): EffectArg = - js.Any.fromFunction0[Unit] { () => f(); () }.asInstanceOf[EffectArg] + /** Return value from callback is discarded. */ + given fromThunk: Conversion[() => ?, EffectArg] = + f => js.Any.fromFunction0[Unit] { () => f(); () }.asInstanceOf[EffectArg] - /** Return value from the callback is discarded. */ - @inline implicit def fromThunkCbA[A](f: () => (() => A)): EffectArg = - convertEffectCallbackArg(f).asInstanceOf[EffectArg] + /** Return value from callback is a react callback whose value is discarded. */ + given fromThunkCbA: Conversion[() => (() => ?), EffectArg] = + convertEffectCallbackArg(_).asInstanceOf[EffectArg] - /** Return value form the callback is discarded. */ - @inline implicit def fromThunkCbJS[A](f: () => js.Function0[A]): EffectArg = - ((() => { val rthunk = f(); js.Any.fromFunction0 { () => rthunk(); () } }): js.Function0[js.Function0[Unit]]) + /** Return value from callback is a react callback whose value is discarded. */ + given fromThunkCbJS: Conversion[() => js.Function0[?], EffectArg] = + f => ((() => { val rthunk = f(); js.Any.fromFunction0 { () => rthunk(); () } }): js.Function0[js.Function0[Unit]]) .asInstanceOf[EffectArg] @js.native @@ -218,7 +218,7 @@ trait Hooks extends js.Object: */ @js.native @JSImport("react", JSImport.Namespace) -private[react] object ReactJS extends ReactJS with Hooks +private[react] object ReactJS extends ReactJS with Hooks with Concurrent /** The module "object" that you import when you use react. */ @js.native diff --git a/react/src/main/scala/syntax/conversions.scala b/react/src/main/scala/syntax/conversions.scala index 4926062b..8cc0e41e 100644 --- a/react/src/main/scala/syntax/conversions.scala +++ b/react/src/main/scala/syntax/conversions.scala @@ -76,7 +76,7 @@ given anyValToElement: Conversion[AnyVal, ReactNode] = _.asInstanceOf[ReactNode] given undefOrAnyValToElement: Conversion[js.UndefOr[AnyVal], ReactNode] = _.map(_.asInstanceOf[ReactNode]).getOrElse(nullNode) given nullOrAnyValToElement: Conversion[AnyVal | Null, ReactNode] = - v => if (v == null) nullNode else v.asInstanceOf[ReactNode] + v => if v == null then nullNode else v.asInstanceOf[ReactNode] given null2Node: Conversion[Null, ReactNode] = _.asInstanceOf[ReactNode] diff --git a/react/src/test/scala/EffectArgTests.scala b/react/src/test/scala/EffectArgTests.scala new file mode 100644 index 00000000..f5c96c57 --- /dev/null +++ b/react/src/test/scala/EffectArgTests.scala @@ -0,0 +1,22 @@ +package react + +import scala.scalajs.js +import utest._ + +import jshelpers.syntax.* + + +object EffectArgTests extends TestSuite: + import scala.language.unsafeNulls + val tests = Tests { + test("EffectArg magnet pattern") { + val ef1: EffectArg = () => 10 + val ef2: EffectArg = () => () => () + val cb1: js.Function0[Unit] = () => () + val ef3: EffectArg = () => cb1 + val cb2: js.Function0[Int] = () => 10 + val ef4: EffectArg = () => cb2 + } + + } +end EffectArgTests diff --git a/website/scalajs-reaction/docs/misc/bundling.md b/website/scalajs-reaction/docs/misc/bundling.md index d213b474..90f7adf9 100644 --- a/website/scalajs-reaction/docs/misc/bundling.md +++ b/website/scalajs-reaction/docs/misc/bundling.md @@ -3,6 +3,8 @@ id: bundling title: Bundling --- +## General + Bundling is the process of taking your scala.js program, ensuring that all dependencies are identified then creating an output file suitable for use on the web or on a mobile device. @@ -12,3 +14,50 @@ A special bundler that understands http and file URLs is available at for bundling scalajs-reaction programs. It will work with any scala.js source file and is not specific to scalajs-reaction. Please see that page for directions. + + +## Obtaining sbt Output Information for npm + +You may use a bundler for your scalajs output in order to bundle the application for +your webapp or other target. + +Generally, the `npm build` command needs to know here the scala artifact is located +in order to pull it in. To wire in a well known location you have a few choices: + +* Hard code the sbt output directory into your bundler config file +* Change the output file location in sbt (I built a plugin for this but it is unmaintained now) +* Program sbt to output the location. + +To have sbt output the location you can use some code devleopde by sjrd: https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/blob/main/build.sbt + + +```scala +// top of build.sbt +val publicDev = taskKey[String]("output directory for `npm run dev`") +val publicProd = taskKey[String]("output directory for `npm run build`") + +// in the project of your choice +publicDev := linkerOutputDirectory((Compile / fastLinkJS).value).getAbsolutePath(), +publicProd := linkerOutputDirectory((Compile / fullLinkJS).value).getAbsolutePath(), + +// bottom of build.sbt +def linkerOutputDirectory(v: Attributed[org.scalajs.linker.interface.Report]): File = { + v.get(scalaJSLinkerOutputDirectory.key).getOrElse { + throw new MessageOnlyException( + "Linking report was not attributed with output directory. " + + "Please report this as a Scala.js bug.") + } +} +``` + +With this you can issue an external process command in your javascript bundler +tool and pickup the location. You will never be out of date again! + +Now at the cli: + +```sh +// since sbt must start, try to run and cache the result +// at the project level use `print /publicDev` +sbt --error --batch print publicDev +sbt --error --batch print publicProd +``` \ No newline at end of file