Reloading JS scripts on page (transition) change when using Barba JS - javascript

I'm trying to implement barba.js on a HubSpot site.
Consider the following two pages:
Resources
Customers
With my current barba.js implementation, this is the current flow I'm experiencing:
I access the resources page (not by transitioning to it, by directly accessing it via /resources).
At this point, all my js is working (slick-sliders, scroll functions etc)
I then use the navigation to access the customers page. The page loads, but all of the js specific to modules and forms for that page are not working.
In short, js for pages that I transition to do not work.
To resolve this, what I'm trying to do is to reload all scripts within the container beforeEnter().
See below:
$(function() {
function delay(n){
n = n || 2000;
return new Promise((done) => {
setTimeout(() => {
done();
}, n);
});
}
barba.init({
sync: true,
prefetchIgnore: true,
debug: true,
transitions: [{
async leave(data){
const done = this.async();
await delay(200);
done();
},
async beforeEnter({ next, container }) {
$(container).find('script').each(function (i, script) {
var $script = $(script);
$.ajax({
url: $script.attr('src'),
cache: true,
dataType: 'script',
success: function () {
$script.trigger('load');
}
});
});
},
async enter(data){
let scrollX = 0
let scrollY = 0
barba.hooks.leave(() => {
scrollX = barba.history.current.scroll.x;
scrollY = barba.history.current.scroll.y;
});
window.scrollTo(scrollX, scrollY);
},
async once(data){
console.log("done");
},
}]
});
});
However, my current beforeEnter() still yields the same results. Any way around this?
Edit
To provide more details, this barba.js implementation is for a HubSpot site. When you create custom modules in HubSpot it spits out the JS for that module on the page (in script tags). For example, here is how JS is rendered on the page:
<script>
// js from custom module is here
</script>
<script src=".../scripts-min.min.js"></script>
As such, when a barba transition is executed, I need all JS that have src and those that are rendered inline (like custom modules) to reload.
Latest approach based on User863's feedback
$(function() {
function delay(n){
n = n || 2000;
return new Promise((done) => {
setTimeout(() => {
done();
}, n);
});
}
function reload_scripts(param){
$(param).find('script').each(function (i, script) {
var $script = $(script);
$.ajax({
url: $script.attr('src'),
cache: true,
dataType: 'script',
success: function () {
$script.trigger('load');
console.log("scripts loaded");
}
});
});
}
barba.init({
sync: true,
prefetchIgnore: true,
debug: true,
transitions: [{
async leave(data){
const done = this.async();
await delay(200);
done();
},
async beforeEnter(data) {
reload_scripts(data.next.container);
},
async beforeEnter({ next }) {
reload_scripts(next.container);
},
async enter(data){
// scroll to top of page when transitioning
let scrollX = 0
let scrollY = 0
barba.hooks.leave(() => {
scrollX = barba.history.current.scroll.x;
scrollY = barba.history.current.scroll.y;
});
window.scrollTo(scrollX, scrollY);
},
async once(data){
console.log("transition complete");
},
}]
});
});
Current behaviour: Same as before (scripts for modules are broken when page changes). Modules with slick slider's for example, do not work (slick isn't initiated).

Might be solved by using eval as mentioned here: https://github.com/barbajs/barba/issues/32
Mentioned in the comments on the issue:
barba.hooks.after((data) => {
let js = data.next.container.querySelectorAll('main script');
if(js != null){
js.forEach((item) => {
console.log(js)
eval(item.innerHTML);
});
}
});

According to docs
The arguments of beforeEnter transition will be object of
Property
Description
data.current
Current page related
data.next
Next page related
data.trigger
Link that triggered the transition
https://barba.js.org/docs/advanced/hooks/#data-properties
Therefore the container property available inside both data.current and data.next properties. In this case, we have to use script from data.next which related to a new page.
Fixed code #1
async beforeEnter(data) {
$(data.next.container).find('script').each(function (i, script) {
// ...
})
}
Fixed code #2
async beforeEnter({ next }) {
$(next.container).find('script').each(function (i, script) {
// ...
})
}

Related

How to add a process bar when you waiting for a response from the server

could someone help me with one problem? I want to add a process bar when you waiting for a response from the server (Django 3.x).
Step to reproduce:
On the page 'A' we have the form.
Enter data to form.
Submit POST request by clicking to button on the page 'A'.
Waiting for getting the result on the page 'A'.
Get the result on the page 'A'.
So, I want to add process bar after 4th and before 5th points on the page 'A'. When you will get the result on the page 'A' it should disappear.
Python 3.7
Django 3.x
You can use nprogress, it's a library used for progress bars. Use this inside the interceptor where you can config it for displaying only when request is in progress until finished.
There are lots of ways to do this. I think using jquery would be easier. Basically you just need to prevent submitting the page and do an Ajax request to server. something like
<script type='text/javascript'>
$(document).ready(function () {
$("form").submit(function (e) {
// prevent page loading
e.preventDefault(e);
$('#loadinAnimation').show();
// preapre formdata
$.ajax({
type: "yourRequestType",
url: "yourUrlEndpoint",
data: formdata,
success: function (data) {
$('#loadinAnimation').hide();
// do rest of the work with data
}
});
});
});
</script>
and show appropriate loading animation in your html part
<div id='loadinAnimation' style='display:none'>
<div>loading gif</div>
</div>
You can also do it using UiKit Library in Javascript on your Django Template Page.
Below code is when a file is Uploaded
In your template file (template.html)
<body>
..
<form>
<progress id="js-progressbar" class="uk-progress" value="0" max="100" hidden></progress>
...
<div class="uk-alert-danger uk-margin-top uk-hidden" id="upload_error" uk-alert></div>
...
</form>
</head>
<script type="text/javascript">
$(document).ready(function(){
var bar = document.getElementById('js-progressbar');
UIkit.upload('.js-upload-list', {
url: '',
name : "customer-docs",
params :{
"csrfmiddlewaretoken":"{{csrf_token}}"
},
method : "POST",
concurrent:1,
allow:'*.(csv|xlsx)',
beforeSend: function (environment) {
console.log('beforeSend', arguments);
// The environment object can still be modified here.
// var {data, method, headers, xhr, responseType} = environment;
},
beforeAll: function (args,files) {
console.log('beforeAll', arguments);
},
load: function () {
console.log('load', arguments);
},
error: function (files) {
console.log("---------------")
},
complete: function () {
console.log('complete', arguments);
},
loadStart: function (e) {
console.log('loadStart', arguments);
bar.removeAttribute('hidden');
bar.max = e.total;
bar.value = e.loaded;
},
progress: function (e) {
console.log('progress', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
loadEnd: function (e) {
console.log('loadEnd', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
completeAll: function (data) {
console.log('completeAll', arguments);
console.log('completeAll', data);
let redirect_loc = ""
setTimeout(function () {
bar.setAttribute('hidden', 'hidden');
}, 1000);
// This is the response from your POST method of views.py
data.responseText = JSON.parse(data.responseText)
if(data.responseText.status == 201){
// swal is another library to show sweet alert pop ups
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Done: true
},
}).then((value) => {
switch (value) {
case "Done":
window.location.href = ""
break;
}
});
}
else if(data.responseText.status == 500){
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Ok: true
},
}).then((value) => {
switch (value) {
case "Ok":
window.location.href = ""
break;
}
});
}
}
});
// This block of code is to restrict user to upload only specific FILE formats (below example is for CSV & XLSX files)
(function() {
var _old_alert = window.alert;
window.alert = function(e) {
console.log(e)
if(e.includes("csv|xlsx") || e.includes("Invalid file type")) {
$("#upload_error").html("Invalid file format. Valid formats are CSV, XLSX").removeClass('uk-hidden')
}else if(e.includes("Internal Server Error")) {
$("#upload_error").html("Internal Server Error Kindly upload Documents again").removeClass('uk-hidden')
}
else {
_old_alert.apply(window,arguments);
$("#upload_error").addClass('uk-hidden').html("")
}
};
})();
});
</script>
On your views.py you can do your computation and once done, you can return a response like below
resp_json = {
"status" : 201,
"status_icon" : "success",
"url" : "/",
"message": message
}
return HttpResponse(json.dumps(resp_json))
For more info on SWAL (Sweet Alerts), visit https://sweetalert.js.org/guides/

What is the proper way to work with nested iFrames and Elements in NightwatchJS?

Disclaimer: This is going to be a long question, so apologies up front.
I'm working to get a more reliable UI Automation suite to run and execute for a project. However, there are some outstanding questions and concerns regarding working with the NightwatchJS API that I have been unable to track down an actual answer to.
The primary question I have is when should I chain calls vs using the callback methods in the API?
Here is a very very simple example showcasing what I'm after.
Given the following very, very simplistic HTML :
<html>
<body>
<iframe src="..." class="depth-one-frame">
<html>
<body>
<iframe src="..." id="depth-two-frame">
<html>
<body>
<div id="main">
Page Link
</div>
<iframe src="..." id="depth-three-frame">
<html>
<body>
<div id="container">
<button class="pageButton">Page Button</button>
</div>
</body>
</html>
</iframe>
</body>
</html>
</iframe>
</body>
</html>
</iframe>
</body>
</html>
Say we have a page consisting of 3 or 4 iFrames. For this, we have a basic PageObject like the following:
const MyPageObj = {
elements: {
// in depth-two frame
pageLink: {
selector: '#main .pageLink'
},
// in depth-three frame
pageButton: {
selector: '#container .pageButton'
}
},
commands: [{
getDepth2Frame: function() {
this.api.frame(null)
.waitForElementVisible('.depth-one-frame', 15000)
.element('css selector', '.depth-one-frame', (frame) => {
self.api.frame({
ELEMENT: frame.value.ELEMENT
})
})
.waitForElementVisible('#depth-two-frame', 15000)
.frame('depth-two-frame');
return this;
},
getDepth3Frame: function() {
this.api.frame(null)
.waitForElementVisible('.depth-one-frame', 15000)
.element('css selector', '.depth-one-frame', (frame) => {
self.api.frame({
ELEMENT: frame.value.ELEMENT
})
})
.waitForElementVisible('#depth-two-frame', 15000)
.frame('depth-two-frame')
.waitForElementVisible('#depth-three-frame', 15000)
.frame('depth-three-frame');
return this;
},
waitForView: function(browser) {
return this.waitForElementVisible('#myView', 5000);
},
clickLink: function() {
return this.waitForElementVisible('#pageLink', 5000)
.click('#pageLink');
},
clickButton: function() {
return this.waitForElementVisible('#pageButton', 5000)
.click('#pageButton');
}
}]
};
module.exports = MyPageObj;
There are two ways to interact with this PageObject and I'm having trouble understanding the proper, most efficient way of doing so.
Path #1 (using chaining):
module.exports = {
'The page should load, and we should be able to click a link and a button #smoke': function(browser) {
const pageObj = browser.page.MyPageObj;
pageObj.switchToDepth2Frame()
.waitForView()
.clickLink()
.switchToDepth3Frame()
.clickButton();
}
};
Path #2 (using callbacks):
module.exports = {
'The page should load, and we should be able to click a link and a button #smoke': function(browser) {
const pageObj = browser.page.MyPageObj;
browser.frame(null, () => {
pageObj.waitForElementVisible('.depth-one-frame', 15000, d1Frame => {
browser.execute(function(iframe) {
document.querySelector(iframe).setAttribute('id', 'depth-one-frame')
}, ['.depth-one-frame'])
.frame('depth-one-frame', () => {
pageObj.waitForElementVisible('#depth-two-frame', 15000, () => {
browser.frame('depth-two-frame', d2Frame => {
pageObj.waitForElementVisible('#myView', 5000, myViewEl => {
pageObj.waitForElementVisible('#pageLink', 5000, linkEl => {
pageObj.click('#pageLink');
pageObj.waitForElementVisible('#depth-three-frame', 15000, () => {
browser.frame('depth-three-frame', d3Frame => {
pageObj.waitForElementVisible('#pageButton', 15000, buttonEl => {
pageObj.click('#pageButton');
});
});
});
});
});
});
});
});
});
});
}
};
In Path #1, we use chaining. In Path #2, we use callbacks.
Primary Question: When should you using chaining (like in Path #1) vs callbacks (like in Path #2)?
Secondary Question: Should page objects just return this or the result of the method calls? Ex:
waitForView: function(browser) {
return this.waitForElementVisible('#myView', 5000);
},
vs
waitForView: function(browser) {
this.waitForElementVisible('#myView', 5000);
return this;
},
The issue I'm seeing is that sometimes, the page just sits there and times out while attempting to find elements or sometimes clicks too quickly and doesn't actually perform the action.
Any insight into understanding the best practices path of working with the amazing NightwatchJS API would be muchos appreciated! Thanks!

AngularJS $interval dont stop on .cancel();

I have SPA multi view application in AngularJS, I have defined $interval which is started from another view Controller. When i click a btn with function called and line $interval.cancel(); in it, it does not stop.
Here are examples of my code:
MainController:
$scope.$on("startInterval", function () {
$interval(function warningsControl() {
console.log("Timer stamp!");
$.ajax({
// some web api call which works fine
})
}, 10000);
});
$scope.stop = function () {
$interval.cancel();
}
$scope.logoutButton = {
text: "Logout",
type: "normal",
visible: false,
onClick: function () {
// some working code
$scope.stop();
var logoutBtn = $("#logout-btn").dxButton("instance");
logoutBtn.option({
visible: false
});
}
}
And SecondController:
$scope.authenticateButton = {
type: "default",
text: "Log In",
onClick: function () {
$.ajax({
// some web api calling
success: (data) => {
// some working code
$rootScope.$broadcast("startInterval");
}
})
}
}
This code start interval and everithing is running OK, until the point i click Logout btn - it made everithing except stoping the interval.
Any ideas how to fix it? I would be grateful for advice.
The $interval function should return some sort of ID which you can pass into $interval.cancel(). For example:
var int_id = $interval( func, time )
//...later...
$interval.cancel(int_id)

Expanding a fancyTree via lazy load

I have set up my fancytree to open via lazy loading, and it works very nicely.
$("#tree").fancytree({
selectMode: 1, quicksearch:true, minExpandLevel:2,
autoScroll: true,
source: [{
title: "Root",
key: "1",
lazy: true,
folder: true
}],
lazyLoad: function(event, data) {
var node = data.node;
data.result = {
url: "getTreeData.jsp?parent=" + node.key,
data: {
mode: "children",
parent: node.key
},
cache: false
};
}
});
However, if a user has previously selected a point on the tree, I would like the tree to open to that point.
I have a variable called hierarchy which looks like "1/5/10/11/200" and holds the sequence of keys to that certain point.
The following will not work:
$("#tree").fancytree("getTree").getNodeByKey("1").setExpanded();
$("#tree").fancytree("getTree").getNodeByKey("5").setExpanded();
$("#tree").fancytree("getTree").getNodeByKey("10").setExpanded();
$("#tree").fancytree("getTree").getNodeByKey("11").setExpanded();
$("#tree").fancytree("getTree").getNodeByKey("200").setExpanded();
The reason why it will not work, apparently, is because there needs to be some delay between one statement and the next.
The following code works, however it is in my mind messy:
function openNode(item) {
$("#tree").fancytree("getTree").getNodeByKey(String(item)).setExpanded();
}
function expandTree(hierarchy) {
var i=0;
hierarchy.split("/").forEach(function (item) {
if (item!="") {
i++;
window.setTimeout(openNode, i*100,item);
}
});
Is there any neater way of opening to a specific point on the tree?
The following code seems to do the trick.
It was adapted from
http://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html#loadKeyPath
function expandTree(hierarchy) {
$('#tree').fancytree("getTree").loadKeyPath(hierarchy).progress(function(data) {
if (data.status === "loaded") {
console.log("loaded intermediate node " + data.node);
$('#tree').fancytree("getTree").activateKey(data.node.key);
} else if (data.status === "ok") {}
}).done(function() {});
}

Backbone.js - History is creating two entries

I'm not sure how to express this in code, as I can't seem to locate the problem, but my issue is that Backbone.history seems to be recording two items when a user clicks on a list item in my app.
This is not consistent.
My app has a 4 item navigation at the bottom that links to 4 main sections (the first one being home - routed to '/'). If I load up the app, go to one of the other navigation pages, then click the 'Home' button again and then click one of the navigation options I get a list of items to choose from. If I then choose one two entries are added - Firstly, for some reason, a reference to the home route with /# at the end and then the route for the item I clicked.
The end result is that 'back' then inexplicably takes me to the home page.
If it helps, my router looks like this...
var siansplanRouter = Backbone.Router.extend({
initialize: function () {
var that = this;
this.routesHit = 0;
//keep count of number of routes handled by your application
Backbone.history.on('route', function() { that.routesHit++; }, this);
window.SiansPlanApp.render();
window.SiansPlanApp.router = this;
},
routes: {
'': 'showHome',
'home': 'showHome',
'hub': 'showHome',
'samples': 'showJqmSamples',
'mealplanner': 'showCurrentMealPlanner',
'mealplanner/:planId': 'showMealPlanner',
'recipes': 'showRecipeSearch',
'recipes/:recipeId': 'showRecipe',
'settings': 'showSettings',
'versioninfo': 'showVersionInfo',
'*other': 'showHome'
},
routesHit: 0,
back: function() {
if(this.routesHit > 1) {
window.history.back();
} else {
//otherwise go to the home page. Use replaceState if available so
//the navigation doesn't create an extra history entry
this.navigate('/', { trigger: true, replace: true });
}
},
showHome: function () {
SiansPlanApp.renderHome();
},
showJqmSamples: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Hub.Samples());
},
showMealPlanner: function (planId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ id: planId }));
},
showCurrentMealPlanner: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ current: true }));
},
showRecipeSearch: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Search());
},
showRecipe: function (recipeId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Recipe({ id: recipeId }));
},
showSettings: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.System.Settings());
},
showVersionInfo: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.About.VersionInfo.ListView());
}
});
I've got some basic elements in a kick off file too here...
define(['router', 'regions/r-app', 'jquery', 'domReady'],
function (SiansPlanRouter, AppRegion) {
var run = function () {
// Global click event handler to pass through links to navigate
$(document).on("click", "a:not([data-bypass])", function (e) {
var href = { prop: $(this).prop("href"), attr: $(this).attr("href") };
var root = location.protocol + "//" + location.host + SiansPlanApp.root;
if (href.prop && href.prop.slice(0, root.length) === root) {
e.preventDefault();
Backbone.history.navigate(href.attr, true);
}
});
$.ajaxPrefilter(function (options, originalOptions, jqXhr) {
//options.url = '/api' + options.url;
});
// Create the global namespace region object.
window.SiansPlanApp = new AppRegion();
// Adds the authorization header to all of the API requests.
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("Authorization", 'SiansPlan ' + SiansPlanApp.cookies.getSessionData());
});
// Load up session data if any is present yet - this can't happen until the XHR headers are set up.
SiansPlanApp.session.loadSession();
// Instantiate the router.
window.SiansPlanApp.router = new SiansPlanRouter();
// Boot up the app:
Backbone.history.start();
};
return {
run: run
};
});

Categories