Last updated Apr 2, 2024

Writing a Blueprint - Intermediate

Introduction

If you completed Confluence Blueprints you should have a functional Blueprint plugin. Functional, but basic. A step up from a basic Blueprint is one that populates its template with data, and there are two ways to do this:

  1. In Java, with a <context-provider> module
  2. In JavaScript, with a UI Wizard

These two methods are not exclusive, so you can populate some template variables with Java and others from JavaScript.

1. Creating a dynamic template

Templates for Confluence Blueprint plugins are written in Confluence Storage Format, and may include variables. For example, the following template has the variable myName:

1
2
<p>Hello, reader.</p>
<p>My name is: <at:var at:name="myName" /></p>

The variable "myName" will need to be in the template's render context in order for the <at:var at:name="myName"/> placeholder to be replaced correctly. Below you'll see how to do this.

As of Confluence 5.1, support for <at:var> elements inside of other <at:> namespace elements is incomplete. For example, adding an <at:var> inside a macro parameter will render correctly for the end-user creating a page, but an admin attempting to edit the template will "lose" the variable in the round-trip to the editor and back.

Fixing this is a priority for the next Confluence release but in the meantime, the recommended workaround is to make the entire macro element an <at:var> in the template. To correctly insert XHTML from a variable into the template, you'll need to define the variable with an extra "rawxhtml" attribute, like so:

1
2
<at:var at:name="myNameWithSomeXhtmlInIt" at:rawxhtml="true"/>

If the rawxhtml attribute was not present, all XHTML in the attribute value would be escaped when rendering.

2. Filling a dynamic template via a context provider

context-provider elements have been used in Atlassian plugins for web-items and web-panels for some time, and are well-documented - see _Context-Provider Element. In the Blueprint plugin, the context-provider will go inside the content-template module that it is providing context to, like so:

1
2
<content-template key="myplugin-template" i18n-name-key="myplugin.templates.content.name">
  <resource name="template" type="download" location="xml/template-body.xml"/>
  <context-provider class="myplugin.MyContextProvider"/>
</content-template> 

MyContextProvider will need to implement com.atlassian.plugin.web.ContextProvider. A very simple provider that would fill the example template would be:

1
2
public class MyContextProvider implements ContextProvider
{
    public void init(Map<String, String> params) throws PluginParseException
    {
    }

    public Map<String, Object> getContextMap(Map<String, Object> context)
    {
        context.put("myName", "Sherlock");
        return context;
    }
}

Your ContextProvider can be injected with any components available to the Confluence plugin system, so you can fill your templates with almost anything. For now, if you reload your plugin with the above changes and create a new page from your Blueprint, it will include the text "My name is: Sherlock".

3. Filling a dynamic template from a JavaScript 'wizard'

The same "myName" example template above could be filled with a name supplied by the user, via a JavaScript-driven wizard. To help reduce boilerplate in your plugin and to provide users with a consistent experience, the Blueprints API has a modular approach to defining and writing wizards.

If you don't already have the "Hello Blueprint" example plugin (see previous tutorial) now might be a good time to get it. It has a detailed example of a Create dialog Wizard configuration.

Let's look at a minimal Wizard:

1
2
<blueprint key="myplugin-blueprint"
           index-key="myplugin-index">
  ...
  <dialog-wizard key="myplugin-wizard">
    <dialog-page id="page1Id"
                 template-key="MyPlugin.Blueprints.Hello.page1Form"
                 title-key="myplugin.wizard.page1.title"/>
  </dialog-wizard>
</blueprint>

The dialog-wizard element can wrap any number of dialog-page elements, but for now we'll just add one. The key attribute is required for the Confluence plugin system but is not visible to the user.

dialog-page element has three required attributes:

  1. id - a unique camel-case id (in this dialog-wizard) for use with JavaScript hooks.
  2. template-key - this is a fully-qualified Soy template reference for the content that will be displayed on the Wizard page
  3. title-key - this is the i18n key for the title of the Wizard page

