Understanding the Difference VisualForce vs Lightning Web Components DOM Behavior

Understanding the Difference: VisualForce vs Lightning Web Components DOM Behavior

When working with web development in Salesforce, developers often transition from Visualforce (VF) to Lightning Web Components (LWC). They encounter subtle differences. These differences affect how the DOM (Document Object Model) behaves.

These subtle differences are impactful. A common question arises. Why does a static html element like <div> in Visualforce update automatically when manipulated by a script? In LWC, a similar approach requires dynamically creating the <div> element. This difference is rooted in the underlying architecture and principles of the two frameworks.

Create the JavaScript Plugin (simplePlugin.js)

Lets create a js file named “simplePlugin.js” and upload it as a static resource in Salesforce, naming it simplePlugin.

(function () {
    window.simplePlugin = {
        load: function (containerId) {
			console.log('containerId+++',containerId);
            const container = document.getElementById(containerId);
			console.log('container+++',container);
            if (!container) {
                console.error(`Container with ID "${containerId}" not found.`);
                return;
            }

            const message = document.createElement('div');
            message.textContent = '🎉 Plugin Loaded Successfully!';
            message.style.padding = '10px';
            message.style.backgroundColor = '#e0f7fa';
            message.style.border = '1px solid #006064';
            message.style.borderRadius = '5px';
            message.style.marginTop = '10px';

            container.appendChild(message);
        }
    };
})();

Now let’s see the various behavior of calling simplePlugin.js in VF page and LWC

Visualforce DOM Behavior

Here’s why the simplePlugin script works automatically with Visualforce:

  • Global Scope: Visualforce elements are rendered in the global document namespace, meaning any element with an ID or class name can be directly accessed using standard DOM methods (e.g., document.getElementById or document.querySelector).
  • Script Execution Timing: When the DOM finishes loading, scripts like simplePlugin can immediately locate elements. They can manipulate elements because these exist in the global DOM context.
  • No Shadow DOM: Visualforce does not use Shadow DOM or any form of encapsulation. This means all DOM elements are accessible and modifiable without restriction.

In the provided VF example:

<apex:page>
    <!-- Include the simplePlugin.js static resource -->
    <apex:includeScript value="{!$Resource.simplePlugin}"/>
    
    <!-- Container where the plugin will render its content -->
    <div id="plugin-container" style="margin-top:20px;"></div>
    
    <!-- Inline script to call the plugin's load function -->
    <script>
    function invokeSimplePlugin() {
        if (window.simplePlugin && typeof window.simplePlugin.load === 'function') {
            console.log('simplePlugin is available.');
            window.simplePlugin.load('plugin-container');
        } else {
            console.error('simplePlugin is not available.');
        }
    }
    // Add an event listener to execute the initializeLightningOut function once the DOM is fully loaded.
    document.addEventListener('DOMContentLoaded', invokeSimplePlugin);
    </script>
</apex:page>
  • The <div id="plugin-container"> is part of the global DOM.
  • window.simplePlugin.load can directly find and modify the <div> element using its ID (plugin-container).
  • The VF page loads “simplePlugin.js” and the <div> element with ID (plugin-container) gets dynamic style applied from window.simplePlugin.load method.

2. Lightning Web Components (LWC) DOM Behavior

  • Encapsulation: Styles and DOM manipulations inside a component do not inadvertently affect other parts of the page. Other parts of the page do not inadvertently affect the component.
  • Scoped Access: Elements inside the Shadow DOM are not accessible through the global document object.
Problem Statement –

If you try to replicate the same like VF page in LWC, issues will arise. After loading the script, it will not access the div. As a result, nothing will come on UI.

Sample Code –
<template>
    <lightning-card title="Plugin Load From External Script Example - LWC" icon-name="custom:custom14">
        <div id="plugin-container" style="margin-top:20px;"></div>
    </lightning-card>
