This is part of a tutorial series on creating extension components for Design Studio.
In our last instalment, we built a padding visualizer in raw HTML, using D3. Now we'll bring it into the design environment. The Additional Properties Sheet (APS) runs in a separate browser instance, inside the Design Studio (Eclipse) designer environment. It connects to the server using a very similar mechanism to what user's browser and design time canvas use. The Design Studio SDK provides JavaScript infrastructure that connects to the server behind the scenes. It is called the Property Sheet Handler and handles the communication and property synchronization between the APS and server.
To use the Property Sheet Handler, you need to do three things:
When you have your PropertyPage subclass instance, your can use firePropertiesChanged() to push updated property values to the server and when property values are changed in the server (via the main property sheet) and tie your getter/setter functions into the SDK infrastructure.
In your contribution .xml file, you should have a reference to the additional properties sheet html and JavaScript files. Unless you have modified it, the reference should read as:
propertySheetPath="res/additional_properties_sheet/additional_properties_sheet.html"
We'll begin by building the html file and then construct the JavaScript file. Recall from Part 1,that the we already have the html and JavaScript files The html file is empty and the Javascript file currently looks like this:
sap.designstudio.sdk.PropertyPage.subclass("com.sap.sample.scngauge.SCNGaugePropertyPage", function() {
}
Constructing additional_properties_sheet.html
This is a fairly straightforward file. We are going to want an html5 form, where we can edit the four padding values and display the height and width. Our visualizer diagram will sit below the form and be updated whenever the padding values are changed in the properties sheet, or whenever the form is submitted.
All we are going to do in the head element is state the title and import a few scripts. The scripts are:
The head element should now look like this:
<head>
<title>Gauge Padding Visualizer</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="/aad/zen.rt.components.sdk/resources/js/sdk_propertysheets_handler.js"></script>
<script src="additional_properties_sheet.js"></script>
</head>
In between the head and body elements, we'll slip a script in. This will be the script that instantiates our PropertyPage subclass component, so that the APS can hook into the property sheet handler. Since our (still empty) class is called "com.sap.sample.scngauge.SCNGaugePropertyPage", we'll instantiate that.
<script>
new com.sap.sample.scngauge.SCNGaugePropertyPage();
</script>
Next, we build up the form. HTML5 forms essentially enhances table. If you are not familiar with html tables or forms, there are online tutorials for both tables and forms.
<form id="form">
<fieldset>
<legend>Gauge Padding Visualizer</legend>
<table>
<tr>
<td>Padding Top</td>
<td>
<input id="aps_padding_top" type="number" name="paddingTop" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Bottom</td>
<td>
<input id="aps_padding_bottom" type="number" name="paddingBottom" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Left</td>
<td>
<input id="aps_padding_left" type="number" name="paddingLeft" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Right</td>
<td>
<input id="aps_padding_right" type="number" name="paddingRight" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Width</td>
<td>
<input id="aps_width" type="number" name="widthProxy" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Height</td>
<td>
<input id="aps_height" type="number" name="heightProxy" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Outer Radius</td>
<td>
<p id="aps_radius"></p>
</td>
</tr>
<tr>
<td>
<input name="submit" type="submit" value="Refresh"/>
</td>
</tr>
</table>
</fieldset>
</form>
The last members of the body element are two divs. The div with id #content will be the one where we insert the visualizer SVG that we defined last time, in Part 4b. #componentproxy is something that we"ll get to know better later on, when we directly interact with the canvas.
<div id='content'></div>
<div id='componentproxy'></div>
The completed HTML file
<html>
<head>
<title>Gauge Padding Visualizert</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="/aad/zen.rt.components.sdk/resources/js/sdk_propertysheets_handler.js"></script>
<script src="additional_properties_sheet.js"></script>
</head>
<script>
new com.sap.sample.scngauge.SCNGaugePropertyPage();
</script>
<body>
<form id="form">
<fieldset>
<legend>Gauge Padding Visualizer</legend>
<table>
<tr>
<td>Padding Top</td>
<td>
<input id="aps_padding_top" type="number" name="paddingTop" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Bottom</td>
<td>
<input id="aps_padding_bottom" type="number" name="paddingBottom" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Left</td>
<td>
<input id="aps_padding_left" type="number" name="paddingLeft" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Padding Right</td>
<td>
<input id="aps_padding_right" type="number" name="paddingRight" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Width</td>
<td>
<input id="aps_width" type="number" name="widthProxy" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Height</td>
<td>
<input id="aps_height" type="number" name="heightProxy" size="40" maxlength="40"></input>
</td>
</tr>
<tr>
<td>Outer Radius</td>
<td>
<p id="aps_radius"></p>
</td>
</tr>
<tr>
<td>
<input name="submit" type="submit" value="Refresh"/>
</td>
</tr>
</table>
</fieldset>
</form>
<div id='content'></div>
<div id='componentproxy'></div>
</body>
</html>
Constructing additional_properties_sheet.js
Now we are going to refactor and migrate our visualise Javascript code from last time to the com.sap.sample.scngauge.SCNGaugePropertyPage class in additional_properties_sheet.js. Most of the JavaScript code from the raw html document can be migrate with only minimal refactoring. As we did in Part 2b, when we moved the gauge from plain html to the canvas, we'll be doing a few major things in our refactoring:
So let's build up the com.sap.sample.scngauge.SCNGaugePropertyPage class.
Variable Definitions
As usual, we declare the "self proxy"
var me = this;
Next, come the other class wide variables. The line thickness and height/width proxies get a me. Prifix, as do the four padding properties.
//Viz definitiions
me.lineThickness = 2;
//Height and Width Proxies
me.widthProxy = 200;
me.heightProxy = 200;
//Outer Dimensions & Positioning
me._paddingTop = 0;
me._paddingBottom = 0;
me._paddingLeft = 0;
me._paddingRight = 0;
Getter/Setters
The getter/setter functions are fairly straightforward and follow the same pattern as in the canvas, with the getter returning the value of me.<property> and the setter setting it and then calling me.redraw().
me.paddingTop = function(value) {
if (value === undefined) {
return me._paddingTop
}
else {
me._paddingTop = value;
me.redraw();
return me;
}
};
me.paddingBottom = function(value) {
if (value === undefined) {
return me._paddingBottom
}
else {
me._paddingBottom = value;
me.redraw();
return me;
}
};
me.paddingLeft = function(value) {
if (value === undefined) {
return me._paddingLeft
}
else {
me._paddingLeft = value;
me.redraw();
return me;
}
};
me.paddingRight = function(value) {
if (value === undefined) {
return me._paddingRight
}
else {
me._paddingRight = value;
me.redraw();
return me;
}
};
Before we contineue - A note on JQuery selection
Note that unlike in the canvas, where we select the root of the component, we're have complete access to the APS document object model (DOM). When we are working in the canvas, we would make an empty Jquery selection, which is the root node of the container and then select the first child. In the APS, we don't have this single div restriction and are free to use Jquery selection in a more conventional and freeform manner. Therefore, well jQuery element selection by ID approach.
E.g. if we want to select the form input element with the ID aps_padding_top and use it's value to fill me._paddingTop, then, we'd use:
me._paddingTop = $("#aps_padding_top").val();
Remember method chaining. :wink:
Full documentation of Jquery's selectors is available in the Jquery documentation.
me.init()
We're going to do two things in our init() function:
me.init = function() {
$("#form").submit(function() {
me._paddingTop = $("#aps_padding_top").val();
me._paddingBottom = $("#aps_padding_bottom").val();
me._paddingLeft = $("#aps_padding_left").val();
me._paddingRight = $("#aps_padding_right").val();
me._widthProxy = $("#aps_width").val();
me._heightProxy = $("#aps_height").val();
me.firePropertiesChanged(["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"]);
me.redraw();
return false;
});
me.redraw();
};
me.redraw()
The redraw function works similarly to its canvas based cousin. It deletes any existing SVG elements from the #content div, inserts a new, empty SVG element and draws the component - in this case the visualizer - anew. We are going to do one thing differently. We're also going to write the height, width and padding values into the HTML form element. Redraw is called whenever a setter is triggered, so it makes sense to update the form as well.
With method chaining, we can select each input element and fill it in a single statement.
$("#aps_padding_top").val(me._paddingTop);
$("#aps_padding_bottom").val(me._paddingBottom);
$("#aps_padding_left").val(me._paddingLeft);
$("#aps_padding_right").val(me._paddingRight);
$("#aps_width").val(me._widthProxy);
$("#aps_height").val(me._heightProxy);
The rest of the redraw function is the remainder of the main script from the html file, with the variable names refactored to match the new convention.
The full additional_properties_sheet.js
sap.designstudio.sdk.PropertyPage.subclass("com.sap.sample.scngauge.SCNGaugePropertyPage", function() {
var me = this;
//Viz definitiions
me.lineThickness = 2;
//Outer Dimensions & Positioning
me._paddingTop = 0;
me._paddingBottom = 0;
me._paddingLeft = 0;
me._paddingRight = 0;
//Height and Width Proxies
me._widthProxy = 200;
me._heightProxy = 200;
me.init = function() {
$("#form").submit(function() {
me._paddingTop = $("#aps_padding_top").val();
me._paddingBottom = $("#aps_padding_bottom").val();
me._paddingLeft = $("#aps_padding_left").val();
me._paddingRight = $("#aps_padding_right").val();
me._widthProxy = $("#aps_width").val();
me._heightProxy = $("#aps_height").val();
me.firePropertiesChanged(["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"]);
me.redraw();
return false;
});
me.redraw();
};
me.paddingTop = function(value) {
if (value === undefined) {
return me._paddingTop
}
else {
me._paddingTop = value;
me.redraw();
return me;
}
};
me.paddingBottom = function(value) {
if (value === undefined) {
return me._paddingBottom
}
else {
me._paddingBottom = value;
me.redraw();
return me;
}
};
me.paddingLeft = function(value) {
if (value === undefined) {
return me._paddingLeft
}
else {
me._paddingLeft = value;
me.redraw();
return me;
}
};
me.paddingRight = function(value) {
if (value === undefined) {
return me._paddingRight
}
else {
me._paddingRight = value;
me.redraw();
return me;
}
};
me.redraw = function() {
$("#aps_padding_top").val(me._paddingTop);
$("#aps_padding_bottom").val(me._paddingBottom);
$("#aps_padding_left").val(me._paddingLeft);
$("#aps_padding_right").val(me._paddingRight);
$("#aps_width").val(me._widthProxy);
$("#aps_height").val(me._heightProxy);
// Clear any existing content. We'll redraw from scratch
d3.select("#content").selectAll("*").remove();
var vis = d3.select("#content").append("svg:svg").attr("width", "100%").attr("height", "100%");
var pi = Math.PI;
//Line Accessor Function
var lineAccessor = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
///////////////////////////////////////////
//Gauge Dummy
///////////////////////////////////////////
//Determing the position of the gauge dummy (black circle)
// Find the larger left/right padding
var lrPadding = me._paddingLeft + me._paddingRight;
var tbPadding = me._paddingTop + me._paddingBottom;
var maxPadding = lrPadding;
if (maxPadding < tbPadding){
maxPadding = tbPadding
}
//Do the same with the overall height and width
var smallerAxis = me._heightProxy;
if (me._widthProxy < smallerAxis){
smallerAxis = me._widthProxy
}
var outerRad = (smallerAxis - 2*(maxPadding))/2;
$("#aps_radius").text(outerRad);
//The offset will determine where the center of the arc shall be
var offsetLeft = outerRad + me._paddingLeft;
var offsetDown = outerRad + me._paddingTop;
//The black Circle
var arcDef = d3.svg.arc()
.innerRadius(0)
.outerRadius(outerRad)
.startAngle(-180 * (pi/180)) //converting from degs to radians
.endAngle(180 * (pi/180)); //converting from degs to radians
var guageDummy = vis.append("path")
.style("fill", "black")
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so arc is visible
.attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")")
.attr("d", arcDef);
///////////////////////////////////////////
//Line Data
///////////////////////////////////////////
var lineDataOuter = [{"x":0, "y":0}, {"x": me._widthProxy, "y":0}, {"x": me._widthProxy, "y":me._heightProxy}, {"x":0, "y":me._heightProxy}, {"x":0, "y":0}];
var lineDataPaddingLeft = [{"x":0, "y":0}, {"x":me._paddingLeft, "y":0}, {"x":me._paddingLeft, "y":me._heightProxy}, {"x":0, "y":me._heightProxy}, {"x":0, "y":0}];
var lineDataPaddingRight = [{"x":( me._widthProxy - me._paddingRight), "y":0}, {"x": me._widthProxy, "y":0}, {"x": me._widthProxy, "y":me._heightProxy}, {"x":( me._widthProxy - me._paddingRight), "y":me._heightProxy}, {"x":( me._widthProxy - me._paddingRight), "y":0}];
var lineDataPaddingUpper= [{"x":0, "y":0}, {"x": me._widthProxy, "y":0}, {"x": me._widthProxy, "y":me._paddingTop}, {"x":0, "y":me._paddingTop}, {"x":0, "y":0}];
var lineDataPaddingLower = [{"x":0, "y":(me._heightProxy - me._paddingBottom)}, {"x": me._widthProxy, "y":(me._heightProxy - me._paddingBottom)}, {"x": me._widthProxy, "y":me._heightProxy}, {"x":0, "y":me._heightProxy}, {"x":0, "y":(me._heightProxy - me._paddingBottom)}];
var lineDataCrosshairsHorizontal = [{"x":me._paddingLeft, "y":(me._paddingTop + outerRad) }, {"x":(me._paddingLeft + 2*outerRad), "y":(me._paddingTop + outerRad) }];
var lineDataCrosshairsVertical = [{"x":(me._paddingLeft + outerRad), "y":me._paddingTop }, {"x":(me._paddingLeft + outerRad), "y":(me._paddingTop + 2*outerRad) }];
var borderLinesPaddingLeft = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataPaddingLeft))
.attr("stroke", "blue")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesPaddingRight = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataPaddingRight))
.attr("stroke", "blue")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesPaddingUpper = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataPaddingUpper))
.attr("stroke", "blue")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesPaddingLower = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataPaddingLower))
.attr("stroke", "blue")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesOuter = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataOuter))
.attr("stroke", "black")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesCrosshairHorizontal = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataCrosshairsHorizontal))
.attr("stroke", "white")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
var borderLinesCrosshairVertical = vis
.attr("width", me._widthProxy).attr("height", me._heightProxy) // Added height and width so line is visible
.append("path")
.attr("d", lineAccessor(lineDataCrosshairsVertical))
.attr("stroke", "white")
.attr("stroke-width", me.lineThickness)
.attr("fill", "none");
}
});
Now when we debug the component and put a gauge into the canvas and select it, we have access to a padding visualizer in the APS.
Height and Width of the component can't be directly determined within the APS. Therefore, we're still manually asking the designer to maintain these values manually in the visualizer property values form. This is a severe usability problem and we have to fix it. It is possible to determine these values from within the canvas. Forthermore, it is possible to call canvas (component.js) javascript functions from the APS. Next time, we'll create a way for the APS to ask the canvas for the height and width, so that these values are always automatically synchronized.
As always, the completed extension (as of part 4) is available as a Github repository.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
10 | |
10 | |
10 | |
9 | |
8 | |
8 | |
6 | |
6 | |
5 | |
5 |