JavaScript Environment

Explore JavaScript scope, starting from the global scope to the local scope, and learn how to declare and use variables in different scopes.

Purpose

JavaScript is a versatile programming language that can be used to create interactive elements on a web page. Understanding the scope of variables in JavaScript is essential for writing efficient and maintainable code.

Function Declaration and Scope

When you declare a function in a JavaScript script tag, you can control its accessibility by using the export keyword. Functions declared with the export keyword, whether as a classic function or an arrow function, will be accessible from outside the script. For example:

export function myFunc() {
    console.log("This is an exported function.");
}

export const myArrowFunc = () => {
    console.log("This is an exported arrow function.");
};

If a function does not have the export keyword, it will be treated as a private function scoped only to the script tag and will not be accessible from the outside. For example:

function privateFunc() {
    console.log("This is a private function.");
}

const privateArrowFunc = () => {
    console.log("This is a private arrow function.");
};

In summary, use the export keyword for functions that need to be accessible globally or from other modules, and omit it for private functions that should remain scoped to the script tag.

1. DOMContentLoaded Event:

  1. What it is: The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.
  2. Purpose: Useful if you want to execute a script after the HTML has been fully parsed but before the entire page (including images, etc.) has been fully loaded.
document.addEventListener("DOMContentLoaded", function() {
    // Code to run when the DOM content is fully loaded
  });

2 . window.onload Event:

The window.onload event is triggered when the entire page, including all dependent resources such as images, iframes, and stylesheets, has fully loaded. This is different from DOMContentLoaded, which only waits for the HTML to be parsed and does not wait for external resources.

Differences Between DOMContentLoaded and window.onload:

DOMContentLoaded Fires when the initial HTML document is completely loaded and parsed, without waiting for stylesheets, images, or subframes.

window.onload: Fires only after the entire content of the page has been loaded, including images, scripts, and stylesheets.

Purpose Use window.onload when you need to ensure that all external resources (such as images and CSS files) are fully loaded before executing your JavaScript code.

<body>
    <script>
        // Attaching a window.onload event
        window.onload = function() {
            console.log("Entire page including resources has been loaded.");

            // Example: Dynamically change an image source after the page loads
            const img = document.querySelector('#myImage');
            if (img) {
                img.src = '/path/to/new/image.jpg';
            }
        };
    </script>

    <img id="myImage" src="/path/to/initial/image.jpg" alt="Example Image" />
</body>

When to Use: Use window.onload when your script relies on fully loaded resources, such as images or iframes. Use DOMContentLoaded when you only need the HTML to be parsed before executing your script, making it suitable for faster execution without waiting for resources.

3. async Attribute in <script> Tag:

  1. Loading: The script is fetched asynchronously (in parallel with the HTML parsing), but its execution happens as soon as the script is downloaded. This can disrupt the order of execution of scripts since the script may execute before or after other scripts.
  2. When to use: Best for scripts that do not rely on the DOM or on each other, such as analytics scripts.
  3. Execution order: No guarantee of execution order if there are multiple scripts with async.
<script async src="script.js"></script>

4. defer Attribute in <script> Tag:

  1. Loading: The script is fetched asynchronously (in parallel with the HTML parsing), but execution happens only after the HTML document has been fully parsed (i.e., when the DOMContentLoaded event is about to fire).
  2. When to use: Useful for scripts that rely on the DOM being fully available, but should not block the parsing of the HTML document.
  3. Execution order: Scripts with defer are executed in the order they appear in the document.
<script defer src="script.js"></script>

Global Scope

To load your own JavaScript code, you can use the head or body section of your HTML document. You can declare variables and functions in the global scope, which are accessible from anywhere in the script.

The Difference Between Loading in the head and body Sections: When you load your JavaScript code in the head section, the script is executed before the HTML content is loaded. If you load your JavaScript code in the body section, the script is executed after the HTML content is loaded, which is the normal behavior for most scripts.

Loading JavaScript Code in the head Section

<head>
      <title>Global Scope Example</title>
      <script src="<?= Request::baseUrl; ?>/js/custom.js"></script>
  </head>

Note: The head section is typically used to load external resources such as stylesheets, scripts, and metadata. It is recommended to load JavaScript code in the head section to ensure that the script is executed before the HTML content is loaded.

Loading JavaScript Code in the body Section

<body>
      <h1>Global Scope Example</h1>
      <script src="<?= Request::baseUrl; ?>/js/index.js"></script>
  </body>

Note: The body section is typically used to load scripts that interact with the HTML content. By loading the script in the body section, you ensure that the script is executed after the HTML content is loaded, allowing it to interact with the elements on the page. You can use the Request class to get the baseUrl, which will return the path src/app. Alternatively, you can directly use src/app/your-path. Prisma PHP will create a new instance of the script and execute it after the HTML content is loaded, making it easier to interact with the elements on the page. This allows you to load your own dynamic script in different sections of the page and make it interact with the elements on the page.

Global scope

Variables declared outside of any function are considered global variables and can be accessed from anywhere in the script. Global variables are not recommended because they can be modified by any part of the script, leading to unexpected behavior.

<head>
      <title>Global Scope Example</title>
      <script>
          // Global scope
          var globalVariable = "I am a global variable";
  
          function globalFunction() {
              console.log(globalVariable);
          }
      </script>
  </head>
  <body>
      <h1>Check the console for global variable output</h1>
      <script>
          // Accessing global variable and function
          console.log(globalVariable);
          globalFunction();
      </script>
  </body>

Local Scope