</template>
import { LightningElement } from "lwc";
import { loadScript } from "lightning/platformResourceLoader";
import simplePlugin from "@salesforce/resourceUrl/simplePlugin";

export default class SimplePluginLwc extends LightningElement {
  pluginInitialized = false;

  renderedCallback() {
    if (this.pluginInitialized) {
      return;
    }
    this.pluginInitialized = true;

    loadScript(this, simplePlugin)
      .then(() => {
        debugger;
        console.log("simple plugin loaded");
        if (
          window.simplePlugin &&
          typeof window.simplePlugin.load === "function"
        ) {
          console.log("simplePlugin is available.");
          window.simplePlugin.load("plugin-container");
        } else {
          console.error("simplePlugin is not available.");
        }
      })
      .catch((error) => {
        console.error("Error loading simplePlugin:", error);
      });
  }
}

Below is the high-level functionality of this LWC example:

  • The <div> with the class plugin-container exists inside the Shadow DOM of the LWC.
  • External scripts like simplePlugin cannot directly access elements within the Shadow DOM using global methods like document.getElementById.
the container document.getElementById in simplePlugin.js is null in LWC
Fix – the <div> Need to Be Dynamically Created in LWC
  • Scoped DOM: The simplePlugin is designed to look for elements in the global DOM. Therefore, it cannot locate the plugin-container div within the Shadow DOM of the LWC. Dynamically creating the <div> and attaching it to the component’s template ensures it is available for the script to manipulate.
  • Shadow DOM Restrictions: Scripts that do not account for Shadow DOM encapsulation need access to elements. This access must be explicitly provided through dynamic creation or other techniques.
