2. Sample web application integrating an XML editor Previous topic Parent topic Child topic Next topic

XXE_INSTALL_DIR/web/doc/manual/apidemo/ contains newsapp.html Opens in new window, newsapp.js Opens in new window, newapp.css Opens in new window, a sample web application we'll use in this chapter to explain how to integrate <xxe-client> (defined by JavaScript class XMLEditor Opens in new window) into any other web application.
The NewsApp web application mimics a Content Management System (CMS Opens in new window) containing a number of news articles about XMLmind Software products. A news article is a short HTML file. Some news articles have an image attachment. NewsApp lets you browse or edit news articles and also “save”(1) the changes you made to an article.

Figure 6-1. newsapp.html opened in Google chrome; article "DITA Converter v3.12" opened in <xxe-client>

newsapp.png
In order to mimics a CMS, NewsApp loads https://www.xmlmind.com/news/xmlmind.xml Opens in new window, an RSS Opens in new window file containing news items about XMLmind Software products. Each news item simulates a different, standalone HTML document contained in the CMS.

Figure 6-2. Excerpts from https://www.xmlmind.com/news/xmlmind.xml

<rss version="2.0">
  <channel>
    <title>XMLmind News</title>
    <link>http://www.xmlmind.com/</link>
    ...
    <item>
      <title>Open Source XMLmind DITA Converter v3.12</title>
      <link>http://www.xmlmind.com/ditac/download.shtml</link>
      <description><![CDATA[Updated several
        software components. Official support of Java&trade;&nbsp;19.
        &ldquo;Plus distribution&rdquo; now bundled with <a
        href="https://xmlgraphics.apache.org/fop/2.8/" target="_blank">Apache
        FOP 2.8</a>.<br />More info <a
href="http://www.xmlmind.com/ditac/changes.html#v3.12.0">here</a>.]]></description>
      <pubDate>Mon, 05 Dec 2022 18:00:00 +0100</pubDate>
      <guid isPermaLink="true">http://www.xmlmind.com/ditac/changes.html#v3.12.0</guid>
    </item>
    ...
  </channel>
</rss>

Running NewsApp

As explained in Section 1. Overview, xxeserver normally runs side by side with MyBackend on a server computer. Therefore the most “realistic” method for running NewsApp is:
  1. Copy XXE_INSTALL_DIR/web/doc/manual/apidemo/newsapp.html, newsapp.js, news.css and also the whole XXE_INSTALL_DIR/web/webapp/xxeclient/ to a directory published by your HTTP server.
    For example, on a Linux box having Apache httpd publishing the contents of $HOME/public_html/ directory as http://localhost/~USER/, copy all these files to $HOME/public_html/tmp/.
  2. Start XXE_INSTALL_DIR/web/bin/xxeserver.
    For example, on a Linux box:
    .../web/bin$ xxeserverenter_key.png
  3. Open newsapp.html in a web browser.
    For example, on a Linux box, open http://localhost/~USER/tmp/newsapp.html.
Alternatively, if you don't have an HTTP server available for testing NewsApp, remember that xxeserver is not only a WebSocket server but also an HTTP server.
  1. Copy XXE_INSTALL_DIR/web/doc/manual/apidemo/newsapp.html, newsapp.js, news.css to XXE_INSTALL_DIR/web/webapp/.
  2. Start XXE_INSTALL_DIR/web/bin/xxeserver.
  3. Open http://localhost:18078/newsapp.html in a web browser.

NewsApp initialization

An HTML page containing <xxe-client> must include xxeclient/xxeclient.css and xxeclient/xxeclient.js as follows:
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    ...
    <link href="xxeclient/xxeclient.css" rel="stylesheet" type="text/css" />
    <script type="module" src="./xxeclient/xxeclient.js"></script>
    ...
  </head>
  <body>
    ...
    <xxe-client></xxe-client>
    ...
  </body>
</html>
apidemo/newsapp.js, being a JavaScript module Opens in new window itself, imports everything it needs from JavaScript module xxeclient/xxeclient.js. Therefore apidemo/newsapp.html does not need to directly include xxeclient/xxeclient.js.
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  ...
  <link href="xxeclient/xxeclient.css" rel="stylesheet" type="text/css" />
  <link href="newsapp.css" rel="stylesheet" type="text/css" />
  
  <script type="module">//<![CDATA[

import { NewsApp } from "./newsapp.js";

window.onload = (event) => {
    new NewsApp();
}

//]]></script>
  </head>
  <body>
    ...
    <table id="paneLayout">
      <tr>
        <td rowspan="4">
          <select id="itemList" size="6">
            <option value="">Please choose a news item.</option>
          </select>
        </td>
        <td><button type="button" id="viewButton">View</button></td>
      </tr>
      <tr><td><button type="button" id="editButton">Edit</button></td></tr>
      <tr><td><button type="button" id="saveButton">Save</button></td></tr>
      <tr><td><button type="button" id="closeButton">Close</button></td></tr>
    </table>

    <xxe-client id="xmlEditor"
      serverurl="${protocol}://${hostname}:${defaultPort}/xxe/ws"></xxe-client>

  </body>
</html>
JavaScript class NewsApp, part of JavaScript module apidemo/newsapp.js, does all its initializations in its constructor.
import * as XUI from './xxeclient/xui.js';
import * as XXE from './xxeclient/xxeclient.js';

...

