Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature mvc #1

Merged
merged 18 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
ThisBuild / scalaVersion := "3.3.0"

wartremoverErrors ++= Warts.unsafe
wartremoverErrors ++= Warts.all
wartremoverWarnings ++= Warts.all

lazy val scatan = (project in file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
name := "Scatan",
scalaVersion := "3.3.0",
libraryDependencies ++= Seq(
// Do not remove or change order
"org.scalatest" %%% "scalatest" % "3.3.0-SNAP4" % Test,
Expand Down
49 changes: 31 additions & 18 deletions src/main/scala/scatan/Main.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
package scatan
import com.raquo.laminar.api.L.{*, given}
import org.scalajs.dom
import scatan.example.controller.{AboutController, AboutControllerImpl, HomeController, HomeControllerImpl}
import scatan.example.model.CounterAppState
import scatan.example.view.{AboutView, HomeView, ScalaJSAboutView, ScalaJsHomeView}
import scatan.mvc.lib.application.NavigableApplication
import scatan.mvc.lib.page.PageFactory
import scatan.mvc.lib.{Controller, Model, NavigableApplicationManager, ScalaJSView}

def appElement(): Element =
div(
h1("Hello Laminar!"),
div(
className := "card",
button(
className := "button",
"Click me!",
tpe := "button"
import scala.util.Random

// Route
enum Pages(val pageFactory: PageFactory[?, ?, CounterAppState]):
case Home
extends Pages(
PageFactory[HomeController, HomeView, CounterAppState](
viewFactory = new ScalaJsHomeView(_, "root"),
controllerFactory = new HomeControllerImpl(_)
)
)
case About
extends Pages(
PageFactory[AboutController, AboutView, CounterAppState](
viewFactory = new ScalaJSAboutView(_, "root"),
controllerFactory = new AboutControllerImpl(_)
)
)
),
p(className := "read-the-docs", "Click on the Vite logo to learn more")
)
@main
def main(): Unit =
val containerNode = dom.document.querySelector("#root")

// this is how you render the rootElement in the browser
render(containerNode, appElement())
// Application
val CounterApplication: NavigableApplication[CounterAppState, Pages] = NavigableApplication[CounterAppState, Pages](
initialState = CounterAppState(0),
pagesFactories = Pages.values.map(p => p -> p.pageFactory).toMap
)

@main def main(): Unit =
NavigableApplicationManager.startApplication(CounterApplication, Pages.Home)
14 changes: 14 additions & 0 deletions src/main/scala/scatan/example/controller/AboutController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package scatan.example.controller

import scatan.example.model.CounterAppState
import scatan.example.view.AboutView
import scatan.mvc.lib.Controller

trait AboutController extends Controller:
def about(): Unit

class AboutControllerImpl(requirements: Controller.Requirements[AboutView, CounterAppState])
extends AboutController
with Controller.Dependencies(requirements):
override def about(): Unit =
println("About")
7 changes: 7 additions & 0 deletions src/main/scala/scatan/example/controller/HomeController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package scatan.example.controller

import scatan.mvc.lib.Controller

trait HomeController extends Controller:
def counter: Int
def increment(): Unit
17 changes: 17 additions & 0 deletions src/main/scala/scatan/example/controller/HomeControllerImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package scatan.example.controller

import scatan.example.model.CounterAppState
import scatan.example.view
import scatan.example.view.HomeView
import scatan.mvc.lib.{Controller, Model, ScalaJSView, View}

class HomeControllerImpl(requirements: Controller.Requirements[HomeView, CounterAppState])
extends HomeController
with Controller.Dependencies(requirements):

override def counter: Int = this.model.state.count
override def increment(): Unit =
this.model.update { m =>
m.copy(count = m.count + 1)
}
this.view.onCounterUpdated(this.model.state.count)
5 changes: 5 additions & 0 deletions src/main/scala/scatan/example/model/CounterAppState.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package scatan.example.model

import scatan.mvc.lib.Model

case class CounterAppState(count: Int) extends Model.State
24 changes: 24 additions & 0 deletions src/main/scala/scatan/example/view/AboutView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package scatan.example.view

import com.raquo.laminar.api.L.*
import scatan.example.controller.AboutController
import scatan.mvc.lib.{NavigableApplicationManager, ScalaJSView, View}

trait AboutView extends View:
def about(): Unit

class ScalaJSAboutView(requirements: View.Requirements[AboutController], container: String)
extends AboutView
with View.Dependencies[AboutController](requirements)
with ScalaJSView(container):

override def about(): Unit = ???

override def element: Element = div(
h1("About"),
p("This is a ScalaJS view"),
button(
"Back",
onClick --> (_ => NavigableApplicationManager.navigateBack())
)
)
6 changes: 6 additions & 0 deletions src/main/scala/scatan/example/view/HomeView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package scatan.example.view

import scatan.mvc.lib.{ScalaJSView, View}

trait HomeView extends View:
def onCounterUpdated(counter: Int): Unit
31 changes: 31 additions & 0 deletions src/main/scala/scatan/example/view/ScalaJsHomeView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package scatan.example.view

import com.raquo.laminar.api.L.*
import scatan.Pages
import scatan.example.controller.HomeController
import scatan.mvc.lib.{NavigableApplicationManager, ScalaJSView, View}

class ScalaJsHomeView(requirements: View.Requirements[HomeController], container: String)
extends HomeView
with View.Dependencies(requirements)
with ScalaJSView(container):

private val reactiveCounter = Var(this.controller.counter)

override def element: Element =
div(
h1("Scala.js Home"),
p("This is a Scala.js view"),
p("The counter is: ", child.text <-- reactiveCounter.signal),
button(
"Increment",
onClick --> (_ => controller.increment())
),
button(
"Switch to About Page",
onClick --> (_ => NavigableApplicationManager.navigateTo(Pages.About))
)
)

override def onCounterUpdated(counter: Int): Unit =
reactiveCounter.set(counter)
18 changes: 18 additions & 0 deletions src/main/scala/scatan/mvc/lib/Controller.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package scatan.mvc.lib

import scatan.mvc.lib

trait Controller

/** The Controller object.
*/
object Controller:
type Factory[V <: View, C <: Controller, S <: Model.State] = Requirements[V, S] => C
trait Requirements[V <: View, S <: Model.State] extends Model.Provider[S] with View.Provider[V]

trait Dependencies[V <: View, S <: Model.State](requirements: Requirements[V, S]) extends Controller:
protected def view: V = requirements.view
protected def model: Model[S] = requirements.model

trait Provider[C <: Controller]:
def controller: C
12 changes: 12 additions & 0 deletions src/main/scala/scatan/mvc/lib/Model.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package scatan.mvc.lib

trait Model[S <: Model.State](private var _state: S):
def state: S = _state
def update(f: S => S): Unit = _state = f(_state)

object Model:
trait State
def apply[S <: State](state: S): Model[S] = new Model(state) {}

trait Provider[S <: State]:
def model: Model[S]
16 changes: 16 additions & 0 deletions src/main/scala/scatan/mvc/lib/NavigableApplicationManager.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scatan.mvc.lib

import scatan.mvc.lib.application.{Application, NavigableApplication}

object NavigableApplicationManager:
private var _application: Option[NavigableApplication[?, ?]] = None

def startApplication[Route](application: NavigableApplication[?, Route], initialRoute: Route): Unit =
_application = Some(application)
application.show(initialRoute)

def navigateTo[Route](route: Route): Unit =
_application.foreach(_.asInstanceOf[NavigableApplication[?, Route]].show(route))

def navigateBack(): Unit =
_application.foreach(_.back())
17 changes: 17 additions & 0 deletions src/main/scala/scatan/mvc/lib/ScalaJSView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package scatan.mvc.lib

import org.scalajs.dom
import com.raquo.laminar.api.L.*

trait ScalaJSView(val container: String) extends View:
def element: Element

override def show(): Unit =
val containerElement = dom.document.getElementById(container)
containerElement.children.foreach(_.remove())
render(containerElement, element)

override def hide(): Unit =
val containerElement = dom.document.getElementById(container)
containerElement.children.foreach(_.remove())
render(containerElement, div())
18 changes: 18 additions & 0 deletions src/main/scala/scatan/mvc/lib/View.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package scatan.mvc.lib

trait View:
def show(): Unit
def hide(): Unit

/** The View object.
*/
object View:
type Factory[C <: Controller, V <: View] = Requirements[C] => V

trait Requirements[C <: Controller] extends Controller.Provider[C]

trait Dependencies[C <: Controller](requirements: Requirements[C]) extends View:
protected def controller: C = requirements.controller

trait Provider[V <: View]:
def view: V
37 changes: 37 additions & 0 deletions src/main/scala/scatan/mvc/lib/application/Application.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package scatan.mvc.lib.application

import scatan.mvc.lib.Model
import scatan.mvc.lib.page.{ApplicationPage, PageFactory}

/** An application is a collection of pages that share a model.
*
* @tparam S
* The state type of the model.
* @tparam Route
* The type of the route.
*/
trait Application[S <: Model.State, Route]:
val model: Model[S]
val pages: Map[Route, ApplicationPage[S, ?, ?]]

object Application:
/** Create an application from a model and a list of pages.
* @param initialState
* The initial state of the model.
* @param pagesFactories
* The pages.
* @tparam S
* The state type of the model.
* @tparam Route
* The type of the route.
* @return
* The application.
*/
def apply[S <: Model.State, Route](
initialState: S,
pagesFactories: Map[Route, PageFactory[?, ?, S]]
): Application[S, Route] =
new Application[S, Route]:
override val model: Model[S] = Model(initialState)
override val pages: Map[Route, ApplicationPage[S, ?, ?]] =
pagesFactories.map((route, pageFactory) => (route, ApplicationPage(model, pageFactory)))
28 changes: 28 additions & 0 deletions src/main/scala/scatan/mvc/lib/application/Navigable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package scatan.mvc.lib.application

import scatan.mvc.lib.*
import scatan.mvc.lib.page.{ApplicationPage, PageFactory}

trait Navigable[Route] extends Application[?, Route]:
private var pagesHistory: Seq[Route] = Seq.empty
def show(route: Route): Unit =
pagesHistory.lastOption.foreach(this.pages(_).view.hide())
pagesHistory = pagesHistory :+ route
pages(route).view.show()
def back(): Unit =
pagesHistory.lastOption.foreach(pages(_).view.hide())
pagesHistory = pagesHistory.dropRight(1)
pagesHistory.lastOption.foreach(pages(_).view.show())

trait NavigableApplication[S <: Model.State, Route] extends Application[S, Route] with Navigable[Route]

object NavigableApplication:
def apply[S <: Model.State, Route](
initialState: S,
pagesFactories: Map[Route, PageFactory[?, ?, S]]
): NavigableApplication[S, Route] =
new NavigableApplication[S, Route]:
override val model: Model[S] = Model(initialState)
override val pages: Map[Route, ApplicationPage[S, ?, ?]] = pagesFactories.map { (route, pageFactory) =>
route -> ApplicationPage(this.model, pageFactory)
}
35 changes: 35 additions & 0 deletions src/main/scala/scatan/mvc/lib/page/ApplicationPage.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package scatan.mvc.lib.page

import scatan.mvc.lib.*

/** A page of an application. It is a combination of a model, a view and a controller.
*
* @tparam S
* The type of the state of the model.
* @tparam C
* The type of the controller.
* @tparam V
* The type of the view.
* @param model
* The model.
* @param pageFactory
* The page factory.
*/
trait ApplicationPage[S <: Model.State, C <: Controller, V <: View](
override val model: Model[S],
val pageFactory: PageFactory[C, V, S]
) extends View.Requirements[C]
with Controller.Requirements[V, S]:
private lazy val _controller: C = pageFactory.controllerFactory(this)
private lazy val _view: V = pageFactory.viewFactory(this)
override def controller: C = _controller
override def view: V = _view

object ApplicationPage:
type Factory[S <: Model.State, C <: Controller, V <: View] =
Model[S] => ApplicationPage[S, C, V]
def apply[S <: Model.State, C <: Controller, V <: View](
model: Model[S],
pageFactory: PageFactory[C, V, S]
): ApplicationPage[S, C, V] =
new ApplicationPage[S, C, V](model, pageFactory) {}
8 changes: 8 additions & 0 deletions src/main/scala/scatan/mvc/lib/page/PageFactory.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package scatan.mvc.lib.page

import scatan.mvc.lib.{Controller, Model, View}

case class PageFactory[C <: Controller, V <: View, S <: Model.State](
viewFactory: View.Factory[C, V],
controllerFactory: Controller.Factory[V, C, S]
)
11 changes: 0 additions & 11 deletions src/test/scala/scatan/TestMain.scala

This file was deleted.

Loading