Web Applications server-side public web pages in Adobe Campaign Classic, created via a drag and drop editor. They are built on top of the ACC JSSP framework and provide access to the workflow ctx variable.

How web apps work

An ACC web app is just a visual tool to generate a JSSP page:

The JSSP code is generated via the XSL template web-webApp.xsl which calls web-core.xsl, which contains:

import core.xsl
<xsl:call-template name="serverScriptInit"/>
response.addHeader(Pragma, Cache-Control, Expires, Content-type)
<xsl:call-template name="initActivities"/>
if !g_bNoRendering
  <html>
    <head>
      <meta http-equiv="MSThemeCompatible" content="Yes"/>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
      <xsl:call-template name="css"/>
      [...]

core.xsl contains XSL templates such as

<xsl:template name="serverScriptInit">
<xsl:template name="initActivities">
<xsl:template name="css">

Using the ctx variable in the Javascript frontend

Create a dead simple web app with a query on Recipients (named queryRecipients) and a page (without transition):

HTML code of the Page, using the Bootstrap 4 starter template:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Starter Template Β· Bootstrap</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css">
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"><a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" id="input155197075600849" aria-expanded="false" aria-controls="navbarsExampleDefault" aria-label="Toggle navigation" type="button" data-target="#navbarsExampleDefault" data-toggle="collapse"> <span class="navbar-toggler-icon"></span> </button>
    <div class="collapse navbar-collapse" id="navbarsExampleDefault">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item active"><a class="nav-link" href="#">Home</a></li>
        <li class="nav-item nav-link">Bootstrap 4 starter template</li>
      </ul>
    </div>
    </nav>
    <main class="container mt-5 pt-3"></main>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/js/bootstrap.min.js" type="text/javascript"></script>
  </body>
</html>

The ctx variable is an XML created automatically by Adobe Campaign and injected into the HTML. With _debug enabled, it is shown in a <pre> block:

It is available as a Javascript DOMElement in the frontend with document.controller.ctx:

See w3c reference for DOMElement

DO NOT EXPOSE ANY SENSITIVE DATA IN `CTX` (credentials, server-side data..)

Using the ctx variable in the Javascript backend

This ctx var can also be used in the backend Scripts and in the Page itself. Insert in <main></main> the following table:

<table class="table table-bordered table-hover table-striped table-sm">
  <thead>
    <tr><th>@firstName</th><th>@lastName</th><th>@email</th></tr>
  </thead>
  <tbody>
    <% for each(var recipient in ctx.queryRecipients.recipient){ %>
    <tr>
      <td><%= recipient.@firstName %></td>
      <td><%= recipient.@lastName %></td>
      <td><%= recipient.@email %></td>
    </tr>
    <% } %>
  </tbody>
</table>

Output:

Recap:

Using ctx for interaction between pages

Now that we have a list of recipients, let’s display the customer profile of one selected Recipient. First, create a Web app variable which will store the value of the selected recipient id:

To enable transitions and interactions between pages, the page must contain a <form id="page-form>">. Insert it in the <main:

<main>[...]</main>

becomes

<main><form method="post" name="page" id="page-form">[...]</form></main>

Add a transition named transition1, connected to a query where @id = $([vars/recipientId]) and another page:

In the first page, add a new <td> with a Button as follow:

<td>
  <a href="#" class="btn btn-sm btn-primary"
    onclick="
      document.controller.setValue('/ctx/vars/recipientId', '<%= recipient.@id %>'); // make sure to wrap the recipient-id in quotes, otherwise the ACC editor will throw a JS error)
      document.controller.submit('next', '_self', 'transition1');">
    Show
  </a>
</td>

This is made possible via the setValue(path, value) and getValue(path) functions of document.controller which is a UIController:

and submit(formAction, formTarget, transitionName):

/**
 * Inject an <input name="ctx"> in the <form id="page-form">,
 * Then submits the form
 *
 * @param strAction      can be any of none|refresh|next|previous
 * @param strTarget      can be any of the HTML attribute target _blank|_self|_parent...
 * @param strTransition  name of the transition to call in case of accAction=next
 * @param bForceNoFile   file are already uploaded, don't reupload it
 * @param bNoWait        don't display waiting box
 */
UIController.prototype.submit = function(strAction, strTarget=null, strTransition=null, bForceNoFile=null, bNoWait=null){};

Save, refresh, click on Show and you’ll get this:

For both pages, download the HTML codes on gist.github.com.

Update a recipient based on ctx.recipient.@id

var now = formatDate(new Date(), "%4Y-%2M-%2D %02H:%02N:%02S")

