LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Rails, Javascript and Turbolinks cached pages (https://www.linuxquestions.org/questions/programming-9/rails-javascript-and-turbolinks-cached-pages-4175668341/)

grail 01-24-2020 05:10 AM

Rails, Javascript and Turbolinks cached pages
 
I am obviously missing a key point here, so this is what I (think I) know so far:

1. Rails app uses Turbolinks

2. Turbolinks takes over the loading of the page and caches it once done (I think)

So I have 3 javascript files.

1. Allows the user to click a link and makes a form appear, which then adds the form data
to the end of a list of entries in a div
Code:

#code to show either link or form - app/javascript/packs/new_task.js
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const changeLinkForm = element => {
        let new_tasks = document.querySelectorAll(element);
        const [div, link, form] = new_tasks;

        div.replaceChild(link, form);

        link.addEventListener('click', e => {
                div.replaceChild(form, link);
        },false);

        form.addEventListener('submit',async e => {
                div.replaceChild(link, form);
                await sleep(1000);
                form.reset();
        },false);
}

window.addEventListener('turbolinks:load', event => {
        changeLinkForm('#new_task');
}, false);

#code called once form is submitted to create entry and add to list - app/views/tasks/create.js.erb
var task = document.querySelector('#incomplete_tasks');

task.insertAdjacentHTML("beforeend", '<%= j render(@task) %>');

Now this is where I think the first part of the caching shows as after I add a new entry and then look at the source for the page,
the new entry does not appear in the source but is on the page, hence the DOM has been updated but not the cached page

2. I have javascript to remove an entry from the list
Code:

# code called when 'remove' link is clicked - app/javascript/packs/remove_task.js
const remove_line = name => {
        let elements = document.querySelectorAll(name);

        elements.forEach(element => {
                element.lastElementChild.addEventListener('click', event => {
                        element.remove();
                }, false);
        });
}

window.addEventListener('turbolinks:load', event => {
        remove_line('form');
}, false);

Now this code works just fine on all entries that were on the page when it first loaded.
However, the newly added entries from code above do not disappear, but the destroy method within rails constructor does remove the entry from database, so on a page refresh the entry is now gone

So my question is, how do I get my remove code to work on the newly added entries from the create/show_hide code?

Please let me know if I have missed any vital details to help solve the problem/educate me?

phantom_cyph 01-24-2020 06:18 AM

Code:

form.addEventListener('submit',async e => {
                div.replaceChild(link, form);
                await sleep(1000);
                form.reset();
        },false);

Now.. I'm not sure but something here doesn't look right.

Normally you define the top level function as "async", then put anything synchronous into another function which then returns a promise. I.e. "await renderElement()".

JS is naturally asynchronous, so you're actually stating inside an async function that it's async, rather than having an async function which waits for the synchronous function to finish and return it's "resolution".

This itself could play a big role in what is causing your issue.

Honestly if you want automatic rendering on something like this, I'd go React.js.. as you could just render based on an array/list that is populated in state, and upon any change to that array, it would automatically re-render.

grail 01-24-2020 07:27 AM

@phantom_cyph - thanks for the feedback :)

The reason for that little cludge was to get a pause to happen so the create.js.erb could do its thing and the reset the form so it is blank when user next clicks the link.
I can remove the entire thing as once the div.replaceChild is finished the other is nice to have and I probably need to learn a better way ;)

So, if i change it to:
Code:

form.addEventListener('submit',e => {
  div.replaceChild(link, form);
},false);

My original issue still persists.

I have read of other gems and things I can install to change functionality, but I am starting out at a slow/low level as javascript is new to me
and I wanted to get it working with rails to then see how I can extend my apps/pages

If on the other hand there is no natural way to do what I want outside installing something else I am happy to look at that too?

SoftSprocket 01-24-2020 07:54 AM

Quote:

Originally Posted by grail (Post 6082352)
Now this is where I think the first part of the caching shows as after I add a new entry and then look at the source for the page,
the new entry does not appear in the source but is on the page, hence the DOM has been updated but not the cached page

AFAIK there is no way to update the source. Javascript doesn't act on the source it acts on the DOM. Are you sure you are adding the listener on the correct element?

dugan 01-24-2020 08:31 AM

I was going ask this last time but didn't for some reason.

Why are you passing false as the third argument to addEventListener? It's false by default.

grail 01-24-2020 08:59 AM

Quote:

Originally Posted by SoftSprocket
Are you sure you are adding the listener on the correct element?

I am learning, but as far as I can tell, I would say yes as all javascript files are working when the page is first loaded and works after any refresh to the page,
just not on the items added using javascript.
Quote:

Originally Posted by SoftSprocket
Javascript doesn't act on the source it acts on the DOM.

I am assuming the DOM is updated correctly as when I click the remove link in the page the rails side, ie the firing of the constructor, seems to work just fine

