Skip to content

Commit

Permalink
Support a single input element. (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
zainab-ali authored Aug 17, 2024
1 parent 07006b0 commit 03a8e70
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 104 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ lazy val docs = project
// Add highlight.js for code examples
.internalJS(Root / "highlight.min.js")
.site
.internalJS(Root / "highlightWrapper.js")
.internalJS(Root / "aquascape.js")
.site
.internalCSS(Root / "a11y-dark.min.css"),
laikaExtensions += AquascapeDirectives,
Expand Down
55 changes: 55 additions & 0 deletions docs/aquascape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
var aquascape = (function () {

/** Draws an aquascape svg in-browser.
*
* @param exampleObject An instance of `aquascape.examples.Example`.
* @param codeId The html id of the <code> element that should be populated.
* @param frameIds An instance of `aquascape.examples.FrameIds`
*/
function example(exampleObject, codeId, frameIds) {
highlightExampleCode(codeId);
exampleObject.draw(codeId, frameIds);
};

/** Draws an aquascape svg in-browser with a single user input.
*
* The user-input is provided through an <input> element.
*
* @param exampleObject An instance of `aquascape.examples.Example`.
* @param codeId The html id of the <code> element that should be populated.
* @param frameIds An instance of `aquascape.examples.FrameIds`
* @param labelId The html id of the <label> element for the input.
* @param inputId The html id of the <input> element.
*/
function exampleWithInput(exampleObject, codeId, frameIds, labelId, inputId) {
highlightExampleCode(codeId);
document.getElementById(inputId).addEventListener("input", function (e) {
exampleObject.draw(codeId, frameIds, this.value);
});
exampleObject.setup(labelId, inputId, codeId, frameIds);
};

/** Highlight Scala code examples.
*
* The @:example directive fills in <code> blocks
* asynchronously. This function adds an observer to highlight the
* <code> block after its text content has been set.
*/
function highlightExampleCode(codeId) {
const targetNode = document.getElementById(codeId);
const config = { attributes: true, childList: true, subtree: false };

const callback = (mutationList, observer) => {
hljs.highlightElement(targetNode);
observer.disconnect();
};

const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
};

return {
example: example,
exampleWithInput: exampleWithInput
};
})();
26 changes: 18 additions & 8 deletions docs/basic/take.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# take
## fewer

@:example(TakeFewer)
Experiment with taking a number of elements from an infinite stream.

## more
Note that the resulting stream is finite because a fixed number of elements are pulled.

@:example(TakeMore)
@:exampleWithInput(TakeFromAnInfiniteStream) {
drawChunked = false
}

## from an infinite stream

@:example(TakeFromAnInfiniteStream)
## `take` from finite streams

## from a drained stream
Experiment with taking a number of elements from a finite stream.

@:example(TakeFromADrainedStream)
@:exampleWithInput(TakeFromAFiniteStream) {
drawChunked = false
}

## Drained streams

Drained streams output no elements, but may still capture side-effects.

@:example(TakeFromADrainedStream) {
drawChunked = false
}
23 changes: 0 additions & 23 deletions docs/highlightWrapper.js

This file was deleted.

14 changes: 14 additions & 0 deletions docs/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ div.example-frame svg {
pre.example-code {
padding: 0;
}

div.example-input {
display: grid;
grid-template-columns: 1fr 1fr;
}

div.example-input > label {
font-size: var(--header3-font-size);

}

div.example-input > input {
padding: 12px;
}
89 changes: 82 additions & 7 deletions examples/src/main/scala/Example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import cats.effect.unsafe.implicits.global
import doodle.svg.*
import doodle.syntax.all.*
import org.scalajs.dom
import cats.syntax.all.*

import scala.scalajs.js.annotation.JSExport
import scala.scalajs.js.annotation.JSExportTopLevel
Expand All @@ -48,17 +49,71 @@ object FrameIds {
trait Example {
@JSExport
final def draw(codeId: String, frameIds: FrameIds): Unit = {
drawFrames(codeId, frameIds)(apply)
drawFrames(codeId, frameIds)(apply).unsafeRunAsync {
case Left(err) => throw err
case Right(_) => ()
}
}

def apply(using Scape[IO]): StreamCode
}

/** Describes how a value encodes to and from an <input> element.
*
* In practice, this only works for numbers. More work is needed to support
* coproducts (e.g. with radio buttons).
*/
trait FormCodec[A] {

def attributes: Map[String, String]
def inputType: String

def decode(text: String): Option[A]
def encode(a: A): String
}

trait ExampleWithInput[A: FormCodec] {

def label: String
def default: A

@JSExport
final def setup(
labelId: String,
inputId: String,
codeId: String,
frameIds: FrameIds
): Unit = {
val program = for {
_ <- drawFrames(codeId, frameIds)(apply(default))
_ <- drawLabel(labelId, label)
_ <- setValue(inputId, default)
} yield ()
program.unsafeRunAsync {
case Left(err) => throw err
case Right(_) => ()
}
}

@JSExport
final def draw(codeId: String, frameIds: FrameIds, param: String): Unit = {
summon[FormCodec[A]]
.decode(param)
.traverse_ { a => drawFrames(codeId, frameIds)(apply(a)) }
.unsafeRunAsync {
case Left(err) => throw err
case Right(_) => ()
}
}

def apply(a: A)(using Scape[IO]): StreamCode
}

private def drawFrames(
codeId: String,
frameIds: FrameIds
)(stream: Scape[IO] ?=> StreamCode): Unit = {
val program = for {
)(stream: Scape[IO] ?=> StreamCode): IO[Unit] = {
for {
code <- {
frameIds match {
case FrameIds.Unchunked(id) => drawFrame(id, false)(stream)
Expand All @@ -71,11 +126,8 @@ private def drawFrames(
}
codeEl <- IO(dom.document.getElementById(codeId))
_ <- IO(codeEl.textContent = code.code)
.whenA(codeEl.textContent.trim.isEmpty)
} yield ()
program.unsafeRunAsync {
case Left(err) => throw err
case Right(_) => ()
}
}

private def drawFrame(frameId: String, chunked: Boolean)(
Expand All @@ -90,6 +142,29 @@ private def drawFrame(frameId: String, chunked: Boolean)(
streamCode.stream.attempt.void.draw(),
seed = Some("MTIzNDU=")
)
frameEl <- IO(dom.document.getElementById(frameId))
// If there is already an image, remove it.
_ <- IO(
Option(frameEl.firstChild).foreach(child => frameEl.removeChild(child))
)
_ <- picture.drawWithFrameToIO(frame)
} yield streamCode
}

private def drawLabel(labelId: String, label: String): IO[Unit] = {
for {
labelEl <- IO(dom.document.getElementById(labelId))
_ <- IO(labelEl.textContent = label)
} yield ()
}
private def setValue[A: FormCodec](inputId: String, default: A): IO[Unit] = {
for {
inputEl <- IO(dom.document.getElementById(inputId))
codec = summon[FormCodec[A]]
_ <- IO(inputEl.setAttribute("type", codec.inputType))
_ <- IO(inputEl.setAttribute("value", codec.encode(default)))
_ <- codec.attributes.toList.traverse_ { case (k, v) =>
IO(inputEl.setAttribute(k, v))
}
} yield ()
}
53 changes: 28 additions & 25 deletions examples/src/main/scala/Examples.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,46 +103,47 @@ object BasicCompileOnlyOrError extends Example {
)
}

@JSExportTopLevel("TakeFewer")
object TakeFewer extends Example {
def apply(using Scape[IO]): StreamCode =
import formCodecs.given

@JSExportTopLevel("TakeFromAnInfiniteStream")
object TakeFromAnInfiniteStream extends ExampleWithInput[Int] {

def label: String = "n (number of elements to take)"

def default: Int = 3

def apply(n: Int)(using Scape[IO]): StreamCode =
code(
Stream('a', 'b', 'c')
.stage("Stream('a','b','c')")
.take(2)
.stage("take(2)")
Stream('a').repeat
.stage("Stream('a').repeat")
.take(n)
.stage(s"take($n)")
.compile
.toList
.compileStage("compile.toList")
)
}

@JSExportTopLevel("TakeMore")
object TakeMore extends Example {
def apply(using Scape[IO]): StreamCode =
@JSExportTopLevel("TakeFromAFiniteStream")
object TakeFromAFiniteStream extends ExampleWithInput[Int] {

def label: String = "n (number of elements to take)"

def default: Int = 2

def apply(n: Int)(using Scape[IO]): StreamCode =
code(
Stream('a', 'b', 'c')
.stage("Stream('a','b','c')")
.take(5)
.stage("take(5)")
.compile
.toList
.compileStage("compile.toList")
)
}
@JSExportTopLevel("TakeFromAnInfiniteStream")
object TakeFromAnInfiniteStream extends Example {
def apply(using Scape[IO]): StreamCode =
code(
Stream('a').repeat
.stage("Stream('a').repeat")
.take(2)
.stage("take(2)")
.take(n)
.stage(s"take($n)")
.compile
.toList
.compileStage("compile.toList")
)
}


@JSExportTopLevel("TakeFromADrainedStream")
object TakeFromADrainedStream extends Example {
import NothingShow.given
Expand Down Expand Up @@ -912,3 +913,5 @@ object TimeDebounceAwake extends Example {
.compileStage("compile.toList")
)
}


30 changes: 30 additions & 0 deletions examples/src/main/scala/formCodecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 Zainab Ali
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package aquascape.examples

object formCodecs {
given FormCodec[Int] = new FormCodec[Int] {
def attributes: Map[String, String] = Map(
"min" -> "0",
"max" -> "10"
)
def inputType: String = "number"
def encode(i: Int): String = i.toString
def decode(text: String): Option[Int] =
scala.util.Try(Integer.parseInt(text)).toOption
}
}
Loading

0 comments on commit 03a8e70

Please sign in to comment.