Adding a view to an App Builder application

Author: Dave Cassel  |  Category: Software Development

This post is based on how MarkLogic’s Application Builder worked in MarkLogic versions 4 and 5; it does not apply to versions 6+. 

In my last post, I showed how to add maps to the front page of a MarkLogic Server Application Builder-based application. But what if you want to add a whole new view? I’ll show you how in this tutorial.

The Application

I’m going to use my pin collection app, as I did in the previous tutorial. You won’t need any data for this one, because I’m going to add a view that lets you put a pin into the collection. I won’t assume that you’ve gone through the add-a-map tutorial, but if you have, just skip the steps for creating a database and going through App Builder.

Here are the high-level steps:

  1. Create the database
  2. Use Application Builder to build a starting point
  3. Override the rewriter
  4. Add a navigation bar
  5. Define the new page

Five easy steps. Let’s take a closer look.

1. Create the database

MarkLogic Server 4.2’s new Application Services page will make this very simple. Go to http://localhost:8002 and click the New Database button. Call it “pins”. When you click the “Create Database” button, MarkLogic Server will create a database and a forest for you. Step one: complete.

2. Use Application Builder to build a starting point

We didn’t put any data into our database, so Application Builder won’t find anything to work with. We’ll use a handy App Builder feature and run through again after we’ve added some data. For now, just start at port 8002, go through App Builder and take the defaults. The only change you’ll need to make is setting the database. Go ahead and deploy the new application. You’ll find it’s not very exciting yet, but that’s okay.

App Builder deploys the source code for your new application to a new modules database that it creates for you. You can edit this directly, by setting up a WebDAV application server pointing to the modules database for your new application. You can also get the support package, which contains your new app’s source code, deploy that to the file system and edit that way (if you take this approach, don’t forget to change your application server to point to the file system). Either way is fine. If you want to redeploy at some point, there are fewer steps if you stick with the modules database.

3. Override the rewriter

The URLs used in the source code generated by Application Builder don’t explicitly point to a module. Rather, they identify a view, and the rewrite.xqy module changes the URL to point to the main.xqy module with the view passed as a parameter. For instance, doing a search for “pin” hits the URL /search?q=pin. rewrite.xqy changes this to /main.xqy?view=search&q=pin. This happens for any of the views that the rewriter knows about.

Our goal is to add a new view. We now have a choice to make: do we want our new view to have a nice URL (/add) or not (/main.xqy?view=add). While functionally equivalent, the nice URL looks better and makes my new view match the rest of the application. Let’s set up rewriting for our new view.

This seems simple enough: just add another case to the rewrite module to handle our new view. Unfortunately, this leads to a problem: the next time we redeploy from App Builder, the modifications to rewrite.xqy are lost — only files under /application/custom/ are safe.

What we’ll do instead is set up our own rewrite module. Make a copy of /application/rewrite.xqy as /application/custom/rewrite.xqy. Now go to the admin view at http://localhost:8001, click your way to Configure -> Groups -> Default -> App Servers and click on the name of your server. Scroll down and change the “url rewriter” field from “rewrite.xqy” to “/custom/rewrite.xqy”. Click the OK button and give your application a quick test. You have now taken control of the URL rewriting process!

We’re going to make a small change to the rewriter so that it will recognize our new view. We’ll simply add a condition to the if-then-else chain. For brevity’s sake, this is only part of rewrite.xqy:

let $new-url :=
    if (fn:matches($url,"^/search"))
        then local:construct-new($url,"/search","search")
    else
        if (fn:matches($url,"^/detail"))
        then local:construct-new($url,"/detail","detail")
    else
       if (fn:matches($url,"^/help"))
       then local:construct-new($url,"/help","help")
    else
       if (fn:matches($url,"^/contact"))
       then local:construct-new($url,"/contact","contact")
    else
       if (fn:matches($url,"^/terms"))
       then local:construct-new($url,"/terms","terms")
    else
       if (fn:matches($url,"^/$"))
       then local:construct-new($url,"/","intro")
    else
       if (fn:matches($url,"^/add"))
       then local:construct-new($url,"/add","add")
    else $url
return $new-url

There it is near the end: /add will now be mapped to /main.xqy?view=add. Of course, that corresponds to a view that hasn’t yet been built, so it’s still not that helpful. But we’re making progress.