export class NewsApp {
    constructor() {
        this._itemList = document.getElementById("itemList");
        this._itemList.disabled = true;
        this._itemList.onchange = this.itemSelected.bind(this);
        
        this._viewButton = document.getElementById("viewButton");
        this._viewButton.disabled = true;
        this._viewButton.onclick = this.viewItem.bind(this);
        
        ...INITIALIZE 3 MORE BUTTONS...

        this._xmlEditor = document.getElementById("xmlEditor");
        this._xmlEditor.addEventListener("saveStateChanged",
                                         this.itemSaved.bind(this));
        
        this._xmlEditor.autoRecover = false;
        window.addEventListener("beforeunload", (event) => {
            if (this._xmlEditor.saveNeeded) {
                event.preventDefault();
                return (event.returnValue = true);
            }
        });
        
        this._items = [];
        this.loadNews(NewsStorage.baseURI + "xmlmind.xml");
    }

    async loadNews(rssURL) {...}
    ...
    itemSaved(event) {
        this.enableButtons();
    }
}
After obtaining a “handle” to <xxe-client> (defined by JavaScript class XMLEditor Opens in new window) using document.getElementById, NewsApp configures this instance of XMLEditor by invoking method addEventListener Opens in new window and by setting property autoRecover Opens in new window to false.
Remember
Remember
The default value of property autoRecover Opens in new window is true. This means, that by default, the full state of <xxe-client> is automatically recovered when the user goes away from the page containing <xxe-client>, either intentionally (e.g. the user clicks the "Reload current page" button of the browser) or by mistake (e.g. the user closes the web browser tab without saving the changes made to the document).
Having this automatic recovery feature enabled is very reassuring for the user but implies that your web application as whole either have a similar automatic recovery feature or is stateless. The sample XML Editor application, <xxe-app>, included in the XXEW distribution is stateless and works fine with xmlEditor.autoRecover=true.
NewApp is also stateless and would work fine with xmlEditor.autoRecover=true. However in this apidemo/newsapp.html demo, we have chosen to set autoRecover to false to explain what to do in the general case. The answer is the "beforeunload" event listener found in the above excerpts of apidemo/newsapp.js.

Opening a news article

Opening the news article selected in the list is done by invoking XMLEditor method openDocument Opens in new window. The optional readOnly parameter, which is false by default, may be used to open an XML document in read-only mode.
Of course before doing that, you must make sure that the user does not unintentionally loose changes made to the news article. This verification/confirmation step is implemented using XMLEditor properties documentIsOpened Opens in new window and saveNeeded Opens in new window.
async openItem(readOnly) {
    let sel = this._itemList.selectedIndex;
    if (sel < 0) {
        return;
    }
    const selItem = this._items[sel];

    let confirmed = await NewsApp.confirmDiscardChanges(this._xmlEditor);
    if (!confirmed) {
        return;
    }

    let closed = await NewsApp.closeDocument(this._xmlEditor);
    if (!closed) {
        return;
    }

    let opened = await this._xmlEditor.openDocument(selItem.htmlSource,
                                                    selItem.uri, readOnly);
    if (!opened) {
        return;
    }

    this.enableButtons();
}

static confirmDiscardChanges(xmlEditor) {
    if (!xmlEditor.documentIsOpened || !xmlEditor.saveNeeded) {
        // No changes.
        return Promise.resolve(true);
    }

    return XUI.Confirm.showConfirm(
        `"${xmlEditor.documentURI}" has been modified\nDiscard changes?`);
}
Important
Important
As you can see it in the above and following excerpts of apidemo/newsapp.js, almost all the methods of XMLEditor are asynchronous and return a Promise. This is why async and await are used in these excerpts.

Saving a news article after modifying it

A modified news article is not really saved. Clicking the Save button just let the user preview the modified news article in a new browser tab. This action is implemented using XMLEditor methods getDocument Opens in new window and saveDocument Opens in new window.
async saveItem(event) {
    if (!this._xmlEditor.documentIsOpened || !this._xmlEditor.saveNeeded) {
        return;
    }

    let savedItem = this.findItem(this._xmlEditor.documentURI);
    if (savedItem === null) {
        // Should not happen.
        return;
    }

    const htmlSource = await this._xmlEditor.getDocument();
    if (htmlSource === null) {
        return;
    }
    savedItem.htmlSource = htmlSource;

    let saved = await this._xmlEditor.saveDocument();
    if (!saved) {
        return;
    }
    // No need to enableButtons, there is itemSaved.

    let newWin = window.open("", "_blank");
    newWin.document.write(htmlSource);
    newWin.document.close();
}

findItem(docURI) {
    for (let item of this._items) {
        if (item.uri === docURI) {
            return item;
        }
    }
    return null;
}

Closing the news article being viewed or edited

Closing the news article being viewed or edited is done by invoking XMLEditor method closeDocument Opens in new window. Unless its optional discardChanges parameter, false by default, is set to true, closeDocument will not close a document having unsaved changes.
static closeDocument(xmlEditor) {
    if (!xmlEditor.documentIsOpened) {
        return Promise.resolve(true);
    }

    return xmlEditor.closeDocument(/*discardChanges*/ true);
}

...

async closeItem(event) {
    let confirmed = await NewsApp.confirmDiscardChanges(this._xmlEditor);
    if (!confirmed) {
        return;
    }

    let closed = await NewsApp.closeDocument(this._xmlEditor);
    if (!closed) {
        return;
    }

    this._itemList.selectedIndex = -1;
    this.enableButtons();
}

 (1) Previewing the modified news article in a new browser tab is used to simulate saving the document.