├── .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 [](https://travis-ci.org/UdashFramework/udash-demos) [](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 | }
--------------------------------------------------------------------------------