A quick note: this is not the only way to add a new page, but I believe it is the least intrusive way. Among other ideas, we could have a URL point directly to a new module in the custom directory and bypass rewriting. That makes sense for AJAX endpoints, but for pages that involve the user interface, we want to work with App Builder’s view mechanism for consistency.

4. Add a navigation bar

App Builder-based applications have a number of controls for search navigation, but there isn’t a built-in navigation bar. That’s okay, we’ll add our own. Take a look at /application/custom/appfunctions.xqy. This file contains a bunch of functions that are duplicates of functions in /application/lib/standard.xqy. You’ll notice these functions are commented out for now. We’ll change that as we need to change functions. For now, find the app:page() function. You’ll see that this function returns the <html>, <head> and <body> elements that we expect in a web page.

Inside the <body> element, you’ll see three calls to xdmp:apply(), one each for $config:logo, $config:user, and $config:canvas. The xdmp:apply() calls are due to a bit of indirection that keeps your code safe when you tell App Builder to redeploy after making some changes. We’ll see more of this later. The thing to know right now is that xdmp:apply($config:logo) will call app:logo() if it is defined, otherwise it will call asc:logo(). You may have noticed that app:logo() is indeed written up in appfunctions.xqy, but it is commented out. As such, asc:logo() will be called instead (it’s defined in /application/lib/standard.xqy).

If you look at app:page() and you look at the application in a browser, it’s not hard to see what each of those three calls is doing. $config:logo refers to the big title at the top of the page. $config:user corresponds to the welcome message (I see “Welcome, admin”, since I’m using the admin account.) And $config:canvas handles everything else. Since our current goal is to add a navigation bar, let’s put it between the logo and the welcome. The first thing to do is to move the close-comment marker from just after the app:page() function definition to just before the “declare function app:page()” line. (It’s easy to forget this step. If you do, you won’t see the result of your changes.) Now that this function is active, we’ll make a change to add a simple navigation bar. Here’s how mine looks when done:

(: -------------------------------------------:)
(: Primary functions, uncomment and modify to override :)
(: the default versions in /lib/standard.xqy :)

(:~
 : Main entry point, constructs page.
:)
declare function app:page()
as element(html)
{
    <html xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" lang="en">
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
      <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>
      <?import namespace="v" implementation="#default#VML" ?>
      <title>{$config:SLOTS/slots:page-title/string()}</title>
      {xdmp:apply($config:css)}
      {xdmp:apply($config:js)}
    </head>
        <body>
            {xdmp:apply($config:logo)}
            {app:navmenu()}
            {xdmp:apply($config:user)}
            {xdmp:apply($config:canvas)}
        </body>
    </html>
};

(:~
 : Display links for site navigation
 :)
declare function app:navmenu()
    as element(ul)
{
    <ul class="navmenu">
        <li><a href="/add">Add Pin</a></li>
    </ul>
};

As you can see, I’ve added an app:navmenu() function and inserted a call to it into app:page(). It has a link to where the new page will be: /add. Go ahead and refresh your browser to make sure you see it. Right now, if you click the link, you won’t get much interesting, since we haven’t defined the page yet.

This tutorial is focused on getting a new page to show up, not to make it look pretty, but we can throw in a little CSS. Paste the following into /application/custom/appcss.css:

.navmenu {
	margin: 0 auto 10px;
	padding-left: 0;
	width: 940px;
}

.navmenu li {
	list-style-type: none;
}

.navmenu a {
	color: #4B4B4B;
}

.navmenu a:hover {
	text-decoration: none;
}

The appcss.css is included after the auto-generated /application/css/master.css, so we can use it to format new elements or to override default formatting.

5. Define the new page

So what do we need to actually make something interesting out of this new page? We need to make things happen when the add view is selected. In our case, we actually need to things: we need a form, and we need something to process the form.

If you start at the app:page() function in appfunctions.xqy, you can trace your way down through app:canvas() and app:content() to app:get-content(). This last function is where things get interesting from a view perspective. The first line of get-content() figures out which view the user has requested. get-content() then enters a sequence of if-then-else branches, based on the view, that determine what will be displayed for that view.

Activate the function by moving the close-comment marker from the end of the function to before its “declare” line, closing the descriptive comment. Now we can add to the sequence and handle the “add” view.

