Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
david_stocker
Advisor
Advisor

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:

  • Include its JavaScript file among the APS HTML file's scripts.  The Property Sheet Handler JavaScript file (called sdk_propertysheets_handler.js) is part of the SDK.
  • Add your own JavaScript file.  This file follows the same pattern as the Component JavaScript file in that you extend an SDK class and put your custom code inside this extension.  The class that you'll be extending for the APS is called sap.designstudio.sdk.PropertyPage.
  • Add a script element to the APS html file that instantiates an instance of this class.

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:

  • A reference to a public copy of D3.  We're not using the SDK copy of D3 here, but instead we'll use the "official" D3 URL.
  • The Design Studio SDK framework's property sheet handler.  It is always at /aad/zen.rt.components.sdk/resources/js/sdk_propertysheets_handler.js.
  • Our own JavaScript file.  It is customary to put this file into res/additional_properties_sheet, alongside the html file.

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:

  • Just like with the canvas, any properties that need to be synchronized between the APS and server need to have getters and setters.
  • The variable definitions won't go into the me.redraw() method.  Instead, they'll move to the root of the class and take on the "me" namespace.  (e.g. _paddingTop becomes me._paddingTop)
  • The HTML form needs to be synchronized with the property values.  In practice, this means updating the property values whenever the form is submitted (the values in the forms, become the new property values) and updating the form values whenever the setter of a property is invoked.
  • The actual code governing the redraws (including the path definitions) will get packaged into a method, called me.redraw()
  • The SDK framework will be calling the class' init() function when it is instantiated, so we'll need to do some bootstrapping in there.

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:

  • We're going to trigger the redraw() function, just as in the canvas init() function (in component.js).

  • We'll want to attach a function to the form's submit event (HTML5 forms have a built in submit event) which will do the following:
    • Make sure that all editable variables are updated to whatever values the user has added to the form.
    • Fire firePropertiesChanged(), with the padding properties, so that the server side values can also be updated.
    • Trigger the redraw() event, so that the visualizer can be redrawn to match the new values.

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.