Search code examples
javascripttypescriptserver-side-renderingjavascript-framework

ReferenceError when trying to add logic to a Fresh page


I'm trying to create a website with the Fresh framework, in particular I was trying to put a simple drop-down feature of a button inside a navbar, but I'm not sure where to put the code needed. I've try to create a class inside the index.tsx file and initialize it inside the export default function Home(), but the only thing that I get is a ReferenceError because document is not defined.

index.tsx

/** @jsx h */
import { h } from "preact";
import HeaderWithLogin from "../islands/HeaderWithLogin.tsx";
import Navbar from "../islands/Navbar.tsx";

class Dropdown {
    private _targetElement: Element | null;
    private _triggerElement: Element | null;
    private _visible: boolean;

    constructor(
        targetElement: Element | null = null,
        triggerElement: Element | null = null
    ) {
        this._targetElement = targetElement;
        this._triggerElement = triggerElement;
        this._visible = false;
        this._init();
    }

    _init() {
        if (this._triggerElement) {
            this._triggerElement.addEventListener("click", () => {
                this.toggle();
            });
        }
    }

    _handleClickOutside(ev: MouseEvent) {
        const clickedE = ev.target as Element;
        if (
            clickedE !== this._targetElement &&
            !(this._targetElement as Element).contains(clickedE) &&
            !this._triggerElement?.contains(clickedE) &&
            this._visible
        ) {
            this.hide();
        }
        document.body.removeEventListener(
            "click",
            this._handleClickOutside,
            true
        );
    }

    toggle() {
        if (this._visible) {
            this.hide();
            document.body.removeEventListener(
                "click",
                this._handleClickOutside,
                true
            );
        } else {
            this.show();
        }
    }

    show() {
        this._targetElement?.classList.remove("hidden");
        this._targetElement?.classList.add("block");

        document.body.addEventListener("click", this._handleClickOutside, true);

        this._visible = true;
    }

    hide() {
        this._targetElement?.classList.remove("block");
        this._targetElement?.classList.add("hidden");

        this._visible = false;
    }
}

function initDropdown() {
    document
        .querySelectorAll("[data-dropdown-toggle]")
        .forEach((triggerElement) => {
            const targetElement: Element | null = document.getElementById(
                triggerElement.getAttribute("data-dropdown-toggle") as string
            );

            new Dropdown(targetElement, triggerElement);
        });
}

export default function Home() {
    if (document.readyState !== 'loading')  {
        initDropdown();
    }   else    {
        document.addEventListener('DOMContentLoaded', initDropdown);
    }
    return (
        <body>
            <Navbar></Navbar>
        </body>
    );
}

Navbar.tsx

/** @jsx h */
import { h } from "preact";
import { tw } from "@twind";

export default function Navbar()    {
    return (
        <nav>
            <ul>
                <li>
                    <button data-dropdown-toggle="dropdownElement">Dropdown Element</button>
                    <div id="dropdownElement">
                        <ul>
                            <li>
                                <a href="/">Link To Other Page</a>
                            </li>
                        </ul>
                    </div>
                </li>
                <li>
                    <button>Button 1</button>
                </li>
                <li>
                    <a href="/">Button 2</a>
                </li>
                <li>
                    <a href="/">Button 3</a>
                </li>
            </ul>
            <div>
                <input type="text" id="search-navbar" class={tw`block p-2 pl-10 w-full text-gray-900 bg-gray-50 rounded-lg border border-gray-300 sm:text-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500`} placeholder="Search..."></input>
            </div>
        </nav>
    );
}

Error given:

An error occured during route handling or page rendering.

ReferenceError: document is not defined
    at Object.Home (file:///.../routes/index.tsx:89:5)
    at h (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:4:1003)
    at h (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:4:1103)
    at h (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:4:1103)
    at h (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:4:1103)
    at h (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:4:1103)
    at m (https://esm.sh/v87/[email protected]/X-ZC9wcmVhY3RAMTAuOC4y/deno/preact-render-to-string.js:3:684)
    at render (https://deno.land/x/[email protected]/src/server/render.tsx:180:16)
    at Object.render [as renderFn] (file:///.../main.ts:20:3)
    at render (https://deno.land/x/[email protected]/src/server/render.tsx:184:14)

How can I resolve this error? I believe that including the code needed in a script tag would be enough, but I'm curious to know if there are other ways to incorporate code inside the pages or if that's the best way to do it.


Solution

  • document is a browser global. It does not exist in environments outside of the browser.

    You will need to add a check rather than assuming document is universally defined.

    if (typeof document !== 'undefined') {
      // use document
    }
    

    Really though you shouldn't be using browser globals like that at all with UI frameworks like Preact. You should be using refs with wrapping elements if need be.