Information on writing a Soy template can be found at Writing Soy Templates in Your Plugin, but you can skip the steps for creating and building the plugin, plus the step for calling the Soy template from JavaScript. The template will be picked up automatically by Confluence.

Adding Required Web Resources

In your atlassian-plugin.xml, add the following web resource:

1
2
<web-resource name="Resources" key="myplugin-resources">
  <transformation extension="soy">
    <transformer key="soyTransformer">
      <functions>com.atlassian.confluence.plugins.soy:soy-core-functions</functions>
    </transformer>
  </transformation>

  <resource type="download" name="wizard-templates-soy.js" location="soy/wizard-templates.soy" />
  <resource type="download" name="blueprint-wizard.js" location="js/blueprint-wizard.js" />

  <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency>
  <context>atl.general</context>
  <context>atl.admin</context>
</web-resource>

The atl.general and atl.admin contexts need to be specified so that the JavaScript resources are made available wherever the Create dialog is shown. The blueprint-wizard.js file is required to register the wizard. The contents look like this:

1
2
Confluence.Blueprint.setWizard('com.atlassian.confluence.plugins.myplugin:blueprint-item', function(wizard) {});

The contents of the Soy file:

1
2
{namespace MyPlugin.Blueprints.Hello}

/**
 * A form that accepts a person's name
 */
{template .page1Form}
    <form action="#" method="post" class="aui">
        <fieldset>
            <div class="field-group">
                <label for="myname">{getText('myplugin.blueprint.form.label.myname')}</label>
                <input id="myname" class="text" type="text" name="myName">
            </div>
        </fieldset>
    </form>
{/template}

You will need to add an i18n value for myplugin.blueprint.form.label.myname to your i18n.properties file.

To give users a consistent user experience, you may find it helpful to have a look at the Atlassian Design Guidelines when designing your templates.

You can now build and install the plugin in your Confluence instance and you should see your rendered Soy template when you select the Blueprint in the dialog. (If you're following all of the tutorial steps in order you'll want to comment out the <context-provider> element in your <content-template> so that the "myName" variable comes from the Wizard input and not from the "MyContextProvider" class). Entering a name in the text field and pressing Create should take you to the Editor with the entered name injected into the template.

Adding a Description to your Wizard page

Although the Wizard form from the previous step is quite simple, a complex or multi-page Wizard will benefit from some explanatory text in the Wizard page. Confluence has a plugin point for this, to make the Wizard consistent across different Blueprints.

1
2
<blueprint key="myplugin-blueprint"
           index-key="myplugin-index">
  ...
  <dialog-wizard key="myplugin-wizard">
    <dialog-page id="page1Id"
                 template-key="MyPlugin.Blueprints.Hello.page1Form"
                 title-key="myplugin.wizard.page1.title"
                 description-header-key="myplugin.wizard.page1.desc.title"
                 description-content-key="myplugin.wizard.page1.desc.content"/>
  </dialog-wizard>
</blueprint>

Adding values to your i18n.properties file for myplugin.wizard.page1.desc.title and myplugin.wizard.page1.desc.content will result in the text appearing on the right-hand side of your Wizard page.

Adding JavaScript to your Wizard page

You might want to add validation to the form in your Wizard. To do this, you can use the submit hook provided by the API. Update the setWizard call in blueprint-wizard.js to add the hook:

1
2
Confluence.Blueprint.setWizard('com.atlassian.confluence.plugins.myplugin:blueprint-item', function(wizard) {
    wizard.on('submit.page1Id', function(e, state) {
        var myName = state.pageData.myName;
        if (myName == 'abc') {
            alert('That is not a real name!');
            return false;
        }
    });
});

As you can see from the above code, values from the Wizard form will be present as properties of the state.pageData object. In addition to the submit hook, there are 2 other hooks, explained in this table:

Name of hookTimingUsage
pre-renderCalled before the Soy template is rendered.This can be used to add data to the Soy rendering context.
post-renderCalled after the Soy template is rendered.This can be used to add JavaScript behaviour to elements rendered by the template. e.g. autocomplete.
submitCalled when the dialog page is submitted.

