Connecting HTML Web Resource and Forms in Dynamics 365 Model-Driven Apps with JavaScript (Implementing Dynamic Percentage Circles)

 


In the realm of Dynamics 365, customization often extends beyond the built-in features. Recently, I encountered a challenge: how to visually represent dynamic percentages for various skills on a Dynamics 365 form. This blog post delves into the HTML and JavaScript work that led to the creation of dynamic percentage circles, providing an engaging and informative solution.

1) Create Web Resources in PowerApps

a. Form_onload.js

Create a new JavaScript web resource named "Form_onload.js" in PowerApps. This script will handle the form onload event and set the client API context.

function form_onload(executionContext) {
    debugger;
    var formContext = executionContext.getFormContext();
    var wrControl = formContext.getControl("WebResource_name");
// Replace "WebResource_name" with the actual name of your HTML web resource

    if (wrControl) {
        wrControl.getContentWindow().then(
            function (contentWindow) {
                contentWindow.setClientApiContext(Xrm, formContext);
            }
        )
    }
}

b. Diagram.html

 Create a new HTML web resource named "Diagram.html" in PowerApps. This file will contain the HTML code, CSS styling, and JavaScript logic for dynamic percentage circles.

Overview of the HTML

Our HTML document is structured to accommodate dynamic percentage circles for different skills. We use counters, input fields, and canvases to create an interactive and visually appealing representation of skill levels. The design incorporates responsive elements for an optimal user experience.

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Set the character set and viewport for responsive design -->
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- Set the title of the document -->
  <title>Combined Code</title>
</head>
<body>
  <!-- Main container with a class of 'wrapper' -->
  <div class="wrapper">
    <!-- Row containing both existing and new counters, with additional styling -->
    <div class="row pt-5 pb-5" id="countersRow">
      <!-- Existing counters -->
      <div class="col-6 col-sm-3">
        <!-- Counter for Technical Skills with a unique ID and color attribute -->
        <div class="counter" id="skillsCounter" data-cp-color="#53405e"></div>
        <!-- Heading for Technical Skills -->
        <h4>Technical Skills</h4>
        <!-- Input for entering a numerical value related to Technical Skills -->
        <input type="number" id="TechnicalSkills" class="percentage-input" placeholder="0-100"/>
      </div>
      <div class="col-6 col-sm-3">
        <!-- Counter for Problem Solving with a unique ID and color attribute -->
        <div class="counter" id="contentCounter" data-cp-color="#e0bd93"></div>
        <!-- Heading for Problem Solving -->
        <h4>Problem Solving</h4>
        <!-- Input for entering a numerical value related to Problem Solving -->
        <input type="number" id="ProblemSolving" class="percentage-input" placeholder="0-100"  />
      </div>
      <div class="col-6 col-sm-3">
        <!-- Counter for Communication Skills with a unique ID and color attribute -->
        <div class="counter" id="websitesCounter" data-cp-color="#FF675B"></div>
        <!-- Heading for Communication Skills -->
        <h4>Communication Skills</h4>
        <!-- Input for entering a numerical value related to Communication Skills -->
        <input type="number" id="CommunicationSkills" class="percentage-input" placeholder="0-100"/>
      </div>
      <div class="col-6 col-sm-3">
        <!-- Counter for Analytical Thinking with a unique ID and color attribute -->
        <div class="counter" id="employeesCounter" data-cp-color="#529f82"></div>
        <!-- Heading for Analytical Thinking -->
        <h4>Analytical Thinking</h4>
        <!-- Input for entering a numerical value related to Analytical Thinking -->
        <input type="number" id="AnalyticalThinking" class="percentage-input" placeholder="0-100" />
      </div>
      <!-- New counters -->
      <div class="col-6 col-sm-3">
        <!-- Counter for Coding Proficiency with a unique ID and color attribute -->
        <div class="counter" id="domainCounter" data-cp-color="#a23658"></div>
        <!-- Heading for Coding Proficiency -->
        <h4>Coding Proficiency</h4>
        <!-- Input for entering a numerical value related to Coding Proficiency -->
        <input type="number" id="CodingProficiency" class="percentage-input" placeholder="0-100" />
      </div>
      <div class="col-6 col-sm-3">
        <!-- Counter for Domain Knowledge with a unique ID and color attribute -->
        <div class="counter" id="codingCounter" data-cp-color="#1e656d"></div>
        <!-- Heading for Domain Knowledge -->
        <h4>Domain Knowledge</h4>
        <!-- Input for entering a numerical value related to Domain Knowledge -->
        <input type="number" id="DomainKnowledge" class="percentage-input" placeholder="0-100"/>
      </div>
    </div>
  </div>
</body>
</html>

CSS Styling

The CSS styling plays a crucial role in crafting the visual aesthetics of our dynamic percentage circles. The counters are designed with a circular shape, each representing a specific skill. Responsive design considerations ensure that the layout adapts to different screen sizes, offering a seamless experience.

