Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ val diodeVersion = "1.1.14"
lazy val token = sys.env.getOrElse("GITHUB_TOKEN", "No Token")

val publishSettings = Seq(
version := "0.3.0-SNAPSHOT",
version := "0.3.2-SNAPSHOT",
versionScheme := Some(sbt.VersionScheme.SemVerSpec),
organization := "rocks.earlyeffect",
organizationName := "earlyeffect",
Expand Down Expand Up @@ -48,7 +48,7 @@ val baseSettings = Seq(
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature"),
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "1.1.0",
"org.scalameta" %%% "munit" % "0.7.23" % Test,
"org.scalameta" %%% "munit" % "0.7.25" % Test,
),
testFrameworks += new TestFramework("munit.Framework"),
installJsdom / version.withRank(KeyRanks.Invisible) := "16.4.0",
Expand Down Expand Up @@ -89,6 +89,27 @@ lazy val core = project
baseDirectory.value / "webpack" / "no-fs-config.js"
),
)
lazy val jsdocs = project
.enablePlugins(ScalaJSBundlerPlugin)
.dependsOn(core)
.settings(
webpackBundlingMode := BundlingMode.LibraryOnly(),
scalaJSUseMainModuleInitializer := true,
scalaVersion := "2.13.5",
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "1.1.0"
)
lazy val docs = project
.in(file("core-docs"))
.enablePlugins(MdocPlugin)
.dependsOn(core)
.settings(
publish / skip := true,
scalaVersion := "2.13.5",
mdocJS := Some(jsdocs),
mdocOut := file("mdoc-output"),
mdocJSLibraries := webpack.in(jsdocs, Compile, fullOptJS).value
)


lazy val diodeSupport = project
.in(file("diode-support"))
Expand All @@ -104,7 +125,7 @@ lazy val diodeSupport = project
)
lazy val formable = project
.in(file("formable"))
.dependsOn(core)
.dependsOn(core % "compile->compile", core % "test->test")
.enablePlugins(ScalaJSBundlerPlugin)
.settings(
baseSettings,
Expand All @@ -114,6 +135,7 @@ lazy val formable = project
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
Compile / fastOptJS / webpackEmitSourceMaps := true,
Compile / fullOptJS / webpackEmitSourceMaps := false,
Test / webpackConfigFile := (core / Test / webpackConfigFile).value
)

lazy val demoModel = project
Expand Down
6 changes: 4 additions & 2 deletions core/src/test/scala/anode/AnodeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import org.scalajs.dom
import org.scalajs.dom.Element

import scala.scalajs.js
import scala.scalajs.js.timers.SetTimeoutHandle

trait AnodeOps {
def render(vn: VNode): Unit = preact.render(vn, parent)
def render(args: Args): Unit = preact.render(E.div(args), parent)

val parent: Element = {
val res = dom.document.createElement("div")
dom.document.documentElement.appendChild(res)
res
}

def check(s: String) = munit.Assertions.assertEquals(parent.innerHTML, s)
def checkAfter(n: Int)(s: String) = js.timers.setTimeout(n)(check(s))
def check(s: String): Unit = munit.Assertions.assertEquals(parent.innerHTML, s)
def checkAfter(n: Int)(s: String): SetTimeoutHandle = js.timers.setTimeout(n)(check(s))
}
21 changes: 19 additions & 2 deletions demo-app/src/main/scala/todo/App.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package todo

import anode.dsl.Elements.div
import anode.dsl.css.CssClass
import anode.{ClassSelector, E, S, VNode, fragment, when}
import anode.{A, Args, ClassSelector, E, S, VNode, args, fragment, log, when}
import diode.ModelR
import org.scalajs.dom
import todo.model.{Root, TodoList}



