This example has two domain model objects - Blog and Post. You can choose your blog, then choose a post in that blog. It runs in three different modes:
server only:scc jetty.schtml example.simpleBlog.queryParam
client only: scc js.schtml example.simpleBlog.queryParam
client/server: scc jetty.schtml js.schtml example.simpleBlog.queryParam
The simpleBlog example has several different but similar implementations to show how to build both page-by-page and single-page applications using both query parameters and URL based parameters.
Here's the ViewBlog template page that shows the use of QueryParam annotations to bind page properties to query parameters in the URL using a "page-by-page" navigation style:
<%@ @URL(pattern="/blog/", testURLs={"/blog/?blogId=1&post=perfGoals", "/blog/?blogId=1&post=usableAndEfficient"}) %> <html> <%! @QueryParam int blogId; @QueryParam(name="post") String postShortName; Blog blog := BlogManager.findBlog(blogId); Post post := blog == null ? null : blog.getPost(postShortName); %> <body> <div class="appFrame" id="appFrame"> <div class="titleText">All blogs:</div> <div id="blogList" repeat=":= BlogManager.blogs" repeatVarName="curBlog" class="blogIndent"> <a href='= pageBaseURL + "?blogId=" + curBlog.blogId' class=':= blog == curBlog ? "currentLink" : "normalLink"'><%= curBlog.blogName %></a> </div> <div id="blogDisplay" visible=":= blog != null"> <div id="blogDesc"> <div><span class="titleText">Current blog:</span> <%= blog.blogName %></div> <div><span class="titleText">Description:</span> <%= blog.blogDesc %></div> <div class="titleText">Posts:</div> </div> <div id="postList" repeat=":= blog.posts" repeatVarName="curPost"> <a href='= pageBaseURL + "?blogId=" + blog.blogId + "&post=" + curPost.shortName' class=':= post == curPost ? "currentLink" : "normalLink"'><%= curPost.title %></a> </div> <div id="postContent" visible=":= post != null"> <h3><%= post.title %></h3> <%= post.postText %> <div id="alt"> Please select a post. </div> </div> <div id="alt"> Please select a blog. </div> </div> </div> <style type="text/css"> .currentLink { text-decoration: underline; } .normalLink { text-decoration: none; } .normalLink:hover { text-decoration: underline; } .titleText { font-weight: bold; } .postContent { width: 350px; } .blogIndent { padding-left: 20px; } </style> </body> </html>
The @URL annotation specifies a fixed pattern as the base URL. Two page properties are populated from the URL using @QueryParam. The testURLs attribute specifies example URLs to request in test mode and are accessible as links in the default index page for convenience. Ordinarily they would be specified in a separate layer.
Here's a different version of the ViewBlog template using URL parameters:
<%@ @URL(pattern="/blog/[{blogId=integerLiteral}/][{postShortName=identifier}/]", testURLs={"/blog/1/perfGoals/", "/blog/1/usableAndEfficient/"}) %> <html> <%! int blogId; String postShortName; Blog blog := BlogManager.findBlog(blogId); Post post := blog == null ? null : blog.getPost(postShortName); %> <body> <div class="appFrame" id="appFrame"> <div class="titleText">All blogs:</div> <span id="blogList" repeat=":= BlogManager.blogs" repeatVarName="curBlog" class="blogIndent"> <a href='= "/blog/" + curBlog.blogId + "/"' class=':= blog == curBlog ? "currentLink" : "normalLink"'><%= curBlog.blogName %></a> </span> <div id="blogDisplay" visible=":= blog != null"> <div id="blogDesc"> <%= blog.blogDesc %> </div> <div id="postList" repeat=":= blog.posts" repeatVarName="curPost"> <a href='= "/blog/" + blog.blogId + "/" + curPost.shortName + "/"' class=':= post == curPost ? "currentPostLink" : "postLink"'><%= curPost.title %></a> </div> <div id="postContent" visible=":= post != null"> <h3><%= post.title %></h3> <%= post.postText %> <div id="alt"> Please select a post. </div> </div> <div id="alt"> Please select a blog. </div> </div> </div> <style type="text/css"> .currentLink { text-decoration: underline; } .normalLink { text-decoration: none; } .normalLink:hover { text-decoration: underline; } .titleText { font-wieght: bold; } .postContent { width: 350px; } .blogIndent { padding-left: 20px; } </style> </body> </html>
Now the pattern specifies the mapping from page property to place in the URL, along with the data type.
Here's a one page version of the ViewBlog template back to using query parameters:
<%@ @URL(pattern="/blog/", testURLs={"/blog/?blogId=1&post=perfGoals", "/blog/?blogId=1&post=usableAndEfficient"}) %> <html extends="EditablePage"> <%! // View model, usually in a separate layer @QueryParam int blogId; @QueryParam(name="post") String postShortName; Blog blog := BlogManager.findBlog(blogId); Post post := blog == null ? null : blog.getPost(postShortName); boolean newPost := post.daysSincePost < 4; void switchToBlog(int newBlogId) { this.blogId = newBlogId; this.postShortName = null; } void switchToPost(String shortName) { postShortName = shortName; } %> <head> <link rel="stylesheet" type="text/css" href="blogStyle.css" /> </head> <body> <div class="appFrame" id="appFrame"> <div class="label">All blogs:</div> <div id="blogList" repeat=":= BlogManager.blogs" repeatVarName="curBlog" class="blogIndent"> <a clickEvent="=: switchToBlog(curBlog.blogId)" class=':= blog == curBlog ? "currentLink" : "normalLink"'> <%= curBlog.blogName %> </a> </div> <p/> <div id="blogDisplay" visible=":= blog != null"> <div id="alt"> Please select a blog. </div> <div><span class="label">Current blog:</span><%= blog.blogName %></div> <div><span class="label">Description:</span><%= blog.blogDesc %></div> <div class="label">Posts:</div> <div repeat=":= blog.posts" repeatVarName="curPost" class="blogIndent"> <a clickEvent="=: switchToPost(curPost.shortName)" class=':= post == curPost ? "currentLink" : "normalLink"'> <%= curPost.title %> </a> </div> <div id="postContent" visible=":= post != null" class="postContent"> <h4> <%= post.title %> <img visible=":= newPost" src="/newIcon.png" class="newIcon"/> </h4> <%= post.postText %> <div id="alt"> Please select a post. </div> </div> </div> </div> </body> </html>
The link uses a clickCount binding that inserts href='#' (if href is not specified). That creates a new browser history state without leaving the page and runs the clickCount reverse-only binding to call the switchToBlog or switchToPost methods. Those methods just change the @QueryParam properties that then automatically update the URL and handle the back button.