<style>
    .counter {
      display: inline-flex;
      cursor: pointer;
      width: 100px; /* Adjusted width */
      height: 100px; /* Adjusted height */
      max-width: 100%;
      position: relative;
      justify-content: center;
      align-items: center;
      font-size: calc(1em + 1vmin);
      transition: height .2s ease-in-out;
      background: #fff;
      border-radius: 50%;
      box-shadow: 0px 1px 10px 2px rgba(0, 0, 0, 0.2);
      margin: 1em 0;
    }
    h4 {
        margin-top: 0px;
    }
    .col-6.col-sm-3 {
    margin: 46px;
}
    .percentage {
      position: absolute;
      text-align: center;
      top: 50%;
      left: 0;
      right: 0;
      vertical-align: middle;
      transform: translate3d(0, -50%, 0);
    }
    .wrapper {
        display: flex;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
    }
    div#countersRow {
    display: flex;
}
    input {
      width: 100px; /* Adjusted input width */
    }
    body {
      font-family: 'Open Sans', sans-serif;
      text-align: center;
    }
    #TechnicalSkills,
    #ProblemSolving,
    #CommunicationSkills,
    #AnalyticalThinking,
    #CodingProficiency,
    #DomainKnowledge{
    display: none;
}
</style>

JavaScript Logic

The heart of our solution lies in the JavaScript logic. The circleProgress function initializes and animates circular progress based on percentage values. Input fields dynamically update the circles, providing real-time feedback. The logic also includes a smooth animation function for an enhanced user experience.

<script>
    document.addEventListener("DOMContentLoaded", function () {

      var circleProgress = (function (selector) {
        var wrapper = document.querySelectorAll(selector);
        Array.prototype.forEach.call(wrapper, function (wrapper, i) {
          var wrapperWidth,
            wrapperHeight,
            percent,
            innerHTML,
            context,
            lineWidth,
            centerX,
            centerY,
            radius,
            newPercent,
            speed,
            from,
            to,
            duration,
            start,
            strokeStyle,
            text;

          var getValues = function () {
            wrapperWidth = parseInt(window.getComputedStyle(wrapper).width);
            wrapperHeight = wrapperWidth;
            percent = wrapper.getAttribute('data-cp-percentage');
            innerHTML = '<span class="percentage"><strong>' + percent + '</strong> %</span>
            <canvas class="circleProgressCanvas" width="' +
(wrapperWidth * 2) + '" height="' + wrapperHeight * 2 + '">
            </canvas>';
            wrapper.innerHTML = innerHTML;
            text = wrapper.querySelector(".percentage");
            canvas = wrapper.querySelector(".circleProgressCanvas");
            wrapper.style.height = canvas.style.width = canvas.style.height = wrapperWidth + "px";
            context = canvas.getContext('2d');
            centerX = canvas.width / 2;
            centerY = canvas.height / 2;
            newPercent = 0;
            speed = 1;
            from = 0;
            to = percent;
            duration = 1000;
            lineWidth = 15;
            radius = canvas.width / 2 - lineWidth;
            strokeStyle = wrapper.getAttribute('data-cp-color');
            start = new Date().getTime();
          };

          function animate() {
            requestAnimationFrame(animate);
            var time = new Date().getTime() - start;
            if (time <= duration) {
              var x = easeInOutQuart(time, from, to - from, duration);
              newPercent = x;
              text.innerHTML = Math.round(newPercent) + " %";
              drawArc();
            }
          }

          function drawArc() {
            var circleStart = 1.5 * Math.PI;
            var circleEnd = circleStart + (newPercent / 50) * Math.PI;
            context.clearRect(0, 0, canvas.width, canvas.height);
            context.beginPath();
            context.arc(centerX, centerY, radius, circleStart, 4 * Math.PI, false);
            context.lineWidth = lineWidth;
            context.strokeStyle = "#ddd";
            context.stroke();
            context.beginPath();
            context.arc(centerX, centerY, radius, circleStart, circleEnd, false);
            context.lineWidth = lineWidth;
            context.strokeStyle = strokeStyle;
            context.stroke();
          }

          var update = function () {
            getValues();
            animate();
          }
          update();

          // Update circular progress on input change
          var inputFields = document.querySelectorAll(".percentage-input");
          Array.prototype.forEach.call(inputFields, function (inputField) {
            debugger;
            inputField.addEventListener("input", function () {
              // Get values from input fields
              var TechnicalSkills = parseInt(document.getElementById("TechnicalSkills").value) || 0;
              var ProblemSolving = parseInt(document.getElementById("ProblemSolving").value) || 0;
              var CommunicationSkills = parseInt(document.getElementById("CommunicationSkills").value) || 0;
              var AnalyticalThinking = parseInt(document.getElementById("AnalyticalThinking").value) || 0;
              var CodingProficiency = parseInt(document.getElementById("CodingProficiency").value) || 0;
              var DomainKnowledge = parseInt(document.getElementById("DomainKnowledge").value) || 0;

              // Set data-cp-percentage attributes
              document.querySelector(".counter[data-cp-color='#53405e']").
setAttribute("data-cp-percentage", TechnicalSkills);
              document.querySelector(".counter[data-cp-color='#e0bd93']").
setAttribute("data-cp-percentage", ProblemSolving);
              document.querySelector(".counter[data-cp-color='#FF675B']").
setAttribute("data-cp-percentage", CommunicationSkills);
              document.querySelector(".counter[data-cp-color='#529f82']").
setAttribute("data-cp-percentage", AnalyticalThinking);
              document.querySelector(".counter[data-cp-color='#a23658']").
setAttribute("data-cp-percentage", CodingProficiency);
              document.querySelector(".counter[data-cp-color='#1e656d']").
setAttribute("data-cp-percentage", DomainKnowledge);

              // Update circular progress
              update();
            });
          });

          var resizeTimer;
          window.addEventListener("resize", function () {
            debugger;
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
              clearTimeout(resizeTimer);
              start = new Date().getTime();
              update();
            }, 250);
          });
        });

        function easeInOutQuart(t, b, c, d) {
            debugger;
          if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
          return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
        }

      });

      circleProgress('.counter');

      function getRandom(min, max) {
        debugger;
        return Math.random() * (max - min) + min;
      }
    });
  </script>