(:~
 : Create content based on current view.
:)
declare function app:get-content()
{
    let $view := $config:CONTEXT/*:view
    return
        if ($view eq "search")
        then (
            xdmp:apply($config:toolbar),
            if (data($config:RESPONSE/@total) eq 0)
            then xdmp:apply($config:error-message, concat("Your search for ",$config:CONTEXT/*:q," did not match anything. Make sure all words are spelled correctly or try different keywords."))
            else (
                xdmp:apply($config:result-navigation),
                xdmp:apply($config:results),
                xdmp:apply($config:result-navigation)))
        else if ($view eq "detail")
        then (
            xdmp:apply($config:toolbar),
            xdmp:apply($config:item-render)  )
        else if ($view eq "terms")
        then xdmp:apply($config:terms)
        else if ($view eq "help")
        then xdmp:apply($config:help)
        else if ($view eq "contact")
        then xdmp:apply($config:contact)
        else if ($view eq "intro") then
            <div id="map" style="width:100%; height:300px"/>
        else if ($view eq "add") then
            if (fn:exists(xdmp:get-request-field("name"))) then
                app:add-pin()
            else
                app:add-form()
        else xdmp:apply($config:browse)
};

declare function app:add-form()
{
    <form class="add-pin-form">
        <h2>Add a pin</h2>
        <div><label>Name</label><input type="text" name="name" value=""/></div>
        <div><label>Location</label>
            <div>
                <label>Label</label>
                <input type="text" name="loc-label" value=""/>
            </div>
            <div>
                <label>Latitude</label>
                <input id="lat-box" type="text" name="lat" value=""/>
            </div>
            <div>
                <label>Longitude</label>
                <input id="long-box" type="text" name="long" value=""/>
            </div>
        </div>
        <div>
            <label>Description</label>
            <input type="text" name="desc" value=""/>
        </div>
        <div>
            <label>Date Acquired</label>
            <input type="text" name="date" value=""/>
        </div>
        <div>
            <label>Notes</label>
            <textarea name="notes"></textarea>
        </div>
        <input type="submit"/>
    </form>
};

declare function app:add-pin()
{
    let $uri := pin:add-pin(
        xdmp:get-request-field("name"),
        xdmp:get-request-field("loc-label"),
        xdmp:get-request-field("lat"),
        xdmp:get-request-field("long"),
        xdmp:get-request-field("desc"),
        xdmp:get-request-field("date"),
        xdmp:get-request-field("notes"))
    return xdmp:redirect-response(fn:concat("/detail", $uri))
};

In get-content() when looking at the add view, the code checks whether the “name” parameter is available. If not, display the form; if so, the form has been submitted, so a new pin needs to be added.

In my local version of this application, I have a “lookup” button as part of the form. This lets the user type in a location, like an address or a place name, and have the coordinates looked up. This feature is handy if you add a map to the application. I’m skipping that here to focus on simply adding the page.

We also have the app:add-pin() function, which adds the pin based on the form’s fields, then redirects the user to the new pin’s detail page. The pin:add-pin() function isn’t defined in appfunctions.xqy — it’s very useful to separate presentation code from the database interaction code. Because this tutorial is about the presentation layer, download the pin model module, create a new /application/custom/modules/ directory, save the module in that directory as pin-model.xqy, and add this import statement to your appfunctions.xqy file (you’ll see other imports near the top):

import module namespace pin="http://davidcassel.net/pins/pin-model"
    at "/custom/modules/pin-model.xqy";

As a final touch, I added a little CSS to /application/custom/appcss.css to make the form look like a form:

.add-pin-form label {
	display: inline-block;
	zoom: 1;
	*display: inline-block;
	width: 100px;
}

.add-pin-form div div, {
	margin-left: 20px;
}

.add-pin-form div div label {
	width: 80px;
}

.add-pin-form input[type=text], .add-pin-form textarea {
	width: 400px;
}

Wrap up

I’ve added a number of extensions to my own version of this application, but what’s here is boiled down to the essentials. Now you know how to extend your Application Builder-based app by creating new views.

Tags: , , ,

2 Responses to “Adding a view to an App Builder application”

  1. Rajesh Says:

    This functionality is completely changed in marklogic 7. Cant find the files mentioned above. Are these still doable in Marklogic 7.

  2. Dave Cassel Says:

    Rajesh, you’re right, the way App Builder works changed from MarkLogic version 5 to version 6, with the introduction of the REST API.

    To see how to make new views in MarkLogic 6+, take a look at /application/custom/rewrite.xml, along with /application/custom/help.html, contact.html, and terms.html.

Leave a Reply