How to Connect HTML Web Resources and Forms in Dynamics 365 Model-Driven Apps Using JavaScript

Bincy RoyBincy Roy
8 min read

Table of contents

In Dynamics 365, customization often goes beyond the built-in features. Recently, I faced a challenge: how to visually display dynamic percentages for various skills on a Dynamics 365 form. This blog post explores the HTML and JavaScript work that led to the creation of dynamic percentage circles, offering 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 display dynamic percentage circles for different skills. The design includes responsive elements to ensure 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) {
                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 () {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(function () {
                    clearTimeout(resizeTimer);
                    start = new Date().getTime();
                    update();
                }, 250);
            });
        });

        function easeInOutQuart(t, b, c, d) {
            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) {
        return Math.random() * (max - min) + min;
    }
});

</script>

Integration with Dynamics 365

To seamlessly integrate our solution with Dynamics 365, we use a web resource. The setClientApiContext function connects to the Dynamics 365 form, allowing us to retrieve and update skill values in real-time. This integration improves the user interface and provides a smooth experience.

// This script sets the client API context for Dynamics 365 Model-Driven Apps.

// It retrieves attribute values from the form context and updates the 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();
}
  1. 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. In the form designer, add an HTML web resource component.

a. Select the Diagram.html file.

  1. Identify HTML Web Resource Name & Update Form_onload.js

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

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

  1. 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 easily 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

Imagine a user needs to evaluate and display their skills on a Dynamics 365 platform. The dynamic percentage circles provide a visually appealing way to show technical skills, problem-solving abilities, communication skills, analytical thinking, coding proficiency, and domain knowledge. This enhances the user experience and offers a clear and intuitive representation of skills.

0
Subscribe to my newsletter

Read articles from Bincy Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bincy Roy
Bincy Roy

I specialize in customizing and configuring Dynamics 365 modules like Sales, Service, and Customer Insights. My work involves creating effective solutions with Power Automate and developing dynamic web resources using HTML, CSS, and JavaScript. This blog is where I share insights and practical tips on software development and business automation.