This can be used for a number of things:

  • Validating the wizard form
  • Setting the next wizard page
  • Override the default submission behaviour (e.g. to go to a custom page)

The callback function for each hook gets passed the jQuery event object and a state object containing several properties. The properties are listed below along with the hooks for which they apply.

PropertyDescriptionpre-renderpost-rendersubmit
wizardDataContains all data gathered by the Wizard pages. This is contained in the pages property of wizardData. The pages property is a map where the key is the id of the dialog page (e.g. page1Id) and the value is the pageData collected from that page.(tick)(tick)(tick)
$containerThe jQuery object wrapping the rendered Soy template for this page.(error)(tick)(tick)
pageDataFilled with the values from the form on this page, and can have further data added to it.(error)(error)(tick)
nextPageId

Blank when the submit callback is called, this value can be set to change which Wizard page should be shown next. If blank, the next page defined in <dialog-wizard> will be shown.

(error)(error)(tick)
finalUrl

Blank when the function is called, if set on the last page of the Wizard, this value is where the Wizard will go on completion. If blank, the Wizard will take the user to the Editor page or View page based on the create-result specified in your blueprint config. See "Skipping the Editor" for details.

(error)(error)(tick)
soyRenderContextThe context Object for adding values to, for use in the Soy render. The context is empty by default.(tick)(error)(error)

4. Skipping the editor

Some templates might get enough data from the Wizard that they can skip the Editor and create the page directly. To do this, update your blueprint module to include a create-result attribute:

1
2
<blueprint key="myplugin-blueprint"
    ...
    create-result="view"/> 

A result of "view" will create the page immediately and take the user to the view of that page. The default result is "edit" to go to the Editor, and can be omitted.

Note that if you choose to skip the editor you must specify a title for the new page; this can be done in two main ways:

  1. Passed from the JavaScript Wizard, or
  2. Passed in from the ContextProvider

These two methods generally correspond to user-specified versus generated page titles.

a. User-specified Page Titles

If the page title is be passed in through the JavaScript Wizard, there are two ways to do it. The easiest is to have a form field with the name "title" somewhere in your Wizard. This form value will automatically be passed to the server as the page title when the Wizard is submitted. The alternative is to add the "title" property to the wizardData state object manually somewhere in your JavaScript Wizard hooks.

An example of a title field in the Wizard is the File List Blueprint that is bundled with Confluence.

b. Generated Page Titles

If the title of the page should be generated from back-end logic (e.g. from a call to a remote site, or by adding a prefix/suffix to the Blueprint name), you can use your Context Provider (outlined in a previous section) to add a property with the key "ContentPageTitle" to the context map. For example,

1
2
public Map<String, Object> getContextMap(Map<String, Object> context)
{
    context.put("myName", "Sherlock");
    context.put("ContentPageTitle", yourPageTitleProvider.makeTitle());
    return context;
}

An example of the ContentPageTitle being set by a ContextProvider is the Meeting Notes Blueprint that is bundled with Confluence.  

5. Adding a "Let's get started" page

With or without a Wizard, when the user chooses your Blueprint you might want to show them a page explaining what the Blueprint does and how it can be used. To add a "Let's get started" page, you just add two things: a Soy template and a reference to it in your <blueprint> element.

1
2
<blueprint key="myplugin-blueprint"
           index-key="myplugin-index"
           how-to-use-template="MyPlugin.Blueprints.Hello.letsGetStarted">
  ...
</blueprint>

Assuming that you already have a Soy resource from the "Adding Required Web Resources" section above, you just need to add your new content to wizard-templates.soy:

1
2
/**
 * Let's-get-started page for the Create dialog.
 */
{template .letsGetStarted}
    <h2>{getText('myplugin.blueprint.letsgetstarted.title')}</h2>
    <p>{getText('myplugin.blueprint.letsgetstarted.content')}</p>
{/template}

When you build and install your plugin and choose your Blueprint, you will now see the contents of the template in a new Create dialog page. Note the "Don't show this again" checkbox that users can click to opt out of seeing the page in future.