<body>
    <script>
      // Local scope using an immediately invoked function expression
      // If you are using functions remember to expose to the global scope if needed using window.functionName = functionName;
      (function () {
        var localVariable = "I am a local variable";
  
        function localFunction() {
          console.log(localVariable);
        }
  
        // Call the function within the local scope
        localFunction();
      })();
  
      // Trying to access localVariable or localFunction here would result in an error
      // console.log(localVariable); // Uncaught ReferenceError: localVariable is not defined
      // localFunction(); // Uncaught ReferenceError: localFunction is not defined
    </script>
  </body>

DOMContentLoaded

The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. This event is useful if you want to execute a script after the HTML has been fully parsed but before the entire page (including images, etc.) has been fully loaded.

<body>
    <script>
        document.addEventListener("DOMContentLoaded", () => {
            // Global scope
            var globalVariable = "I am a global variable";

            // Local scope
            let letVariable = "I am a local variable";

            // Local scope
            const constVariable = "I am a constant variable";
        
            function globalFunction() {
                console.log(globalVariable);
            }

            // Call the global function within DOMContentLoaded
            globalFunction();
        });
    </script>
  </body>

onHydrationComplete()

onHydrationComplete lets you run code after the initial reactive hydration finishes—i.e., once refs, declarative state, attribute/if/loop bindings, and wire listeners are ready and the first batch updates have flushed. Think of it as a reactive equivalent to DOMContentLoaded tailored for your hydration pipeline.

🔎 Analogy: DOMContentLoaded fires when the DOM is parsed. onHydrationComplete fires when the reactive layer is initialized and stable.

Signature

public onHydrationComplete(callback: () => void): () => void

Behavior

  • Registers callback to run after hydration completes.
  • If hydration already finished (_isInitialHydrationComplete === true), it calls your callback immediately.
  • Returns an unsubscribe function to remove the listener.
  • Safe to register multiple listeners; order is registration order.

When does it fire?

At the end of the hydration pipeline—after:

  • initRefs, bootstrapDeclarativeState, processInlineModuleScripts
  • initializeAllReferencedProps, manageAttributeBindings, processIfChains, initLoopBindings
  • attachWireFunctionEvents (when enabled)
  • Initial _bindings updates flushed in small chunks

Internally this is triggered via notifyHydrationComplete() at the end of scheduleInitialHydration() and also after incremental hydration via initReactiveOn(...).

Basic Usage

<script>
  // Register a one-time effect after hydration:
  const off = pphp.onHydrationComplete(() => {
    // All reactive values/refs/bindings are ready here.
    const firstInput = document.querySelector('input[data-autofocus]');
    firstInput?.focus();
  });

  // Optionally unsubscribe (if you set up multi-use handlers somewhere):
  // off();
</script>

Compare with DOMContentLoaded

// DOM is parsed but reactive bindings may NOT be ready yet
document.addEventListener('DOMContentLoaded', () => {
  // Do minimal DOM setup only, avoid relying on reactive values here.
});

// Hydration is fully complete; safe to read reactive state/refs/bindings
pphp.onHydrationComplete(() => {
  // Ideal for measuring layout, reading computed attrs, or wiring UI logic
});

Common Patterns

1) Focus/Selection after UI mounts

pphp.onHydrationComplete(() => {
  const el = document.querySelector('[data-initial-focus]');
  el?.focus();
});

2) Safe layout reads / measurements

pphp.onHydrationComplete(() => {
  // bindings are applied; sizes are meaningful now
  const panel = document.getElementById('metrics-panel');
  const rect = panel?.getBoundingClientRect();
  console.log('Panel size:', rect?.width, rect?.height);
});

3) One-shot promise helper

function whenHydrated() {
  return new Promise((resolve) => pphp.onHydrationComplete(resolve));
}

await whenHydrated();
// ...run code that needs fully ready reactive state

4) Incremental hydration (portals/fragments)

If you hydrate a subtree using initReactiveOn(root, { preserveHierarchy, wire }), notifyHydrationComplete() is called at the end, so your existing onHydrationComplete listeners will also run after that subtree is ready.

Unsubscribe

const off = pphp.onHydrationComplete(() => {
  console.log('Hydration finished');
});

// Later, if you no longer need it:
off();

Internals (for reference)

The hook stores callbacks in _hydrationListeners. On completion, notifyHydrationComplete() executes each callback. If _isInitialHydrationComplete is already true, new subscribers are invoked immediately.

public onHydrationComplete(callback: () => void): () => void {
  this._hydrationListeners.add(callback);
  if (this._isInitialHydrationComplete) callback();
  return () => this._hydrationListeners.delete(callback);
}

Best Practices

  • Do use it for focus management, layout reads, and wiring third-party widgets that need the final DOM.
  • Avoid heavy reactive writes inside the callback; if needed, queue them via queueMicrotask or requestAnimationFrame to keep the UI responsive.
  • If you attach global listeners inside the callback, remember to clean them up when appropriate.

FAQ

Q: Will it run more than once?
A: It runs each time the system calls notifyHydrationComplete(). That includes initial hydration and any explicit incremental hydration you trigger on a subtree. If you only need a single run, call the returned off() immediately after the first invocation.

Q: Can I rely on reactive values and bindings inside the callback?
A: Yes. The hook is called after the binding batches flush, so values/attrs are stable.

Q: How is this different from window.onload?
A: window.onload waits for all external resources (images, stylesheets). onHydrationComplete cares about the reactive system being ready—which typically happens sooner and is the right moment to touch reactive state and bound DOM.