Skip to content

Instantly share code, notes, and snippets.

@mcallisto
Last active April 30, 2019 09:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mcallisto/5fd8a0576e635f1bd60829a396e4c56e to your computer and use it in GitHub Desktop.
Save mcallisto/5fd8a0576e635f1bd60829a396e4c56e to your computer and use it in GitHub Desktop.
Slinky version of Intro to React official tutorial

Starter code at revision 1

Spoiler alert

The code at the current revision is the complete tutorial plus the 6 suggested improvements and using the State Hook.

You can check the tutorial without State Hooks at:

  • revision 3: completed with the improvements
  • revision 2: completed without the improvements
package slinkyIntro
import slinky.core._
import slinky.core.annotations.react
import slinky.core.facade.ReactElement
import slinky.core.facade.Hooks.useState
import slinky.web.html._
import scala.scalajs.js
import scala.scalajs.js.annotation.{JSImport, ScalaJSDefined}
@JSImport("resources/App.css", JSImport.Default)
@js.native
object AppCSS extends js.Object
@JSImport("resources/logo.svg", JSImport.Default)
@js.native
object ReactLogo extends js.Object
@react object Square {
case class Props(value: String, isHighlighted: Boolean, onClick: () => Unit)
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
button(
className := (if (props.isHighlighted) "highlighted " else "square"),
onClick := props.onClick
)({
props.value
})
}
}
@react object Board {
case class Props(squares: Array[String], highlights: Array[Boolean], onClick: Int => Unit)
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
def renderSquare(i: Int): ReactElement = Square(props.squares(i), isHighlighted = props.highlights(i), () => props.onClick(i))
val grid = for {
y <- 0 until 3
} yield div(className := "board-row")(for {
x <- 0 until 3
} yield renderSquare(x + y * 3))
div()(grid)
}
}
object Game {
def apply(): ReactElement = component()
val component: FunctionalComponent[Unit] = FunctionalComponent[Unit] { _ =>
val (history, updateHistory) = useState(Array(Array.fill(9)("")))
val (stepNumber, updateStepNumber) = useState(0)
val (isAscending, updateIsAscending) = useState(true)
val (xIsNext, updateXIsNext) = useState(true)
def calculateWinningLine(squares: Array[String]): Option[List[Int]] = {
val lines: List[List[Int]] = List(
List(0, 1, 2),
List(3, 4, 5),
List(6, 7, 8),
List(0, 3, 6),
List(1, 4, 7),
List(2, 5, 8),
List(0, 4, 8),
List(2, 4, 6),
)
lines.find({
case a :: b :: c :: Nil => squares(a) != "" && squares(a) == squares(b) && squares(b) == squares(c)
case _ => throw new Error
})
}
def getValue(condition: Boolean): String = if (condition) "X" else "O"
def handleClick(i: Int): Unit = {
val historyToStep = history.take(stepNumber + 1)
val current = historyToStep.last
val squares: Array[String] = Array(current: _*)
if (squares(i) == "" && calculateWinningLine(squares).isEmpty) {
squares(i) = getValue(xIsNext)
updateHistory(historyToStep :+ squares)
updateStepNumber(historyToStep.length)
updateXIsNext(!xIsNext)
}
}
def jumpTo(step: Int): Unit = {
updateStepNumber(step)
updateXIsNext((step % 2) == 0)
}
val current = history(stepNumber)
val highlights = Array.fill(9)(false)
val status = calculateWinningLine(current) match {
case Some(line) =>
line.foreach(highlights(_) = true)
"Winner: " + getValue(!xIsNext)
case None if stepNumber < 9 => "Next player: " + getValue(xIsNext)
case _ => "It's a draw"
}
def getMove(index: Int): Int = (0 until 9).find(m => history(index)(m) != history(index - 1)(m)).get
def toCoords(move: Int): (Int, Int) = {
val x = move % 3
(x, (move - x) / 3)
}
val moves = history.indices.map(index => {
val desc =
if (index == 0) "Go to game start"
else "Go to move #" + index + " (" + toCoords(getMove(index)) + ")"
li(key := index.toString)(
button(
className := (if (index == stepNumber) "bold-button" else ""),
onClick := { _ => jumpTo(index) }
)({
desc
})
)
})
val orderButton = button(onClick := { _ =>
updateIsAscending(!isAscending)
})("Toggle order")
div(className := "game")(
div(className := "game-board")(Board(current, highlights, handleClick)),
div(className := "game-info")(
div(className := "status")(status),
orderButton,
ol(if (isAscending) moves else moves.reverse)
)
)
}
}
object App {
def apply(): ReactElement = component()
private val css = AppCSS
val component: FunctionalComponent[Unit] = FunctionalComponent[Unit] { _ =>
div(className := "App")(
header(className := "App-header")(
img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
h1(className := "App-title")("Welcome to React (with Scala.js!)")
),
p(className := "App-intro")(
b("Slinky"),
" version of the official ",
a(href := "https://reactjs.org/tutorial/tutorial.html")(
b("React"),
"'s ",
i("learn by doing"),
" practical tutorial"
),
". To modify it, edit ", code("App.scala"), " and save to reload."
),
Game()
)
}
}
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.highlighted {
background: yellow;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
.bold-button {
font-weight: bold;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment