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
Product and Topic Expert
Product and Topic Expert
1,056

This is part of a tutorial series on creating extension components for Design Studio.

In the last instalment, we noted that the APS can't directly determine the component dimensions, height and width.  Therefore, we added a requirement that the user maintain those values.  This is problematic , from a usability perspective.  The user may end up with different values in the APS than in the canvas.  In this installment, we'll fix that; at least partially.

You may have noticed something in Part 3d, when we wired up our gauge for properties.  The height and width of the component are editable properties in the property sheet.  In fact, they are always there and are inherited from the SDK.  You might also have noticed that:

  • Unlike other properties, we did not mirror them in the canvas.

  • We had no getters and setters for them.

  • We used an odd syntax for determining them; something along the lines of

var myWidth = me.$().width();




Height and width are not like conventional, developer defined properties.  They are not synchronized via the normal SDK mechanisms and are handled via deeper Design Studio plumbing, but they are available directly from the component's container.  Hence the JQuery call fetch their values.  Since the APS is not inheriting these "hidden" properties, we can't simply make the same call in the APS.  Since they don't participate in the standard getter/setter based property synchronization, we can simply create getters and setters for them.  We can ask the canvas, however. 

Introducing  callRuntimeHandler()

One of the functions that you inheret, when you subclass sap.designstudio.sdk.PropertyPage is callRuntimeHandler().  With it, your APS JavaScript class can make calls directly into the canvas JavaScript class.  It is quite simple actually.  The canvas based function may return a value, or it may be void.  So you may use it to trigger an action in the canvas; from the APS.  Today, we're going to use it to extract some information.

We are going to do three things:

  • We'll add two "getter" methods for height and width to the canvas class in component.js; one for each of the two properties.

  • We'll use callRuntimeHandler() to call these two getter methods.  We'll do this in the redraw() function.

  • As we no longer need the Height and Width fields of the form input enabled, we'll change them from input to p elements.

In component.js from last time, we'll insert the two getter functions.


//Getters for the height and width of the component


me.getWidth = function(){


  return me.$().width();


};



me.getHeight = function(){


  return me.$().height();


};




In additional_properties_sheet.js, at the very start of the me.redraw() function, we'll insert the callRuntimeHandler() calls to get the height and width values and store them in our local copies.


//Update the height and width by getting them from the canvas


me._widthProxy = me.callRuntimeHandler("getWidth");


me._heightProxy = me.callRuntimeHandler("getHeight");




The width and height rows in additional_properties_sheet.html


<tr>


  <td>Width</td>


  <td>


  <p id="aps_width"></p>


  </td>


</tr>


<tr>


  <td>Height</td>


  <td>


  <p id="aps_height"></p>


  </td>


</tr>




That's it!  Now, whenever we refresh the padding visualizer, we'll be using the current value of the height and width from the properties sheet.  There is one caveat and we still have a minor usability problem, with no workaround.  If the user updated ONLY the height or width, then no property changed event is triggered and no refresh happens (because there is no place for us to trigger me.redraw()).  It is also not  possible to message out from the canvas, so we can't trigger anything in the APS from there.  This means that if the user modified these two settings and does nothing afterwards in the properties pane, she'll have to manually click on the refresh button in the APS.  If she edits a padding property afterwards, however, the visualizer will automatically be refreshed.

Reference

We'll show the full code of the three files that we changed in this installment.

Our component.js now looks like this:


sap.designstudio.sdk.Component.subclass("com.sap.sample.scngauge.SCNGauge", function() {




  var me = this;


  //Properties


  me._colorCode = 'blue';


  me._innerRad = 0.0;


  me._outerRad = 0.0;


  me._endAngleDeg = 90.0;


  me._startAngleDeg = -90.0;


  me._paddingTop = 0;


  me._paddingBottom = 0;


  me._paddingLeft = 0;


  me._paddingRight = 0;


  me._offsetLeft = 0;


  me._offsetDown = 0;



  //Validate the Inner and Outer Radii


  me.validateRadii = function(inner, outer) {


  if (inner <= outer) {


  return true;


  } else {


  return false;


  }


  };




  //Recalculate Outer Radius.  Also, double check that the new value fits with me._innerRad


  me.recalculateOuterRadius = function(paddingLeft, paddingRight, paddingTop, paddingBottom){


  // Find the larger left/right padding


  var lrPadding = paddingLeft + paddingRight;


  var tbPadding = paddingTop + paddingBottom;


  var maxPadding = lrPadding;


  if (maxPadding < tbPadding){


  maxPadding = tbPadding


  }


  var newOuterRad = (me.$().width() - 2*(maxPadding))/2;


  var isValid = me.validateRadii(me._innerRad, newOuterRad);


  if (isValid === true){


  me._outerRad = newOuterRad;


  return true;


  }


  else {


  return false;


  }


  }



  //Getters and Setters


  me.colorCode = function(value) {


  if (value === undefined) {


  return me._colorCode;


  } else {


  me._colorCode = value;


  me.redraw();


  return me;


  }


  };



  me.innerRad = function(value) {


  if (value === undefined) {


  return me._innerRad;


  } else {



  var isValid = me.validateRadii(value, me._outerRad);


  if (isValid === false){


  alert("Warning!  The gauge arc can't have a small inner radius than outer!  Inner Radius must be equal to or less than " + me._outerRad);


  alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  } else {


  me._innerRad = value;


  me.redraw();


  }


  return this;


  }


  };



  me.endAngleDeg = function(value) {


  if (value === undefined) {


  return me._endAngleDeg;


  } else {


  me._endAngleDeg = value;


  me.redraw();


  return this;


  }


  };




  me.startAngleDeg = function(value) {


  if (value === undefined) {


  return me._startAngleDeg;


  } else {


  me._startAngleDeg = value;


  me.redraw();


  return this;


  }


  };



  me.paddingTop = function(value) {


  if (value === undefined) {


  return me._paddingTop;


  } else {


  var isValid =me.recalculateOuterRadius(me._paddingLeft, me._paddingRight, value, me._paddingBottom);


  if (isValid === false){


  alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);


  alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  } else {


  me._paddingTop = value;


  me.redraw();


  }


  return this;


  }


  };



  me.paddingBottom = function(value) {


  if (value === undefined) {


  return me._paddingBottom;


  } else {


  var isValid = me.recalculateOuterRadius(me._paddingLeft, me._paddingRight, me._paddingTop, value);


  if (isValid === false){


  alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);


  alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  } else {


  me._paddingBottom = value;


  me.redraw();


  }


  return this;


  }


  };



  me.paddingLeft = function(value) {


  if (value === undefined) {


  paddingLeft = me._paddingLeft;


  return paddingLeft;


  } else {


  var isValid = me.recalculateOuterRadius(value, me._paddingRight, me._paddingTop, me._paddingBottom);


  if (isValid === false){


  alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);


  alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  } else {


  me._paddingLeft = value;


  me.redraw();


  }


  return this;




  }


  };



  me.paddingRight = function(value) {


  if (value === undefined) {


  paddingRight = me._paddingRight;


  } else {


  var isValid = me.recalculateOuterRadius(me._paddingLeft, value, me._paddingTop, me._paddingBottom);


  if (isValid === false){


  alert("Warning!  The gauge arc can't have a small inner radius than outer!  Outer Radius must be equal to or greater than " + me._innerRad);


  alert("Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  } else {


  me._paddingRight = value;


  me.redraw();


  }


  return this;


  }


  };







  me.redraw = function() {




  var myDiv = me.$()[0];



  // Clear any existing gauges.  We'll redraw from scratch


  d3.select(myDiv).selectAll("*").remove();



  var vis = d3.select(myDiv).append("svg:svg").attr("width", "100%").attr("height", "100%");


  var pi = Math.PI;



  // 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


  }



  me._outerRad = (me.$().width() - 2*(maxPadding))/2;



  //Don't let the innerRad be greater than outer rad


  if (me._outerRad <= me._innerRad){


  alert("Warning!  The gauge arc can't have a negative radius!  Please decrease the inner radius, or increase the size of the control.  Height & width (including subtraction for padding) must me at least twice as large as Internal Radius!");


  }



  //The offset will determine where the center of the arc shall be


  me._offsetLeft = me._outerRad + me._paddingLeft;


  me._offsetDown = me._outerRad + me._paddingTop;



  var arcDef = d3.svg.arc()


  .innerRadius(me._innerRad)


  .outerRadius(me._outerRad)


  .startAngle(me._startAngleDeg * (pi/180)) //converting from degs to radians


  .endAngle(me._endAngleDeg * (pi/180)); //converting from degs to radians




  var guageArc = vis.append("path")


     .style("fill", me._colorCode)


     .attr("width", me.$().width()).attr("height", me.$().height()) // Added height and width so arc is visible


     .attr("transform", "translate(" + me._offsetLeft + "," + me._offsetDown + ")")


     .attr("d", arcDef);


  };




  me.init = function() {


  me.redraw();


  };




  //Getters for the height and width of the component


  me.getWidth = function(){


  return me.$().width();


  };



  me.getHeight = function(){


  return me.$().height();


  };






});



Our additional_properties_sheet.js now looks like this:


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 = parseInt($("#aps_padding_top").val());


  me._paddingBottom = parseInt($("#aps_padding_bottom").val());


  me._paddingLeft = parseInt($("#aps_padding_left").val());


  me._paddingRight = parseInt($("#aps_padding_right").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() {


  //Update the height and width by getting them from the canvas


  me._widthProxy = me.callRuntimeHandler("getWidth");


  me._heightProxy = me.callRuntimeHandler("getHeight");



  $("#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").text(me._widthProxy);


  $("#aps_height").text(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");


  }


});



Our additional_properties_sheet.html now looks like this:


<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>


  <p id="aps_width"></p>


  </td>


  </tr>


  <tr>


  <td>Height</td>


  <td>


  <p id="aps_height"></p>


  </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>



As always, the completed extension (as of part 4) is available as a Github repository.

Next time, we'll add a scripting API to our component.