Internal link initializes custom JS, but after page refresh it won't (NodeJS App)

Hi all,

In my NodeJS app I have a number of text-area’s that I’m trying to force bullet-points. Currently I have a custom JS file (below) that references the class on the text-areas.

If I navigate to the page these text-areas exist and I call the functions:

convertContentToBullets('bullet-textarea'); initializeBulletTextareas();

Everything works as expected (albeit still needs improving as the user can still workaround the bullets if they wish).

However, if I then refresh this page, the bullets will not initialize within the text-area. Even if I manually call the above functions.

Logging shows that the text-areas are found, and that the functions are being called - but on the front-end the textarea bullets just won’t initialize.

Here is the JS (it’s a little bloated due to trying to find solutions):

// JavaScript Document
document.addEventListener('DOMContentLoaded', function () {
    console.log("Document ready. Initializing bullet textareas...");
    // Check if the elements exist and initialize them
    if (document.querySelectorAll('.bullet-textarea').length) {
        initializeBulletTextareas();
    } else {
        // If not, start observing the DOM for changes
        observeBulletTextareas();
    }
});

function initializeBulletTextareas() {
    const bulletTextareas = document.querySelectorAll('.bullet-textarea');
    console.log("Found", bulletTextareas.length, "bullet textareas");

    bulletTextareas.forEach(textarea => {
        textarea.removeEventListener('focus', onTextareaFocus);
        textarea.removeEventListener('keyup', onTextareaKeyup);

        textarea.addEventListener('focus', onTextareaFocus);
        textarea.addEventListener('keyup', onTextareaKeyup);
    });
}

// Define the focus event listener function separately
function onTextareaFocus() {
    console.log("Focus event on textarea with id:", this.id);
    if (this.value === '') {
        this.value += '• ';
    }
}

// Define the keyup event listener function separately
function onTextareaKeyup(event) {
    console.log("Keyup event on textarea with id:", this.id, "Key:", event.key);
    handleEnterAndBackspace(this, event);
}

function handleEnterAndBackspace(textarea, event) {
    const keycode = event.keyCode || event.which;
    console.log("Handling Enter/Backspace. Keycode:", keycode);

    if (keycode === 13) {
        insertBulletAfterEnter(textarea);
    } else if (keycode === 8) {
        handleBackspace(textarea, event);
    }
}

function insertBulletAfterEnter(textarea) {
    let txtval = textarea.value;
    console.log("Inserting bullet after Enter. Current value:", txtval);

    if (txtval.substr(txtval.length - 1) === '\n') {
        textarea.value = txtval + '• ';
    }
}

function handleBackspace(textarea, event) {
    const cursorPosition = textarea.selectionStart;
    console.log("Handling Backspace. Cursor position:", cursorPosition);

    if (cursorPosition === 0 || textarea.value.substring(cursorPosition - 2, cursorPosition) === '• ') {
        event.preventDefault();
        if (cursorPosition > 2) {
            textarea.selectionStart = cursorPosition - 2;
            textarea.selectionEnd = cursorPosition - 2;
        }
    }
}

function htmlToListFormat(html) {
    console.log("Converting HTML to list format. HTML:", html);
    const div = document.createElement('div');
    div.innerHTML = html;
    const items = div.querySelectorAll('li');
    return Array.from(items).map(item => '• ' + item.textContent).join('\n');
}

function convertContentToBullets(textareaClass) {
    const textareas = document.querySelectorAll('.' + textareaClass);
    console.log("Converting content to bullets for class:", textareaClass);

    textareas.forEach(textarea => {
        // Check if the content already starts with a bullet point
        if (!textarea.value.startsWith('• ')) {
            console.log("Original value for textarea with id:", textarea.id, "is being converted");
            textarea.value = htmlToListFormat(textarea.value);
        } else {
            console.log("Textarea with id:", textarea.id, "already has bullets, skipping conversion");
        }
    });
}

window.convertContentToBullets = convertContentToBullets;

function observeBulletTextareas() {
    const observer = new MutationObserver((mutations, obs) => {
        const bulletTextareas = document.querySelectorAll('.bullet-textarea');
        if (bulletTextareas.length) {
            console.log("Bullet textareas detected through MutationObserver.");
            initializeBulletTextareas();
            // Optionally, disconnect the observer once the elements are found and initialized
            obs.disconnect();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
}

Troubleshooting with our friendly AI donesn’t come up with a solution and instead points towards how states are managed in the code.

I know this is a long-shot but does anyone have any ideas here? @patrick sorry for the tag but just want to raise this in case I’m missing something specific to how AC2 / Wappler works in this regard.

Appreciate any help.

Hi.
Looks like a timing issue with elements being drawn on the page and JS being initialized.
I usually use some Wappler even to call the init function - list server connect success event.

Are the text areas on the page always visible? Without any dynamic data or show/hide conditions?
If that is the case, you can also set a page-flow with auto run, and call your JS there.

The code seems to be very buggy but didn’t notice any conflicts with App Connect when I tried it on a simple test page. The script kept working after a refresh of the page.

Textareas are automatically converted to App Connect components, if you want to prevent this add the followin code to your script. Make sure the code is executed before the DOMContentLoaded event since that is the moment App Connect start parsing the DOM.

dmx.config.mapping.textarea = 'textarea:not(.bullet-textarea)';

Hey @patrick thank you for your reply.

I understand re: buggy. As a non-developer i’m happy it even works - but will try and clean it up.

I’ve included this code at the top of my JS file:

// JavaScript Document

dmx.config.mapping.textarea = 'textarea:not(.bullet-textarea)';

document.addEventListener('DOMContentLoaded', () => {...

On refresh this is the error I see in the console:

Unknown component found! textarea:not(.bullet-textarea)

What I don’t understand in terms of wappler is what I would need to do to ensure the code is executed before the DOMContentLoaded event. Can you provide any assistance here?

Is it as simple as running a page-flow with auto-run as @sid suggests above (thanks Sid!) or is there something else I would need to do?

Noting that the text-area’s are hidden until I click a button, one the button is clicked the text-area’s appear - without refresh this works fine (the bullet initialization).

Sid - I’m using this click event to call the init function as I don’t need it until the text-area appears as they are dynamically hidden

And does that resolve the problem at hand?

It doesn’t - I’ve even tried manually triggering the init with the text-area’s visible, but still no solution. It’s odd, it’s only when the page is refreshed manually, versus navigating via an internal link. Like you and Patrick have mentioned it must be to do with how things are loaded on the page - that’s just a new area for me to learn about before I can solve this myself.

Hey @patrick trying not to bug you too much here.

To clarify what i’m trying to accomplish:

  • force users to write in bullet-point format.

If it’s am empty text-area then when they click inside the TA it creates a bullet. All new lines create new bullets. When I submit the form, the data is converted to html and sent to the backend and stored in a form field.

When the text-area is not empty then it loads the data from the SC and views the html as bullets.

In this scenario, that all works fine, but, on page refresh the bullets are no longer enforced.

I’ve included your code at the top of my javascript file that loads on the page but i’m still having no luck here.

My expertise in when and how Wappler dynamically loads certain elements is basically zero - so if you can help to point me in the right direction I’d be appreciate this.

Thanks.