@atlassian #atlascamp
Modernizing Your
Plugin UI
Jonathon Creenaune, Front End Architect, Atlassian
Build more features
Build more features
Make features better
Make features better
Beauty
Beauty
Interactivity
Beauty
AUI
<header class="aui-page-header">
  <div class="aui-page-header-inner">
    my header text
  </div>
</header>
Side-by-side
<header class="aui-page-header">
  <div class="aui-page-header-inner">
    my header text
  </div>
</header>
Side-by-side
<header class="aui-page-header">

{call aui.page.pageHeader}

  <div class="aui-page-header-inner">

  {param content}

    my header text

    my header text

  </div>

  {/param}

</header>

{/call}
AUI 5.0 is in
JIRA 6.0
Confluence 5.0
Bamboo 4.3
Stash 2.0
FE/CRU 3.0
Targeting non-soy
platforms
“ not just a side-effect of the technology
you used to create a page
”
URLs should be designed for the user,
/secure/MemeAction.jspa
/plugins/servlet/memes
/memes

/secure/MemeAction.jspa

/memes/{x}

/secure/MemeAction.jspa?key={x}

/memes/create/

=>

/secure/MemeAction.jspa?upload=

/memes/create/{x}

/secure/MemeAction.jspa?createBaseImageKey={x}

/memes/create

/secure/MemeAction.jspa?create=
Pretty URLs
<routing key="meme-pretty-urls" path="/memes">
<get from="" to="/secure/MemeAction.jspa"/>
<get from="/create/" to="/secure/MemeAction.jspa?upload="/>
<get from="/create/{key}" to="/secure/MemeAction.jspa?
createBaseImageKey={key}"/>
<get from="/create" to="/secure/MemeAction.jspa?create="/>
<get from="/{key}" to="/secure/MemeAction.jspa?key={key}"/>
</routing>
Pretty URLs
P2: Yes
Old products: Yes
Connect: Yes
Interactivity
Getting code closer to the user
Pushstate
history.pushState(stateObject, title, url);
// Using history.js
History.pushState(null, null, url);
Pushstate
gallery.onSelectImage(function(key) {
// Add it to the URL
History.pushState(null, null, AJS.contextPath() + “/memes” +
key);
// Load the hero image
hero.load($container, key);
});
Pushstate - back / forward
History.Adapter.bind(window, 'statechange', function() {
var match;
if (match = getUrl().match(/^$/)) { // gallery
gallery.load($container);
}
else if (match = getUrl().match(/^/(.*)$/)) { // hero
hero.load($container, match[1]);
}
});
Pushstate
P2: Yes
Old products: Yes
Connect: Yes
Soy renders on server
and on client
Render on server
<webwork1 key="meme-webwork" class="java.lang.Object">
<actions>
<action name="com.atlassian.meme.action.MemeAction" ...>
<view name="hero" type="soy">
:server-templates/meme.page.hero
</view>
<view name="gallery" type="soy">
:server-templates/meme.page.gallery
</view>
...
Render on server
$(function() {
var $container = getContainer();
hero.initEvents($container);
});
Render on client
<web-resource key="hero">
<transformation extension="soy">
<transformer key="soyTransformer" />
</transformation>
<resource type="download" name="hero.js" location="hero.soy"/>
...
Render on client
function render($container, key) {
$.get(getMemeUrl(key)).done(function(memeData) {
$container.html(meme.hero.main({
meme: memeData
}));
initEvents($container);
...
Render on client + server
P2: Yes
Old products: If soy available
Connect: JVM server
Injecting Page Data
// Bad
<script>
var myData = “${getMyData}”;
</script>
// Why? Because it’s an XSS hole
<script>
var myData = “</script><script>alert(‘naughty’);””;
</script>
Injecting Page Data
// In action
public String getSelectData() {
return ImmutableMap.of(
“baseImages”, getAllBaseImages(),
“baseImagesJson”, marshalAsJson(getAllBaseImages())
);
}
Injecting Page Data
// In template
<div class="base-images-json"
data-base-images-json="{$baseImagesJson}">
</div>
Accessing Page Data
// From javascript
var myData = AJS.$(".base-images-json").
data("base-images-json");
Page Data
P2: Yes
Old products: Yes
Connect: Yes
Future ...
Injecting Page Data
<web-resource key="blah">
  <data name="my-data" provider="com.acme.MyDataProvider" />
  <resource type="download" name="my-code.js" />
</web-resource>
Injecting Page Data
// pageBuilderService is injectable
pageBuilderService.getData().set("my-data-key", myJsonable);
Accessing Page Data
var data = AJS.data.get("my-plugin:blah:my-data");
Gallery

Hero
Main

Select
Create

Read files
Form

Utils

Render Meme
Defining Modules
define(“create/canvasDrawer”, function() {
...
return {
drawImage: function() {},
drawText: function() {},
getAsBase64: function() {}
}
});
Requiring Modules
define(“create/main”, [“./formView”, “./canvasDrawer”],
function(formView, canvasDrawer) {
formView.onSubmit(function() {
canvasDrawer.drawImage(myImage);
save(canvasDrawer.getAsBase64);
});
});
almond.js
JS modules
P2: Yes
Old products: Yes
Connect: Yes
“

Amazon: For every 100ms increase in load time
of Amazon.com decreased sales by 1%
Google: From 10 results in 0.4 seconds to to 30
results in 0.9 seconds decreased traffic and ad
revenues by 20%
Google: An extra 500ms in loading time
resulted in 20% drop in traffic.
Yahoo: 400ms slower page would see 5-9% more
people leave before the page finished loading.

”
Gallery

Hero
Main

Select
Create

Read Files
Form

Utils

Render Meme
Gallery

Hero
Main

Select
Create

Read Files
Form

Utils

Render Meme
Async Resource Loading
<web-resource key="create">
<resource type="download" name="create/main.js"
location="create/main.js" />
<resource type="download" name="create/create.css"
location="create/create.css" />
<resource type="download" name="create/create.js"
location="create/create.soy" />
...
</web-resource>
Async Resource Loading
require("gallery/main").onSelectCreate(function() {
WRM.require([
"wr!com.atlassian.atlassian-meme-generator:create"
]).done(function() {
showCreateView();
});
});
Async Resource Loading
P2: Yes
Old products: Yes
Connect: Other libs
Beauty:
Soy
Pretty URLs
Interactivity:
Pushstate
Server and Client rendering
Data
JS modules
Async resource loading
Sample App:
http://bitbucket.org/jcreenaune/atlassian-memegenerator

AUI Debugger:
http://bit.ly/10DXYlG
Thank you!
Party Starts at 19.00!
SkyLounge, Oosterdoksstraat 4, 11th Floor

AtlasCamp 2013: Modernizing your Plugin UI