├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── file-upload ├── .gitignore ├── backend │ └── src │ │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── scala │ │ └── io │ │ └── udash │ │ └── demos │ │ └── files │ │ ├── Launcher.scala │ │ ├── jetty │ │ ├── ApplicationServer.scala │ │ ├── DemoFileDownloadServlet.scala │ │ └── DemoFileUploadServlet.scala │ │ ├── rpc │ │ ├── ClientRPC.scala │ │ └── MainRpcEndpoint.scala │ │ └── services │ │ └── FilesStorage.scala ├── build.sbt ├── frontend │ └── src │ │ └── main │ │ ├── assets │ │ ├── images │ │ │ ├── icon_avsystem.png │ │ │ ├── icon_github.png │ │ │ ├── icon_stackoverflow.png │ │ │ ├── udash_logo.png │ │ │ └── udash_logo_m.png │ │ └── index.html │ │ └── scala │ │ └── io │ │ └── udash │ │ └── demos │ │ └── files │ │ ├── ApplicationContext.scala │ │ ├── JSLauncher.scala │ │ ├── RoutingRegistryDef.scala │ │ ├── StatesToViewFactoryDef.scala │ │ ├── config │ │ └── ExternalUrls.scala │ │ ├── rpc │ │ └── RPCService.scala │ │ ├── states.scala │ │ └── views │ │ ├── ErrorView.scala │ │ ├── RootView.scala │ │ ├── components │ │ ├── Header.scala │ │ └── ImageFactory.scala │ │ └── index │ │ ├── IndexPresenter.scala │ │ ├── IndexView.scala │ │ ├── IndexViewFactory.scala │ │ └── UploadViewModel.scala ├── project │ ├── Dependencies.scala │ ├── build.properties │ └── plugins.sbt ├── readme.md └── shared │ └── src │ └── main │ └── scala │ └── io │ └── udash │ └── demos │ └── files │ ├── ApplicationServerContexts.scala │ ├── UploadedFile.scala │ └── rpc │ ├── MainClientRPC.scala │ └── MainServerRPC.scala ├── rest-akka-http ├── .gitignore ├── backend │ └── src │ │ └── main │ │ ├── resources │ │ ├── application.conf │ │ └── logback.xml │ │ └── scala │ │ └── io │ │ └── udash │ │ └── demos │ │ └── rest │ │ ├── Launcher.scala │ │ ├── api │ │ └── PhoneBookWebService.scala │ │ └── services │ │ ├── ContactService.scala │ │ └── PhoneBookService.scala ├── build.sbt ├── frontend │ └── src │ │ └── main │ │ ├── assets │ │ ├── images │ │ │ ├── icon_avsystem.png │ │ │ ├── icon_github.png │ │ │ ├── icon_stackoverflow.png │ │ │ ├── udash_logo.png │ │ │ └── udash_logo_m.png │ │ └── index.html │ │ └── scala │ │ └── io │ │ └── udash │ │ └── demos │ │ └── rest │ │ ├── ApplicationContext.scala │ │ ├── JSLauncher.scala │ │ ├── RoutingRegistryDef.scala │ │ ├── StatesToViewFactoryDef.scala │ │ ├── config │ │ └── ExternalUrls.scala │ │ ├── states.scala │ │ └── views │ │ ├── ErrorView.scala │ │ ├── RootView.scala │ │ ├── book │ │ ├── PhoneBookEditorModel.scala │ │ ├── PhoneBookFormPresenter.scala │ │ ├── PhoneBookFormView.scala │ │ └── PhoneBookFormViewFactory.scala │ │ ├── components │ │ ├── Header.scala │ │ └── ImageFactory.scala │ │ ├── contact │ │ ├── ContactEditorModel.scala │ │ ├── ContactFormPresenter.scala │ │ ├── ContactFormView.scala │ │ └── ContactFormViewFactory.scala │ │ └── index │ │ ├── IndexPresenter.scala │ │ ├── IndexView.scala │ │ ├── IndexViewFactory.scala │ │ └── model.scala ├── project │ ├── Dependencies.scala │ ├── build.properties │ └── plugins.sbt ├── readme.md └── shared │ └── src │ └── main │ └── scala │ └── io │ └── udash │ └── demos │ └── rest │ ├── MainServerREST.scala │ └── model │ ├── Contact.scala │ └── PhoneBook.scala ├── test.sh ├── todo-rpc ├── .gitignore ├── backend │ └── src │ │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── scala │ │ └── io │ │ └── udash │ │ └── todo │ │ ├── Launcher.scala │ │ ├── jetty │ │ └── ApplicationServer.scala │ │ ├── rpc │ │ ├── ClientRPC.scala │ │ └── ExposedRpcInterfaces.scala │ │ └── services │ │ ├── InMemoryTodoStorage.scala │ │ └── TodoStorage.scala ├── build.sbt ├── frontend │ └── src │ │ └── main │ │ ├── assets │ │ ├── css │ │ │ ├── base.css │ │ │ └── index.css │ │ ├── index.html │ │ └── scripts │ │ │ └── base.js │ │ └── scala │ │ └── io │ │ └── udash │ │ └── todo │ │ ├── ApplicationContext.scala │ │ ├── JSLauncher.scala │ │ ├── RoutingRegistryDef.scala │ │ ├── StatesToViewFactoryDef.scala │ │ ├── rpc │ │ └── RPCService.scala │ │ ├── states.scala │ │ ├── storage │ │ ├── RemoteTodoStorage.scala │ │ └── TodoStorage.scala │ │ └── views │ │ ├── ErrorView.scala │ │ ├── RootView.scala │ │ └── todo │ │ ├── Todo.scala │ │ ├── TodoPresenter.scala │ │ ├── TodoView.scala │ │ └── TodoViewFactory.scala ├── project │ ├── Dependencies.scala │ ├── build.properties │ └── plugins.sbt ├── readme.md └── shared │ └── src │ └── main │ └── scala │ └── io │ └── udash │ └── todo │ └── rpc │ ├── MainClientRPC.scala │ ├── MainServerRPC.scala │ └── model │ └── Todo.scala └── todo ├── .gitignore ├── build.sbt ├── index.html ├── node_modules ├── todomvc-app-css │ └── index.css └── todomvc-common │ └── base.css ├── package.json ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── readme.md └── src └── main └── scala └── io └── udash └── todo ├── ApplicationContext.scala ├── JSLauncher.scala ├── RoutingRegistryDef.scala ├── StatesToViewFactoryDef.scala ├── states.scala ├── storage └── TodoStorage.scala └── views ├── ErrorView.scala ├── RootView.scala └── todo ├── Todo.scala ├── TodoPresenter.scala ├── TodoView.scala └── TodoViewFactory.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Eclipse template 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # Java annotation processor (APT) 32 | .factorypath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | ### Maven template 43 | target/ 44 | pom.xml.tag 45 | pom.xml.releaseBackup 46 | pom.xml.versionsBackup 47 | pom.xml.next 48 | release.properties 49 | dependency-reduced-pom.xml 50 | buildNumber.properties 51 | .mvn/timing.properties 52 | ### JetBrains template 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | ### Java template 100 | *.class 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.ear 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | ### Scala template 113 | *.class 114 | *.log 115 | 116 | # sbt specific 117 | .cache 118 | .history 119 | .lib/ 120 | dist/* 121 | target/ 122 | lib_managed/ 123 | src_managed/ 124 | project/boot/ 125 | project/plugins/project/ 126 | 127 | # Scala-IDE specific 128 | .scala_dependencies 129 | .worksheet 130 | 131 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.11.8 5 | 6 | jdk: 7 | - oraclejdk8 8 | 9 | script: 10 | - ./test.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Udash Demos [![Build Status](https://travis-ci.org/UdashFramework/udash-demos.svg?branch=master)](https://travis-ci.org/UdashFramework/udash-demos) [![Join the chat at https://gitter.im/UdashFramework/udash-demos](https://badges.gitter.im/UdashFramework/udash-demos.svg)](https://gitter.im/UdashFramework/udash-demos?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](http://www.avsystem.com/) 2 | 3 | Collection of demo applications based on Udash. 4 | 5 | # Note: this repository is not updated for Udash 0.8+. If you're looking to setup a Udash application, please check out the [g8 template](https://github.com/UdashFramework/udash.g8). Some of these demos will be updated after the 1.0 release and available either within the template or [Udash Guide](https://guide.udash.io/). 6 | 7 | ## Udash Files Upload 8 | 9 | A demo presenting usage of the Udash file upload/download utilities. It also uses the Udash Bootstrap and RPC modules. 10 | 11 | ## Todo 12 | 13 | A demo based on [TodoMVC](http://todomvc.com/). This is a frontend-only project. 14 | 15 | ## Todo RPC 16 | 17 | A demo based on [TodoMVC](http://todomvc.com/) with a server-side storage using the Udash RPC system. 18 | 19 | ## Udash REST with Bootstrap Components and Akka HTTP 20 | 21 | A demo presenting usage of the Udash REST module. It serves REST API with Akka HTTP in the backend application 22 | and uses it through type-safe Udash REST interfaces in the frontend. This demo uses the Udash Bootstrap Components library. 23 | -------------------------------------------------------------------------------- /file-upload/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Eclipse template 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # Java annotation processor (APT) 32 | .factorypath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | ### Maven template 43 | target/ 44 | pom.xml.tag 45 | pom.xml.releaseBackup 46 | pom.xml.versionsBackup 47 | pom.xml.next 48 | release.properties 49 | dependency-reduced-pom.xml 50 | buildNumber.properties 51 | .mvn/timing.properties 52 | ### JetBrains template 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | ### Java template 100 | *.class 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.ear 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | ### Scala template 113 | *.class 114 | *.log 115 | 116 | # sbt specific 117 | .cache 118 | .history 119 | .lib/ 120 | dist/* 121 | target/ 122 | lib_managed/ 123 | src_managed/ 124 | project/boot/ 125 | project/plugins/project/ 126 | 127 | # Scala-IDE specific 128 | .scala_dependencies 129 | .worksheet 130 | 131 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | logs/udash-guide-${bySecond}.log 12 | true 13 | 14 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/Launcher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash.demos.files.jetty.ApplicationServer 4 | import io.udash.logging.CrossLogging 5 | 6 | import scala.io.StdIn 7 | 8 | object Launcher extends CrossLogging { 9 | def main(args: Array[String]): Unit = { 10 | val server = new ApplicationServer(8080, "frontend/target/UdashStatics/WebContent") 11 | server.start() 12 | logger.info(s"Application started...") 13 | 14 | // wait for user input and then stop the server 15 | logger.info(s"Click `Enter` to close application...") 16 | StdIn.readLine() 17 | server.stop() 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/jetty/ApplicationServer.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.jetty 2 | 3 | import javax.servlet.MultipartConfigElement 4 | 5 | import io.udash.demos.files.ApplicationServerContexts 6 | import io.udash.demos.files.rpc.{MainRpcEndpoint, MainServerRPC} 7 | import org.eclipse.jetty.server.Server 8 | import org.eclipse.jetty.server.handler.gzip.GzipHandler 9 | import org.eclipse.jetty.server.session.SessionHandler 10 | import org.eclipse.jetty.servlet.{DefaultServlet, ServletContextHandler, ServletHolder} 11 | 12 | class ApplicationServer(val port: Int, resourceBase: String) { 13 | 14 | private val server = new Server(port) 15 | private val contextHandler = new ServletContextHandler 16 | 17 | contextHandler.setSessionHandler(new SessionHandler) 18 | contextHandler.setGzipHandler(new GzipHandler) 19 | server.setHandler(contextHandler) 20 | 21 | def start(): Unit = server.start() 22 | def stop(): Unit = server.stop() 23 | 24 | private val appHolder = { 25 | val appHolder = new ServletHolder(new DefaultServlet) 26 | appHolder.setAsyncSupported(true) 27 | appHolder.setInitParameter("resourceBase", resourceBase) 28 | appHolder 29 | } 30 | contextHandler.addServlet(appHolder, "/*") 31 | 32 | private val uploadsHolder = { 33 | val holder = new ServletHolder(new DemoFileUploadServlet(resourceBase + "/uploads")) 34 | holder.getRegistration.setMultipartConfig(new MultipartConfigElement("")) 35 | holder 36 | } 37 | contextHandler.addServlet(uploadsHolder, ApplicationServerContexts.uploadContextPrefix + "/*") 38 | 39 | private val downloadsHolder = { 40 | val holder = new ServletHolder(new DemoFileDownloadServlet(resourceBase + "/uploads", ApplicationServerContexts.downloadContextPrefix)) 41 | holder.getRegistration.setMultipartConfig(new MultipartConfigElement("")) 42 | holder 43 | } 44 | contextHandler.addServlet(downloadsHolder, ApplicationServerContexts.downloadContextPrefix + "/*") 45 | 46 | private val atmosphereHolder = { 47 | import io.udash.rpc._ 48 | 49 | val config = new DefaultAtmosphereServiceConfig[MainServerRPC]((_) => 50 | new DefaultExposesServerRPC[MainServerRPC](new MainRpcEndpoint) 51 | ) 52 | val framework = new DefaultAtmosphereFramework(config) 53 | 54 | val atmosphereHolder = new ServletHolder(new RpcServlet(framework)) 55 | atmosphereHolder.setAsyncSupported(true) 56 | atmosphereHolder 57 | } 58 | contextHandler.addServlet(atmosphereHolder, ApplicationServerContexts.atmosphereContextPrefix + "/*") 59 | } 60 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/jetty/DemoFileDownloadServlet.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.jetty 2 | 3 | import java.io.File 4 | import java.net.URLDecoder 5 | import java.nio.charset.StandardCharsets 6 | import javax.servlet.http.HttpServletRequest 7 | 8 | import io.udash.demos.files.services.FilesStorage 9 | import io.udash.rpc.utils.FileDownloadServlet 10 | 11 | class DemoFileDownloadServlet(filesDir: String, contextPrefix: String) extends FileDownloadServlet { 12 | override protected def resolveFile(request: HttpServletRequest): File = { 13 | val name = URLDecoder.decode(request.getRequestURI.stripPrefix(contextPrefix + "/"), StandardCharsets.UTF_8.name()) 14 | new File(filesDir, name) 15 | } 16 | 17 | override protected def presentedFileName(name: String): String = 18 | FilesStorage.allFiles 19 | .find(_.serverFileName == name) 20 | .map(_.name) 21 | .getOrElse(name) 22 | } 23 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/jetty/DemoFileUploadServlet.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.jetty 2 | 3 | import scala.concurrent.ExecutionContext.Implicits.global 4 | import java.io.{File, InputStream} 5 | import java.nio.file.Files 6 | import java.util.UUID 7 | 8 | import io.udash.demos.files.UploadedFile 9 | import io.udash.demos.files.rpc.ClientRPC 10 | import io.udash.demos.files.services.FilesStorage 11 | import io.udash.rpc._ 12 | 13 | class DemoFileUploadServlet(uploadDir: String) extends FileUploadServlet(Set("file", "files")) { 14 | new File(uploadDir).mkdir() 15 | 16 | override protected def handleFile(name: String, content: InputStream): Unit = { 17 | val targetName: String = s"${UUID.randomUUID()}_${name.replaceAll("[^a-zA-Z0-9.-]", "_")}" 18 | val targetFile = new File(uploadDir, targetName) 19 | Files.copy(content, targetFile.toPath) 20 | FilesStorage.add( 21 | UploadedFile(name, targetName, targetFile.length()) 22 | ) 23 | 24 | // Notify clients 25 | ClientRPC(AllClients).fileStorageUpdated() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/rpc/ClientRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.rpc 2 | 3 | import io.udash.rpc._ 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | object ClientRPC { 8 | def apply(target: ClientRPCTarget)(implicit ec: ExecutionContext): MainClientRPC = { 9 | new DefaultClientRPC[MainClientRPC](target).get 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/rpc/MainRpcEndpoint.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.rpc 2 | import io.udash.demos.files.UploadedFile 3 | import io.udash.demos.files.services.FilesStorage 4 | 5 | import scala.concurrent.Future 6 | 7 | class MainRpcEndpoint extends MainServerRPC { 8 | override def loadUploadedFiles(): Future[Seq[UploadedFile]] = 9 | Future.successful(FilesStorage.allFiles) 10 | } 11 | -------------------------------------------------------------------------------- /file-upload/backend/src/main/scala/io/udash/demos/files/services/FilesStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.services 2 | 3 | import io.udash.demos.files.UploadedFile 4 | 5 | import scala.collection.mutable 6 | 7 | object FilesStorage { 8 | private val files: mutable.ArrayBuffer[UploadedFile] = mutable.ArrayBuffer.empty 9 | 10 | def add(file: UploadedFile): Unit = 11 | files.append(file) 12 | 13 | def allFiles: Seq[UploadedFile] = 14 | files 15 | } 16 | -------------------------------------------------------------------------------- /file-upload/build.sbt: -------------------------------------------------------------------------------- 1 | import sbtcrossproject.{crossProject, CrossType} 2 | 3 | name := "file-upload" 4 | 5 | inThisBuild(Seq( 6 | version := "0.7.0-SNAPSHOT", 7 | scalaVersion := "2.12.6", 8 | organization := "io.udash", 9 | scalacOptions ++= Seq( 10 | "-feature", 11 | "-deprecation", 12 | "-unchecked", 13 | "-language:implicitConversions", 14 | "-language:existentials", 15 | "-language:dynamics", 16 | "-Xfuture", 17 | "-Xfatal-warnings", 18 | "-Xlint:_,-missing-interpolator,-adapted-args" 19 | ), 20 | )) 21 | 22 | // Custom SBT tasks 23 | val copyAssets = taskKey[Unit]("Copies all assets to the target directory.") 24 | val compileStatics = taskKey[File]( 25 | "Compiles JavaScript files and copies all assets to the target directory." 26 | ) 27 | val compileAndOptimizeStatics = taskKey[File]( 28 | "Compiles and optimizes JavaScript files and copies all assets to the target directory." 29 | ) 30 | 31 | lazy val `rest-akka-http` = project.in(file(".")) 32 | .aggregate(sharedJS, sharedJVM, frontend, backend) 33 | .dependsOn(backend) 34 | .settings( 35 | publishArtifact := false, 36 | Compile / mainClass := Some("io.udash.demos.files.Launcher"), 37 | ) 38 | 39 | lazy val shared = crossProject(JSPlatform, JVMPlatform) 40 | .crossType(CrossType.Pure).in(file("shared")) 41 | .settings( 42 | libraryDependencies ++= Dependencies.crossDeps.value, 43 | ) 44 | 45 | lazy val sharedJVM = shared.jvm 46 | lazy val sharedJS = shared.js 47 | 48 | lazy val backend = project.in(file("backend")) 49 | .dependsOn(sharedJVM) 50 | .settings( 51 | libraryDependencies ++= Dependencies.backendDeps.value, 52 | Compile / mainClass := Some("io.udash.demos.files.Launcher"), 53 | ) 54 | 55 | val frontendWebContent = "UdashStatics/WebContent" 56 | lazy val frontend = project.in(file("frontend")).enablePlugins(ScalaJSPlugin) 57 | .dependsOn(sharedJS) 58 | .settings( 59 | libraryDependencies ++= Dependencies.frontendDeps.value, 60 | 61 | // Make this module executable in JS 62 | Compile / mainClass := Some("io.udash.demos.files.JSLauncher"), 63 | scalaJSUseMainModuleInitializer := true, 64 | 65 | // Implementation of custom tasks defined above 66 | copyAssets := { 67 | IO.copyDirectory( 68 | sourceDirectory.value / "main/assets", 69 | target.value / frontendWebContent / "assets" 70 | ) 71 | IO.copyFile( 72 | sourceDirectory.value / "main/assets/index.html", 73 | target.value / frontendWebContent / "index.html" 74 | ) 75 | }, 76 | 77 | // Compiles JS files without full optimizations 78 | compileStatics := { (Compile / fastOptJS / target).value / "UdashStatics" }, 79 | compileStatics := compileStatics.dependsOn( 80 | Compile / fastOptJS, Compile / copyAssets 81 | ).value, 82 | 83 | // Compiles JS files with full optimizations 84 | compileAndOptimizeStatics := { (Compile / fullOptJS / target).value / "UdashStatics" }, 85 | compileAndOptimizeStatics := compileAndOptimizeStatics.dependsOn( 86 | Compile / fullOptJS, Compile / copyAssets 87 | ).value, 88 | 89 | // Target files for Scala.js plugin 90 | Compile / fastOptJS / artifactPath := 91 | (Compile / fastOptJS / target).value / 92 | frontendWebContent / "scripts" / "frontend.js", 93 | Compile / fullOptJS / artifactPath := 94 | (Compile / fullOptJS / target).value / 95 | frontendWebContent / "scripts" / "frontend.js", 96 | Compile / packageJSDependencies / artifactPath := 97 | (Compile / packageJSDependencies / target).value / 98 | frontendWebContent / "scripts" / "frontend-deps.js", 99 | Compile / packageMinifiedJSDependencies / artifactPath := 100 | (Compile / packageMinifiedJSDependencies / target).value / 101 | frontendWebContent / "scripts" / "frontend-deps.js" 102 | ) -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/images/icon_avsystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/file-upload/frontend/src/main/assets/images/icon_avsystem.png -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/images/icon_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/file-upload/frontend/src/main/assets/images/icon_github.png -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/images/icon_stackoverflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/file-upload/frontend/src/main/assets/images/icon_stackoverflow.png -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/images/udash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/file-upload/frontend/src/main/assets/images/udash_logo.png -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/images/udash_logo_m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/file-upload/frontend/src/main/assets/images/udash_logo_m.png -------------------------------------------------------------------------------- /file-upload/frontend/src/main/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | File upload demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/ApplicationContext.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash.Application 4 | import io.udash.demos.files.rpc.{MainClientRPC, MainServerRPC, RPCService} 5 | 6 | object ApplicationContext { 7 | private val routingRegistry = new RoutingRegistryDef 8 | private val viewPresenterRegistry = new StatesToViewFactoryDef 9 | 10 | val applicationInstance = new Application[RoutingState](routingRegistry, viewPresenterRegistry) 11 | 12 | import io.udash.rpc._ 13 | val rpcService: RPCService = new RPCService 14 | val serverRpc = DefaultServerRPC[MainClientRPC, MainServerRPC](rpcService) 15 | } 16 | -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/JSLauncher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash.logging.CrossLogging 4 | import io.udash.wrappers.jquery._ 5 | import org.scalajs.dom.Element 6 | 7 | import scala.scalajs.js.annotation.JSExport 8 | 9 | object JSLauncher extends CrossLogging { 10 | @JSExport 11 | def main(args: Array[String]): Unit = { 12 | jQ((_: Element) => { 13 | val appRoot = jQ("#application").get(0) 14 | if (appRoot.isEmpty) { 15 | logger.error("Application root element not found! Check your index.html file!") 16 | } else { 17 | ApplicationContext.applicationInstance.run(appRoot.get) 18 | } 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/RoutingRegistryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash._ 4 | 5 | class RoutingRegistryDef extends RoutingRegistry[RoutingState] { 6 | def matchUrl(url: Url): RoutingState = 7 | url2State.applyOrElse(url.value.stripSuffix("/"), (x: String) => ErrorState) 8 | 9 | def matchState(state: RoutingState): Url = 10 | Url(state2Url.apply(state)) 11 | 12 | private val (url2State, state2Url) = bidirectional { 13 | case "" => IndexState 14 | } 15 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/StatesToViewFactoryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash._ 4 | import io.udash.demos.files.views._ 5 | import io.udash.demos.files.views.index.IndexViewFactory 6 | 7 | class StatesToViewFactoryDef extends ViewFactoryRegistry[RoutingState] { 8 | def matchStateToResolver(state: RoutingState): ViewFactory[_ <: RoutingState] = state match { 9 | case RootState => RootViewFactory 10 | case IndexState => IndexViewFactory 11 | case _ => ErrorViewFactory 12 | } 13 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/config/ExternalUrls.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.config 2 | object ExternalUrls { 3 | val udashGithub = "https://github.com/UdashFramework/" 4 | val udashDemos = "https://github.com/UdashFramework/udash-demos" 5 | val stackoverflow = "http://stackoverflow.com/questions/tagged/udash" 6 | val avsystem = "http://www.avsystem.com/" 7 | val homepage = "http://udash.io/" 8 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/rpc/RPCService.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.rpc 2 | 3 | import io.udash.utils.{CallbacksHandler, Registration} 4 | 5 | class RPCService extends MainClientRPC { 6 | private val listeners = new CallbacksHandler[Unit] 7 | 8 | def listenStorageUpdate(callback: () => Unit): Registration = 9 | listeners.register({ case () => callback() }) 10 | 11 | override def fileStorageUpdated(): Unit = 12 | listeners.fire(()) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/states.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash._ 4 | 5 | sealed abstract class RoutingState(val parentState: Option[ContainerRoutingState]) extends State { 6 | type HierarchyRoot = RoutingState 7 | 8 | def url(implicit application: Application[RoutingState]): String = 9 | s"#${application.matchState(this).value}" 10 | } 11 | 12 | sealed abstract class ContainerRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with ContainerState 13 | sealed abstract class FinalRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with FinalState 14 | 15 | case object RootState extends ContainerRoutingState(None) 16 | case object ErrorState extends FinalRoutingState(Some(RootState)) 17 | case object IndexState extends FinalRoutingState(Some(RootState)) -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/ErrorView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views 2 | 3 | import io.udash._ 4 | import io.udash.demos.files.IndexState 5 | 6 | object ErrorViewFactory extends StaticViewFactory[IndexState.type](() => new ErrorView) 7 | 8 | class ErrorView extends FinalView { 9 | import scalatags.JsDom.all._ 10 | 11 | private val content = h3( 12 | "URL not found!" 13 | ).render 14 | 15 | override def getTemplate: Modifier = content 16 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/RootView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.utils.UdashJumbotron 5 | import io.udash.bootstrap.{BootstrapStyles, UdashBootstrap} 6 | import io.udash.css.CssView 7 | import io.udash.demos.files.RootState 8 | import io.udash.demos.files.views.components.Header 9 | 10 | import scalatags.JsDom.tags2.main 11 | 12 | object RootViewFactory extends StaticViewFactory[RootState.type](() => new RootView) 13 | 14 | class RootView extends ContainerView with CssView { 15 | import scalatags.JsDom.all._ 16 | 17 | private val content = div( 18 | UdashBootstrap.loadBootstrapStyles(), 19 | Header.getTemplate, 20 | main(BootstrapStyles.container)( 21 | div( 22 | UdashJumbotron( 23 | h1("file-upload"), 24 | p("Welcome in the Udash file upload demo!") 25 | ).render, 26 | childViewContainer 27 | ) 28 | ) 29 | ).render 30 | 31 | override def getTemplate: Modifier = content 32 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/components/Header.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.components 2 | import io.udash.SeqProperty 3 | import io.udash.bootstrap.navs.{UdashNav, UdashNavbar} 4 | import io.udash.demos.files.{ApplicationContext, IndexState} 5 | import io.udash.demos.files.config.ExternalUrls 6 | import org.scalajs.dom.raw.Element 7 | 8 | import scalatags.JsDom.all._ 9 | 10 | object Header { 11 | class NavItem(val href: String, val imageSrc: String, val name: String) 12 | 13 | private val brand = a(href := IndexState.url(ApplicationContext.applicationInstance))( 14 | Image("udash_logo_m.png", "Udash Framework", style := "height: 44px; margin-top: 10px;") 15 | ).render 16 | 17 | private val navItems = SeqProperty( 18 | new NavItem(ExternalUrls.udashGithub, "icon_github.png", "Github"), 19 | new NavItem(ExternalUrls.stackoverflow, "icon_stackoverflow.png", "StackOverflow"), 20 | new NavItem(ExternalUrls.avsystem, "icon_avsystem.png", "Proudly made by AVSystem") 21 | ) 22 | 23 | private val nav = UdashNav.navbar(navItems)( 24 | (prop) => { 25 | val item = prop.get 26 | a(href := item.href, target := "_blank")( 27 | Image(item.imageSrc, item.name) 28 | ).render 29 | } 30 | ) 31 | 32 | private val header = UdashNavbar.inverted( 33 | brand = brand, 34 | nav = nav 35 | ) 36 | 37 | val getTemplate: Element = 38 | header.render 39 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/components/ImageFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.components 2 | import org.scalajs.dom 3 | 4 | import scalatags.JsDom 5 | 6 | class ImageFactory(prefix: String) { 7 | import scalatags.JsDom.all._ 8 | def apply(name: String, altText: String, xs: Modifier*): JsDom.TypedTag[dom.html.Image] = { 9 | img(src := s"$prefix/$name", alt := altText, xs) 10 | } 11 | } 12 | 13 | object Image extends ImageFactory("assets/images") -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/index/IndexPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.index 2 | 3 | import io.udash._ 4 | import io.udash.demos.files.{ApplicationServerContexts, IndexState} 5 | import io.udash.logging.CrossLogging 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.util.{Failure, Success} 9 | 10 | class IndexPresenter(model: ModelProperty[UploadViewModel]) extends Presenter[IndexState.type] with CrossLogging { 11 | import io.udash.demos.files.ApplicationContext._ 12 | 13 | private val uploader = new FileUploader(Url(ApplicationServerContexts.uploadContextPrefix)) 14 | 15 | rpcService.listenStorageUpdate(() => reloadUploadedFiles()) 16 | 17 | override def handleState(state: IndexState.type): Unit = { 18 | reloadUploadedFiles() 19 | } 20 | 21 | def uploadSelectedFiles(): Unit = { 22 | uploader 23 | .upload("files", model.subSeq(_.selectedFiles).get) 24 | .listen(model.subProp(_.state).set(_)) 25 | } 26 | 27 | def reloadUploadedFiles(): Unit = { 28 | serverRpc.loadUploadedFiles() onComplete { 29 | case Success(files) => 30 | model.subProp(_.uploadedFiles).set(files) 31 | case Failure(ex) => 32 | logger.error(ex.getMessage) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/index/IndexView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.index 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.UdashBootstrap 5 | import io.udash.bootstrap.UdashBootstrap.ComponentId 6 | import io.udash.bootstrap.button.{ButtonStyle, UdashButton} 7 | import io.udash.bootstrap.form.UdashForm 8 | import io.udash.bootstrap.label.UdashLabel 9 | import io.udash.bootstrap.panel.UdashPanel 10 | import io.udash.bootstrap.progressbar.UdashProgressBar 11 | import io.udash.bootstrap.table.UdashTable 12 | import io.udash.demos.files.ApplicationServerContexts 13 | import org.scalajs.dom.File 14 | import org.scalajs.dom.raw.Event 15 | 16 | class IndexView(model: ModelProperty[UploadViewModel], presenter: IndexPresenter) extends FinalView { 17 | import io.udash.utils.FileUploader._ 18 | 19 | import scalatags.JsDom.all._ 20 | 21 | private def normalizeSize(size: Double): String = { 22 | val units = Iterator("B", "KB", "MB", "GB", "TB") 23 | var selectedUnit = units.next() 24 | var sizeWithUnit = size 25 | while (sizeWithUnit >= 1024) { 26 | sizeWithUnit /= 1024 27 | selectedUnit = units.next() 28 | } 29 | "%.2f %s".format(sizeWithUnit, selectedUnit) 30 | } 31 | 32 | private def onFormSubmit(ev: Event): Unit = { 33 | presenter.uploadSelectedFiles() 34 | } 35 | 36 | override def getTemplate: Modifier = { 37 | val sendButton = UdashButton(block = true, buttonStyle = ButtonStyle.Primary)(tpe := "submit", "Send") 38 | model.subProp(_.state.state).listen { 39 | case FileUploadState.InProgress => sendButton.disabled.set(true) 40 | case _ => sendButton.disabled.set(false) 41 | } 42 | 43 | val progress = model.subProp(_.state.bytesSent) 44 | .combine(model.subProp(_.state.bytesTotal))( 45 | (sent, total) => ((100 * sent) / total).toInt 46 | ) 47 | val progressBar = UdashProgressBar.animated(progress)() 48 | 49 | div( 50 | h3("Select files and click send..."), 51 | UdashForm(onFormSubmit _)( 52 | ComponentId("files-form"), 53 | UdashForm.fileInput()("Select files")("files", 54 | acceptMultipleFiles = Property(true), 55 | selectedFiles = model.subSeq(_.selectedFiles) 56 | ), 57 | sendButton.render 58 | ).render, 59 | h3("Check upload progress here..."), 60 | UdashPanel()( 61 | UdashPanel.heading( 62 | "Upload state ", 63 | produce(model.subProp(_.state.state), checkNull = false) { 64 | case FileUploadState.NotStarted => UdashLabel.info(UdashBootstrap.newId(), "Waiting").render 65 | case FileUploadState.InProgress => UdashLabel.info(UdashBootstrap.newId(), "In progress").render 66 | case FileUploadState.Completed => UdashLabel.success(UdashBootstrap.newId(), "Completed").render 67 | case FileUploadState.Cancelled => UdashLabel.warning(UdashBootstrap.newId(), "Cancelled").render 68 | case FileUploadState.Failed => UdashLabel.danger(UdashBootstrap.newId(), "Failed").render 69 | } 70 | ), 71 | UdashPanel.body( 72 | h4("Selected files"), 73 | ul( 74 | repeat(model.subSeq(_.selectedFiles))(file => 75 | li(s"${file.get.name} (${normalizeSize(file.get.size)})").render 76 | ), 77 | showIf(model.subSeq(_.selectedFiles).transform((s: Seq[File]) => s.isEmpty))( 78 | i("No files were selected.").render 79 | ) 80 | ), 81 | showIf(model.subProp(_.state.state).transform(_ == FileUploadState.InProgress))(Seq( 82 | h4("Sending progress").render, 83 | progressBar.render 84 | )) 85 | ) 86 | ).render, 87 | h3("You can download uploaded files..."), 88 | UdashTable( 89 | bordered = Property(true), hover = Property(true) 90 | )(model.subSeq(_.uploadedFiles))( 91 | headerFactory = Some(() => tr(th("File name"), th("Size")).render), 92 | rowFactory = (file) => { 93 | val fileModel = file.asModel 94 | tr( 95 | td(produce(fileModel)(f => 96 | a(href := ApplicationServerContexts.downloadContextPrefix + "/" + f.serverFileName)(f.name).render 97 | )), 98 | td(produce(fileModel.subProp(_.size))(s => 99 | i(normalizeSize(s)).render 100 | )) 101 | ).render 102 | } 103 | ).render 104 | ) 105 | } 106 | } -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/index/IndexViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.index 2 | 3 | import io.udash._ 4 | import io.udash.demos.files.IndexState 5 | 6 | object IndexViewFactory extends ViewFactory[IndexState.type] { 7 | override def create(): (View, Presenter[IndexState.type]) = { 8 | val model = ModelProperty(new UploadViewModel()) 9 | val presenter = new IndexPresenter(model) 10 | val view = new IndexView(model, presenter) 11 | (view, presenter) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /file-upload/frontend/src/main/scala/io/udash/demos/files/views/index/UploadViewModel.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.views.index 2 | 3 | import org.scalajs.dom._ 4 | import io.udash.demos.files.UploadedFile 5 | import io.udash.properties.HasModelPropertyCreator 6 | import io.udash.utils.FileUploader.{FileUploadModel, FileUploadState} 7 | 8 | class UploadViewModel( 9 | val state: FileUploadModel = new FileUploadModel(Seq.empty, FileUploadState.NotStarted, 0, 0), 10 | val selectedFiles: Seq[File] = Seq.empty, 11 | val uploadedFiles: Seq[UploadedFile] = Seq.empty 12 | ) 13 | 14 | object UploadViewModel extends HasModelPropertyCreator[UploadViewModel] 15 | -------------------------------------------------------------------------------- /file-upload/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 2 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 3 | import sbt._ 4 | 5 | object Dependencies { 6 | val udashVersion = "0.7.1" 7 | 8 | val logbackVersion = "1.1.3" 9 | val jettyVersion = "9.4.11.v20180605" 10 | 11 | val crossDeps = Def.setting(Seq[ModuleID]( 12 | "io.udash" %%% "udash-core-shared" % udashVersion, 13 | "io.udash" %%% "udash-rpc-shared" % udashVersion 14 | )) 15 | 16 | val frontendDeps = Def.setting(Seq[ModuleID]( 17 | "io.udash" %%% "udash-core-frontend" % udashVersion, 18 | "io.udash" %%% "udash-rpc-frontend" % udashVersion, 19 | "io.udash" %%% "udash-bootstrap" % udashVersion 20 | )) 21 | 22 | val frontendJSDeps = Def.setting(Seq[org.scalajs.sbtplugin.JSModuleID]( 23 | )) 24 | 25 | val backendDeps = Def.setting(Seq[ModuleID]( 26 | "io.udash" %%% "udash-rpc-backend" % udashVersion, 27 | "ch.qos.logback" % "logback-classic" % logbackVersion, 28 | "org.eclipse.jetty" % "jetty-server" % jettyVersion, 29 | "org.eclipse.jetty" % "jetty-servlet" % jettyVersion, 30 | "org.eclipse.jetty.websocket" % "websocket-server" % jettyVersion 31 | )) 32 | } -------------------------------------------------------------------------------- /file-upload/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.1.6 -------------------------------------------------------------------------------- /file-upload/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.5.0") 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.24") -------------------------------------------------------------------------------- /file-upload/readme.md: -------------------------------------------------------------------------------- 1 | # Udash Files Upload Example 2 | 3 | The Udash framework provides utilities useful for web applications with file upload/download features: 4 | 5 | * `FileInput` object in `core` - creates a file input with binded `Property` 6 | * `FileUploader` class in `core` - uploads files to the server and provides information about the progress via returned `Property` 7 | * `FileDownloadServlet` and `FileUploadServlet` in `rpc` - servlet templates for uploading and downloading files 8 | 9 | ## Learning Scala 10 | 11 | * [Documentation](http://scala-lang.org/documentation/) 12 | * [API Reference](http://www.scala-lang.org/api/2.11.7/) 13 | * [Functional Programming Principles in Scala, free on Coursera.](https://www.coursera.org/course/progfun) 14 | * [Tutorials](http://docs.scala-lang.org/tutorials/) 15 | 16 | 17 | ## Learning Scala.js 18 | 19 | * [Documentation](http://www.scala-js.org/doc/) 20 | * [Tutorials](http://www.scala-js.org/tutorial/) 21 | * [Scala.js Fiddle](http://www.scala-js-fiddle.com/) 22 | 23 | 24 | ## Learning Udash 25 | 26 | * [Homepage](http://udash.io/) 27 | * [Documentation](http://guide.udash.io/) 28 | 29 | 30 | ## Development 31 | 32 | The build tool for this project is [sbt](http://www.scala-sbt.org), which is 33 | set up with a [plugin](http://www.scala-js.org/doc/sbt-plugin.html) 34 | to enable compilation and packaging of Scala.js web applications. 35 | 36 | The Scala.js plugin for SBT supports two compilation modes: 37 | 38 | * `fullOptJS` is a full program optimization, which is slower, 39 | * `fastOptJS` is fast, but produces large generated javascript files - use it for development. 40 | 41 | The configuration of this project provides additional SBT tasks: `compileStatics` and `compileAndOptimizeStatics`. 42 | These tasks compile the sources to JavaScript and prepare other static files. The former task uses `fastOptJS`, 43 | the latter `fullOptJS`. 44 | 45 | After installation, run `sbt` like this: 46 | 47 | ``` 48 | $ sbt 49 | ``` 50 | 51 | You can compile the project: 52 | 53 | ``` 54 | sbt> compile 55 | ``` 56 | 57 | You can compile static frontend files as follows: 58 | 59 | ``` 60 | sbt> compileStatics 61 | ``` 62 | 63 | Then you can run the Jetty server: 64 | 65 | ``` 66 | sbt> run 67 | ``` 68 | 69 | Open: [http://localhost:8080/](http://localhost:8080/) 70 | 71 | ## What's next? 72 | 73 | Take a look at [Udash application template](https://github.com/UdashFramework/udash.g8). You can generate 74 | customized SBT project with Udash application by calling: `sbt new UdashFramework/udash.g8`. -------------------------------------------------------------------------------- /file-upload/shared/src/main/scala/io/udash/demos/files/ApplicationServerContexts.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | object ApplicationServerContexts { 4 | val atmosphereContextPrefix = "/atm" 5 | val downloadContextPrefix = "/download" 6 | val uploadContextPrefix = "/upload" 7 | } 8 | -------------------------------------------------------------------------------- /file-upload/shared/src/main/scala/io/udash/demos/files/UploadedFile.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files 2 | 3 | import io.udash.rpc.HasGenCodecAndModelPropertyCreator 4 | 5 | case class UploadedFile(name: String, serverFileName: String, size: Long) 6 | object UploadedFile extends HasGenCodecAndModelPropertyCreator[UploadedFile] 7 | -------------------------------------------------------------------------------- /file-upload/shared/src/main/scala/io/udash/demos/files/rpc/MainClientRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.rpc 2 | 3 | import io.udash.rpc._ 4 | 5 | trait MainClientRPC { 6 | def fileStorageUpdated(): Unit 7 | } 8 | object MainClientRPC extends DefaultClientUdashRPCFramework.RPCCompanion[MainClientRPC] -------------------------------------------------------------------------------- /file-upload/shared/src/main/scala/io/udash/demos/files/rpc/MainServerRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.files.rpc 2 | 3 | import io.udash.demos.files.UploadedFile 4 | import io.udash.rpc._ 5 | 6 | import scala.concurrent.Future 7 | 8 | trait MainServerRPC { 9 | def loadUploadedFiles(): Future[Seq[UploadedFile]] 10 | } 11 | object MainServerRPC extends DefaultServerUdashRPCFramework.RPCCompanion[MainServerRPC] 12 | -------------------------------------------------------------------------------- /rest-akka-http/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Eclipse template 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # Java annotation processor (APT) 32 | .factorypath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | ### Maven template 43 | target/ 44 | pom.xml.tag 45 | pom.xml.releaseBackup 46 | pom.xml.versionsBackup 47 | pom.xml.next 48 | release.properties 49 | dependency-reduced-pom.xml 50 | buildNumber.properties 51 | .mvn/timing.properties 52 | ### JetBrains template 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | ### Java template 100 | *.class 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.ear 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | ### Scala template 113 | *.class 114 | *.log 115 | 116 | # sbt specific 117 | .cache 118 | .history 119 | .lib/ 120 | dist/* 121 | target/ 122 | lib_managed/ 123 | src_managed/ 124 | project/boot/ 125 | project/plugins/project/ 126 | 127 | # Scala-IDE specific 128 | .scala_dependencies 129 | .worksheet 130 | 131 | -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/resources/application.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/backend/src/main/resources/application.conf -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | logs/udash-guide-${bySecond}.log 12 | true 13 | 14 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/scala/io/udash/demos/rest/Launcher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.stream.ActorMaterializer 6 | import io.udash.demos.rest.api.PhoneBookWebService 7 | import io.udash.logging.CrossLogging 8 | 9 | import scala.io.StdIn 10 | 11 | object Launcher extends CrossLogging { 12 | def main(args: Array[String]): Unit = { 13 | implicit val system = ActorSystem("my-system") 14 | implicit val materializer = ActorMaterializer() 15 | // needed for the future flatMap/onComplete in the end 16 | implicit val executionContext = system.dispatcher 17 | 18 | val service = new PhoneBookWebService 19 | val bindingFuture = Http().bindAndHandle(service.route, "localhost", 8080) 20 | 21 | logger.info(s"Server online at http://localhost:8080/\nPress Enter to stop...") 22 | StdIn.readLine() // let it run until user presses return 23 | bindingFuture 24 | .flatMap(_.unbind()) // trigger unbinding from the port 25 | .onComplete(_ => system.terminate()) // and shutdown when done 26 | 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/scala/io/udash/demos/rest/api/PhoneBookWebService.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.api 2 | 3 | import akka.http.scaladsl.marshalling._ 4 | import akka.http.scaladsl.model.{HttpEntity, MediaTypes, StatusCodes} 5 | import com.avsystem.commons.serialization.GenCodec 6 | import io.udash.demos.rest.model._ 7 | import io.udash.demos.rest.services.{ContactService, InMemoryContactService, InMemoryPhoneBookService, PhoneBookService} 8 | import akka.http.scaladsl.server.Directives._ 9 | import akka.http.scaladsl.server.{RequestContext, Route} 10 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} 11 | import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} 12 | 13 | 14 | trait PhoneBookWebServiceSpec { 15 | private val staticsDir = "frontend/target/UdashStatics/WebContent" 16 | 17 | val phoneBookService: PhoneBookService 18 | val contactService: ContactService 19 | 20 | implicit def optionMarshaller[T](implicit codec: GenCodec[T]): ToResponseMarshaller[Option[T]] = 21 | gencodecMarshaller[Option[T]](GenCodec.optionCodec(codec)) 22 | 23 | implicit def gencodecMarshaller[T](implicit codec: GenCodec[T]): ToEntityMarshaller[T] = 24 | Marshaller.withFixedContentType(MediaTypes.`application/json`) { value => 25 | HttpEntity(MediaTypes.`application/json`, JsonStringOutput.write(value)) 26 | } 27 | 28 | implicit def gencodecUnmarshaller[T](implicit codec: GenCodec[T]): FromEntityUnmarshaller[T] = 29 | Unmarshaller.stringUnmarshaller.forContentTypes(MediaTypes.`application/json`).map{ data => 30 | JsonStringInput.read[T](data) 31 | } 32 | 33 | private def completeIfNonEmpty[T](ctx: RequestContext)(opt: Option[T])(implicit rm: ToResponseMarshaller[T]) = 34 | opt match { 35 | case Some(v) => complete(v)(ctx) 36 | case None => complete(StatusCodes.NotFound)(ctx) 37 | } 38 | 39 | val route: Route = { 40 | pathPrefix("scripts"){ 41 | getFromDirectory(s"$staticsDir/scripts") 42 | } ~ 43 | pathPrefix("assets"){ 44 | getFromDirectory(s"$staticsDir/assets") 45 | } ~ 46 | pathPrefix("api") { 47 | pathPrefix("book") { 48 | pathPrefix(Segment) { segment => 49 | val bookId = PhoneBookId(segment.toInt) 50 | pathPrefix("contacts") { 51 | pathPrefix("count") { 52 | get { 53 | /** Adds contact to phone book */ 54 | complete { phoneBookService.contactsCount(bookId) } 55 | } 56 | } ~ 57 | pathPrefix("manage") { 58 | post { 59 | /** Adds contact to phone book */ 60 | entity(as[ContactId]) { contactId => 61 | complete { phoneBookService.addContact(bookId, contactId) } 62 | } 63 | } ~ 64 | delete { 65 | /** Removes contact from phone book */ 66 | entity(as[ContactId]) { contactId => 67 | complete { phoneBookService.removeContact(bookId, contactId) } 68 | } 69 | } 70 | } ~ 71 | get { 72 | /** Return contacts ids from selected book */ 73 | complete { phoneBookService.contacts(bookId) } 74 | } 75 | } ~ 76 | get { ctx => 77 | /** Return phone book info */ 78 | completeIfNonEmpty(ctx) { phoneBookService.load(bookId) } 79 | } ~ 80 | put { 81 | /** Updates phone book info */ 82 | entity(as[PhoneBookInfo]) { phoneBookInfo => 83 | complete { phoneBookService.update(bookId, phoneBookInfo) } 84 | } 85 | } ~ 86 | delete { ctx => 87 | /** Removes phone book */ 88 | completeIfNonEmpty(ctx) { phoneBookService.remove(bookId) } 89 | } 90 | } ~ 91 | get { 92 | /** Return phone books list */ 93 | complete { phoneBookService.load() } 94 | } ~ 95 | post { 96 | /** Creates new phone book */ 97 | entity(as[PhoneBookInfo]) { phoneBookInfo => 98 | complete { phoneBookService.create(phoneBookInfo) } 99 | } 100 | } 101 | } ~ 102 | pathPrefix("contact") { 103 | path(Segment) { segment => 104 | val contactId = ContactId(segment.toInt) 105 | get { ctx => 106 | /** Return contact details */ 107 | completeIfNonEmpty(ctx) { contactService.load(contactId) } 108 | } ~ 109 | put { 110 | /** Updates contact */ 111 | entity(as[Contact]) { contact => 112 | complete { contactService.update(contactId, contact) } 113 | } 114 | } ~ 115 | delete { ctx => 116 | /** Removes contact */ 117 | completeIfNonEmpty(ctx) { 118 | phoneBookService.contactRemoved(contactId) 119 | contactService.remove(contactId) 120 | } 121 | } 122 | } ~ 123 | get { 124 | /** Creates new contact */ 125 | complete { contactService.load() } 126 | } ~ 127 | post { 128 | /** Creates new contact */ 129 | entity(as[Contact]) { contact => 130 | complete { contactService.create(contact) } 131 | } 132 | } 133 | } 134 | } ~ get { 135 | getFromFile(s"$staticsDir/index.html") 136 | } 137 | } 138 | } 139 | 140 | class PhoneBookWebService() extends PhoneBookWebServiceSpec { 141 | override val phoneBookService: PhoneBookService = InMemoryPhoneBookService 142 | override val contactService: ContactService = InMemoryContactService 143 | } 144 | -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/scala/io/udash/demos/rest/services/ContactService.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.services 2 | 3 | import io.udash.demos.rest.model._ 4 | 5 | import scala.collection.mutable 6 | 7 | trait ContactService { 8 | def load(): Seq[Contact] 9 | def load(id: ContactId): Option[Contact] 10 | 11 | def create(contact: Contact): Contact 12 | def update(id: ContactId, contact: Contact): Contact 13 | def remove(id: ContactId): Option[Contact] 14 | } 15 | 16 | object InMemoryContactService extends ContactService { 17 | private val contacts: mutable.Map[ContactId, Contact] = mutable.Map.empty 18 | 19 | private var idc = -1 20 | private def newId() = synchronized { 21 | idc += 1 22 | ContactId(idc) 23 | } 24 | 25 | override def load(): Seq[Contact] = synchronized { 26 | contacts.values.toSeq 27 | } 28 | 29 | override def load(id: ContactId): Option[Contact] = synchronized { 30 | contacts.get(id) 31 | } 32 | 33 | override def create(contact: Contact): Contact = synchronized { 34 | val id = newId() 35 | val withId = contact.copy(id = id) 36 | contacts(id) = withId 37 | withId 38 | } 39 | 40 | override def update(id: ContactId, contact: Contact): Contact = synchronized { 41 | val oldContact = contacts(id) 42 | contacts(id) = contact 43 | oldContact 44 | } 45 | 46 | override def remove(id: ContactId): Option[Contact] = synchronized { 47 | contacts.remove(id) 48 | } 49 | 50 | // Init data 51 | synchronized { 52 | Seq.fill(5)(newId()).foreach(id => 53 | contacts(id) = Contact(id, s"John ${id.value}", "Doe", s"+22 123 43 2${id.value}", s"john.${id.value}@dmail.com") 54 | ) 55 | } 56 | } -------------------------------------------------------------------------------- /rest-akka-http/backend/src/main/scala/io/udash/demos/rest/services/PhoneBookService.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.services 2 | 3 | import io.udash.demos.rest.model._ 4 | 5 | import scala.collection.mutable 6 | import scala.util.Random 7 | 8 | trait PhoneBookService { 9 | def load(): Seq[PhoneBookInfo] 10 | def load(id: PhoneBookId): Option[PhoneBookInfo] 11 | 12 | def create(contact: PhoneBookInfo): PhoneBookInfo 13 | def update(id: PhoneBookId, contact: PhoneBookInfo): PhoneBookInfo 14 | def remove(id: PhoneBookId): Option[PhoneBookInfo] 15 | 16 | def contacts(id: PhoneBookId): Seq[ContactId] 17 | def contactsCount(id: PhoneBookId): Int 18 | def addContact(id: PhoneBookId, contactId: ContactId): Unit 19 | def removeContact(id: PhoneBookId, contactId: ContactId): Unit 20 | 21 | def contactRemoved(id: ContactId): Unit 22 | } 23 | 24 | object InMemoryPhoneBookService extends PhoneBookService { 25 | private val books: mutable.Map[PhoneBookId, (PhoneBookInfo, mutable.HashSet[ContactId])] = mutable.Map.empty 26 | 27 | private var idc = -1 28 | private def newId() = synchronized { 29 | idc += 1 30 | PhoneBookId(idc) 31 | } 32 | 33 | override def load(): Seq[PhoneBookInfo] = synchronized { 34 | books.values.map(_._1).toSeq 35 | } 36 | 37 | override def load(id: PhoneBookId): Option[PhoneBookInfo] = synchronized { 38 | books.get(id).map(_._1) 39 | } 40 | 41 | override def create(book: PhoneBookInfo): PhoneBookInfo = synchronized { 42 | val id = newId() 43 | val withId = book.copy(id = id) 44 | books(id) = (withId, mutable.HashSet.empty) 45 | withId 46 | } 47 | 48 | override def update(id: PhoneBookId, phoneBook: PhoneBookInfo): PhoneBookInfo = synchronized { 49 | val oldPhoneBook = books(id) 50 | books(id) = oldPhoneBook.copy(_1 = phoneBook) 51 | oldPhoneBook._1 52 | } 53 | 54 | override def remove(id: PhoneBookId): Option[PhoneBookInfo] = synchronized { 55 | books.remove(id).map(_._1) 56 | } 57 | 58 | override def contacts(id: PhoneBookId): Seq[ContactId] = synchronized { 59 | books(id)._2.toSeq 60 | } 61 | 62 | override def contactsCount(id: PhoneBookId): Int = synchronized { 63 | books(id)._2.size 64 | } 65 | 66 | override def addContact(id: PhoneBookId, contactId: ContactId): Unit = synchronized { 67 | books.get(id).map(_._2).foreach(_.+=(contactId)) 68 | } 69 | 70 | override def removeContact(id: PhoneBookId, contactId: ContactId): Unit = synchronized { 71 | books.get(id).map(_._2).foreach(_.-=(contactId)) 72 | } 73 | 74 | def contactRemoved(id: ContactId): Unit = synchronized { 75 | books.foreach (book => book._2._2.remove(id)) 76 | } 77 | 78 | // Init data 79 | synchronized { 80 | Seq.fill(2)(newId()).foreach(id => 81 | books(id) = (PhoneBookInfo(id, s"Book ${id.value}", "Yet another phone book..."), mutable.HashSet(ContactId(Random.nextInt(5)))) 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rest-akka-http/build.sbt: -------------------------------------------------------------------------------- 1 | import sbtcrossproject.{crossProject, CrossType} 2 | 3 | name := "rest-akka-http" 4 | 5 | inThisBuild(Seq( 6 | version := "0.7.0-SNAPSHOT", 7 | scalaVersion := "2.12.6", 8 | organization := "io.udash", 9 | scalacOptions ++= Seq( 10 | "-feature", 11 | "-deprecation", 12 | "-unchecked", 13 | "-language:implicitConversions", 14 | "-language:existentials", 15 | "-language:dynamics", 16 | "-Xfuture", 17 | "-Xfatal-warnings", 18 | "-Xlint:_,-missing-interpolator,-adapted-args" 19 | ), 20 | )) 21 | 22 | // Custom SBT tasks 23 | val copyAssets = taskKey[Unit]("Copies all assets to the target directory.") 24 | val compileStatics = taskKey[File]( 25 | "Compiles JavaScript files and copies all assets to the target directory." 26 | ) 27 | val compileAndOptimizeStatics = taskKey[File]( 28 | "Compiles and optimizes JavaScript files and copies all assets to the target directory." 29 | ) 30 | 31 | lazy val `rest-akka-http` = project.in(file(".")) 32 | .aggregate(sharedJS, sharedJVM, frontend, backend) 33 | .dependsOn(backend) 34 | .settings( 35 | publishArtifact := false, 36 | Compile / mainClass := Some("io.udash.demos.rest.Launcher"), 37 | ) 38 | 39 | lazy val shared = crossProject(JSPlatform, JVMPlatform) 40 | .crossType(CrossType.Pure).in(file("shared")) 41 | .settings( 42 | libraryDependencies ++= Dependencies.crossDeps.value, 43 | ) 44 | 45 | lazy val sharedJVM = shared.jvm 46 | lazy val sharedJS = shared.js 47 | 48 | lazy val backend = project.in(file("backend")) 49 | .dependsOn(sharedJVM) 50 | .settings( 51 | libraryDependencies ++= Dependencies.backendDeps.value, 52 | Compile / mainClass := Some("io.udash.demos.rest.Launcher"), 53 | ) 54 | 55 | val frontendWebContent = "UdashStatics/WebContent" 56 | lazy val frontend = project.in(file("frontend")).enablePlugins(ScalaJSPlugin) 57 | .dependsOn(sharedJS) 58 | .settings( 59 | libraryDependencies ++= Dependencies.frontendDeps.value, 60 | 61 | // Make this module executable in JS 62 | Compile / mainClass := Some("io.udash.demos.rest.JSLauncher"), 63 | scalaJSUseMainModuleInitializer := true, 64 | 65 | // Implementation of custom tasks defined above 66 | copyAssets := { 67 | IO.copyDirectory( 68 | sourceDirectory.value / "main/assets", 69 | target.value / frontendWebContent / "assets" 70 | ) 71 | IO.copyFile( 72 | sourceDirectory.value / "main/assets/index.html", 73 | target.value / frontendWebContent / "index.html" 74 | ) 75 | }, 76 | 77 | // Compiles JS files without full optimizations 78 | compileStatics := { (Compile / fastOptJS / target).value / "UdashStatics" }, 79 | compileStatics := compileStatics.dependsOn( 80 | Compile / fastOptJS, Compile / copyAssets 81 | ).value, 82 | 83 | // Compiles JS files with full optimizations 84 | compileAndOptimizeStatics := { (Compile / fullOptJS / target).value / "UdashStatics" }, 85 | compileAndOptimizeStatics := compileAndOptimizeStatics.dependsOn( 86 | Compile / fullOptJS, Compile / copyAssets 87 | ).value, 88 | 89 | // Target files for Scala.js plugin 90 | Compile / fastOptJS / artifactPath := 91 | (Compile / fastOptJS / target).value / 92 | frontendWebContent / "scripts" / "frontend.js", 93 | Compile / fullOptJS / artifactPath := 94 | (Compile / fullOptJS / target).value / 95 | frontendWebContent / "scripts" / "frontend.js", 96 | Compile / packageJSDependencies / artifactPath := 97 | (Compile / packageJSDependencies / target).value / 98 | frontendWebContent / "scripts" / "frontend-deps.js", 99 | Compile / packageMinifiedJSDependencies / artifactPath := 100 | (Compile / packageMinifiedJSDependencies / target).value / 101 | frontendWebContent / "scripts" / "frontend-deps.js" 102 | ) -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/images/icon_avsystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/frontend/src/main/assets/images/icon_avsystem.png -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/images/icon_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/frontend/src/main/assets/images/icon_github.png -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/images/icon_stackoverflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/frontend/src/main/assets/images/icon_stackoverflow.png -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/images/udash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/frontend/src/main/assets/images/udash_logo.png -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/images/udash_logo_m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UdashFramework/udash-demos/6f5efbc62ec189f5a624d7a932fba7fbf0e0a0eb/rest-akka-http/frontend/src/main/assets/images/udash_logo_m.png -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rest-akka-http 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/ApplicationContext.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import io.udash._ 4 | import org.scalajs.dom 5 | 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.util.Try 8 | 9 | object ApplicationContext { 10 | private val routingRegistry = new RoutingRegistryDef 11 | private val viewPresenterRegistry = new StatesToViewFactoryDef 12 | 13 | val applicationInstance = new Application[RoutingState](routingRegistry, viewPresenterRegistry, WindowUrlPathChangeProvider) 14 | 15 | import io.udash.rest._ 16 | val restServer: MainServerREST = DefaultServerREST[MainServerREST]( 17 | Protocol.Http, dom.window.location.hostname, Try(dom.window.location.port.toInt).getOrElse(80), "/api/" 18 | ) 19 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/JSLauncher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import io.udash.logging.CrossLogging 4 | import io.udash.wrappers.jquery._ 5 | import org.scalajs.dom.Element 6 | 7 | import scala.scalajs.js.annotation.JSExport 8 | 9 | object JSLauncher extends CrossLogging { 10 | import ApplicationContext._ 11 | 12 | @JSExport 13 | def main(args: Array[String]): Unit = { 14 | jQ((_: Element) => { 15 | jQ("#application").get(0) match { 16 | case None => 17 | logger.error("Application root element not found! Check your index.html file!") 18 | case Some(root) => 19 | applicationInstance.run(root) 20 | } 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/RoutingRegistryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.model.{ContactId, PhoneBookId} 5 | 6 | import scala.util.Try 7 | 8 | class RoutingRegistryDef extends RoutingRegistry[RoutingState] { 9 | def matchUrl(url: Url): RoutingState = 10 | url2State.applyOrElse("/" + url.value.stripPrefix("/").stripSuffix("/"), (x: String) => ErrorState) 11 | 12 | def matchState(state: RoutingState): Url = 13 | Url(state2Url.apply(state)) 14 | 15 | private val url2State: PartialFunction[String, RoutingState] = { 16 | case "/" => IndexState 17 | case "/contact" => ContactFormState() 18 | case "/contact" / arg => ContactFormState(Try(ContactId(arg.toInt)).toOption) 19 | case "/book" => PhoneBookFormState() 20 | case "/book" / arg => PhoneBookFormState(Try(PhoneBookId(arg.toInt)).toOption) 21 | } 22 | 23 | private val state2Url: PartialFunction[RoutingState, String] = { 24 | case IndexState => "/" 25 | case ContactFormState(None) => "/contact" 26 | case ContactFormState(Some(ContactId(id))) => s"/contact/$id" 27 | case PhoneBookFormState(None) => "/book" 28 | case PhoneBookFormState(Some(PhoneBookId(id))) => s"/book/$id" 29 | } 30 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/StatesToViewFactoryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.views._ 5 | import io.udash.demos.rest.views.book.PhoneBookFormViewFactory 6 | import io.udash.demos.rest.views.contact.ContactFormViewFactory 7 | import io.udash.demos.rest.views.index.IndexViewFactory 8 | 9 | class StatesToViewFactoryDef extends ViewFactoryRegistry[RoutingState] { 10 | def matchStateToResolver(state: RoutingState): ViewFactory[_ <: RoutingState] = state match { 11 | case RootState => RootViewFactory 12 | case IndexState => IndexViewFactory 13 | case ContactFormState(contactId) => ContactFormViewFactory(contactId) 14 | case PhoneBookFormState(contactId) => PhoneBookFormViewFactory(contactId) 15 | case _ => ErrorViewFactory 16 | } 17 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/config/ExternalUrls.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.config 2 | 3 | object ExternalUrls { 4 | val udashGithub = "https://github.com/UdashFramework/" 5 | val udashDemos = "https://github.com/UdashFramework/udash-demos" 6 | val stackoverflow = "http://stackoverflow.com/questions/tagged/udash" 7 | val avsystem = "http://www.avsystem.com/" 8 | val homepage = "http://udash.io/" 9 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/states.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.model.{ContactId, PhoneBookId} 5 | 6 | sealed abstract class RoutingState(val parentState: Option[ContainerRoutingState]) extends State { 7 | type HierarchyRoot = RoutingState 8 | 9 | def url(implicit application: Application[RoutingState]): String = 10 | s"${application.matchState(this).value}" 11 | } 12 | 13 | sealed abstract class ContainerRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with ContainerState 14 | sealed abstract class FinalRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with FinalState 15 | 16 | object RootState extends ContainerRoutingState(None) 17 | object ErrorState extends FinalRoutingState(Some(RootState)) 18 | 19 | case object IndexState extends FinalRoutingState(Some(RootState)) 20 | case class ContactFormState(id: Option[ContactId] = None) extends FinalRoutingState(Some(RootState)) 21 | case class PhoneBookFormState(id: Option[PhoneBookId] = None) extends FinalRoutingState(Some(RootState)) -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/ErrorView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.IndexState 5 | 6 | object ErrorViewFactory extends StaticViewFactory[IndexState.type](() => new ErrorView) 7 | 8 | class ErrorView extends FinalView { 9 | import scalatags.JsDom.all._ 10 | 11 | override def getTemplate: Modifier = 12 | h3("URL not found!").render 13 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/RootView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.utils.UdashJumbotron 5 | import io.udash.bootstrap.{BootstrapStyles, UdashBootstrap} 6 | import io.udash.css._ 7 | import io.udash.demos.rest.RootState 8 | import io.udash.demos.rest.views.components.Header 9 | 10 | import scalatags.JsDom.tags2.main 11 | 12 | object RootViewFactory extends StaticViewFactory[RootState.type](() => new RootView) 13 | 14 | class RootView extends ContainerView with CssView { 15 | import scalatags.JsDom.all._ 16 | 17 | private val content = div( 18 | UdashBootstrap.loadBootstrapStyles(), 19 | Header.getTemplate, 20 | main(BootstrapStyles.container)( 21 | div( 22 | UdashJumbotron( 23 | h1("REST with Akka HTTP server"), 24 | p("Welcome in the Udash REST and the Udash Bootstrap modules demo!") 25 | ).render, 26 | childViewContainer 27 | ) 28 | ) 29 | ).render 30 | 31 | override def getTemplate: Modifier = content 32 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/book/PhoneBookEditorModel.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.book 2 | 3 | import io.udash.demos.rest.model.{Contact, ContactId, PhoneBookId} 4 | import io.udash.properties.HasModelPropertyCreator 5 | 6 | case class PhoneBookEditorModel( 7 | loaded: Boolean = false, 8 | loadingText: String = "", 9 | 10 | isNewBook: Boolean = true, 11 | id: PhoneBookId = PhoneBookId(-1), 12 | name: String = "", 13 | description: String = "", 14 | 15 | allContacts: Seq[Contact] = Seq.empty, 16 | selectedContacts: Seq[ContactId] = Seq.empty 17 | ) 18 | 19 | object PhoneBookEditorModel extends HasModelPropertyCreator[PhoneBookEditorModel] 20 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/book/PhoneBookFormPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.book 2 | 3 | import io.udash._ 4 | import io.udash.core.Presenter 5 | import io.udash.demos.rest.model.{ContactId, PhoneBookId, PhoneBookInfo} 6 | import io.udash.demos.rest.{ApplicationContext, IndexState, PhoneBookFormState} 7 | import org.scalajs.dom 8 | 9 | import scala.util.{Failure, Success} 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | class PhoneBookFormPresenter(model: ModelProperty[PhoneBookEditorModel]) extends Presenter[PhoneBookFormState] { 13 | import ApplicationContext._ 14 | 15 | override def handleState(state: PhoneBookFormState): Unit = { 16 | state match { 17 | case PhoneBookFormState(None) => 18 | model.subProp(_.loaded).set(true) 19 | model.subProp(_.loadingText).set("") 20 | 21 | model.subProp(_.isNewBook).set(true) 22 | model.subProp(_.name).set("") 23 | model.subProp(_.description).set("") 24 | case PhoneBookFormState(Some(id)) => 25 | model.subProp(_.loaded).set(false) 26 | model.subProp(_.loadingText).set("Loading phone book data...") 27 | model.subProp(_.isNewBook).set(false) 28 | 29 | loadPhoneBookInfo(id) 30 | loadSelectedContacts(id) 31 | loadContacts() 32 | } 33 | } 34 | 35 | def loadPhoneBookInfo(id: PhoneBookId): Unit = { 36 | restServer.phoneBooks(id).load() onComplete { 37 | case Success(book) => 38 | model.subProp(_.loaded).set(true) 39 | model.subProp(_.id).set(id) 40 | model.subProp(_.name).set(book.name) 41 | model.subProp(_.description).set(book.description) 42 | case Failure(ex) => 43 | model.subProp(_.loadingText).set(s"Problem with phone book details loading: $ex") 44 | } 45 | } 46 | 47 | def loadContacts(): Unit = { 48 | restServer.contacts().load() onComplete { 49 | case Success(contacts) => 50 | model.subProp(_.allContacts).set(contacts) 51 | case Failure(ex) => 52 | dom.window.alert(s"Problem with contacts loading: $ex") 53 | } 54 | } 55 | 56 | def loadSelectedContacts(id: PhoneBookId): Unit = { 57 | restServer.phoneBooks(id).contacts().load() onComplete { 58 | case Success(contacts) => 59 | model.subProp(_.selectedContacts).set(contacts) 60 | model.subSeq(_.selectedContacts).listenStructure(patch => { 61 | patch.added.foreach(item => addContactToBook(id, item.get)) 62 | patch.removed.foreach(item => removeContactFromBook(id, item.get)) 63 | }) 64 | case Failure(ex) => 65 | dom.window.alert(s"Problem with selected contacts loading: $ex") 66 | } 67 | } 68 | 69 | def addContactToBook(id: PhoneBookId, contactId: ContactId): Unit = { 70 | restServer.phoneBooks(id).contacts().add(contactId).failed.foreach { ex => 71 | model.subSeq(_.selectedContacts).remove(contactId) 72 | dom.window.alert(s"Contact adding failed: $ex") 73 | } 74 | } 75 | 76 | def removeContactFromBook(id: PhoneBookId, contactId: ContactId): Unit = { 77 | restServer.phoneBooks(id).contacts().remove(contactId).failed.foreach { ex => 78 | model.subSeq(_.selectedContacts).append(contactId) 79 | dom.window.alert(s"Contact remove failed: $ex") 80 | } 81 | } 82 | 83 | def createPhoneBook(): Unit = { 84 | restServer.phoneBooks().create(PhoneBookInfo( 85 | PhoneBookId(-1), 86 | model.subProp(_.name).get, 87 | model.subProp(_.description).get 88 | )) onComplete { 89 | case Success(_) => 90 | applicationInstance.goTo(IndexState) 91 | case Failure(ex) => 92 | dom.window.alert(s"Phone Book creation failed: $ex") 93 | } 94 | } 95 | 96 | def updatePhoneBook(): Unit = { 97 | restServer.phoneBooks(model.subProp(_.id).get).update(PhoneBookInfo( 98 | model.subProp(_.id).get, 99 | model.subProp(_.name).get, 100 | model.subProp(_.description).get 101 | )) onComplete { 102 | case Success(_) => 103 | applicationInstance.goTo(IndexState) 104 | case Failure(ex) => 105 | dom.window.alert(s"Phone Book update failed: $ex") 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/book/PhoneBookFormView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.book 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.BootstrapStyles 5 | import io.udash.bootstrap.UdashBootstrap.ComponentId 6 | import io.udash.bootstrap.button.{ButtonStyle, UdashButton} 7 | import io.udash.bootstrap.form.UdashForm 8 | import io.udash.css.CssView 9 | import io.udash.demos.rest.model.ContactId 10 | import org.scalajs.dom.raw.Event 11 | 12 | class PhoneBookFormView(model: ModelProperty[PhoneBookEditorModel], presenter: PhoneBookFormPresenter) 13 | extends FinalView with CssView { 14 | 15 | import scalatags.JsDom.all._ 16 | 17 | private def onFormSubmit(ev: Event): Unit = { 18 | if (model.subProp(_.isNewBook).get) presenter.createPhoneBook() 19 | else presenter.updatePhoneBook() 20 | } 21 | 22 | private val selectedStrings: SeqProperty[String] = model.subSeq(_.selectedContacts).transform( 23 | (id: ContactId) => id.value.toString, 24 | (s: String) => ContactId(s.toInt) 25 | ) 26 | 27 | private val saveButton = UdashButton(buttonStyle = ButtonStyle.Primary)( 28 | tpe := "submit", 29 | produce(model.subProp(_.isNewBook)) { 30 | case true => span("Create").render 31 | case false => span("Save changes").render 32 | } 33 | ) 34 | 35 | private val contactsForm = produce(model.subProp(_.isNewBook)) { 36 | case true => 37 | h3("Create phone book to manage contacts").render 38 | case false => 39 | div( 40 | h3("Contacts in book"), 41 | produce(model.subSeq(_.allContacts)) { contacts => 42 | val idToName = contacts.map(c => (c.id.value.toString, c)).toMap 43 | UdashForm( 44 | UdashForm.checkboxes()( 45 | selectedStrings, 46 | idToName.keys.toSeq, 47 | decorator = { (input, id) => 48 | label(BootstrapStyles.Form.checkbox)(input, { 49 | val contact = idToName(id) 50 | s"${contact.firstName} ${contact.lastName}" 51 | }).render 52 | } 53 | ) 54 | ).render 55 | } 56 | ).render 57 | } 58 | 59 | private val content = div( 60 | produce(model.subProp(_.loaded)) { 61 | case false => 62 | span(bind(model.subProp(_.loadingText))).render 63 | case true => 64 | div( 65 | produce(model.subProp(_.isNewBook)) { 66 | case true => h2("Phone Book creator").render 67 | case false => h2("Phone Book editor").render 68 | }, 69 | 70 | UdashForm(onFormSubmit _)( 71 | ComponentId("book-form"), 72 | UdashForm.group( 73 | UdashForm.textInput()("Name: ")(model.subProp(_.name)) 74 | ), 75 | UdashForm.group( 76 | UdashForm.textInput()("Description: ")(model.subProp(_.description)) 77 | ), 78 | UdashForm.group( 79 | saveButton.render 80 | ) 81 | ).render, 82 | 83 | contactsForm 84 | ).render 85 | } 86 | ).render 87 | 88 | override def getTemplate: Modifier = content 89 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/book/PhoneBookFormViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.book 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.PhoneBookFormState 5 | import io.udash.demos.rest.model.PhoneBookId 6 | 7 | case class PhoneBookFormViewFactory(id: Option[PhoneBookId]) extends ViewFactory[PhoneBookFormState] { 8 | override def create(): (View, Presenter[PhoneBookFormState]) = { 9 | val model = ModelProperty(PhoneBookEditorModel()) // use default values defined in model 10 | val presenter = new PhoneBookFormPresenter(model) 11 | val view = new PhoneBookFormView(model, presenter) 12 | (view, presenter) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/components/Header.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.components 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.navs.{UdashNav, UdashNavbar} 5 | import io.udash.demos.rest.config.ExternalUrls 6 | import io.udash.demos.rest.{ApplicationContext, IndexState} 7 | import org.scalajs.dom.raw.Element 8 | 9 | import scalatags.JsDom.all._ 10 | 11 | object Header { 12 | class NavItem(val href: String, val imageSrc: String, val name: String) 13 | 14 | private val brand = a(href := IndexState.url(ApplicationContext.applicationInstance))( 15 | Image("udash_logo_m.png", "Udash Framework", style := "height: 44px; margin-top: 10px;") 16 | ).render 17 | 18 | private val navItems = SeqProperty( 19 | new NavItem(ExternalUrls.udashGithub, "icon_github.png", "Github"), 20 | new NavItem(ExternalUrls.stackoverflow, "icon_stackoverflow.png", "StackOverflow"), 21 | new NavItem(ExternalUrls.avsystem, "icon_avsystem.png", "Proudly made by AVSystem") 22 | ) 23 | 24 | private val nav = UdashNav.navbar(navItems)( 25 | (prop) => { 26 | val item = prop.get 27 | a(href := item.href, target := "_blank")( 28 | Image(item.imageSrc, item.name) 29 | ).render 30 | } 31 | ) 32 | 33 | private val header = UdashNavbar.inverted( 34 | brand = brand, 35 | nav = nav 36 | ) 37 | 38 | val getTemplate: Element = 39 | header.render 40 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/components/ImageFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.components 2 | import org.scalajs.dom 3 | import scalatags.JsDom 4 | 5 | class ImageFactory(prefix: String) { 6 | import scalatags.JsDom.all._ 7 | 8 | @inline 9 | def apply(name: String, altText: String, xs: Modifier*): JsDom.TypedTag[dom.html.Image] = 10 | img(src := s"$prefix/$name", alt := altText, xs) 11 | } 12 | 13 | object Image extends ImageFactory("/assets/images") -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/contact/ContactEditorModel.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.contact 2 | 3 | import io.udash.demos.rest.model.ContactId 4 | import io.udash.properties.HasModelPropertyCreator 5 | 6 | case class ContactEditorModel( 7 | loaded: Boolean = false, 8 | loadingText: String = "", 9 | 10 | isNewContact: Boolean = false, 11 | id: ContactId = ContactId(-1), 12 | firstName: String = "", 13 | lastName: String = "", 14 | phone: String = "", 15 | email: String = "" 16 | ) 17 | 18 | object ContactEditorModel extends HasModelPropertyCreator[ContactEditorModel] 19 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/contact/ContactFormPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.contact 2 | 3 | import io.udash._ 4 | import io.udash.core.Presenter 5 | import io.udash.demos.rest.model.{Contact, ContactId} 6 | import io.udash.demos.rest.{ContactFormState, ApplicationContext, IndexState} 7 | import org.scalajs.dom 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.util.{Failure, Success} 11 | 12 | class ContactFormPresenter(model: ModelProperty[ContactEditorModel]) extends Presenter[ContactFormState] { 13 | import ApplicationContext._ 14 | 15 | override def handleState(state: ContactFormState): Unit = { 16 | state match { 17 | case ContactFormState(None) => 18 | model.subProp(_.loaded).set(true) 19 | model.subProp(_.loadingText).set("") 20 | 21 | model.subProp(_.isNewContact).set(true) 22 | model.subProp(_.firstName).set("") 23 | model.subProp(_.lastName).set("") 24 | model.subProp(_.phone).set("") 25 | model.subProp(_.email).set("") 26 | case ContactFormState(Some(id)) => 27 | model.subProp(_.loaded).set(false) 28 | model.subProp(_.loadingText).set("Loading contact data...") 29 | model.subProp(_.isNewContact).set(false) 30 | 31 | loadContactData(id) 32 | } 33 | } 34 | 35 | def loadContactData(id: ContactId): Unit = { 36 | restServer.contacts(id).load() onComplete { 37 | case Success(contact) => 38 | model.subProp(_.loaded).set(true) 39 | model.subProp(_.id).set(id) 40 | model.subProp(_.firstName).set(contact.firstName) 41 | model.subProp(_.lastName).set(contact.lastName) 42 | model.subProp(_.phone).set(contact.phone) 43 | model.subProp(_.email).set(contact.email) 44 | case Failure(ex) => 45 | model.subProp(_.loadingText).set(s"Problem with contact details loading: $ex") 46 | } 47 | } 48 | 49 | def createContact(): Unit = { 50 | restServer.contacts().create(Contact( 51 | ContactId(-1), 52 | model.subProp(_.firstName).get, 53 | model.subProp(_.lastName).get, 54 | model.subProp(_.phone).get, 55 | model.subProp(_.email).get 56 | )) onComplete { 57 | case Success(contact) => 58 | applicationInstance.goTo(IndexState) 59 | case Failure(ex) => 60 | dom.window.alert(s"Contact creation failed: $ex") 61 | } 62 | } 63 | 64 | def updateContact(): Unit = 65 | restServer.contacts(model.subProp(_.id).get).update(Contact( 66 | model.subProp(_.id).get, 67 | model.subProp(_.firstName).get, 68 | model.subProp(_.lastName).get, 69 | model.subProp(_.phone).get, 70 | model.subProp(_.email).get 71 | )) onComplete { 72 | case Success(contact) => 73 | applicationInstance.goTo(IndexState) 74 | case Failure(ex) => 75 | dom.window.alert(s"Contact update failed: $ex") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/contact/ContactFormView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.contact 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.UdashBootstrap.ComponentId 5 | import io.udash.bootstrap.button.{ButtonStyle, UdashButton} 6 | import io.udash.bootstrap.form.UdashForm 7 | import org.scalajs.dom.raw.Event 8 | 9 | class ContactFormView(model: ModelProperty[ContactEditorModel], presenter: ContactFormPresenter) extends FinalView { 10 | import scalatags.JsDom.all._ 11 | 12 | private def onFormSubmit(ev: Event): Unit = { 13 | if (model.subProp(_.isNewContact).get) presenter.createContact() 14 | else presenter.updateContact() 15 | } 16 | 17 | private val saveButton = UdashButton(buttonStyle = ButtonStyle.Primary)( 18 | tpe := "submit", 19 | produce(model.subProp(_.isNewContact)) { 20 | case true => span("Create").render 21 | case false => span("Save changes").render 22 | } 23 | ) 24 | 25 | private val content = div( 26 | produce(model.subProp(_.loaded)) { 27 | case false => 28 | span(bind(model.subProp(_.loadingText))).render 29 | case true => 30 | div( 31 | produce(model.subProp(_.isNewContact)) { 32 | case true => h2("Contact creator").render 33 | case false => h2("Contact editor").render 34 | }, 35 | UdashForm(onFormSubmit _)( 36 | ComponentId("contact-form"), 37 | UdashForm.group( 38 | UdashForm.textInput()("First name: ")(model.subProp(_.firstName)) 39 | ), 40 | UdashForm.group( 41 | UdashForm.textInput()("Last name: ")(model.subProp(_.lastName)) 42 | ), 43 | UdashForm.group( 44 | UdashForm.textInput()("Phone: ")(model.subProp(_.phone)) 45 | ), 46 | UdashForm.group( 47 | UdashForm.textInput()("E-mail: ")(model.subProp(_.email)) 48 | ), 49 | UdashForm.group( 50 | saveButton.render 51 | ) 52 | ).render 53 | ).render 54 | } 55 | ).render 56 | 57 | override def getTemplate: Modifier = content 58 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/contact/ContactFormViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.contact 2 | 3 | import io.udash._ 4 | import io.udash.core.Presenter 5 | import io.udash.demos.rest.ContactFormState 6 | import io.udash.demos.rest.model.ContactId 7 | 8 | case class ContactFormViewFactory(id: Option[ContactId]) extends ViewFactory[ContactFormState] { 9 | override def create(): (View, Presenter[ContactFormState]) = { 10 | val model = ModelProperty(ContactEditorModel()) // use default values defined in model 11 | val presenter = new ContactFormPresenter(model) 12 | val view = new ContactFormView(model, presenter) 13 | (view, presenter) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/index/IndexPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.index 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.IndexState 5 | import io.udash.demos.rest.model.{Contact, ContactId, PhoneBookId, PhoneBookInfo} 6 | import org.scalajs.dom 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.Future 10 | import scala.util.{Failure, Success} 11 | 12 | class IndexPresenter(model: ModelProperty[IndexViewModel]) extends Presenter[IndexState.type] { 13 | import io.udash.demos.rest.ApplicationContext._ 14 | 15 | override def handleState(state: IndexState.type): Unit = 16 | refresh() 17 | 18 | def removeContact(id: ContactId): Unit = { 19 | restServer.contacts(id).remove() onComplete { 20 | case Success(removedContact) => 21 | model.subSeq(_.contacts.elements).remove(removedContact) 22 | refreshPhoneBooksSizes(model.subModel(_.books)) 23 | case Failure(ex) => 24 | dom.window.alert(s"Contact removing failed! ($ex)") 25 | } 26 | } 27 | 28 | def removePhoneBook(id: PhoneBookId): Unit = { 29 | restServer.phoneBooks(id).remove() onComplete { 30 | case Success(_) => 31 | val elements = model.subSeq(_.books.elements) 32 | val removed = elements.get.find(_.id == id) 33 | removed.foreach(elements.remove) 34 | case Failure(ex) => 35 | dom.window.alert(s"Phone book removing failed! ($ex)") 36 | } 37 | } 38 | 39 | def refresh(): Unit = { 40 | refreshPhoneBooks(model.subModel(_.books), restServer.phoneBooks().load(), "Loading phone books...") 41 | refreshContacts(model.subModel(_.contacts), restServer.contacts().load(), "Loading contacts...") 42 | } 43 | 44 | private def refreshContacts(model: ModelProperty[DataLoadingModel[Contact]], elements: Future[Seq[Contact]], loadingText: String) : Unit = { 45 | model.subProp(_.loaded).set(false) 46 | model.subProp(_.loadingText).set(loadingText) 47 | 48 | elements onComplete { 49 | case Success(elems) => 50 | model.subProp(_.loaded).set(true) 51 | model.subSeq(_.elements).set(elems) 52 | case Failure(ex) => 53 | model.subProp(_.loadingText).set(s"Error: $ex") 54 | } 55 | } 56 | 57 | private def refreshPhoneBooks(model: ModelProperty[DataLoadingModel[PhoneBookExtInfo]], elements: Future[Seq[PhoneBookInfo]], loadingText: String) : Unit = { 58 | model.subProp(_.loaded).set(false) 59 | model.subProp(_.loadingText).set(loadingText) 60 | 61 | elements onComplete { 62 | case Success(elems) => 63 | model.subProp(_.loaded).set(true) 64 | model.subSeq(_.elements).clear() 65 | 66 | elems.foreach { el => 67 | model.subSeq(_.elements).append( 68 | PhoneBookExtInfo(el.id, el.name, el.description, 0) 69 | ) 70 | } 71 | 72 | refreshPhoneBooksSizes(model) 73 | case Failure(ex) => 74 | model.subProp(_.loadingText).set(s"Error: $ex") 75 | } 76 | } 77 | 78 | private def refreshPhoneBooksSizes(model: ModelProperty[DataLoadingModel[PhoneBookExtInfo]]): Unit = { 79 | model.subSeq(_.elements).elemProperties.foreach { el => 80 | val element = el.asModel 81 | restServer.phoneBooks(el.get.id).contacts().count() onComplete { 82 | case Success(count) => 83 | element.subProp(_.contactsCount).set(count) 84 | case Failure(ex) => 85 | dom.window.alert(s"Contacts count for book ${el.get.id} loading failed: $ex") 86 | element.subProp(_.contactsCount).set(-1) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/index/IndexView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.index 2 | 3 | import io.udash._ 4 | import io.udash.bootstrap.button.{ButtonSize, ButtonStyle, UdashButton} 5 | import io.udash.bootstrap.table.UdashTable 6 | import io.udash.css.CssView 7 | import io.udash.demos.rest._ 8 | import io.udash.demos.rest.model.Contact 9 | import io.udash.properties.PropertyCreator 10 | 11 | class IndexView(model: ModelProperty[IndexViewModel], presenter: IndexPresenter) extends FinalView with CssView { 12 | import scalatags.JsDom.all._ 13 | 14 | private def headerButtons(loadedProp: Property[Boolean], creatorState: RoutingState): Modifier = { 15 | produce(loadedProp) { loaded => 16 | if (loaded) { 17 | val btn = UdashButton(ButtonStyle.Primary)("Create new") 18 | 19 | btn.listen { 20 | case UdashButton.ButtonClickEvent(_, _) => 21 | ApplicationContext.applicationInstance.goTo(creatorState) 22 | } 23 | 24 | span(style := "float: right", btn.render).render 25 | } else span().render 26 | } 27 | } 28 | 29 | private def actionButtons(editState: RoutingState, removeCallback: () => Any): Modifier = { 30 | val editBtn = UdashButton(ButtonStyle.Link, ButtonSize.ExtraSmall)("Edit") 31 | editBtn.listen { 32 | case UdashButton.ButtonClickEvent(_, _) => 33 | ApplicationContext.applicationInstance.goTo(editState) 34 | } 35 | 36 | val removeBtn = UdashButton(ButtonStyle.Link, ButtonSize.ExtraSmall)("Remove") 37 | removeBtn.listen { 38 | case UdashButton.ButtonClickEvent(_, _) => 39 | removeCallback() 40 | } 41 | 42 | span(editBtn.render, " | ", removeBtn.render) 43 | } 44 | 45 | private def elementsTable[T: PropertyCreator]( 46 | model: ModelProperty[DataLoadingModel[T]], 47 | headers: Seq[String], 48 | tableElementsFactory: CastableProperty[T] => Seq[Modifier] 49 | ): Modifier = { 50 | produce(model.subProp(_.loaded)) { loaded => 51 | if (loaded) { 52 | UdashTable(hover = Property(true))(model.subSeq(_.elements))( 53 | rowFactory = (p) => tr(tableElementsFactory(p).map(name => td(name))).render, 54 | headerFactory = Some(() => tr(headers.map(name => th(name))).render) 55 | ).render 56 | } else span(bind(model.subProp(_.loadingText))).render 57 | } 58 | } 59 | 60 | private val content = div( 61 | div( 62 | headerButtons(model.subProp(_.books.loaded), PhoneBookFormState(None)), 63 | h2("Phone Books") 64 | ), 65 | hr, 66 | elementsTable[PhoneBookExtInfo]( 67 | model.subModel(_.books), 68 | Seq("Id", "Name", "Description", "Contacts", "Actions"), 69 | (prop: CastableProperty[PhoneBookExtInfo]) => { 70 | val book: PhoneBookExtInfo = prop.get 71 | Seq( 72 | book.id.value, 73 | book.name, 74 | book.description, 75 | i(bind(prop.asModel.subProp(_.contactsCount))).render, 76 | actionButtons( 77 | PhoneBookFormState(Some(book.id)), 78 | () => presenter.removePhoneBook(book.id) 79 | ) 80 | ) 81 | } 82 | ), 83 | 84 | div( 85 | headerButtons(model.subProp(_.contacts.loaded), ContactFormState(None)), 86 | h2("Contacts") 87 | ), 88 | hr, 89 | elementsTable[Contact]( 90 | model.subModel(_.contacts), 91 | Seq("Id", "Name", "Phone", "E-mail", "Actions"), 92 | (prop: Property[Contact]) => { 93 | val contact: Contact = prop.get 94 | Seq( 95 | contact.id.value, 96 | s"${contact.firstName} ${contact.lastName}", 97 | contact.phone, 98 | contact.email, 99 | actionButtons( 100 | ContactFormState(Some(contact.id)), 101 | () => presenter.removeContact(contact.id) 102 | ) 103 | ) 104 | } 105 | ) 106 | ).render 107 | 108 | override def getTemplate: Modifier = content 109 | } -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/index/IndexViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.index 2 | 3 | import io.udash._ 4 | import io.udash.demos.rest.IndexState 5 | 6 | object IndexViewFactory extends ViewFactory[IndexState.type] { 7 | override def create(): (View, Presenter[IndexState.type]) = { 8 | val model = ModelProperty(new IndexViewModel()) 9 | val presenter = new IndexPresenter(model) 10 | val view = new IndexView(model, presenter) 11 | (view, presenter) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rest-akka-http/frontend/src/main/scala/io/udash/demos/rest/views/index/model.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.views.index 2 | 3 | import io.udash.demos.rest.model.{Contact, PhoneBookId} 4 | import io.udash.properties.{HasModelPropertyCreator, ModelPropertyCreator, PropertyCreator} 5 | 6 | class IndexViewModel( 7 | val books: DataLoadingModel[PhoneBookExtInfo] = new DataLoadingModel[PhoneBookExtInfo](), 8 | val contacts: DataLoadingModel[Contact] = new DataLoadingModel[Contact]() 9 | ) 10 | object IndexViewModel extends HasModelPropertyCreator[IndexViewModel] 11 | 12 | class DataLoadingModel[T]( 13 | val loaded: Boolean = false, 14 | val loadingText: String = "", 15 | val elements: Seq[T] = Seq.empty 16 | ) 17 | object DataLoadingModel { 18 | implicit def modelPropertyCreator[T : PropertyCreator]: ModelPropertyCreator[DataLoadingModel[T]] = 19 | ModelPropertyCreator.materialize[DataLoadingModel[T]] 20 | } 21 | 22 | case class PhoneBookExtInfo(id: PhoneBookId, name: String, description: String, contactsCount: Int) 23 | object PhoneBookExtInfo extends HasModelPropertyCreator[PhoneBookExtInfo] -------------------------------------------------------------------------------- /rest-akka-http/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 2 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 3 | import sbt._ 4 | 5 | object Dependencies { 6 | val udashVersion = "0.7.1" 7 | val udashJQueryVersion = "1.2.0" 8 | 9 | val logbackVersion = "1.1.3" 10 | val jettyVersion = "9.4.11.v20180605" 11 | val akkaVersion = "2.5.13" 12 | val akkaHttpVersion = "10.1.3" 13 | 14 | val crossDeps = Def.setting(Seq[ModuleID]( 15 | "io.udash" %%% "udash-core-shared" % udashVersion, 16 | "io.udash" %%% "udash-rest-shared" % udashVersion 17 | )) 18 | 19 | val frontendDeps = Def.setting(Seq[ModuleID]( 20 | "io.udash" %%% "udash-core-frontend" % udashVersion, 21 | "io.udash" %%% "udash-bootstrap" % udashVersion, 22 | "io.udash" %%% "udash-jquery" % udashJQueryVersion 23 | )) 24 | 25 | val frontendJSDeps = Def.setting(Seq[org.scalajs.sbtplugin.JSModuleID]( 26 | )) 27 | 28 | val backendDeps = Def.setting(Seq[ModuleID]( 29 | "ch.qos.logback" % "logback-classic" % logbackVersion, 30 | "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, 31 | "com.typesafe.akka" %% "akka-stream" % akkaVersion, 32 | "com.typesafe.akka" %% "akka-actor" % akkaVersion 33 | )) 34 | } 35 | -------------------------------------------------------------------------------- /rest-akka-http/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.1.6 -------------------------------------------------------------------------------- /rest-akka-http/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.5.0") 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.24") -------------------------------------------------------------------------------- /rest-akka-http/readme.md: -------------------------------------------------------------------------------- 1 | # Udash REST with Bootstrap Components Example 2 | 3 | The Udash REST module provides tools for wrapping REST APIs in type safe interfaces. It includes: 4 | 5 | * REST interface description based on Scala traits and annotations 6 | * mapping Scala methods to REST calls 7 | * versions for both JVM and JS code 8 | 9 | The Udash Bootstrap Components module provides type-safe wrapper for Twitter Bootstrap components. It includes: 10 | 11 | * type-safe API for Twitter Bootstrap components 12 | * components designed to be used with the Udash Properties system 13 | * support of Glyphicons & FontAwesome 14 | 15 | ## Learning Scala 16 | 17 | * [Documentation](http://scala-lang.org/documentation/) 18 | * [API Reference](http://www.scala-lang.org/api/2.11.7/) 19 | * [Functional Programming Principles in Scala, free on Coursera.](https://www.coursera.org/course/progfun) 20 | * [Tutorials](http://docs.scala-lang.org/tutorials/) 21 | 22 | 23 | ## Learning Scala.js 24 | 25 | * [Documentation](http://www.scala-js.org/doc/) 26 | * [Tutorials](http://www.scala-js.org/tutorial/) 27 | * [Scala.js Fiddle](http://www.scala-js-fiddle.com/) 28 | 29 | 30 | ## Learning Udash 31 | 32 | * [Homepage](http://udash.io/) 33 | * [Documentation](http://guide.udash.io/) 34 | 35 | 36 | ## Development 37 | 38 | The build tool for this project is [sbt](http://www.scala-sbt.org), which is 39 | set up with a [plugin](http://www.scala-js.org/doc/sbt-plugin.html) 40 | to enable compilation and packaging of Scala.js web applications. 41 | 42 | The Scala.js plugin for SBT supports two compilation modes: 43 | 44 | * `fullOptJS` is a full program optimization, which is slower, 45 | * `fastOptJS` is fast, but produces large generated javascript files - use it for development. 46 | 47 | The configuration of this project provides additional SBT tasks: `compileStatics` and `compileAndOptimizeStatics`. 48 | These tasks compile the sources to JavaScript and prepare other static files. The former task uses `fastOptJS`, 49 | the latter `fullOptJS`. 50 | 51 | After installation, run `sbt` like this: 52 | 53 | ``` 54 | $ sbt 55 | ``` 56 | 57 | You can compile the project: 58 | 59 | ``` 60 | sbt> compile 61 | ``` 62 | 63 | You can compile static frontend files as follows: 64 | 65 | ``` 66 | sbt> compileStatics 67 | ``` 68 | 69 | Then you can run the Jetty server: 70 | 71 | ``` 72 | sbt> run 73 | ``` 74 | 75 | Open: [http://localhost:8080/](http://localhost:8080/) 76 | 77 | ## What's next? 78 | 79 | Take a look at [Udash application template](https://github.com/UdashFramework/udash.g8). You can generate 80 | customized SBT project with Udash application by calling: `sbt new UdashFramework/udash.g8`. -------------------------------------------------------------------------------- /rest-akka-http/shared/src/main/scala/io/udash/demos/rest/MainServerREST.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest 2 | 3 | import com.avsystem.commons.rpc.rpcName 4 | import io.udash.demos.rest.model._ 5 | import io.udash.rest._ 6 | 7 | import scala.concurrent.Future 8 | 9 | trait MainServerREST { 10 | @RESTName("book") 11 | def phoneBooks(): PhoneBooksREST 12 | 13 | @RESTName("book") @rpcName("selectBook") 14 | def phoneBooks(@URLPart id: PhoneBookId): PhoneBookManagementREST 15 | 16 | @RESTName("contact") 17 | def contacts(): ContactsREST 18 | 19 | @RESTName("contact") @rpcName("selectContact") 20 | def contacts(@URLPart id: ContactId): ContactManagementREST 21 | } 22 | object MainServerREST extends DefaultRESTFramework.RPCCompanion[MainServerREST] 23 | 24 | trait PhoneBooksREST { 25 | @GET @SkipRESTName @rpcName("loadAll") 26 | def load(): Future[Seq[PhoneBookInfo]] 27 | 28 | @POST @SkipRESTName 29 | def create(@Body book: PhoneBookInfo): Future[PhoneBookInfo] 30 | } 31 | object PhoneBooksREST extends DefaultRESTFramework.RPCCompanion[PhoneBooksREST] 32 | 33 | trait PhoneBookManagementREST { 34 | @GET @SkipRESTName 35 | def load(): Future[PhoneBookInfo] 36 | 37 | @PUT @SkipRESTName 38 | def update(@Body book: PhoneBookInfo): Future[PhoneBookInfo] 39 | 40 | @DELETE @SkipRESTName 41 | def remove(): Future[PhoneBookInfo] 42 | 43 | def contacts(): PhoneBookContactsREST 44 | } 45 | object PhoneBookManagementREST extends DefaultRESTFramework.RPCCompanion[PhoneBookManagementREST] 46 | 47 | trait PhoneBookContactsREST { 48 | @GET @SkipRESTName 49 | def load(): Future[Seq[ContactId]] 50 | 51 | @GET 52 | def count(): Future[Int] 53 | 54 | @POST @RESTName("manage") 55 | def add(@Body contactId: ContactId): Future[Unit] 56 | 57 | @DELETE @RESTName("manage") 58 | def remove(@Body contactId: ContactId): Future[Unit] 59 | } 60 | object PhoneBookContactsREST extends DefaultRESTFramework.RPCCompanion[PhoneBookContactsREST] 61 | 62 | trait ContactsREST { 63 | @GET @SkipRESTName @rpcName("loadAll") 64 | def load(): Future[Seq[Contact]] 65 | 66 | @POST @SkipRESTName 67 | def create(@Body contact: Contact): Future[Contact] 68 | } 69 | object ContactsREST extends DefaultRESTFramework.RPCCompanion[ContactsREST] 70 | 71 | trait ContactManagementREST { 72 | @GET @SkipRESTName 73 | def load(): Future[Contact] 74 | 75 | @PUT @SkipRESTName 76 | def update(@Body book: Contact): Future[Contact] 77 | 78 | @DELETE @SkipRESTName 79 | def remove(): Future[Contact] 80 | } 81 | object ContactManagementREST extends DefaultRESTFramework.RPCCompanion[ContactManagementREST] -------------------------------------------------------------------------------- /rest-akka-http/shared/src/main/scala/io/udash/demos/rest/model/Contact.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.model 2 | 3 | import com.avsystem.commons.serialization.{HasGenCodec, transparent} 4 | 5 | @transparent 6 | case class ContactId(value: Int) 7 | object ContactId extends HasGenCodec[ContactId] 8 | 9 | case class Contact(id: ContactId, firstName: String, lastName: String, phone: String, email: String) 10 | object Contact extends HasGenCodec[Contact] 11 | -------------------------------------------------------------------------------- /rest-akka-http/shared/src/main/scala/io/udash/demos/rest/model/PhoneBook.scala: -------------------------------------------------------------------------------- 1 | package io.udash.demos.rest.model 2 | 3 | import com.avsystem.commons.serialization.{HasGenCodec, transparent} 4 | 5 | @transparent 6 | case class PhoneBookId(value: Int) 7 | object PhoneBookId extends HasGenCodec[PhoneBookId] 8 | 9 | case class PhoneBookInfo(id: PhoneBookId, name: String, description: String) 10 | object PhoneBookInfo extends HasGenCodec[PhoneBookInfo] -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ERRORS=0 4 | for subproject in `ls */build.sbt`; do 5 | SUBDIR=`dirname $subproject` 6 | echo "Testing ${SUBDIR}..." 7 | cd "$SUBDIR" 8 | sbt compile > compilation.log 9 | if [ $? -eq 0 ]; then 10 | echo -e "Test \e[32msucceed\e[39m!" 11 | else 12 | echo -e "Test \e[31mfailed\e[39m!" 13 | cat compilation.log 14 | ((ERRORS++)) 15 | fi 16 | cd .. 17 | done 18 | 19 | exit ${ERRORS} -------------------------------------------------------------------------------- /todo-rpc/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Eclipse template 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # Java annotation processor (APT) 32 | .factorypath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | ### Maven template 43 | target/ 44 | pom.xml.tag 45 | pom.xml.releaseBackup 46 | pom.xml.versionsBackup 47 | pom.xml.next 48 | release.properties 49 | dependency-reduced-pom.xml 50 | buildNumber.properties 51 | .mvn/timing.properties 52 | ### JetBrains template 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | ### Java template 100 | *.class 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.ear 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | ### Scala template 113 | *.class 114 | *.log 115 | 116 | # sbt specific 117 | .cache 118 | .history 119 | .lib/ 120 | dist/* 121 | target/ 122 | lib_managed/ 123 | src_managed/ 124 | project/boot/ 125 | project/plugins/project/ 126 | 127 | # Scala-IDE specific 128 | .scala_dependencies 129 | .worksheet 130 | 131 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | logs/udash-guide-${bySecond}.log 12 | true 13 | 14 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/Launcher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash.logging.CrossLogging 4 | import io.udash.todo.jetty.ApplicationServer 5 | 6 | import scala.io.StdIn 7 | 8 | object Launcher extends CrossLogging { 9 | def main(args: Array[String]): Unit = { 10 | val server = new ApplicationServer(8080, "frontend/target/UdashStatics/WebContent") 11 | server.start() 12 | logger.info(s"Application started...") 13 | 14 | // wait for user input and then stop the server 15 | logger.info(s"Click `Enter` to close application...") 16 | StdIn.readLine() 17 | server.stop() 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/jetty/ApplicationServer.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.jetty 2 | 3 | import io.udash.todo.services.InMemoryTodoStorage 4 | import org.eclipse.jetty.server.Server 5 | import org.eclipse.jetty.server.handler.gzip.GzipHandler 6 | import org.eclipse.jetty.server.session.SessionHandler 7 | import org.eclipse.jetty.servlet.{DefaultServlet, ServletContextHandler, ServletHolder} 8 | 9 | class ApplicationServer(val port: Int, resourceBase: String) { 10 | private val server = new Server(port) 11 | private val contextHandler = new ServletContextHandler 12 | private val appHolder = createAppHolder() 13 | private val atmosphereHolder = createAtmosphereHolder() 14 | 15 | contextHandler.setSessionHandler(new SessionHandler) 16 | contextHandler.setGzipHandler(new GzipHandler) 17 | contextHandler.getSessionHandler.addEventListener(new org.atmosphere.cpr.SessionSupport()) 18 | contextHandler.addServlet(atmosphereHolder, "/atm/*") 19 | contextHandler.addServlet(appHolder, "/*") 20 | server.setHandler(contextHandler) 21 | 22 | def start(): Unit = server.start() 23 | def stop(): Unit = server.stop() 24 | 25 | private def createAppHolder() = { 26 | val appHolder = new ServletHolder(new DefaultServlet) 27 | appHolder.setAsyncSupported(true) 28 | appHolder.setInitParameter("resourceBase", resourceBase) 29 | appHolder 30 | } 31 | 32 | private def createAtmosphereHolder() = { 33 | import io.udash.rpc._ 34 | import io.udash.todo.rpc._ 35 | 36 | val config = new DefaultAtmosphereServiceConfig(_ => 37 | new DefaultExposesServerRPC[MainServerRPC](new ExposedRpcInterfaces(InMemoryTodoStorage)) 38 | ) 39 | 40 | val framework = new DefaultAtmosphereFramework(config) 41 | 42 | val atmosphereHolder = new ServletHolder(new RpcServlet(framework)) 43 | atmosphereHolder.setAsyncSupported(true) 44 | atmosphereHolder 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/rpc/ClientRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc 2 | 3 | import io.udash.rpc._ 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | object ClientRPC { 8 | def apply(target: ClientRPCTarget)(implicit ec: ExecutionContext): MainClientRPC = { 9 | new DefaultClientRPC[MainClientRPC](target).get 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/rpc/ExposedRpcInterfaces.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc 2 | 3 | import io.udash.rpc._ 4 | import io.udash.todo.rpc.model.Todo 5 | import io.udash.todo.services.TodoStorage 6 | 7 | import scala.concurrent.Future 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | class ExposedRpcInterfaces(todoStorage: TodoStorage) extends MainServerRPC { 11 | override def store(todos: Seq[Todo]): Future[Boolean] = Future { 12 | if (todoStorage.store(todos)) { 13 | ClientRPC(AllClients).storeUpdated(todos) 14 | true 15 | } else false 16 | } 17 | 18 | override def load(): Future[Seq[Todo]] = Future { 19 | todoStorage.load() 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/services/InMemoryTodoStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.services 2 | 3 | import io.udash.todo.rpc.model.Todo 4 | 5 | object InMemoryTodoStorage extends TodoStorage { 6 | private var storage: Seq[Todo] = Seq.empty[Todo] 7 | 8 | override def store(todo: Seq[Todo]): Boolean = synchronized { 9 | if (storage != todo) { 10 | storage = todo 11 | true 12 | } else false 13 | } 14 | 15 | override def load(): Seq[Todo] = synchronized { 16 | storage 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /todo-rpc/backend/src/main/scala/io/udash/todo/services/TodoStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.services 2 | 3 | import io.udash.todo.rpc.model.Todo 4 | 5 | trait TodoStorage { 6 | def store(todo: Seq[Todo]): Boolean 7 | def load(): Seq[Todo] 8 | } -------------------------------------------------------------------------------- /todo-rpc/build.sbt: -------------------------------------------------------------------------------- 1 | import org.scalajs.sbtplugin.ScalaJSPlugin.AutoImport.scalaJSUseMainModuleInitializer 2 | import sbt.IO 3 | 4 | name := "todo-rpc" 5 | 6 | inThisBuild(Seq( 7 | version := "0.7.0-SNAPSHOT", 8 | scalaVersion := "2.12.6", 9 | organization := "io.udash", 10 | scalacOptions ++= Seq( 11 | "-feature", 12 | "-deprecation", 13 | "-unchecked", 14 | "-language:implicitConversions", 15 | "-language:existentials", 16 | "-language:dynamics", 17 | "-Xfuture", 18 | "-Xfatal-warnings", 19 | "-Xlint:_,-missing-interpolator,-adapted-args" 20 | ), 21 | )) 22 | 23 | // Custom SBT tasks 24 | val copyAssets = taskKey[Unit]("Copies all assets to the target directory.") 25 | val compileStatics = taskKey[File]( 26 | "Compiles JavaScript files and copies all assets to the target directory." 27 | ) 28 | val compileAndOptimizeStatics = taskKey[File]( 29 | "Compiles and optimizes JavaScript files and copies all assets to the target directory." 30 | ) 31 | 32 | lazy val `todo-rpc` = project.in(file(".")) 33 | .aggregate(sharedJS, sharedJVM, frontend, backend) 34 | .dependsOn(backend) 35 | .settings( 36 | publishArtifact := false, 37 | mainClass in Compile := Some("io.udash.todo.Launcher") 38 | ) 39 | 40 | lazy val shared = crossProject 41 | .crossType(CrossType.Pure).in(file("shared")) 42 | .settings( 43 | libraryDependencies ++= Dependencies.crossDeps.value, 44 | ) 45 | 46 | lazy val sharedJVM = shared.jvm 47 | lazy val sharedJS = shared.js 48 | 49 | lazy val backend = project.in(file("backend")) 50 | .dependsOn(sharedJVM) 51 | .settings( 52 | libraryDependencies ++= Dependencies.backendDeps.value, 53 | Compile / mainClass := Some("io.udash.todo.Launcher"), 54 | ) 55 | 56 | val frontendWebContent = "UdashStatics/WebContent" 57 | lazy val frontend = project.in(file("frontend")).enablePlugins(ScalaJSPlugin) 58 | .dependsOn(sharedJS) 59 | .settings( 60 | libraryDependencies ++= Dependencies.frontendDeps.value, 61 | 62 | // Make this module executable in JS 63 | Compile / mainClass := Some("io.udash.todo.JSLauncher"), 64 | scalaJSUseMainModuleInitializer := true, 65 | 66 | // Implementation of custom tasks defined above 67 | copyAssets := { 68 | IO.copyDirectory( 69 | sourceDirectory.value / "main/assets", 70 | target.value / frontendWebContent / "assets" 71 | ) 72 | IO.copyFile( 73 | sourceDirectory.value / "main/assets/index.html", 74 | target.value / frontendWebContent / "index.html" 75 | ) 76 | }, 77 | 78 | // Compiles JS files without full optimizations 79 | compileStatics := { (Compile / fastOptJS / target).value / "UdashStatics" }, 80 | compileStatics := compileStatics.dependsOn( 81 | Compile / fastOptJS, Compile / copyAssets 82 | ).value, 83 | 84 | // Compiles JS files with full optimizations 85 | compileAndOptimizeStatics := { (Compile / fullOptJS / target).value / "UdashStatics" }, 86 | compileAndOptimizeStatics := compileAndOptimizeStatics.dependsOn( 87 | Compile / fullOptJS, Compile / copyAssets 88 | ).value, 89 | 90 | // Target files for Scala.js plugin 91 | Compile / fastOptJS / artifactPath := 92 | (Compile / fastOptJS / target).value / 93 | frontendWebContent / "scripts" / "frontend.js", 94 | Compile / fullOptJS / artifactPath := 95 | (Compile / fullOptJS / target).value / 96 | frontendWebContent / "scripts" / "frontend.js", 97 | Compile / packageJSDependencies / artifactPath := 98 | (Compile / packageJSDependencies / target).value / 99 | frontendWebContent / "scripts" / "frontend-deps.js", 100 | Compile / packageMinifiedJSDependencies / artifactPath := 101 | (Compile / packageMinifiedJSDependencies / target).value / 102 | frontendWebContent / "scripts" / "frontend-deps.js" 103 | ) -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/assets/css/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/assets/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre-line; 201 | word-break: break-all; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Udash Framework • TODO with RPC 6 | 7 | 8 | 9 | 10 |
11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/assets/scripts/base.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | (function () { 3 | 'use strict'; 4 | 5 | /* jshint ignore:start */ 6 | // Underscore's Template Module 7 | // Courtesy of underscorejs.org 8 | var _ = (function (_) { 9 | _.defaults = function (object) { 10 | if (!object) { 11 | return object; 12 | } 13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { 14 | var iterable = arguments[argsIndex]; 15 | if (iterable) { 16 | for (var key in iterable) { 17 | if (object[key] == null) { 18 | object[key] = iterable[key]; 19 | } 20 | } 21 | } 22 | } 23 | return object; 24 | } 25 | 26 | // By default, Underscore uses ERB-style template delimiters, change the 27 | // following template settings to use alternative delimiters. 28 | _.templateSettings = { 29 | evaluate : /<%([\s\S]+?)%>/g, 30 | interpolate : /<%=([\s\S]+?)%>/g, 31 | escape : /<%-([\s\S]+?)%>/g 32 | }; 33 | 34 | // When customizing `templateSettings`, if you don't want to define an 35 | // interpolation, evaluation or escaping regex, we need one that is 36 | // guaranteed not to match. 37 | var noMatch = /(.)^/; 38 | 39 | // Certain characters need to be escaped so that they can be put into a 40 | // string literal. 41 | var escapes = { 42 | "'": "'", 43 | '\\': '\\', 44 | '\r': 'r', 45 | '\n': 'n', 46 | '\t': 't', 47 | '\u2028': 'u2028', 48 | '\u2029': 'u2029' 49 | }; 50 | 51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 52 | 53 | // JavaScript micro-templating, similar to John Resig's implementation. 54 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 55 | // and correctly escapes quotes within interpolated code. 56 | _.template = function(text, data, settings) { 57 | var render; 58 | settings = _.defaults({}, settings, _.templateSettings); 59 | 60 | // Combine delimiters into one regular expression via alternation. 61 | var matcher = new RegExp([ 62 | (settings.escape || noMatch).source, 63 | (settings.interpolate || noMatch).source, 64 | (settings.evaluate || noMatch).source 65 | ].join('|') + '|$', 'g'); 66 | 67 | // Compile the template source, escaping string literals appropriately. 68 | var index = 0; 69 | var source = "__p+='"; 70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 71 | source += text.slice(index, offset) 72 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 73 | 74 | if (escape) { 75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 76 | } 77 | if (interpolate) { 78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 79 | } 80 | if (evaluate) { 81 | source += "';\n" + evaluate + "\n__p+='"; 82 | } 83 | index = offset + match.length; 84 | return match; 85 | }); 86 | source += "';\n"; 87 | 88 | // If a variable is not specified, place data values in local scope. 89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 90 | 91 | source = "var __t,__p='',__j=Array.prototype.join," + 92 | "print=function(){__p+=__j.call(arguments,'');};\n" + 93 | source + "return __p;\n"; 94 | 95 | try { 96 | render = new Function(settings.variable || 'obj', '_', source); 97 | } catch (e) { 98 | e.source = source; 99 | throw e; 100 | } 101 | 102 | if (data) return render(data, _); 103 | var template = function(data) { 104 | return render.call(this, data, _); 105 | }; 106 | 107 | // Provide the compiled function source as a convenience for precompilation. 108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 109 | 110 | return template; 111 | }; 112 | 113 | return _; 114 | })({}); 115 | 116 | if (location.hostname === 'todomvc.com') { 117 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 118 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 119 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 120 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 121 | ga('create', 'UA-31081062-1', 'auto'); 122 | ga('send', 'pageview'); 123 | } 124 | /* jshint ignore:end */ 125 | 126 | function redirect() { 127 | if (location.hostname === 'tastejs.github.io') { 128 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 129 | } 130 | } 131 | 132 | function findRoot() { 133 | var base = location.href.indexOf('examples/'); 134 | return location.href.substr(0, base); 135 | } 136 | 137 | function getFile(file, callback) { 138 | if (!location.host) { 139 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); 140 | } 141 | 142 | var xhr = new XMLHttpRequest(); 143 | 144 | xhr.open('GET', findRoot() + file, true); 145 | xhr.send(); 146 | 147 | xhr.onload = function () { 148 | if (xhr.status === 200 && callback) { 149 | callback(xhr.responseText); 150 | } 151 | }; 152 | } 153 | 154 | function Learn(learnJSON, config) { 155 | if (!(this instanceof Learn)) { 156 | return new Learn(learnJSON, config); 157 | } 158 | 159 | var template, framework; 160 | 161 | if (typeof learnJSON !== 'object') { 162 | try { 163 | learnJSON = JSON.parse(learnJSON); 164 | } catch (e) { 165 | return; 166 | } 167 | } 168 | 169 | if (config) { 170 | template = config.template; 171 | framework = config.framework; 172 | } 173 | 174 | if (!template && learnJSON.templates) { 175 | template = learnJSON.templates.todomvc; 176 | } 177 | 178 | if (!framework && document.querySelector('[data-framework]')) { 179 | framework = document.querySelector('[data-framework]').dataset.framework; 180 | } 181 | 182 | this.template = template; 183 | 184 | if (learnJSON.backend) { 185 | this.frameworkJSON = learnJSON.backend; 186 | this.frameworkJSON.issueLabel = framework; 187 | this.append({ 188 | backend: true 189 | }); 190 | } else if (learnJSON[framework]) { 191 | this.frameworkJSON = learnJSON[framework]; 192 | this.frameworkJSON.issueLabel = framework; 193 | this.append(); 194 | } 195 | 196 | this.fetchIssueCount(); 197 | } 198 | 199 | Learn.prototype.append = function (opts) { 200 | var aside = document.createElement('aside'); 201 | aside.innerHTML = _.template(this.template, this.frameworkJSON); 202 | aside.className = 'learn'; 203 | 204 | if (opts && opts.backend) { 205 | // Remove demo link 206 | var sourceLinks = aside.querySelector('.source-links'); 207 | var heading = sourceLinks.firstElementChild; 208 | var sourceLink = sourceLinks.lastElementChild; 209 | // Correct link path 210 | var href = sourceLink.getAttribute('href'); 211 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); 212 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; 213 | } else { 214 | // Localize demo links 215 | var demoLinks = aside.querySelectorAll('.demo-link'); 216 | Array.prototype.forEach.call(demoLinks, function (demoLink) { 217 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { 218 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); 219 | } 220 | }); 221 | } 222 | 223 | document.body.className = (document.body.className + ' learn-bar').trim(); 224 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); 225 | }; 226 | 227 | Learn.prototype.fetchIssueCount = function () { 228 | var issueLink = document.getElementById('issue-count-link'); 229 | if (issueLink) { 230 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); 231 | var xhr = new XMLHttpRequest(); 232 | xhr.open('GET', url, true); 233 | xhr.onload = function (e) { 234 | var parsedResponse = JSON.parse(e.target.responseText); 235 | if (parsedResponse instanceof Array) { 236 | var count = parsedResponse.length; 237 | if (count !== 0) { 238 | issueLink.innerHTML = 'This app has ' + count + ' open issues'; 239 | document.getElementById('issue-count').style.display = 'inline'; 240 | } 241 | } 242 | }; 243 | xhr.send(); 244 | } 245 | }; 246 | 247 | redirect(); 248 | getFile('learn.json', Learn); 249 | })(); 250 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/ApplicationContext.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash.Application 4 | import io.udash.rpc.DefaultServerRPC 5 | import io.udash.todo.rpc.{MainClientRPC, MainServerRPC, RPCService} 6 | import io.udash.todo.storage.{RemoteTodoStorage, TodoStorage} 7 | 8 | object ApplicationContext { 9 | val todoStorage: TodoStorage = new RemoteTodoStorage 10 | 11 | private val routingRegistry = new RoutingRegistryDef 12 | private val viewFactoriesRegistry = new StatesToViewFactoryDef 13 | 14 | val applicationInstance: Application[RoutingState] = new Application[RoutingState](routingRegistry, viewFactoriesRegistry) 15 | val serverRpc: MainServerRPC = DefaultServerRPC[MainClientRPC, MainServerRPC](new RPCService(todoStorage)) 16 | } 17 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/JSLauncher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash.logging.CrossLogging 4 | import io.udash.wrappers.jquery._ 5 | import org.scalajs.dom.Element 6 | 7 | import scala.scalajs.js.annotation.JSExport 8 | 9 | object JSLauncher extends CrossLogging { 10 | import ApplicationContext._ 11 | 12 | @JSExport 13 | def main(args: Array[String]): Unit = { 14 | jQ((_: Element) => { 15 | jQ(".todoapp").get(0) match { 16 | case None => 17 | logger.error("Application root element not found! Check your index.html file!") 18 | case Some(root) => 19 | applicationInstance.run(root) 20 | } 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/RoutingRegistryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views.todo.TodosFilter 5 | 6 | class RoutingRegistryDef extends RoutingRegistry[RoutingState] { 7 | def matchUrl(url: Url): RoutingState = 8 | url2State.applyOrElse(url.value.stripSuffix("/"), (x: String) => ErrorState) 9 | 10 | def matchState(state: RoutingState): Url = 11 | Url(state2Url.apply(state)) 12 | 13 | private val (url2State, state2Url) = bidirectional { 14 | case "" => TodoState(TodosFilter.All) 15 | case "/active" => TodoState(TodosFilter.Active) 16 | case "/completed" => TodoState(TodosFilter.Completed) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/StatesToViewFactoryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views._ 5 | import io.udash.todo.views.todo.TodoViewFactory 6 | 7 | class StatesToViewFactoryDef extends ViewFactoryRegistry[RoutingState] { 8 | def matchStateToResolver(state: RoutingState): ViewFactory[_ <: RoutingState] = state match { 9 | case RootState => RootViewFactory 10 | case _: TodoState => TodoViewFactory(ApplicationContext.todoStorage) 11 | case _ => ErrorViewFactory 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/rpc/RPCService.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc 2 | 3 | import io.udash.todo.rpc.model.Todo 4 | import io.udash.todo.storage.TodoStorage 5 | 6 | class RPCService(todoStorage: TodoStorage) extends MainClientRPC { 7 | override def storeUpdated(todos: Seq[Todo]): Unit = 8 | todoStorage.storeUpdated(todos) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/states.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views.todo.TodosFilter 5 | 6 | sealed abstract class RoutingState(val parentState: Option[ContainerRoutingState]) extends State { 7 | type HierarchyRoot = RoutingState 8 | 9 | def url(implicit application: Application[RoutingState]): String = 10 | s"#${application.matchState(this).value}" 11 | } 12 | 13 | sealed abstract class ContainerRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with ContainerState 14 | sealed abstract class FinalRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with FinalState 15 | 16 | object RootState extends ContainerRoutingState(None) 17 | object ErrorState extends FinalRoutingState(Some(RootState)) 18 | case class TodoState(filter: TodosFilter) extends FinalRoutingState(Some(RootState)) 19 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/storage/RemoteTodoStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.storage 2 | 3 | import io.udash.todo.rpc.model.Todo 4 | import io.udash.utils.{CallbacksHandler, Registration} 5 | 6 | import scala.concurrent.Future 7 | 8 | class RemoteTodoStorage extends TodoStorage { 9 | import io.udash.todo.ApplicationContext._ 10 | 11 | private val listeners = new CallbacksHandler[Seq[Todo]] 12 | 13 | override def store(todoList: Seq[Todo]): Unit = 14 | serverRpc.store(todoList) 15 | 16 | override def load(): Future[Seq[Todo]] = 17 | serverRpc.load() 18 | 19 | override def storeUpdated(todoList: Seq[Todo]): Unit = 20 | listeners.fire(todoList) 21 | 22 | override def listen(l: Seq[Todo] => Any): Registration = { 23 | listeners.register { case x => l(x) } 24 | } 25 | } -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/storage/TodoStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.storage 2 | 3 | import io.udash.todo.rpc.model.Todo 4 | import io.udash.utils.Registration 5 | 6 | import scala.concurrent.Future 7 | 8 | trait TodoStorage { 9 | def store(todo: Seq[Todo]): Unit 10 | def load(): Future[Seq[Todo]] 11 | def storeUpdated(todoList: Seq[Todo]): Unit 12 | def listen(l: Seq[Todo] => Any): Registration 13 | } -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/ErrorView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views 2 | 3 | import io.udash._ 4 | import io.udash.todo.ErrorState 5 | 6 | object ErrorViewFactory extends StaticViewFactory[ErrorState.type](() => new ErrorView) 7 | 8 | class ErrorView extends FinalView { 9 | import scalatags.JsDom.all._ 10 | 11 | override val getTemplate: Modifier = 12 | h3("URL not found!") 13 | } 14 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/RootView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views 2 | 3 | import io.udash._ 4 | import io.udash.todo.RootState 5 | 6 | object RootViewFactory extends StaticViewFactory[RootState.type](() => new RootView) 7 | 8 | class RootView extends ContainerView { 9 | import scalatags.JsDom.all._ 10 | 11 | override val getTemplate: Modifier = 12 | div( 13 | h1("todos"), 14 | childViewContainer 15 | ) 16 | } -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/todo/Todo.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash.properties.HasModelPropertyCreator 4 | 5 | case class TodoViewModel(todos: Seq[Todo], todosFilter: TodosFilter, newTodoName: String, toggleAllChecked: Boolean) 6 | object TodoViewModel extends HasModelPropertyCreator[TodoViewModel] 7 | 8 | case class Todo(name: String, completed: Boolean = false, editing: Boolean = false) 9 | object Todo extends HasModelPropertyCreator[Todo] 10 | 11 | sealed abstract class TodosFilter(val matcher: Todo => Boolean) 12 | object TodosFilter { 13 | case object All extends TodosFilter(_ => true) 14 | case object Active extends TodosFilter(todo => !todo.completed) 15 | case object Completed extends TodosFilter(todo => todo.completed) 16 | } -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/todo/TodoPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.logging.CrossLogging 5 | import io.udash.todo._ 6 | import io.udash.todo.rpc.model.{Todo => STodo} 7 | import io.udash.todo.storage.TodoStorage 8 | 9 | import scala.util.{Failure, Success} 10 | 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | 13 | class TodoPresenter(model: ModelProperty[TodoViewModel], todoStorage: TodoStorage) 14 | extends Presenter[TodoState] with CrossLogging { 15 | 16 | private val todos = model.subSeq(_.todos) 17 | 18 | // Toggle button state update listener 19 | private val toggleButtonListener = todos.listen { todos => 20 | model.subProp(_.toggleAllChecked).set(todos.forall(_.completed)) 21 | } 22 | 23 | // Load from storage 24 | todoStorage.load() onComplete { 25 | case Success(response) => 26 | updateTodos(response) 27 | 28 | // Persist to do list on every change 29 | todos.listen { v => 30 | todoStorage.store(v.map(todo => STodo(todo.name, todo.completed))) 31 | } 32 | case Failure(ex) => 33 | logger.error("Can not load todos from server!") 34 | } 35 | 36 | // Persist todos on every change 37 | private val todosPersistListener = todoStorage.listen { todos => 38 | updateTodos(todos) 39 | } 40 | 41 | override def handleState(state: TodoState): Unit = { 42 | model.subProp(_.todosFilter).set(state.filter) 43 | } 44 | 45 | override def onClose(): Unit = { 46 | super.onClose() 47 | toggleButtonListener.cancel() 48 | todosPersistListener.cancel() 49 | } 50 | 51 | def addTodo(): Unit = { 52 | val nameProperty: Property[String] = model.subProp(_.newTodoName) 53 | val name = nameProperty.get.trim 54 | if (name.nonEmpty) { 55 | todos.append(Todo(name)) 56 | nameProperty.set("") 57 | } 58 | } 59 | 60 | def startItemEdit(item: ModelProperty[Todo], nameEditor: Property[String]): Unit = { 61 | nameEditor.set(item.subProp(_.name).get) 62 | item.subProp(_.editing).set(true) 63 | } 64 | 65 | def cancelItemEdit(item: ModelProperty[Todo]): Unit = 66 | item.subProp(_.editing).set(false) 67 | 68 | def endItemEdit(item: ModelProperty[Todo], nameEditor: Property[String]): Unit = { 69 | val name = nameEditor.get.trim 70 | if (item.subProp(_.editing).get && name.nonEmpty) { 71 | item.subProp(_.name).set(name) 72 | item.subProp(_.editing).set(false) 73 | } else if (name.isEmpty) { 74 | deleteItem(item.get) 75 | } 76 | } 77 | 78 | def deleteItem(item: Todo): Unit = 79 | todos.remove(item) 80 | 81 | def clearCompleted(): Unit = 82 | todos.set(todos.get.filter(TodosFilter.Active.matcher)) 83 | 84 | def setItemsCompleted(): Unit = 85 | CallbackSequencer().sequence { 86 | val targetValue = !model.subProp(_.toggleAllChecked).get 87 | todos.elemProperties.foreach(p => p.asModel.subProp(_.completed).set(targetValue)) 88 | } 89 | 90 | private def updateTodos(updated: Seq[STodo]): Unit = 91 | todos.set(updated.map(todo => Todo(name = todo.title, completed = todo.completed))) 92 | } 93 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/todo/TodoView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.css._ 5 | import io.udash.properties.single.ReadableProperty 6 | import io.udash.todo.{ApplicationContext, TodoState} 7 | import org.scalajs.dom.ext.KeyCode 8 | import org.scalajs.dom.{Event, KeyboardEvent} 9 | 10 | import scala.concurrent.duration.DurationLong 11 | import scala.language.postfixOps 12 | 13 | class TodoView(model: ModelProperty[TodoViewModel], presenter: TodoPresenter) extends FinalView with CssView { 14 | import scalatags.JsDom.all._ 15 | import scalatags.JsDom.tags2.section 16 | 17 | private val isTodoListNonEmpty: ReadableProperty[Boolean] = 18 | model.subSeq(_.todos).transform(_.nonEmpty) 19 | 20 | private val isCompletedTodoListNonEmpty: ReadableProperty[Boolean] = 21 | model.subSeq(_.todos).filter(TodosFilter.Completed.matcher).transform(_.nonEmpty) 22 | 23 | private val headerTemplate = { 24 | header(cls := "header")( 25 | TextInput(model.subProp(_.newTodoName), debounce = 0 millis)( 26 | cls := "new-todo", 27 | placeholder := "What needs to be done?", 28 | autofocus := true, 29 | onkeydown :+= ((ev: KeyboardEvent) => { 30 | if (ev.keyCode == KeyCode.Enter) { 31 | presenter.addTodo() 32 | true //prevent default 33 | } else false // do not prevent default 34 | }) 35 | ) 36 | ) 37 | } 38 | 39 | private val listTemplate = { 40 | section(cls := "main")( 41 | Checkbox(model.subProp(_.toggleAllChecked))( 42 | cls := "toggle-all", 43 | onclick :+= ((ev: Event) => { 44 | presenter.setItemsCompleted() 45 | false 46 | }) 47 | ), 48 | produce(model.subProp(_.todosFilter))(filter => 49 | ul(cls := "todo-list")( 50 | repeat(model.subSeq(_.todos).filter(filter.matcher))( 51 | (item: CastableProperty[Todo]) => 52 | listItemTemplate(item.asModel).render 53 | ) 54 | ).render 55 | ) 56 | ) 57 | } 58 | 59 | private val footerTemplate = { 60 | footer(cls := "footer")( 61 | produce(model.subSeq(_.todos).filter(TodosFilter.Active.matcher))(todos => { 62 | val size: Int = todos.size 63 | val pluralization = if (size == 1) "item" else "items" 64 | span(cls := "todo-count")(s"$size $pluralization left").render 65 | }), 66 | ul(cls := "filters")( 67 | for { 68 | (name, filter) <- Seq(("All", TodosFilter.All), ("Active", TodosFilter.Active), ("Completed", TodosFilter.Completed)) 69 | } yield li(footerButtonTemplate(name, TodoState(filter).url(ApplicationContext.applicationInstance), filter)) 70 | ), 71 | showIf(isCompletedTodoListNonEmpty)(Seq( 72 | button( 73 | cls := "clear-completed", 74 | onclick :+= ((ev: Event) => presenter.clearCompleted(), true) 75 | )("Clear completed").render 76 | )) 77 | ) 78 | } 79 | 80 | override val getTemplate: Modifier = div( 81 | headerTemplate, 82 | showIf(isTodoListNonEmpty)(Seq( 83 | listTemplate.render, 84 | footerTemplate.render 85 | )) 86 | ) 87 | 88 | private def listItemTemplate(item: ModelProperty[Todo]) = { 89 | val editName = Property("") 90 | 91 | val editorInput = TextInput(editName, debounce = 0 millis)( 92 | cls := "edit", 93 | onkeydown :+= ((ev: KeyboardEvent) => { 94 | if (ev.keyCode == KeyCode.Enter) { 95 | presenter.endItemEdit(item, editName) 96 | true //prevent default 97 | } else if (ev.keyCode == KeyCode.Escape) { 98 | presenter.cancelItemEdit(item) 99 | true //prevent default 100 | } else false // do not prevent default 101 | }), 102 | onblur :+= ((ev: Event) => { 103 | presenter.endItemEdit(item, editName) 104 | true //prevent default 105 | }) 106 | ).render 107 | 108 | val stdView = div(cls := "view")( 109 | Checkbox(item.subProp(_.completed))(cls := "toggle"), 110 | label( 111 | ondblclick :+= ((ev: Event) => { 112 | presenter.startItemEdit(item, editName) 113 | editorInput.focus() 114 | }, true) 115 | )(bind(item.subProp(_.name))), 116 | button( 117 | cls := "destroy", 118 | onclick :+= ((ev: Event) => presenter.deleteItem(item.get), true) 119 | ) 120 | ) 121 | 122 | li( 123 | CssStyleName("completed").styleIf(item.subProp(_.completed)), 124 | CssStyleName("editing").styleIf(item.subProp(_.editing)), 125 | )(stdView, editorInput) 126 | 127 | } 128 | 129 | private def footerButtonTemplate(title: String, link: String, expectedFilter: TodosFilter) = { 130 | val isSelected = model.subProp(_.todosFilter).transform(_ == expectedFilter) 131 | a(href := link, CssStyleName("selected").styleIf(isSelected))(title) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /todo-rpc/frontend/src/main/scala/io/udash/todo/views/todo/TodoViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.TodoState 5 | import io.udash.todo.storage.TodoStorage 6 | 7 | case class TodoViewFactory(todoStorage: TodoStorage) extends ViewFactory[TodoState] { 8 | override def create(): (View, Presenter[TodoState]) = { 9 | val model = ModelProperty(TodoViewModel(Seq.empty, TodosFilter.All, "", toggleAllChecked = false)) 10 | val presenter: TodoPresenter = new TodoPresenter(model, todoStorage) 11 | val view = new TodoView(model, presenter) 12 | (view, presenter) 13 | } 14 | } -------------------------------------------------------------------------------- /todo-rpc/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 2 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 3 | import sbt._ 4 | 5 | object Dependencies { 6 | val udashVersion = "0.7.1" 7 | val udashJQueryVersion = "1.2.0" 8 | 9 | val logbackVersion = "1.1.3" 10 | val jettyVersion = "9.4.11.v20180605" 11 | 12 | val crossDeps = Def.setting(Seq[ModuleID]( 13 | "io.udash" %% "udash-core-shared" % udashVersion, 14 | "io.udash" %% "udash-rpc-shared" % udashVersion 15 | )) 16 | 17 | val frontendDeps = Def.setting(Seq[ModuleID]( 18 | "io.udash" %%% "udash-core-frontend" % udashVersion, 19 | "io.udash" %%% "udash-rpc-frontend" % udashVersion, 20 | "io.udash" %%% "udash-css-frontend" % udashVersion, 21 | "io.udash" %%% "udash-jquery" % udashJQueryVersion 22 | )) 23 | 24 | val backendDeps = Def.setting(Seq[ModuleID]( 25 | "io.udash" %% "udash-rpc-backend" % udashVersion, 26 | "org.eclipse.jetty" % "jetty-server" % jettyVersion, 27 | "org.eclipse.jetty" % "jetty-servlet" % jettyVersion, 28 | "org.eclipse.jetty.websocket" % "websocket-server" % jettyVersion, 29 | "ch.qos.logback" % "logback-classic" % logbackVersion 30 | )) 31 | } -------------------------------------------------------------------------------- /todo-rpc/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.1.6 -------------------------------------------------------------------------------- /todo-rpc/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.24") -------------------------------------------------------------------------------- /todo-rpc/readme.md: -------------------------------------------------------------------------------- 1 | # Udash RPC TodoMVC Example 2 | 3 | Udash is a framework for modern web applications development. Basing on [Scala.js](http://www.scala-js.org/doc/) project, 4 | it provides type safe way of web apps development, which makes them easier to modify and maintain. 5 | 6 | Udash includes: 7 | 8 | * data binding 9 | * support for forms and data validation 10 | * frontend application routing 11 | * grouping DOM templates into reusable components 12 | * type safe server communication with Udash RPC module 13 | 14 | Udash RPC is a type safe server-client communication system for the Udash applications. 15 | 16 | Udash RPC includes: 17 | 18 | * RPC interface declaration based on Scala traits 19 | * Scala method call mapping to RPC calls 20 | * server pushes working out of the box 21 | 22 | ## Learning Scala 23 | 24 | * [Documentation](http://scala-lang.org/documentation/) 25 | * [API Reference](http://www.scala-lang.org/api/2.11.7/) 26 | * [Functional Programming Principles in Scala, free on Coursera.](https://www.coursera.org/course/progfun) 27 | * [Tutorials](http://docs.scala-lang.org/tutorials/) 28 | 29 | 30 | ## Learning Scala.js 31 | 32 | * [Documentation](http://www.scala-js.org/doc/) 33 | * [Tutorials](http://www.scala-js.org/tutorial/) 34 | * [Scala.js Fiddle](http://www.scala-js-fiddle.com/) 35 | 36 | 37 | ## Learning Udash 38 | 39 | * [Homepage](http://udash.io/) 40 | * [Documentation](http://guide.udash.io/) 41 | 42 | 43 | ## Development 44 | 45 | The build tool for this project is [sbt](http://www.scala-sbt.org), which is 46 | set up with a [plugin](http://www.scala-js.org/doc/sbt-plugin.html) 47 | to enable compilation and packaging of Scala.js web applications. 48 | 49 | The Scala.js plugin for SBT supports two compilation modes: 50 | 51 | * `fullOptJS` is a full program optimization, which is slower, 52 | * `fastOptJS` is fast, but produces large generated javascript files - use it for development. 53 | 54 | The configuration of this project provides additional SBT tasks: `compileStatics` and `compileAndOptimizeStatics`. 55 | These tasks compile the sources to JavaScript and prepare other static files. The former task uses `fastOptJS`, 56 | the latter `fullOptJS`. 57 | 58 | After installation, run `sbt` like this: 59 | 60 | ``` 61 | $ sbt 62 | ``` 63 | 64 | You can compile the project: 65 | 66 | ``` 67 | sbt> compile 68 | ``` 69 | 70 | You can compile static frontend files as follows: 71 | 72 | ``` 73 | sbt> compileStatics 74 | ``` 75 | 76 | Then you can run the Jetty server: 77 | 78 | ``` 79 | sbt> run 80 | ``` 81 | 82 | Open: [http://localhost:8080/](http://localhost:8080/) 83 | 84 | ## What's next? 85 | 86 | Take a look at [Udash application template](https://github.com/UdashFramework/udash.g8). You can generate 87 | customized SBT project with Udash application by calling: `sbt new UdashFramework/udash.g8`. -------------------------------------------------------------------------------- /todo-rpc/shared/src/main/scala/io/udash/todo/rpc/MainClientRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc 2 | 3 | import io.udash.rpc.DefaultClientUdashRPCFramework 4 | import io.udash.todo.rpc.model.Todo 5 | 6 | trait MainClientRPC { 7 | def storeUpdated(todos: Seq[Todo]): Unit 8 | } 9 | 10 | object MainClientRPC extends DefaultClientUdashRPCFramework.RPCCompanion[MainClientRPC] -------------------------------------------------------------------------------- /todo-rpc/shared/src/main/scala/io/udash/todo/rpc/MainServerRPC.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc 2 | 3 | import io.udash.rpc.DefaultServerUdashRPCFramework 4 | import io.udash.todo.rpc.model.Todo 5 | 6 | import scala.concurrent.Future 7 | 8 | trait MainServerRPC { 9 | def store(todos: Seq[Todo]): Future[Boolean] 10 | def load(): Future[Seq[Todo]] 11 | } 12 | 13 | object MainServerRPC extends DefaultServerUdashRPCFramework.RPCCompanion[MainServerRPC] -------------------------------------------------------------------------------- /todo-rpc/shared/src/main/scala/io/udash/todo/rpc/model/Todo.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.rpc.model 2 | 3 | import com.avsystem.commons.serialization.HasGenCodec 4 | 5 | case class Todo(title: String, completed: Boolean) 6 | object Todo extends HasGenCodec[Todo] 7 | -------------------------------------------------------------------------------- /todo/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Eclipse template 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # Java annotation processor (APT) 32 | .factorypath 33 | 34 | # PDT-specific 35 | .buildpath 36 | 37 | # sbteclipse plugin 38 | .target 39 | 40 | # TeXlipse plugin 41 | .texlipse 42 | ### Maven template 43 | target/ 44 | pom.xml.tag 45 | pom.xml.releaseBackup 46 | pom.xml.versionsBackup 47 | pom.xml.next 48 | release.properties 49 | dependency-reduced-pom.xml 50 | buildNumber.properties 51 | .mvn/timing.properties 52 | ### JetBrains template 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | ### Java template 100 | *.class 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.ear 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | ### Scala template 113 | *.class 114 | *.log 115 | 116 | # sbt specific 117 | .cache 118 | .history 119 | .lib/ 120 | dist/* 121 | target/ 122 | lib_managed/ 123 | src_managed/ 124 | project/boot/ 125 | project/plugins/project/ 126 | 127 | # Scala-IDE specific 128 | .scala_dependencies 129 | .worksheet 130 | 131 | 132 | node_modules/* 133 | generated/* -------------------------------------------------------------------------------- /todo/build.sbt: -------------------------------------------------------------------------------- 1 | name := "todomvc" 2 | 3 | inThisBuild(Seq( 4 | version := "0.7.0-SNAPSHOT", 5 | scalaVersion := "2.12.6", 6 | organization := "io.udash", 7 | scalacOptions ++= Seq( 8 | "-feature", 9 | "-deprecation", 10 | "-unchecked", 11 | "-language:implicitConversions", 12 | "-language:existentials", 13 | "-language:dynamics", 14 | "-Xfuture", 15 | "-Xfatal-warnings", 16 | "-Xlint:_,-missing-interpolator,-adapted-args" 17 | ), 18 | )) 19 | 20 | val generatedDir = file("generated") 21 | 22 | val todomvc = project.in(file(".")) 23 | .enablePlugins(ScalaJSPlugin) 24 | .settings( 25 | mainClass := Some("io.udash.todo.JSLauncher"), 26 | scalaJSUseMainModuleInitializer := true, 27 | 28 | libraryDependencies ++= Dependencies.frontendDeps.value, 29 | 30 | // Target files for Scala.js plugin 31 | Compile / fastOptJS / artifactPath := generatedDir / "todomvc.js", 32 | Compile / fullOptJS / artifactPath := generatedDir / "todomvc.js", 33 | Compile / packageJSDependencies / artifactPath := generatedDir / "todomvc-deps.js", 34 | Compile / packageMinifiedJSDependencies / artifactPath := generatedDir / "todomvc-deps.js", 35 | ) -------------------------------------------------------------------------------- /todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Udash Framework • TodoMVC 6 | 7 | 8 | 9 | 10 |
11 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /todo/node_modules/todomvc-app-css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre-line; 201 | word-break: break-all; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /todo/node_modules/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "todomvc-app-css": "^2.0.1", 5 | "todomvc-common": "^1.0.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /todo/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ 2 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 3 | import sbt._ 4 | 5 | object Dependencies { 6 | val udashCoreVersion = "0.7.1" 7 | val udashJQueryVersion = "1.2.0" 8 | 9 | val frontendDeps = Def.setting(Seq[ModuleID]( 10 | "io.udash" %%% "udash-core-frontend" % udashCoreVersion, 11 | "io.udash" %%% "udash-css-frontend" % udashCoreVersion, 12 | "io.udash" %%% "udash-jquery" % udashJQueryVersion 13 | )) 14 | } -------------------------------------------------------------------------------- /todo/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /todo/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.24") 2 | -------------------------------------------------------------------------------- /todo/readme.md: -------------------------------------------------------------------------------- 1 | # Udash TodoMVC Example 2 | 3 | Udash is a framework for modern web applications development. Basing on [Scala.js](http://www.scala-js.org/doc/) project, 4 | it provides type safe way of web apps development, which makes them easier to modify and maintain. 5 | 6 | Udash includes: 7 | 8 | * data binding 9 | * support for forms and data validation 10 | * frontend application routing 11 | * grouping DOM templates into reusable components 12 | * type safe server communication with Udash RPC module 13 | 14 | 15 | ## Learning Scala 16 | 17 | * [Documentation](http://scala-lang.org/documentation/) 18 | * [API Reference](http://www.scala-lang.org/api/2.11.7/) 19 | * [Functional Programming Principles in Scala, free on Coursera.](https://www.coursera.org/course/progfun) 20 | * [Tutorials](http://docs.scala-lang.org/tutorials/) 21 | 22 | 23 | ## Learning Scala.js 24 | 25 | * [Documentation](http://www.scala-js.org/doc/) 26 | * [Tutorials](http://www.scala-js.org/tutorial/) 27 | * [Scala.js Fiddle](http://www.scala-js-fiddle.com/) 28 | 29 | 30 | ## Learning Udash 31 | 32 | * [Homepage](http://udash.io/) 33 | * [Documentation](http://guide.udash.io/) 34 | 35 | 36 | ## Development 37 | 38 | The build tool for this project is [sbt](http://www.scala-sbt.org), which is 39 | set up with a [plugin](http://www.scala-js.org/doc/sbt-plugin.html) 40 | to enable compilation and packaging of Scala.js web applications. 41 | 42 | The Scala.js plugin for SBT supports two compilation modes: 43 | 44 | * `fullOptJS` is a full program optimization, which is slower, 45 | * `fastOptJS` is fast, but produces large generated javascript files - use it for development. 46 | 47 | After installation, run `sbt` like this: 48 | 49 | ``` 50 | $ sbt 51 | ``` 52 | 53 | You can compile once like this: 54 | 55 | ``` 56 | sbt> fastOptJS 57 | ``` 58 | 59 | or enable continuous compilation: 60 | 61 | ``` 62 | sbt> ~fastOptJS 63 | ``` 64 | 65 | After compilation open the `index.html` file from the root project directory in your browser. 66 | 67 | ## What's next? 68 | 69 | Take a look at [Udash application template](https://github.com/UdashFramework/udash.g8). You can generate 70 | customized SBT project with Udash application by calling: `sbt new UdashFramework/udash.g8`. -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/ApplicationContext.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash.Application 4 | 5 | object ApplicationContext { 6 | private val routingRegistry = new RoutingRegistryDef 7 | private val viewFactoriesRegistry = new StatesToViewFactoryDef 8 | 9 | val applicationInstance: Application[RoutingState] = new Application[RoutingState](routingRegistry, viewFactoriesRegistry) 10 | } 11 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/JSLauncher.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash.logging.CrossLogging 4 | import io.udash.wrappers.jquery._ 5 | import org.scalajs.dom.Element 6 | 7 | import scala.scalajs.js.annotation.JSExport 8 | 9 | object JSLauncher extends CrossLogging { 10 | import ApplicationContext._ 11 | 12 | @JSExport 13 | def main(args: Array[String]): Unit = { 14 | jQ((_: Element) => { 15 | jQ(".todoapp").get(0) match { 16 | case None => 17 | logger.error("Application root element not found! Check your index.html file!") 18 | case Some(root) => 19 | applicationInstance.run(root) 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/RoutingRegistryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views.todo.TodosFilter 5 | 6 | class RoutingRegistryDef extends RoutingRegistry[RoutingState] { 7 | def matchUrl(url: Url): RoutingState = 8 | url2State.applyOrElse(url.value.stripSuffix("/"), (x: String) => ErrorState) 9 | 10 | def matchState(state: RoutingState): Url = 11 | Url(state2Url.apply(state)) 12 | 13 | private val (url2State, state2Url) = bidirectional { 14 | case "" => TodoState(TodosFilter.All) 15 | case "/active" => TodoState(TodosFilter.Active) 16 | case "/completed" => TodoState(TodosFilter.Completed) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/StatesToViewFactoryDef.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views._ 5 | import io.udash.todo.views.todo.TodoViewFactory 6 | 7 | class StatesToViewFactoryDef extends ViewFactoryRegistry[RoutingState] { 8 | def matchStateToResolver(state: RoutingState): ViewFactory[_ <: RoutingState] = state match { 9 | case RootState => RootViewFactory 10 | case _: TodoState => TodoViewFactory 11 | case _ => ErrorViewFactory 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/states.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.views.todo.TodosFilter 5 | 6 | sealed abstract class RoutingState(val parentState: Option[ContainerRoutingState]) extends State { 7 | type HierarchyRoot = RoutingState 8 | 9 | def url(implicit application: Application[RoutingState]): String = 10 | s"#${application.matchState(this).value}" 11 | } 12 | 13 | sealed abstract class ContainerRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with ContainerState 14 | sealed abstract class FinalRoutingState(parentState: Option[ContainerRoutingState]) extends RoutingState(parentState) with FinalState 15 | 16 | object RootState extends ContainerRoutingState(None) 17 | object ErrorState extends FinalRoutingState(Some(RootState)) 18 | case class TodoState(filter: TodosFilter) extends FinalRoutingState(Some(RootState)) 19 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/storage/TodoStorage.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.storage 2 | 3 | import com.avsystem.commons.serialization.HasGenCodec 4 | import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} 5 | import org.scalajs.dom.ext.LocalStorage 6 | 7 | case class Todo(title: String, completed: Boolean) 8 | object Todo extends HasGenCodec[Todo] 9 | 10 | trait TodoStorage { 11 | def store(todo: Seq[Todo]): Unit 12 | def load(): Seq[Todo] 13 | } 14 | 15 | object LocalTodoStorage extends TodoStorage { 16 | private val storage = LocalStorage 17 | private val namespace = "todos-udash" 18 | 19 | override def store(todos: Seq[Todo]): Unit = 20 | storage(namespace) = JsonStringOutput.write(todos) 21 | 22 | def load(): Seq[Todo] = { 23 | storage(namespace) match { 24 | case Some(todos) => 25 | JsonStringInput.read[Seq[Todo]](todos) 26 | case None => 27 | Seq.empty 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/ErrorView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views 2 | 3 | import io.udash._ 4 | import io.udash.todo.ErrorState 5 | 6 | object ErrorViewFactory extends StaticViewFactory[ErrorState.type](() => new ErrorView) 7 | 8 | class ErrorView extends FinalView { 9 | import scalatags.JsDom.all._ 10 | 11 | override val getTemplate: Modifier = 12 | h3("URL not found!") 13 | } 14 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/RootView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views 2 | 3 | import io.udash._ 4 | import io.udash.todo.RootState 5 | 6 | object RootViewFactory extends StaticViewFactory[RootState.type](() => new RootView) 7 | 8 | class RootView extends ContainerView { 9 | import scalatags.JsDom.all._ 10 | 11 | override val getTemplate: Modifier = 12 | div( 13 | h1("todos"), 14 | childViewContainer 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/todo/Todo.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash.properties.HasModelPropertyCreator 4 | 5 | case class TodoViewModel(todos: Seq[Todo], todosFilter: TodosFilter, newTodoName: String, toggleAllChecked: Boolean) 6 | object TodoViewModel extends HasModelPropertyCreator[TodoViewModel] 7 | 8 | case class Todo(name: String, completed: Boolean = false, editing: Boolean = false) 9 | object Todo extends HasModelPropertyCreator[Todo] 10 | 11 | sealed abstract class TodosFilter(val matcher: Todo => Boolean) 12 | object TodosFilter { 13 | case object All extends TodosFilter(_ => true) 14 | case object Active extends TodosFilter(todo => !todo.completed) 15 | case object Completed extends TodosFilter(todo => todo.completed) 16 | } -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/todo/TodoPresenter.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo._ 5 | import io.udash.todo.storage.LocalTodoStorage 6 | 7 | class TodoPresenter(model: ModelProperty[TodoViewModel]) extends Presenter[TodoState] { 8 | private val todos = model.subSeq(_.todos) 9 | 10 | // Load from storage 11 | todos.set( 12 | LocalTodoStorage.load() 13 | .map(todo => Todo(todo.title, completed = todo.completed)) 14 | ) 15 | 16 | // Toggle button state update listener 17 | private val toggleButtonListener = todos.listen { todos => 18 | model.subProp(_.toggleAllChecked).set(todos.forall(_.completed)) 19 | } 20 | 21 | // Persist todos on every change 22 | private val todosPersistListener = todos.listen { todos => 23 | LocalTodoStorage.store( 24 | todos.map(todo => storage.Todo(todo.name, todo.completed)) 25 | ) 26 | } 27 | 28 | override def handleState(state: TodoState): Unit = { 29 | model.subProp(_.todosFilter).set(state.filter) 30 | } 31 | 32 | override def onClose(): Unit = { 33 | super.onClose() 34 | toggleButtonListener.cancel() 35 | todosPersistListener.cancel() 36 | } 37 | 38 | def addTodo(): Unit = { 39 | val nameProperty: Property[String] = model.subProp(_.newTodoName) 40 | val name = nameProperty.get.trim 41 | if (name.nonEmpty) { 42 | todos.append(Todo(name)) 43 | nameProperty.set("") 44 | } 45 | } 46 | 47 | def startItemEdit(item: ModelProperty[Todo], nameEditor: Property[String]): Unit = { 48 | nameEditor.set(item.subProp(_.name).get) 49 | item.subProp(_.editing).set(true) 50 | } 51 | 52 | def cancelItemEdit(item: ModelProperty[Todo]): Unit = 53 | item.subProp(_.editing).set(false) 54 | 55 | def endItemEdit(item: ModelProperty[Todo], nameEditor: Property[String]): Unit = { 56 | val name = nameEditor.get.trim 57 | if (item.subProp(_.editing).get && name.nonEmpty) { 58 | item.subProp(_.name).set(name) 59 | item.subProp(_.editing).set(false) 60 | } else if (name.isEmpty) { 61 | deleteItem(item.get) 62 | } 63 | } 64 | 65 | def deleteItem(item: Todo): Unit = 66 | todos.remove(item) 67 | 68 | def clearCompleted(): Unit = 69 | todos.set(todos.get.filter(TodosFilter.Active.matcher)) 70 | 71 | def setItemsCompleted(): Unit = 72 | CallbackSequencer().sequence { 73 | val targetValue = !model.subProp(_.toggleAllChecked).get 74 | todos.elemProperties.foreach(p => p.asModel.subProp(_.completed).set(targetValue)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/todo/TodoView.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.css._ 5 | import io.udash.properties.single.ReadableProperty 6 | import io.udash.todo._ 7 | import org.scalajs.dom.ext.KeyCode 8 | import org.scalajs.dom.{Event, KeyboardEvent} 9 | 10 | import scala.concurrent.duration.DurationLong 11 | import scala.language.postfixOps 12 | 13 | class TodoView(model: ModelProperty[TodoViewModel], presenter: TodoPresenter) extends FinalView with CssView { 14 | import scalatags.JsDom.all._ 15 | import scalatags.JsDom.tags2.section 16 | 17 | private val isTodoListNonEmpty: ReadableProperty[Boolean] = 18 | model.subSeq(_.todos).transform(_.nonEmpty) 19 | 20 | private val isCompletedTodoListNonEmpty: ReadableProperty[Boolean] = 21 | model.subSeq(_.todos).filter(TodosFilter.Completed.matcher).transform(_.nonEmpty) 22 | 23 | private val headerTemplate = { 24 | header(cls := "header")( 25 | TextInput(model.subProp(_.newTodoName), debounce = 0 millis)( 26 | cls := "new-todo", 27 | placeholder := "What needs to be done?", 28 | autofocus := true, 29 | onkeydown :+= ((ev: KeyboardEvent) => { 30 | if (ev.keyCode == KeyCode.Enter) { 31 | presenter.addTodo() 32 | true //prevent default 33 | } else false // do not prevent default 34 | }) 35 | ) 36 | ) 37 | } 38 | 39 | private val listTemplate = { 40 | section(cls := "main")( 41 | Checkbox(model.subProp(_.toggleAllChecked))( 42 | cls := "toggle-all", 43 | onclick :+= ((ev: Event) => { 44 | presenter.setItemsCompleted() 45 | false 46 | }) 47 | ), 48 | produce(model.subProp(_.todosFilter))(filter => 49 | ul(cls := "todo-list")( 50 | repeat(model.subSeq(_.todos).filter(filter.matcher))( 51 | (item: CastableProperty[Todo]) => 52 | listItemTemplate(item.asModel).render 53 | ) 54 | ).render 55 | ) 56 | ) 57 | } 58 | 59 | private val footerTemplate = { 60 | footer(cls := "footer")( 61 | produce(model.subSeq(_.todos).filter(TodosFilter.Active.matcher))(todos => { 62 | val size: Int = todos.size 63 | val pluralization = if (size == 1) "item" else "items" 64 | span(cls := "todo-count")(s"$size $pluralization left").render 65 | }), 66 | ul(cls := "filters")( 67 | for { 68 | (name, filter) <- Seq(("All", TodosFilter.All), ("Active", TodosFilter.Active), ("Completed", TodosFilter.Completed)) 69 | } yield li(footerButtonTemplate(name, TodoState(filter).url(ApplicationContext.applicationInstance), filter)) 70 | ), 71 | showIf(isCompletedTodoListNonEmpty)(Seq( 72 | button( 73 | cls := "clear-completed", 74 | onclick :+= ((ev: Event) => presenter.clearCompleted(), true) 75 | )("Clear completed").render 76 | )) 77 | ) 78 | } 79 | 80 | override val getTemplate: Modifier = div( 81 | headerTemplate, 82 | showIf(isTodoListNonEmpty)(Seq( 83 | listTemplate.render, 84 | footerTemplate.render 85 | )) 86 | ) 87 | 88 | private def listItemTemplate(item: ModelProperty[Todo]) = { 89 | val editName = Property("") 90 | 91 | val editorInput = TextInput(editName, debounce = 0 millis)( 92 | cls := "edit", 93 | onkeydown :+= ((ev: KeyboardEvent) => { 94 | if (ev.keyCode == KeyCode.Enter) { 95 | presenter.endItemEdit(item, editName) 96 | true //prevent default 97 | } else if (ev.keyCode == KeyCode.Escape) { 98 | presenter.cancelItemEdit(item) 99 | true //prevent default 100 | } else false // do not prevent default 101 | }), 102 | onblur :+= ((ev: Event) => { 103 | presenter.endItemEdit(item, editName) 104 | true //prevent default 105 | }) 106 | ).render 107 | 108 | val stdView = div(cls := "view")( 109 | Checkbox(item.subProp(_.completed))(cls := "toggle"), 110 | label( 111 | ondblclick :+= ((ev: Event) => { 112 | presenter.startItemEdit(item, editName) 113 | editorInput.focus() 114 | }, true) 115 | )(bind(item.subProp(_.name))), 116 | button( 117 | cls := "destroy", 118 | onclick :+= ((ev: Event) => presenter.deleteItem(item.get), true) 119 | ) 120 | ) 121 | 122 | li( 123 | CssStyleName("completed").styleIf(item.subProp(_.completed)), 124 | CssStyleName("editing").styleIf(item.subProp(_.editing)), 125 | )(stdView, editorInput) 126 | } 127 | 128 | private def footerButtonTemplate(title: String, link: String, expectedFilter: TodosFilter) = { 129 | val isSelected = model.subProp(_.todosFilter).transform(_ == expectedFilter) 130 | a(href := link, CssStyleName("selected").styleIf(isSelected))(title) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /todo/src/main/scala/io/udash/todo/views/todo/TodoViewFactory.scala: -------------------------------------------------------------------------------- 1 | package io.udash.todo.views.todo 2 | 3 | import io.udash._ 4 | import io.udash.todo.TodoState 5 | 6 | object TodoViewFactory extends ViewFactory[TodoState] { 7 | override def create(): (View, Presenter[TodoState]) = { 8 | val model = ModelProperty(TodoViewModel(Seq.empty, TodosFilter.All, "", toggleAllChecked = false)) 9 | val presenter: TodoPresenter = new TodoPresenter(model) 10 | val view = new TodoView(model, presenter) 11 | (view, presenter) 12 | } 13 | } --------------------------------------------------------------------------------