object App extends TodoComponent[Unit, TodoList] with ClassSelector {

object css {
Expand Down Expand Up @@ -61,9 +65,22 @@ object App extends TodoComponent[Unit, TodoList] with ClassSelector {
override def modelReader(p: Unit): ModelR[Root, TodoList] = zoom(_.todoList)
import anode.Formable
import Formable.defaultImplicits._

sealed trait Foo{
def a:String
}
case class Bar(a:String,b:Map[String, String]) extends Foo
implicit val showMap:Formable[Map[String,String]] = Formable(formProps => {
implicit val tuple:Formable[(String,String)] = Formable(tupleProps =>{
args(E.div(tupleProps.field._1), E.input(tupleProps.field._2, A.onKeyUp(x => {
tupleProps.update(tupleProps.field._1, x.target.asInstanceOf[dom.html.Input].value)
})))
})
args(formProps.field.toSeq.map[Args](x => Formable("",x)(x => formProps.update(formProps.field + x))))
})
val f = Bar("Russ",Map("Foo" -> "Bar", "baz" -> "bonk"))
override def render(props: Unit, l: TodoList): VNode =
E.body(
Formable("",f)(f => log("f",f)),
css.App,
E.section(
css.TodoApp,
Expand Down
28 changes: 28 additions & 0 deletions docs/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Anode Components

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄


```scala mdoc:js:shared
import anode._
import preact.render
```

## Constructing a Simple Component

Define a stateless component function with only a property type \[String\]:

```scala mdoc:js:shared
val sayHello:Component[String] = {name =>
E.div(s"Hello $name!",A.style(S.color("blue")))
}
```

A VNode is created when we apply a property instance to the component

```scala mdoc:js:shared
val vnode = sayHello("Russ")
```

Then we can render it by calling `render` with the VNode as well as a dom node to render within

```scala mdoc:js
render(vnode,node)
```
46 changes: 29 additions & 17 deletions formable/src/main/scala/anode/Formable.scala
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package anode

import anode.Formable._
import magnolia.{CaseClass, Magnolia}
import magnolia.{CaseClass, Magnolia, SealedTrait}
import org.scalajs.dom

import scala.language.experimental.macros
import scala.language.implicitConversions

sealed trait Formable[A] { self =>
def apply(props: Props[A, _]): Args
def apply(context: Context[A, _]): Args

def form(p: Props[A, A]): VNode =
def form(context: Context[A, A]): VNode =
E.form(
self(p)
self(context)
)
}

object Formable {
type Typeclass[A] = Formable[A]

case class Props[Field, Product] private[anode] (label: String, field: Field, update: Field => Product)
case class Context[Field, Product] private[anode] (label: String, field: Field, update: Field => Product)

def apply[Field, Product](label: String, field: Field)(update: Field => Product): Props[Field, Product] =
Props(label, field, update)
def apply[Field, Product](label: String, field: Field)(update: Field => Product): Context[Field, Product] =
Context(label, field, update)

def apply[Product, Result](product: Product)(effect: Product => Result): Props[Product, Product] =
Props[Product, Product](
def apply[Product, Result](product: Product)(effect: Product => Result): Context[Product, Product] =
Context[Product, Product](
label = "form",
product,
x => {
Expand All @@ -39,28 +39,40 @@ object Formable {
def apply[A](effect: Product => A): anode.Args = f(Formable[Product, A](product)(effect))
}

def apply[A](f: Props[A, _] => Args): Formable[A] =
new Formable[A] { override def apply(props: Props[A, _]): Args = f(props) }
def apply[A](f: Context[A, _] => Args): Formable[A] =
new Formable[A] { override def apply(context: Context[A, _]): Args = f(context) }

def combine[Product](caseClass: CaseClass[Typeclass, Product]): Formable[Product] =
Formable { props =>
Formable { context =>
args(caseClass.parameters.map { productParam =>
type ParamType = productParam.PType
val update: ParamType => Product = (x: ParamType) => {
val product = caseClass.construct { constructorParam =>
if (constructorParam == productParam) x
else constructorParam.dereference(props.field)
else constructorParam.dereference(context.field)
}
props.update(product)
context.update(product)
product
}
productParam.typeclass(apply(productParam.label, productParam.dereference(props.field))(update))
productParam.typeclass(apply(productParam.label, productParam.dereference(context.field))(update))
})
}

def dispatch[Product](sealedTrait: SealedTrait[Formable, Product]): Formable[Product] =
Formable { context =>
sealedTrait.dispatch(context.field) { subtype =>
type S = subtype.SType
subtype.typeclass.apply(Formable[S,Product](context.label, subtype.cast(context.field))((s:S) => {
context.update(s)
s
}))
}
}

implicit def formable[A]: Formable[A] = macro Magnolia.gen[A]

implicit def summonArgsFromProps[Field](props: Props[Field, _])(implicit formable: Formable[Field]): Args =
formable(props)
implicit def summonArgsFromProps[Field](context: Context[Field, _])(implicit formable: Formable[Field]): Args =
formable(context)

object defaultImplicits {

Expand Down
32 changes: 32 additions & 0 deletions formable/src/test/scala/anode/formable/FormableSpecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package anode.formable

import anode._
import munit.FunSuite

class FormableSpecs extends FunSuite with AnodeOps {
type Form[A] = Formable.Context[A, A]

object Form {
def apply[A](a: A): Form[A] = Formable(s"label-$a", a)(identity)

def apply[A]: Formable[A] = Formable[A](a => args(text((a.label, a.field.toString).toString())))
}

implicit val formString: Formable[String] = Form[String]
implicit val formInt: Formable[Int] = Form[Int]

case class Foo(a: String, b: Int)

test("Formable String") {
render(Form("a"))
check("<div>(label-a,a)</div>")
}
test("Formable int") {
render(Form(1))
check("<div>(label-1,1)</div>")
}
test("Formable Foo") {
render(Form(Foo("foo", 1)))
check("<div>(a,foo)(b,1)</div>")
}
}
8 changes: 8 additions & 0 deletions jsdocs/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
object Main {

def main(args: Array[String]): Unit = {
import anode._
val s:Component[String] = {s => E.div()}
val x = E.span()
}
}
34 changes: 34 additions & 0 deletions mdoc-output/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Anode Components

```scala
import anode._
import preact.render
```

## Constructing a Simple Component

Define a stateless component function with only a property type \[String\]:

```scala
val sayHello:Component[String] = {name =>
E.div(s"Hello $name!",A.style(S.color("blue")))
}
```

A VNode is created when we apply a property instance to the component

```scala
val vnode = sayHello("Russ")
```

Then we can render it by calling `render` with the VNode as well as a dom node to render within

```scala
render(vnode,node)
```
<div id="mdoc-html-run3" data-mdoc-js></div>
<script type="text/javascript" src="jsdocs-opt-library.js" defer></script>
<script type="text/javascript" src="jsdocs-opt-loader.js" defer></script>
<script type="text/javascript" src="intro.md.js" defer></script>
<script type="text/javascript" src="mdoc.js" defer></script>

Loading