xtk.session.Write(
  <recipient xtkschema="nms:recipient" _operation="update" _key="@id" id={ctx.recipient.@id}
    lastModified={now}
    firstName="New first name"
  />
);

// equivalent to:
var recipient = NLWS.nmsRecipient.load(ctx.recipient.@id);
recipient.firstName = "New first name";
recipient.save();

Use images and enum

<%= NL.route('nms:task.png', 'reverse_img') %> // '/nms/img/task.png'

/**
 * Given a list of <enumValue>, return img using name
 *
 * @param enum XML, <node><enumValue img="nms:canceled.png" name="canceled"/><enumValue img="nms:task.png" name="todo"/></node>
 * @param enumName string, 'canceled'
 *
 * @example getEnumFieldWithName(<node><enumValue img="nms:canceled.png" name="canceled"/><enumValue img="nms:task.png" name="todo"/></node>, 'canceled', 'img') // nms:canceled.png
 */
function getEnumFieldWithName(enum, enumName, enumField){
  for each(var e in enum.enumValue){
    if(e.@['name'] == enumName){
      return e.@[enumField];
    }
  }
  return '';
}
<%= getEnumFieldWithName(ctx.queryEnumClientelingStatus, task.@status2, 'img') %> // nms:canceled.png

<img class="nlui-widget" src="<%= NL.route(getEnumFieldWithName(ctx.queryEnum, aVariableHere, 'img'), 'reverse_img') %>"/>

Use NL.QueryDef to execute client-side SOAP calls in Javascript from the browser

Can be used to create a Single Page App (SPA) for specific goals, such as a read-only view of recipients, with filters, orders, etc.

⚠️ the result is in XML format. For JSON format see next chapter.

var queryDef = new NL.QueryDef("nms:recipient", NL.QueryDef.prototype.OPERATION_SELECT);
queryDef.addSelectExpr("@id"); // add the column @id to the select clause
queryDef.addSelectExpr("@firstName", "@newAttr"); // set @firstName as an attr named "newAttr" in the result XML
queryDef.addSelectExpr("@lastName", "newNode"); // set @lastName as a node named "newNode" in the result XML
queryDef.setLineCount(2); // SQL LIMIT
queryDef.setStartLine(10); // SQL OFFSET
queryDef.setShowSQL(true); // create a <dataSQL>SELECT x,y from Z</dataSQL> node in <recipient-collection>
queryDef.addWhereConditionExpr("@email = '"+email+"'");
var callback = {
  onXtkQueryCompleted: function(queryDef, res, error) {
    console.log('Recipients found!', res); // see below for res output
  }
};
queryDef.execute(NL.session.serverURL + "/nl/jsp/soaprouter.jsp", '', callback);

Output, res content:

<recipient-collection>
  <recipient id="1" newAttr="Jane"><newNode>Doe</newNode></recipient>
  <recipient id="2" newAttr="John"><newNode>Doe</newNode></recipient>
  <dataSQL>
    SELECT   R0.iRecipientId, R0.sFirstName, R0.sLastName FROM NmsRecipient R0 WHERE (R0.sEmail = E'x@y.z') LIMIT 2 OFFSET 10
  </dataSQL>
</recipient-collection>

Above code taken from nl6/web/core/dce/contentEditor.js loadFromTemplateId: function(templateId).

Doc for queryDef.execute() @ nl6/web/code/queryDef.js:

/** Do the soap call
 * @strUrl : soap router url
 * @sessionToken
 * @asyncTarget : enable the asynchronious mode and define the objet to
 *                notify. That object must implement a onXtkQueryCompleted()
 *                method.
 */
NL.QueryDef.prototype.execute = function(strUrl, sessionToken, asyncTarget)

Use POST /xtk/queryList.jssp to get objects in Javascript from the browser

Calling \datakit\xtk\fra\jssp\queryList.jssp with a POST param queryDef containing a JSON as an urlencoded string:

var postData = {
   "operation":"select", "schema":"xtk:workflow",
   "startLine":0, "lineCount":30,
   "select":{
      "node":[
        {"expr":"[.]", "alias":"@cs"},
        {"expr":"@label", "alias":"@label"},
        {"expr":"[process/@startState]", "alias":"@startState", "enabledIf":"HasPackage('nms:campaign')"},
      ]
   },
   "where":{
      "condition":[{"expr":"@isModel=0"}]
   },
   "orderBy":{
      "node":[{"expr":"@lastModified", "sortDesc":true}]
   },
};
$.post("/xtk/queryList.jssp", {queryDef:encodeURIComponent(JSON.stringify(postData))}, function(response) {
  console.log(response); // {data: Array(30), needPagination: true}
  console.log(response.data); // [{cs: " ()", label: "", startState: "0", _schema: "xtk:workflow"}]
});