Integration with Dynamics 365

To seamlessly integrate our solution with Dynamics 365, we utilize a web resource. The setClientApiContext function establishes a connection with the Dynamics 365 form, allowing us to retrieve and update skill values dynamically. This integration enhances the user interface and provides a streamlined experience.

// This script sets the client API context for Dynamics 365 Model-Driven Apps.
// It retrieves attribute values from the form context and updates corresponding input fields.
// It triggers input events to dynamically update progress bars (percentage circles)
//and performs an overall update.

function setClientApiContext(xrm, formContext) {
    // Debugger statement for debugging purposes
    debugger;
 
    // Assign the provided Xrm and formContext to global variables for wider accessibility
    window.Xrm = xrm;
    window._formContext = formContext;
 
    // Retrieve attribute values from the form context
    var TechnicalSkills = formContext.getAttribute("baelynn_technicalskill").getValue();
    var ProblemSolving = formContext.getAttribute("baelynn_problemsolving").getValue();
    var CommunicationSkills = formContext.getAttribute("baelynn_communicationskills").getValue();
    var AnalyticalThinking = formContext.getAttribute("baelynn_analyticalthinking").getValue();
    var CodingProficiency = formContext.getAttribute("baelynn_codingproficiency").getValue();
    var DomainKnowledge = formContext.getAttribute("baelynn_domainknowledge").getValue();
 
    // Set values to the percentage-input fields
    document.getElementById("TechnicalSkills").value = TechnicalSkills;
    document.getElementById("ProblemSolving").value = ProblemSolving;
    document.getElementById("CommunicationSkills").value = CommunicationSkills;
    document.getElementById("AnalyticalThinking").value = AnalyticalThinking;
 
    // Trigger input events for newly added fields
    document.getElementById("CodingProficiency").value = CodingProficiency;
    document.getElementById("CodingProficiency").dispatchEvent(new Event("input", { bubbles: true }));
 
    document.getElementById("DomainKnowledge").value = DomainKnowledge;
    document.getElementById("DomainKnowledge").dispatchEvent(new Event("input", { bubbles: true }));
 
    // Perform an overall update
    update();
  }
 

2) Create OOB Form and Add HTML Web Resource

Create a new Out-of-the-Box (OOB) form in Dynamics 365 or use an existing one. Inside the form designer, add an HTML web resource component.

a. Select the Diagram.html file.

3) Identify HTML Web Resource Name & Update Form_onload.js 

Inside the OOB form, the HTML web resource will be assigned a name. Copy the name.

Replace "WebResource_name" in the form_onload script with the actual name of the HTML web resource.

4) Upload JavaScript File to Form Events

Upload the "Form_onload.js" file to the form events:

    OnLoad: Attach the "Form_onload.js" script to the form onload event.

    OnChange: Attach the "Form_onload.js" script to the onchange event of required fields.

    OnSave: Attach the "Form_onload.js" script to the form onsave event.


Usage and Customization

Users can effortlessly interact with the dynamic percentage circles on the Dynamics 365 form. Input fields allow for customization, enabling users to update skill percentages on the fly. The flexibility extends to customization options such as changing circle colors or adjusting dimensions to suit specific preferences.

Practical Example

Consider a scenario where a user needs to evaluate and showcase their skills on a Dynamics 365 platform. The dynamic percentage circles offer a visually engaging way to represent technical skills, problem-solving abilities, communication skills, analytical thinking, coding proficiency, and domain knowledge. This not only enhances the user experience but also provides a clear and intuitive representation of skills.





Courtesy : Christy Thomas


Comments

Popular posts from this blog

Creating a Custom Pop-Up Page in Model-Driven Apps Using JavaScript and Ribbon Workbench