Sample Code –
<!–simplePluginLwc.html–>
<template>
<lightning-card title="Plugin Load From External Script Example – LWC" icon-name="custom:custom14">
</lightning-card>
</template>
/*simplePluginLwc.js*/
// Importing necessary modules from the LWC framework
import { LightningElement } from "lwc";
// Importing the `loadScript` utility for loading external JavaScript resources
import { loadScript } from "lightning/platformResourceLoader";
// Importing the custom plugin as a static resource
import simplePlugin from "@salesforce/resourceUrl/simplePlugin";
export default class SimplePluginLwc extends LightningElement {
// Internal flags to track plugin and script initialization
pluginInitialized = false;
scriptTagCreated = false;
/**
* Lifecycle hook called when the component is added to the DOM.
* Used here to load the external plugin script and initialize it.
*/
connectedCallback() {
// Check if the plugin is already initialized
if (!this.pluginInitialized) {
// Load the external plugin script
Promise.all([loadScript(this, simplePlugin)])
.then(() => {
console.log("simplePlugin script loaded.");
// Verify the plugin's existence and invoke its `load` function
if (
window.simplePlugin &&
typeof window.simplePlugin.load === "function"
) {
// Initialize the plugin with the container ID
window.simplePlugin.load("plugin-container");
} else {
console.error("simplePlugin is not available.");
}
})
.catch((error) => {
// Log any errors encountered during script loading
console.error("Error loading simplePlugin plugin:", error);
});
// Mark the plugin as initialized
this.pluginInitialized = true;
}
}
/**
* Lifecycle hook called after the component’s DOM is updated.
* Used here to dynamically create and append the plugin container element.
*/
renderedCallback() {
// Check if the script tag has already been created
if (!this.scriptTagCreated) {
try {
console.log("Adding plugin-container dynamically.");
// Create a `div` element to serve as the plugin container
const container = document.createElement("div");
container.id = "plugin-container";
// Append the container element to the component's template
this.template.appendChild(container);
// Mark the script tag as created
this.scriptTagCreated = true;
} catch (error) {
// Log any errors encountered during the container creation
console.error("Error adding dd-container:", error);
}
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!–simplePluginLwc.js-meta.xml–>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"&gt;
<apiVersion>63.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>

Below is the high-level functionality of this LWC example:

  • External Plugin Loading:
    The loadScript utility is used to dynamically load the simplePlugin JavaScript file from Salesforce static resources.
  • Plugin Initialization:
    Once the script is loaded, the component checks if window.simplePlugin is available and invokes its load function to initialize the plugin with a specific container ID (plugin-container).
  • Lifecycle Hook Integration:
    The connectedCallback lifecycle hook loads the external plugin script. It initializes the script when the component is inserted into the DOM.
  • Dynamic Element Creation:
    The renderedCallback lifecycle hook creates a <div> element with the ID (plugin-container) dynamically. It then appends the element to the LWC component’s Shadow DOM.
  • Shadow DOM Context:
    The LWC operates in the Shadow DOM. Therefore, external scripts must work with dynamically added DOM elements. This ensures seamless integration.
  • Internal Flags:
    Flags (pluginInitialized and scriptTagCreated) ensure that the script is loaded only once. They also ensure the container is created one time to avoid redundant operations.

Key Differences Between VF and LWC DOM Handling

FeatureVisualforce (VF)Lightning Web Components (LWC)
DOM ScopeGlobalShadow DOM (Scoped)
Element Accessdocument.getElementByIdthis.template.querySelector
Script CompatibilityWorks with global DOM scriptsRequires scoped or dynamic elements
EncapsulationNoneStrong
Risk of ConflictsHighLow

Some Practical Examples of Adding dynamic elements for using external scripts

Here are practical examples of using third-party scripts in Lightning Web Components (LWC) where dynamic elements are added to the DOM to interact with the scripts effectively:


1. Integrating a Chart Library (e.g., Chart.js)
  • Use Case: Render a dynamically updating chart using Chart.js.
  • Third-Party Script: Chart.js.
  • Dynamic Elements: A canvas element is dynamically added as the chart container.
  • High Level Steps:
    • Dynamically create a canvas element inside the template.
    • Use Chart.js to draw a chart on the canvas.
  • Example – LWC Recipes
2. Embedding a Payment Gateway (e.g., Stripe, Paypal)
  • Use Case: Render Stripe Elements for secure card input dynamically.
  • Third-Party Script: Stripe.js.
  • Dynamic Elements: Payment input fields dynamically rendered by Stripe Elements.
  • High Level Steps:
    1. Load Stripe.js in the connectedCallback.
    2. Use Stripe’s API to initialize and dynamically render input fields.
3. Embedding a Map (e.g., Leaflet.js)
  • Use Case: Display an interactive Map on an LWC component..
  • Third-Party Script: Leaflet.js.
  • Dynamic Elements: A div element is dynamically added as a map container for rendering the map.
  • High Level Steps:
    1. Load the Maps script dynamically in the connectedCallback.
    2. Dynamically create a div container for the map.
    3. Initialize the map instance within the dynamically created container in renderedCallback.
  • Example – Using Leaflet Map in LWC

Conclusion and Best Practices

The behavior difference stems from Visualforce’s reliance on a global DOM versus LWC’s modern Shadow DOM approach. To ensure compatibility with external scripts in LWC:

  • Dynamically create and expose necessary elements when integrating scripts.
  • Adapt external scripts to support scoped DOM manipulation or refactor them to accept container elements programmatically.
  • Understand the isolation principles of Shadow DOM to leverage the full power of LWC’s encapsulation.

By embracing these practices, you can seamlessly integrate external scripts into LWC while maintaining clean, modern, and robust code.

One response to “Understanding the Difference: VisualForce vs Lightning Web Components DOM Behavior”

  1. […] in detail. We will use the dynamic element creation technique. This was discussed in our last blog Understanding the Difference: VisualForce vs Lightning Web Components DOM Behavior. This technique allows us to add a dynamic div and render the Leaflet […]

    Like

Leave a reply to Using Leaflet Map in Lightning Web Components (LWC) – Welcome to The Salesforce Dev Cancel reply