Quote:

Originally Posted by dugan
Why are you passing false as the third argument to addEventListener? It's false by default.

As above, I am learning and so at the moment I am trying not to leave anything to chance or misinterpretation (namely, by me:)).
I was aware it is the default, but for now it is there as a reminder/learning tool :)

I thought as additional information I would include the current source from when the page is first loaded:
Code:

<!DOCTYPE html>
<html>
  <head>
    <title>Rc136</title>
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="xyLSXKQZJgtK0amvmG1k8MQ/HZNnPmNkhPS2E5jzIcbjNiW1z3lFt/CnIchqbOKtmqFP+w0cDVjsJYcrkrpt3g==" />
   

    <link rel="stylesheet" media="all" href="/assets/application.debug-3afd09bff0c2914d389ec26e5756550d546c1051aaedbc73714107ffbe8bb3bd.css" data-turbolinks-track="reload" />
    <script src="/packs/js/application-0b67a3cc2fcde6055024.js" data-turbolinks-track="reload"></script>
  </head>

  <body>
    <script src="/packs/js/new_task-0f218e4c845d787adbd8.js"></script>
<script src="/packs/js/remove_task-9851f625eb8db320a754.js"></script>

<h1>Check List</h1>

<div id="new_task">
  <a id="new_task" data-remote="true" href="/tasks/new">New Task</a>
  <form class="new_task" id="new_task" action="/tasks" accept-charset="UTF-8" data-remote="true" method="post">
  <input type="text" name="task[name]" id="task_name" />
  <input type="submit" name="commit" value="Create Task" data-disable-with="Create Task" />
</form>
</div>

<h2>Incomplete Tasks</h2>
<div class="tasks" id="incomplete_tasks">
  <form class="edit_task" id="edit_task_2" action="/tasks/2" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch" /><input type="hidden" name="authenticity_token" value="gRBab/7SAjtIbS6DBGz4waL8XHDPb3DBDl3yGQ+43Kuqil7gBvDGUF0WTpezqCPSTJN4ti+i125GxrJapote1A==" />
  <input name="task[complete]" type="hidden" value="0" /><input type="checkbox" value="1" name="task[complete]" id="task_complete" />
  <input type="submit" name="commit" value="Update" data-disable-with="Update" />
  <label for="task_complete">Wax the car</label>
  <a data-confirm="Are you sure?" data-remote="true" rel="nofollow" data-method="delete" href="/tasks/2">(remove)</a>
</form><form class="edit_task" id="edit_task_157" action="/tasks/157" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch" /><input type="hidden" name="authenticity_token" value="4fLoZz8vruBbGo3VcWhPsL/NAiFvsK6MU96z5bzZra81ORQ19t0aCnvTJSEaR81oZTmBpaCDia7fuHhoISfkew==" />
  <input name="task[complete]" type="hidden" value="0" /><input type="checkbox" value="1" name="task[complete]" id="task_complete" />
  <input type="submit" name="commit" value="Update" data-disable-with="Update" />
  <label for="task_complete">Sand the deck</label>
  <a data-confirm="Are you sure?" data-remote="true" rel="nofollow" data-method="delete" href="/tasks/157">(remove)</a>
</form>
</div>

  </body>
</html>

The 'Incomplete Tasks' is the section where my code adds an additional entry.
If I click either "(remove)" link, tasks 2 and 157, each of those items will be successfully removed both from the database by the rails constructor action and from the visible page
by the remove_task javascript.

Once the link, 'New Task', is clicked the link will disappear and be replaced with the new_task form.
On clicking the 'Create Task' button the new entry will appear as a new entry for incomplete tasks and the form will switch back to the link

It is at this point that if I do a dump as above the new entry will not be displayed in the source and the '(remove)' link will not fire the associated remove_task javascript,
but it will however fire the rails constructor to remove the task from the database.

boughtonp 01-24-2020 09:42 AM

Rails runs on the server.

