liftweb-uirouter
angularjs ui-router module for scala liftweb framework
git clone https://9o.is/git/liftweb-uirouter.git
commit 374a15122248b13debdcce443cb82eafafb74766 parent 8a439c4830fa9080d8231060bf5c81d3fbd391ea Author: Jul <jul@9o.is> Date: Sat, 7 Jun 2014 06:34:58 -0400 uirouter init Diffstat:
12 files changed, 549 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md @@ -1,4 +1,8 @@ -SBT Starter Template +Angular UI-Router Lift Module ==================== -Use this to start any project that requires sbt. +This Lift module handles the routing and ajax page loading for your lift application using the state machine: [Angular UI-Router][1]. + +How To Coming Soon... + +[1]: https://github.com/angular-ui/ui-router diff --git a/project/.s3credentials b/project/.s3credentials @@ -1,2 +0,0 @@ -accessKey = -secretKey = diff --git a/project/Build.scala b/project/Build.scala @@ -5,11 +5,12 @@ object LiftProjectBuild extends Build { import BuildSettings._ - lazy val root = Project("s3", file(".")) + lazy val root = Project("uirouter", file(".")) .settings(appSettings: _*) .settings(libraryDependencies ++= Seq( - "ch.qos.logback" % "logback-classic" % "1.0.13" % "compile", + "net.liftweb" %% "lift-webkit" % Ver.lift % "provided", + "ch.qos.logback" % "logback-classic" % "1.0.13", "org.scalatest" %% "scalatest" % "1.9.2" % "test" ) ) diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala @@ -6,12 +6,27 @@ import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import ohnosequences.sbt.SbtS3Resolver._ object BuildSettings { + + object Ver { + val lift = "3.0-M1" + val lift_edition = "3.0" + } + + val liftVersion = SettingKey[String]("liftVersion", + "Full version number of the Lift Web Framework") + + val liftEdition = SettingKey[String]("liftEdition", + "Lift Edition (short version number to append to artifact name)") + val buildTime = SettingKey[String]("build-time") val basicSettings = Defaults.defaultSettings ++ Seq( - name := "s3", - version := "0.1", - organization := "PYC", + name := "uirouter", + version := "0.1-SNAPSHOT", + organization := "net.liftmodules", + liftVersion <<= liftVersion ?? Ver.lift, + liftEdition <<= liftEdition ?? Ver.lift_edition, + moduleName <<= (name, liftEdition) { (n, e) => n + "_" + e }, scalaVersion := "2.10.3", scalacOptions <<= scalaVersion map { sv: String => if (sv.startsWith("2.10.")) @@ -37,14 +52,19 @@ object BuildSettings { // eclipse EclipseKeys.withSource := true, - publishMavenStyle := false, + publishMavenStyle := true, + + publishArtifact in Test := false, + + + pomIncludeRepository := { _ => false }, publishTo := Some(s3resolver.value( "My "+{if (isSnapshot.value) "snapshots-pyc-inc" else "releases-pyc-inc"}+" S3 bucket", - s3(if (isSnapshot.value) "snapshots-pyc-inc" else "releases-pyc-inc")) withIvyPatterns), + s3(if (isSnapshot.value) "snapshots-pyc-inc" else "releases-pyc-inc"))), s3credentials := { - file(".") / "project" / ".s3credentials" + Path.userHome / ".ivy2" / ".s3credentials" }, s3region := com.amazonaws.services.s3.model.Region.US_Standard diff --git a/src/main/scala/net/liftmodules/uirouter/loc/UiRouter.scala b/src/main/scala/net/liftmodules/uirouter/loc/UiRouter.scala @@ -0,0 +1,11 @@ +package net.liftmodules.uirouter +package loc + +import net.liftweb._ +import sitemap._ +import Loc._ + +/** + * LocGroup that declares a Menu Item that has UI-router state. + */ +object UiRouter extends LocGroup("uirouter") +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/package.scala b/src/main/scala/net/liftmodules/uirouter/package.scala @@ -0,0 +1,63 @@ +package net.liftmodules + +import net.liftweb._ +import http._ +import common._ +import sitemap._ + +package object uirouter extends Factory { + import snippet._ + + /** + * Enables HTML5 Mode + */ + val html5mode = new FactoryMaker[Boolean](true) {} + + /** + * Ignores the UiRouter Loc Group. Adds entire sitemap. + */ + val ignoreUiRouterGroup = new FactoryMaker[Boolean](true) {} + + /** + * Default route when page lands on index. + */ + val defaultRoute = new FactoryMaker[Box[Menu]](Empty) {} + + /** + * The page title when page loads. + * example: pageTitle.set(setTitle(pageName => "Company Name - " + pageName)) + */ + val pageTitle = new FactoryMaker[String](setTitle()) {} + + /** + * Set document title on state change (provided the page name). + */ + def setTitle(edit: String => String = p => p): String = + "document.title = "+edit(snippet.UiRouter.pageName)+";" + + /** + * List of all the UI-router states. + */ + lazy val routes = new FactoryMaker[Seq[Menu]](findRoutes) {} + + /* Finds menu items that are in UiRouterGroup group. */ + private def findRoutes = LiftRules.siteMap map { + siteMap => + if(ignoreUiRouterGroup.vend) + siteMap.menus + else + siteMap.menus.filter(_.loc.inGroup_?(loc.UiRouter.group.head)) + } openOr Nil + + + object UiRouter { + def init: Unit = { + // don't include cometajax.js. served by ui-router module + LiftRules.autoIncludeComet = ((session: LiftSession) => false) + + // increase concurrent requests (since comet's aren't recycled -- need fix) + LiftRules.maxConcurrentRequests.default.set((request: Req) => 50) + } + } + +} +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/SnippetHelper.scala b/src/main/scala/net/liftmodules/uirouter/snippet/SnippetHelper.scala @@ -0,0 +1,50 @@ +package net.liftmodules.uirouter +package snippet + +import xml._ +import net.liftweb._ +import common._ +import util._ +import Helpers._ + +/** + * SnippetHelper from Lift-Extras + * + * https://github.com/eltimn/lift-extras/blob/master/library/src/main/scala/net/liftmodules/extras/SnippetHelper.scala + */ +private[snippet] trait SnippetHelper { + + /** + * Allows for the following to be used when building snippets that return CssSel. + * + * Usage: + * + * for { + * user <- User.currentUser ?~ "You must be logged in to edit your profile." + * } yield ({ + * ... + * }): CssSel + */ + implicit protected def boxCssSelToCssSel(in: Box[CssSel]): CssSel = in match { + case Full(csssel) => csssel + case Failure(msg, _, _) => "*" #> Text(msg) + case Empty => "*" #> Text("Nothing Found") + } + + /** + * Allows for the following to be used when building snippets that return NodeSeq. + * + * Usage: + * + * for { + * user <- User.currentUser ?~ "You must be logged in to edit your profile." + * } yield ({ + * ... + * }): NodeSeq + */ + implicit protected def boxNodeSeqToNodeSeq(in: Box[NodeSeq]): NodeSeq = in match { + case Full(ns) => ns + case Failure(msg, _, _) => Text(msg) + case Empty => Text("Nothing Found") + } +} +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/UiJs.scala b/src/main/scala/net/liftmodules/uirouter/snippet/UiJs.scala @@ -0,0 +1,196 @@ +package net.liftmodules.uirouter +package snippet + +import net.liftweb._ +import http._ +import js._ +import common._ +import util._ +import Helpers._ + +/** + * Just a bunch of javascript to append in html. + */ +trait UiJs extends SnippetHelper { + + /** + * Snippet to configure AngularJs module with all routes. + * + * Required attributes: + * - ngApp: Name of AngularJs module. + * + * Example: + * if script is 'var myapp = angular.module(....' + * then add the following to the end of your html file, + * + * <script data-lift="UiRouter.js?ngApp=myapp"></script> + */ + def js: CssSel = + for{ + app <- S.attr("ngApp") ?~ "ngApp name is missing" + } yield "* *" #> { + s"""// <![CDATA[ + ${init + app + config + + onViewContentLoaded(ifStateNotVisited(updateLiftWatch, evalRenderedJS, restartComet)) + + onStateChangeSuccess(pageTitle.vend)}; + $cometScript + //]]>""" + } + + /** + * Provides default route in js + */ + def routerOtherwise = + defaultRoute.vend map { + "$urlRouterProvider.otherwise('" + S.contextPath + _.loc.calcDefaultHref + "');" + } openOr "" + + /** + * Provides main configurations in js + */ + def config = + """ + .config(function($stateProvider, $urlRouterProvider,$locationProvider) { + $locationProvider.html5Mode("""+html5mode.vend.toString+"""); + """+routerOtherwise+""" + $stateProvider"""+{ + routes.vend.map { menu => + val templateUrl = S.contextPath+menu.loc.calcDefaultHref + val state: String = menu.loc.name.replaceAll(" ","_") + + ".state('"+state+"', {"+ + "url:'"+templateUrl+"',"+ + "templateUrl:'"+templateUrl+".html?ajax'"+ + "})" + }.mkString + }+""" + ;}) + """ + + /** + * Initializing + */ + def init = + "var statesVisited = [];"+ + " var lift_toWatch = {};" + + /** + * Updates all new comet entries + */ + def updateLiftWatch = + """var el = angular.element(document.querySelectorAll(".ui-liftwatch"));"""+ + """for (var i=0;i<el.length;++i) { window.lift_toWatch[el[i].getAttribute("id")] = el[i].getAttribute("when") };""" + + /** + * Evaluates the javascript that was placed in the ajax-uploaded html page. + */ + def evalRenderedJS = + """var promises = angular.element(document.querySelectorAll(".ui-liftjs"));"""+ + """for (var i=0;i<promises.length;++i) { eval(promises[i].innerHTML) };""" + + /** + * Restarts comet + */ + def restartComet = """window.liftComet.lift_cometRestart();""" + + /** + * The current page's name. + */ + def pageName = "toState.name.replace(/_/g, ' ')" + + /** + * Run commands when UI-Router's $viewContentLoaded event is fired. + */ + def onViewContentLoaded(cmds: String*): String = + """.run(['$rootScope', '$state', function($rootScope, $state) {$rootScope.$on('$viewContentLoaded', function() {""" + + cmds.mkString + """}); }])""" + + /** + * Run commands when UI-Router's $stateChangeSuccess event is fired. + */ + def onStateChangeSuccess(cmds: String*): String = + """.run(['$rootScope', function($rootScope) {$rootScope.$on('$stateChangeSuccess', function(e, toState) {""" + + cmds.mkString + """}); }])""" + + /** + * Run commands if state wasn't visited during the current page load. + */ + def ifStateNotVisited(cmds: String*): String = """ + var visited = false; + angular.forEach(statesVisited, function(state){ + if(state === $state.current.name) {visited = true;} + }); + if(!visited){ + statesVisited.push($state.current.name); + """ + cmds.mkString + """ + } + """ + + /** + * Hides (or scopes) js code. + */ + protected def func(body: String): String = "(function() {"+body+"});" + + /** + * Renders the default JS comet script + */ + private def cometScript: String = """ + (function() { + var currentCometRequest = null; + window.liftComet = { + lift_handlerSuccessFunc: function() { + setTimeout("liftComet.lift_cometEntry();",100); + }, + + lift_unlistWatch : function(watchId) { + var ret = []; + for (item in lift_toWatch) { + if (item !== watchId) { + ret.push(item); + } + } + lift_toWatch = ret; + }, + + lift_handlerFailureFunc: function() { + setTimeout("liftComet.lift_cometEntry();",""" + LiftRules.cometFailureRetryTimeout + """); + }, + + + lift_cometError: function(e) { + if (console && typeof console.error == 'function') + console.error(e.stack || e); + throw e; + }, + + lift_sessionLost: function() { window.location = '/' }, + + lift_cometRestart: function() { + if (currentCometRequest) { + currentCometRequest.abort(); + } + + liftComet.lift_handlerSuccessFunc(); + }, + + lift_cometEntry: function() { + var isEmpty = function(){for (var i in lift_toWatch) {return false} return true}(); + if (!isEmpty) { + liftAjax.lift_uriSuffix = undefined; + currentCometRequest = """ + + LiftRules.jsArtifacts.comet(AjaxInfo(JE.JsRaw("lift_toWatch"), + "GET", + LiftRules.cometGetTimeout, + false, + "script", + Full("liftComet.lift_handlerSuccessFunc"), + Full("liftComet.lift_handlerFailureFunc"))) + + """ + } + } + + }})(); + """ + + +} +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/UiMenu.scala b/src/main/scala/net/liftmodules/uirouter/snippet/UiMenu.scala @@ -0,0 +1,99 @@ +package net.liftmodules.uirouter +package snippet + +import xml._ +import net.liftweb._ +import http._ +import util._ +import Helpers._ +import sitemap._ +import common._ + +/** + * Menu so pages can work with UI-router and change with Ajax. + */ +trait UiMenu extends SnippetHelper { + + /** + * Creates an HTML anchor tag of a UI-router Menu. + */ + def item(name: String): NodeSeq = + (for { + loc <- SiteMap.findAndTestLoc(name) + link <- loc.createDefaultLink + } yield { + + def uiSref(el: Elem): Elem = { + if(loc.inGroup_?(net.liftmodules.uirouter.loc.UiRouter.group.head)) + el % ("ui-sref" -> loc.name.replaceAll(" ","_")) + else + el + } + + val linkText = loc.linkText openOr Text(loc.name) + uiSref(<a href={link}>{linkText}</a>) + + }) getOrElse NodeSeq.Empty + + /** + * Links an HTML anchor tag of a UI-router Menu. + * + * Usage: + * + * <a data-lift="UiRouter.item?name=Settings" href="#"> + * <img src="example.jpg" /> + * </a> + */ + def item: CssSel = { + val options = (for { + name: String <- S.attr("name") ?~ "Item name not specified" + } yield for { + loc <- SiteMap.findAndTestLoc(name) + link <- loc.createDefaultLink + } yield { + "* [ui-sref]" #> loc.name.replaceAll(" ","_") & + "* [href]" #> link + }) openOr Empty + options + } + + /** + * Produces a menu ul given a group name. + * + * Dropdowns for children menus are designed with + * Bootstrap 3 and ui-bootstrap. + * + * Usage: + * + * <ul data-lift="UiRouter.group?group=topbar"></ul> + */ + def group = { + val menus: NodeSeq = + for { + group <- S.attr("group") ?~ "Group not specified" + sitemap <- LiftRules.siteMap ?~ "Sitemap is empty" + request <- S.request ?~ "Request is empty" + curLoc <- request.location ?~ "Current location is empty" + } yield ({ + sitemap.locForGroup(group) flatMap { loc => + val nonHiddenKids = loc.menu.kids.filterNot(_.loc.hidden) + + if (nonHiddenKids.length == 0) { + <li>{item(loc.name)}</li> + } + else { + val dropdown: NodeSeq = nonHiddenKids.map { kid => + <li>{item(kid.loc.name)}</li> + } + + <li class="dropdown" on-toggle="toggled(open)"> + <a href="#" class="dropdown-toggle" ng-disabled="disabled">{loc.linkText.openOr(Text("Empty Name"))} <b class="caret"></b></a> + <ul class="dropdown-menu" role="menu">{ dropdown }</ul> + </li> + } + } + }): NodeSeq + + "* *" #> menus + } +} +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/UiRoundTrip.scala b/src/main/scala/net/liftmodules/uirouter/snippet/UiRoundTrip.scala @@ -0,0 +1,35 @@ +package net.liftmodules.uirouter +package snippet + +import net.liftweb._ +import http._ +import util._ +import Helpers._ +import js.JE._ +import js.JsCmds._ + +/** + * When using Lift roundtrips with UI-router. + */ +trait UiRoundTrip extends SnippetHelper { + + def roundTrips: List[RoundTripInfo] + + def render: CssSel = + for (sess <- S.session) yield { + val roundtrips = sess.buildRoundtrip(roundTrips) + + val lastcomet = S.cometAtEnd.last + val whenregex = "<div.*lift:when=\"([0-9]*)\".*/>".r + + val when: String = lastcomet.toString match { + case whenregex(when) => when + case _ => "" + } + + val className: String = this.getClass.getName.split("""\.""").last + + "* *+" #> <script class="ui-liftjs">{SetExp(JsVar("window", className), roundtrips).toJsCmd}</script> & + "* *+" #> <div class="ui-liftwatches">{(lastcomet % ("class" -> "ui-liftwatch") % ("when" -> when))}</div> + } +} +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/UiRouter.scala b/src/main/scala/net/liftmodules/uirouter/snippet/UiRouter.scala @@ -0,0 +1,9 @@ +package net.liftmodules.uirouter +package snippet + +trait UiRouter extends UiJs with UiSurround with UiMenu + +/** + * Main UiRouter Snippet + */ +object UiRouter extends UiRouter +\ No newline at end of file diff --git a/src/main/scala/net/liftmodules/uirouter/snippet/UiSurround.scala b/src/main/scala/net/liftmodules/uirouter/snippet/UiSurround.scala @@ -0,0 +1,43 @@ +package net.liftmodules.uirouter +package snippet + +import xml._ +import net.liftweb._ +import http._ +import util._ +import Helpers._ + +trait UiSurround extends SnippetHelper { + + /** + * Conditional Template Surround Snippet. Allows pages to be surrounded by a + * different template if it's accessed with ajax. + * + * Needed attributes are: + * - with: template name to surround page. + * - at: id of element in template to place the page. + * - withAjax: template name to surround page when accessed with ajax. + * + * <div data-lift="UiRouter.surround?withAjax=no-base&with=base-wrap&at=content"> + * + * When accessing with ajax, assure URL query parameter 'ajax' exists. + * + * NOTE: default.html in templates-hidden cannot exist + * (else Lift will auto-surround everything with default) + */ + def surround(ns: NodeSeq): NodeSeq = + for { + surroundWith <- S.attr("with") ?~ "Surround with not specified" + surroundWithAjax <- S.attr("withAjax") ?~ "Surround with ajax not specified" + at <- S.attr("at") ?~ "Surround at not specified" + } yield { + if(S.param("ajax").isDefined) + Templates("templates-hidden" :: surroundWithAjax :: Nil) map { + s"#$at" #> ns + } openOr Text("Template '"+surroundWithAjax+"' not found") + else + Templates("templates-hidden" :: surroundWith :: Nil) map { + s"#$at" #> ns + } openOr Text("Template '"+surroundWith+"' not found") + } +} +\ No newline at end of file