6. Multiple Templates

A plugin may have multiple Blueprints and a Blueprint may have multiple templates. For example a reports Blueprint could create different kind of reports based on user decisions in the wizard. To do this, we add a second <content-template> to our Blueprint in atlassian-plugin.xml:

1
2
<blueprint key="myplugin-blueprint"
           index-key="myplugin-index">
   <content-template ref="myplugin-template"/>
   <content-template ref="myplugin-template-2"/>
</blueprint>
...
<content-template key="myplugin-template-2" i18n-name-key="myplugin.templates.content2.name">
  <resource name="template" type="download" location="xml/template2-body.xml"/>
</content-template>

By default, the Blueprint page will be created from the first template, but you can set a special property called contentTemplateKey in the wizardData, which is the key of the template that should be rendered. The property can be set directly on the wizardData via a JavaScript hook (see "Adding JavaScript to your wizard page") or implicitly by having a form field with the name contentTemplateKey. This latter method works because pageData automatically loads form field values, and on Wizard submission wizardData gets all values from pageData. Below is an example of a template-switching form-field, based on the Wizard form we worked on earlier:

1
2
{template .page1Form}
    <form action="#" method="post" class="aui">
        <fieldset>
            <div class="field-group">
                <label for="myname">{getText('myplugin.blueprint.form.label.myname')}</label>
                <input id="myname" class="text" type="text" name="myName">
            </div>
            <div class="field-group">
                <label for="template-key">{getText('myplugin.blueprint.form.label.templatekey')}</label>
                <select id="template-key" class="select" name="contentTemplateKey">
                    <option value="myplugin-template">{getText('myplugin.template.name')}</option>
                    <option value="myplugin-template-2">{getText('myplugin.template2.name')}</option>
                </select>
            </div>
        </fieldset>
    </form>
{/template}

When you build and install your plugin and choose your Blueprint, you'll now be able to choose the second template and see it rendered in the Editor.

7. Custom JavaScript Wizards / Callbacks

While we've tried to make the API as flexible as possible, there may be certain behaviours that our API doesn't allow for. In that case, you might need more control of the create experience. This can be done by registering a direct callback that is called when the user clicks the create button after selecting your Blueprint.

If you followed the tutorial so far, you can add the following to your blueprint-wizard.js:

1
2
Confluence.Blueprint.setDirectCallback('com.atlassian.confluence.plugins.myplugin:blueprint-item', function(e, state) {
    state.finalUrl = Confluence.getContextPath() + "/pages/createpage.action?spaceKey=" + encodeURIComponent(state.spaceKey);
});

When you build and install your plugin and choose your Blueprint, you'll now be taken to the URL specified above, which is the Editor with a blank page. There are a few interesting things to note here:

  • The direct callback is passed a state object similar to the state object mentioned in the "Adding JavaScript to your Wizard page" section above. Two important properties for direct callbacks are:
    • state.finalUrl should be used instead of calling window.location or window.open directly. In future versions of Confluence this will open the URL in the correct fashion (new or existing window) based on user preferences. For now this just sets the window.location after the callback completes.
    • state.spaceKey contains the space key of the space the user selected in the Create dialog.
  • If you added a "Let's get started" page you will still see it before being redirected to the URL specified by the direct callback.
  • All other wizard pages are ignored, even if you have JavaScript calls to setWizard and <dialog-wizard> defined in your Blueprint.

Once you send the user to a different browser location, you'll need to wire up the required struts actions in your plugin XML, add a custom Action, and so on. This advanced behaviour is common with the Confluence plugins so you'll want to check Struts module.

To provide users with a consistent experience and simplify the development experience for you, we recommend using the Blueprint API as much as possible. If you need to use setDirectCallback because of a deficiency in the API, please let us know! You can use the "Feedback" link on the right-hand-side of the screen to let us know what you need from the Confluence Blueprints API.

In the next tutorial, you'll learn about listening for the Blueprint created event, creating a custom Blueprint action, and about some more API components. Read on, in Writing a Blueprint - Advanced.

Rate this page: