[SOLVED] Rails, Javascript and Turbolinks cached pages
ProgrammingThis forum is for all programming questions.
The question does not have to be directly related to Linux and any language is fair game.
Notices
Welcome to LinuxQuestions.org, a friendly and active Linux Community.
You are currently viewing LQ as a guest. By joining our community you will have the ability to post topics, receive our newsletter, use the advanced search, subscribe to threads and access many other special features. Registration is quick, simple and absolutely free. Join our community today!
Note that registered members see fewer ads, and ContentLink is completely disabled once you log in.
If you have any problems with the registration process or your account login, please contact us. If you need to reset your password, click here.
Having a problem logging in? Please visit this page to clear all LQ-related cookies.
Get a virtual cloud desktop with the Linux distro that you want in less than five minutes with Shells! With over 10 pre-installed distros to choose from, the worry-free installation life is here! Whether you are a digital nomad or just looking for flexibility, Shells can put your Linux machine on the device that you want to use.
Exclusive for LQ members, get up to 45% off per month. Click here for more info.
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?
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.
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
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?
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?
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:
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.
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.
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?
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).
@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);
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:
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?
LinuxQuestions.org is looking for people interested in writing
Editorials, Articles, Reviews, and more. If you'd like to contribute
content, let us know.