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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.class
*.log
node_modules
*/node_modules

# sbt specific
.cache/
Expand Down
7 changes: 3 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: scala
scala:
- 2.11.8
- 2.12.8
sudo: false
jdk:
- oraclejdk8
Expand All @@ -16,7 +16,6 @@ cache:
- $HOME/.sbt/boot/
install:
- . $HOME/.nvm/nvm.sh
- nvm install stable
- nvm use stable
- nvm install --latest-npm --lts=Dubnium
- nvm use --lts
- npm install
- npm install jsdom
92 changes: 49 additions & 43 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import sbt.Keys._
import sbt.Project.projectToRef
// shadow sbt-scalajs' crossProject and CrossType from Scala.js 0.6.x
import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType}
import org.irundaia.sbt.sass._

// a special crossProject for configuring a JS/JVM/shared structure
lazy val shared = (crossProject.crossType(CrossType.Pure) in file("shared"))
lazy val shared = (crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("shared"))
.settings(
scalaVersion := Settings.versions.scala,
libraryDependencies ++= Settings.sharedDependencies.value
scalaVersion := Settings.v.scala,
libraryDependencies ++= Settings.sharedDependencies.value
)
// set up settings specific to the JS project
.jsConfigure(_ enablePlugins ScalaJSWeb)
Expand All @@ -20,24 +23,25 @@ lazy val elideOptions = settingKey[Seq[String]]("Set limit for elidable function
// instantiate the JS project for SBT with some additional settings
lazy val client: Project = (project in file("client"))
.settings(
name := "client",
version := Settings.version,
scalaVersion := Settings.versions.scala,
scalacOptions ++= Settings.scalacOptions,
libraryDependencies ++= Settings.scalajsDependencies.value,
// by default we do development build, no eliding
elideOptions := Seq(),
scalacOptions ++= elideOptions.value,
jsDependencies ++= Settings.jsDependencies.value,
// RuntimeDOM is needed for tests
jsDependencies += RuntimeDOM % "test",
// yes, we want to package JS dependencies
skip in packageJSDependencies := false,
// use Scala.js provided launcher code to start the client app
scalaJSUseMainModuleInitializer := true,
scalaJSUseMainModuleInitializer in Test := false,
// use uTest framework for tests
testFrameworks += new TestFramework("utest.runner.Framework")
name := "client",
version := Settings.v.app,
scalaVersion := Settings.v.scala,
scalacOptions ++= Settings.scalacOptions,
libraryDependencies ++= Settings.scalajsDependencies.value,
// by default we do development build, no eliding
elideOptions := Seq(),
scalacOptions ++= elideOptions.value,
jsDependencies ++= Settings.jsDependencies.value,
// RuntimeDOM is needed for tests
jsEnv in Test := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv,
// yes, we want to package JS dependencies
skip in packageJSDependencies := false,
// use Scala.js provided launcher code to start the client app
scalaJSUseMainModuleInitializer := true,
scalaJSUseMainModuleInitializer in Test := false,
// use uTest framework for tests
testFrameworks += new TestFramework("utest.runner.Framework"),
dependencyOverrides ++= Settings.dependencyOverrides.value
)
.enablePlugins(ScalaJSPlugin, ScalaJSWeb)
.dependsOn(sharedJS)
Expand All @@ -48,20 +52,22 @@ lazy val clients = Seq(client)
// instantiate the JVM project for SBT with some additional settings
lazy val server = (project in file("server"))
.settings(
name := "server",
version := Settings.version,
scalaVersion := Settings.versions.scala,
scalacOptions ++= Settings.scalacOptions,
libraryDependencies ++= Settings.jvmDependencies.value,
commands += ReleaseCmd,
// triggers scalaJSPipeline when using compile or continuous compilation
compile in Compile := ((compile in Compile) dependsOn scalaJSPipeline).value,
// connect to the client project
scalaJSProjects := clients,
pipelineStages in Assets := Seq(scalaJSPipeline),
pipelineStages := Seq(digest, gzip),
// compress CSS
LessKeys.compress in Assets := true
name := "server",
version := Settings.v.app,
scalaVersion := Settings.v.scala,
scalacOptions ++= Settings.scalacOptions,
libraryDependencies ++= Settings.jvmDependencies.value,
libraryDependencies += guice,
commands += ReleaseCmd,
// triggers scalaJSPipeline when using compile or continuous compilation
compile in Compile := ((compile in Compile) dependsOn scalaJSPipeline).value,
// connect to the client project
scalaJSProjects := clients,
pipelineStages in Assets := Seq(scalaJSPipeline),
pipelineStages := Seq(digest, gzip),
// compress CSS
SassKeys.cssStyle in Assets := Minified,
dependencyOverrides ++= Settings.dependencyOverrides.value
)
.enablePlugins(PlayScala)
.disablePlugins(PlayLayoutPlugin) // use the standard directory layout instead of Play's custom
Expand All @@ -70,14 +76,14 @@ lazy val server = (project in file("server"))

// Command for building a release
lazy val ReleaseCmd = Command.command("release") {
state => "set elideOptions in client := Seq(\"-Xelide-below\", \"WARNING\")" ::
"client/clean" ::
"client/test" ::
"server/clean" ::
"server/test" ::
"server/dist" ::
"set elideOptions in client := Seq()" ::
state
state => "set elideOptions in client := Seq(\"-Xelide-below\", \"WARNING\")" ::
"client/clean" ::
"client/test" ::
"server/clean" ::
"server/test" ::
"server/dist" ::
"set elideOptions in client := Seq()" ::
state
}

// lazy val root = (project in file(".")).aggregate(client, server)
Expand Down
13 changes: 7 additions & 6 deletions client/src/main/scala/spatutorial/client/SPAMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import spatutorial.client.logger._
import spatutorial.client.modules._
import spatutorial.client.services.SPACircuit

import scala.scalajs.js
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
import CssSettings._
import scalacss.ScalaCssReact._

@JSExportTopLevel("SPAMain")
object SPAMain extends js.JSApp {
object SPAMain {

@JSExportTopLevel("SPAMain")
protected def getInstance(): this.type = this

// Define the locations (pages) used in this application
sealed trait Loc
Expand All @@ -39,9 +40,9 @@ object SPAMain extends js.JSApp {
def layout(c: RouterCtl[Loc], r: Resolution[Loc]) = {
<.div(
// here we use plain Bootstrap class names as these are specific to the top level layout defined here
<.nav(^.className := "navbar navbar-inverse navbar-fixed-top",
<.nav(^.className := "navbar navbar-dark bg-dark fixed-top navbar-expand-md",
<.div(^.className := "container",
<.div(^.className := "navbar-header", <.span(^.className := "navbar-brand", "SPA Tutorial")),
<.span(^.className := "navbar-brand", "SPA Tutorial"),
<.div(^.className := "collapse navbar-collapse",
// connect menu to model, because it needs to update when the number of open todos changes
todoCountWrapper(proxy => MainMenu(c, r.page, proxy))
Expand All @@ -54,7 +55,7 @@ object SPAMain extends js.JSApp {
}

@JSExport
def main(): Unit = {
def main(args: Array[String]): Unit = {
log.warn("Application starting")
// send log messages also to the server
log.enableServerLogging("/logging")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ object Bootstrap {
def apply() = component
}

object Panel {
object Card {

case class Props(heading: String, style: CommonStyle.Value = CommonStyle.default)

val component = ScalaComponent.builder[Props]("Panel")
val component = ScalaComponent.builder[Props]("Card")
.renderPC((_, p, c) =>
<.div(bss.panelOpt(p.style),
<.div(bss.panelHeading, p.heading),
<.div(bss.panelBody, c)
<.div(bss.cardOpt(p.style),
<.div(bss.cardHeading, p.heading),
<.div(bss.cardBody, c)
)
).build

Expand All @@ -67,7 +67,7 @@ object Bootstrap {
class Backend(t: BackendScope[Props, Unit]) {
def hide =
// instruct Bootstrap to hide the modal
t.getDOMNode.map(jQuery(_).modal("hide")).void
t.getDOMNode.map(n => jQuery(n.toElement.get).modal("hide")).void

// jQuery event handler to be fired when the modal has been hidden
def hidden(e: JQueryEventObject): js.Any = {
Expand All @@ -94,9 +94,9 @@ object Bootstrap {
.componentDidMount(scope => Callback {
val p = scope.props
// instruct Bootstrap to show the modal
jQuery(scope.getDOMNode).modal(js.Dynamic.literal("backdrop" -> p.backdrop, "keyboard" -> p.keyboard, "show" -> true))
jQuery(scope.getDOMNode.asElement).modal(js.Dynamic.literal("backdrop" -> p.backdrop, "keyboard" -> p.keyboard, "show" -> true))
// register event listener to be notified when the modal is closed
jQuery(scope.getDOMNode).on("hidden.bs.modal", null, null, scope.backend.hidden _)
jQuery(scope.getDOMNode.asElement).on("hidden.bs.modal", null, null, scope.backend.hidden _)
})
.build

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ class BootstrapStyles(implicit r: mutable.Register) extends StyleSheet.Inline()(

val button = buttonOpt(default)

val panelOpt = commonStyle(csDomain, "panel")
val cardOpt = commonStyle(csDomain, "card")

val panel = panelOpt(default)
val card = cardOpt(default)

val labelOpt = commonStyle(csDomain, "label")
val badgeOpt = commonStyle(csDomain, "badge")

val label = labelOpt(default)
val badge = badgeOpt(default)

val alert = commonStyle(contextDomain, "alert")

val panelHeading = styleWrap("panel-heading")
val cardHeading = styleWrap("card-header")

val panelBody = styleWrap("panel-body")
val cardBody = styleWrap("card-body")

// wrap styles in a namespace, assign to val to prevent lazy initialization
object modal {
Expand All @@ -61,14 +61,19 @@ class BootstrapStyles(implicit r: mutable.Register) extends StyleSheet.Inline()(
}

val _listGroup = listGroup
val pullRight = styleWrap("pull-right")
val buttonXS = styleWrap("btn-xs")
val floatRight = styleWrap("float-right")
val buttonSM = styleWrap("btn-sm")
val buttonSecondary = styleWrap("btn-secondary")
val close = styleWrap("close")

val labelAsBadge = style(addClassName("label-as-badge"), borderRadius(1.em))
val badgePill = style(addClassName("badge-pill"), borderRadius(1.em))

val navbar = styleWrap("nav", "navbar-nav")
val navbar = styleWrap("navbar-nav", "mr-auto")

val formGroup = styleWrap("form-group")
val formControl = styleWrap("form-control")

val spacingMR1 = styleWrap("mr-1")
val spacingMT3 = styleWrap("mt-3")

}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object Chart {
)
.componentDidMount(scope => Callback {
// access context of the canvas
val ctx = scope.getDOMNode.asInstanceOf[HTMLCanvasElement].getContext("2d")
val ctx = scope.getDOMNode.asElement.asInstanceOf[HTMLCanvasElement].getContext("2d")
// create the actual chart using the 3rd party component
scope.props.style match {
case LineChart => new JSChart(ctx, ChartConfiguration("line", scope.props.data))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ object Motd {
// create the React component for holding the Message of the Day
val Motd = ScalaComponent.builder[ModelProxy[Pot[String]]]("Motd")
.render_P { proxy =>
Panel(Panel.Props("Message of the day"),
Card(Card.Props("Message of the day"),
// render messages depending on the state of the Pot
proxy().renderPending(_ > 500, _ => <.p("Loading...")),
proxy().renderFailed(ex => <.p("Failed to load")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ object TodoList {
<.input.checkbox(^.checked := item.completed, ^.onChange --> p.stateChange(item.copy(completed = !item.completed))),
<.span(" "),
if (item.completed) <.s(item.content) else <.span(item.content),
Button(Button.Props(p.editItem(item), addStyles = Seq(bss.pullRight, bss.buttonXS)), "Edit"),
Button(Button.Props(p.deleteItem(item), addStyles = Seq(bss.pullRight, bss.buttonXS)), "Delete")
Button(Button.Props(p.editItem(item), addStyles = Seq(bss.floatRight, bss.buttonSM, bss.buttonSecondary)), "Edit"),
Button(Button.Props(p.deleteItem(item), addStyles = Seq(bss.floatRight, bss.buttonSM, bss.buttonSecondary, bss.spacingMR1)), "Delete")
)
}
<.ul(style.listGroup)(p.items toTagMod renderItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import spatutorial.client.SPAMain.{Loc, TodoLoc}
import spatutorial.client.components._

import scala.util.Random
import scala.language.existentials

object Dashboard {

Expand Down
12 changes: 6 additions & 6 deletions client/src/main/scala/spatutorial/client/modules/MainMenu.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ object MainMenu {

case class Props(router: RouterCtl[Loc], currentLoc: Loc, proxy: ModelProxy[Option[Int]])

private case class MenuItem(idx: Int, label: (Props) => VdomNode, icon: Icon, location: Loc)
private case class MenuItem(idx: Int, badge: (Props) => VdomNode, icon: Icon, location: Loc)

// build the Todo menu item, showing the number of open todos
private def buildTodoMenu(props: Props): VdomElement = {
val todoCount = props.proxy().getOrElse(0)
<.span(
<.span("Todo "),
<.span(bss.labelOpt(CommonStyle.danger), bss.labelAsBadge, todoCount).when(todoCount > 0)
<.span(" Todo "),
<.span(bss.badgeOpt(CommonStyle.danger), bss.badgePill, todoCount).when(todoCount > 0)
)
}

private val menuItems = Seq(
MenuItem(1, _ => "Dashboard", Icon.dashboard, DashboardLoc),
MenuItem(1, _ => " Dashboard", Icon.dashboard, DashboardLoc),
MenuItem(2, buildTodoMenu, Icon.check, TodoLoc)
)

Expand All @@ -43,8 +43,8 @@ object MainMenu {
<.ul(bss.navbar)(
// build a list of menu items
menuItems.toVdomArray(item =>
<.li(^.key := item.idx, (^.className := "active").when(props.currentLoc == item.location),
props.router.link(item.location)(item.icon, " ", item.label(props))
<.li(^.key := item.idx, if(props.currentLoc == item.location) (^.className := "nav-item active") else (^.className := "nav-item"),
props.router.link(item.location)(item.icon, "", item.badge(props))(^.`class` := "nav-link")
))
)
}
Expand Down
12 changes: 7 additions & 5 deletions client/src/main/scala/spatutorial/client/modules/TODO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import spatutorial.shared._
import scalacss.ScalaCssReact._

object Todo {
// shorthand for styles
@inline private def bss = GlobalStyles.bootstrapStyles

case class Props(proxy: ModelProxy[Pot[Todos]])

Expand Down Expand Up @@ -41,13 +43,13 @@ object Todo {
}

def render(p: Props, s: State) =
Panel(Panel.Props("What needs to be done"), <.div(
Card(Card.Props("What needs to be done"), <.div(
p.proxy().renderFailed(ex => "Error loading"),
p.proxy().renderPending(_ > 500, _ => "Loading..."),
p.proxy().render(todos => TodoList(todos.items, item => p.proxy.dispatchCB(UpdateTodo(item)),
item => editTodo(Some(item)), item => p.proxy.dispatchCB(DeleteTodo(item)))),
Button(Button.Props(editTodo(None)), Icon.plusSquare, " New")),
// if the dialog is open, add it to the panel
Button(Button.Props(editTodo(None), addStyles = Seq(bss.buttonSecondary, bss.spacingMT3)), Icon.plusSquare, " New")),
// if the dialog is open, add it to the card
if (s.showTodoForm) TodoForm(TodoForm.Props(s.selectedItem, todoEdited))
else // otherwise add an empty placeholder
VdomArray.empty())
Expand Down Expand Up @@ -103,9 +105,9 @@ object TodoForm {
val headerText = if (s.item.id == "") "Add new todo" else "Edit todo"
Modal(Modal.Props(
// header contains a cancel button (X)
header = hide => <.span(<.button(^.tpe := "button", bss.close, ^.onClick --> hide, Icon.close), <.h4(headerText)),
header = hide => React.Fragment(<.h4(headerText), <.button(^.tpe := "button", bss.close, ^.onClick --> hide, Icon.close)),
// footer has the OK button that submits the form before hiding it
footer = hide => <.span(Button(Button.Props(submitForm() >> hide), "OK")),
footer = hide => <.span(Button(Button.Props(submitForm() >> hide, addStyles = Seq(bss.buttonSecondary)), "OK")),
// this is called after the modal has been hidden (animation is completed)
closed = formClosed(s, p)),
<.div(bss.formGroup,
Expand Down
Loading