JavaScript runs in the browser (on the client's computer), and has no control over your server.

Both HTML forms and JavaScript can send HTTP requests to your server, but it is up to Rails to process those requests and perform whatever logic is necessary (e.g. validate correct input, update the database, clear any caches, etc).

In short: forget about JavaScript for the moment - go to the browser network tab and deal with the HTTP requests and responses (either there, or by crafting a suitable curl command) and make sure you're getting the correct responses & behaviour when you make requests manually.

Once you have that working, you can return to the JavaScript side and ensure JS is generating the correct HTTP requests.

grail 01-24-2020 10:27 AM

Quote:

Originally Posted by boughtonp (Post 6082441)
Rails runs on the server.

JavaScript runs in the browser (on the client's computer), and has no control over your server.

Both HTML forms and JavaScript can send HTTP requests to your server, but it is up to Rails to process those requests and perform whatever logic is necessary (e.g. validate correct input, update the database, clear any caches, etc).

In short: forget about JavaScript for the moment - go to the browser network tab and deal with the HTTP requests and responses (either there, or by crafting a suitable curl command) and make sure you're getting the correct responses & behaviour when you make requests manually.

Once you have that working, you can return to the JavaScript side and ensure JS is generating the correct HTTP requests.

If I am understanding you correctly, please let me know if not, but I started this app with zero javascript and all features were working perfectly
via html/rails.
The obvious difference being that each time the items were added or removed from the page it was associated with a return to the page, ie so the page was refreshed
each time and all changes were observed to have worked.

I initially added the link/form exchange and create javascript first and again (after some learning, see here) the items were created and added but now without the use of the page refresh as performed by javascript.

On adding the remove javascript it was only then did I notice the lack of updating in the source (which I understand is not required as the DOM is being altered correctly)
and the knock on effect of not allowing the remove option to work on newly created entries.

Please advise if this is equivalent to what you have mentioned and if not please advise what I have missed?

boughtonp 01-24-2020 05:32 PM

Ok, I understand the issue now - it's different to what I thought before.

Currently you're attaching remove_line on turbolinks:load event (and thus only attaching clicks to the entries around when that event fires, which is on navigation), so when you add a new entry, there is no click event on its remove button.

One option would be to use dispatchEvent to trigger the event manually after you create your new entry - whilst you could just re-trigger turbolinks:load event itself, that may involve other side-effects so it's probably better to create your own separate event and tie remove_line function to both events (i.e. two calls to addEventListener).

Another option would be to not add individual click event listeners to each remove button, but just have a single instance attached to the containing element, then have logic to check event.target to determine which button was clicked, and thus which entry to remove (or indeed any other actions).

grail 01-27-2020 11:35 AM

@boughtonp - Thank you for this information, turns out, after some head scratching about why a form couldn't dispatch an event and that querySelector had to be on a tag and not a class or id to be able to
catch the event, that this was the way forward :)

For others who may find this thread useful, here is my updated solution:
Code:

$ cat app/javascript/packs/new_task.js
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const changeLinkForm = element => {
        let new_tasks = document.querySelectorAll(element);
        const new_event = new Event('task_added', { bubbles: true });
        const [div, link, form] = new_tasks;

        div.replaceChild(link, form);

        link.addEventListener('click', e => {
                div.replaceChild(form, link);
        },false);

        form.addEventListener('submit',async e => {
                div.replaceChild(link, form);
                await sleep(1000);
                form.reset();
        },false);

        form.addEventListener('reset', e => link.dispatchEvent(new_event));
}

window.addEventListener('turbolinks:load', event => {
        changeLinkForm('.new_task');
}, false);

Code:

$ cat app/javascript/packs/remove_task.js
const remove_line = name => {
        let elements = document.querySelectorAll(name);

        elements.forEach(element => {
                element.lastElementChild.addEventListener('click', event => {
                        element.remove();
                }, false);
        });
}

window.addEventListener('turbolinks:load', event => {
        remove_line('form');
       
        document.addEventListener('task_added', event => remove_line('form'));
}, false);

Now, not sure if this is just being greedy, but it appears to be another issue I had missed.

When the rails side creates each entry for the tasks, the '(remove)' link also has a confirmation assigned in case you wish to cancel the removal.
In the resulting HTML a single entry looks like:
Code:

<form class="edit_task" id="edit_task_157" action="/tasks/157" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch" /><input type="hidden" name="authenticity_token" value="3ki5PDrWw17b5DhEJ0/zJ38vs9GWoT+0nzVSL1MOqbYpU4AITgeq6WNsX5ch8h1ookZphDFxpiLzRLJmO/OALg==" />
  <input name="task[complete]" type="hidden" value="0" /><input type="checkbox" value="1" name="task[complete]" id="task_complete" />
  <input type="submit" name="commit" value="Update" data-disable-with="Update" />
  <label for="task_complete">Sand the deck</label>
  <a data-confirm="Are you sure?" data-remote="true" rel="nofollow" data-method="delete" href="/tasks/157">(remove)</a>
</form>

On pressing OK everything works as expected, however, on Cancel, rails rightly ignores calling the delete/destroy method so the entry stays in the database.
But my javascript is predicated on the click of the '(remove)' link and not the following conmfirmation dialogue box.

Is anyone able to tell me how I might catch which option is selected?

If moderator thinks this should be a new question I can raise?

grail 02-03-2020 01:31 AM

I was not able to find a solution to catching the confirmation, but will go that a bit further on my own.

For now I have also changed to the regular .js.erb scenarios and have all point working.

Thanks for all the help


All times are GMT -5. The time now is 02:14 PM.