I am trying to build a web app with aurelia and I couldn't find a way to add data via AJAX after everything on page has been rendered. The scenario (simplified):
There's a page, some part of which is dynamically composed from a component (say, data-table). data-table has a title, and table-rows to show the data. data-table should load it's data dynamically via an AJAX call. What I want is, loading the data after the page is rendered.
I tried using promises but it does the opposite, ie, aurelia waits until the promise is resolved before attaching the view (explained by Jeremy Danyow as well: "...In this example Aurelia will wait for the Promise returned by the activate method to resolve before binding the view to the viewmodel." (in his post titled "ES7 async/await with Aurelia")
This causes the page remain stalled until all data is loaded.
A simple code example is provided below. Here, if you navigate to this page, you won't see anything (or the page wont get attached) until all data is loaded. What I want is, to load the page and show "Table for ..." title, and at the same time start loading data in the background and show the table itself when loading completes. The desired behavior is illustrated in the following "mock" screenshots.
before the ajax requrest is completed
after the ajax request is completed
Additionally, the tables may need to be updated based on user choices (additional data may be loaded and added to the tables) or additional tables may need to be added to the page.
I don't think the desired behavior matches any of the bind/attached/detached etc. life-cycle behaviors (but could be wrong). This could be implemented utilizing a variant of body.onload (or jquery etc.) but I wonder if this is possible to do using aurelia only (or mostly).
Maybe, being able to load data after everything is attached (eg. a "postattached" callback) could help. In that case, I would load all necessary components with their already loaded data (eg. their titles) and show them. Then, in the "postattached" section I will start loading data.
Sample code:
test.ts
export class testPage {
ids: number[] = [1,2,3] // for example 1,2,3; will be dynamically loaded as well
}
test.html
<template>
<h1>Test</h1>
<div repeat.for="id of ids">
<compose view-model="./components/table" model.bind="id"></compose>
</div>
</template>
table.ts
import { Loader } from './loader';
export class table {
id: number
tableData: number[][] = []
activate(model) {
this.id = model
}
attached() {
Loader.LoadData(this.id).then((res)=>{this.tableData = res})
}
}
table.html
<template>
<h2>Table for ${id}</h2>
<div repeat.for="rows of tableData">${rows}</div>
</template>
loader.ts
export class Loader {
static LoadData(tid): Promise<number[][]> { //simple stub to imitate real data loading
let data: number[][] = []
switch (tid) {
case 1:
data.push([11, 12, 13])
data.push([14, 15, 16])
break;
case 2:
data.push([21, 22, 23])
data.push([24, 25, 26])
break;
case 3:
data.push([31, 32, 33])
data.push([34, 35, 36])
break;
}
this.sleep()
return new Promise((resolve, reject) => {
this.sleep()
resolve(data)
})
}
protected static sleep(): boolean { // just to imitate loading time
let miliseconds = Math.floor(Math.random() * (3 - 1 + 1) + 1);
var currentTime = new Date().getTime();
console.debug("Wait for a sec: " + miliseconds)
while (currentTime + miliseconds * 1000 >= new Date().getTime()) {
}
return true
}
}
edit: Corrected code which was miscarried to the example here
You should try using window.timeout to emulate the loading time. Because Javascript is single threaded, your sleep function will block all other execution on the thread.
This answer is possibly a bit over the top but explains in more detail how to write a sleep function in javascript: What is the JavaScript version of sleep()?
Related
I have a Virtualize table that does exactly what it needs to do and does it great. I now have the need to add a tooltip/popover to certain rows. This is not difficult, but I am noticing that it will not get the JavaScript portion of the setup for these tooltips to run. It seems like when I click in the table somewhere it realizes things have changed and calls the JS setup function. At that point it works great.
<Virtualize #ref="RowsVirtualizerLeft" ItemsProvider="RowsLoader" Context="row" ItemSize="RowHeight">
<ItemContent>
...
And the loader:
public async ValueTask<ItemsProviderResult<VirtualizedRow>> RowsLoader(ItemsProviderRequest request)
{
var result = BodyRows
.Where(x => x.IsVisible);
return new ItemsProviderResult<VirtualizedRow>(
result
.Skip(request.StartIndex)
.Take(request.Count),
result.Count()
);
}
The overridden OnAfterRender event:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JSRuntime.InvokeVoidAsync("setupPopovers");
}
And lasty the JavaScript method:
function setupPopovers() {
const triggers = document.querySelectorAll("[id^='swift-trigger-']");
triggers.forEach((trig) => {
...
That is until I scroll down the dataset to a new area of data. These new rows also need to be "setup" and are not. It seems like any interaction on the table (like the click) will alert Blazor to call the OnAfterRenderAsyn which in turn calls me JS setup function and those tooltips now work.
So, my question is, is there a way to know when the UI is done rendering from the Virtualize component? I am not seeing an event I can catch from it? I suppose at worst, I could make a call that is paused for a second and then does the setup, but that does not seem to be a guarantee to always be enough time. Not like I would want to slow the app down either.
Is there a JavaScript page event I can catch that knows when things are done rendering? Perhaps whatever JavaScript Microsoft is doing has a way to catch an event from there?
More Info
I am not able to add any third party projects to the solution. So the suggestion by #Vikram-reddy will not work. We have the Bootstrap 5 (css only, not JS) and Popper.js. Plus whatever we write ourselves.
const triggers = document.querySelectorAll("[id^='swift-trigger-']");
The tooltip will not work in a virtualization scenario.
Please try the below component from BlazorBootstrap.
Demo: https://demos.getblazorbootstrap.com/tooltips
Github: https://github.com/vikramlearning/blazorbootstrap
Sample Code:
<Tooltip Title="Tooltip Left" Placement="TooltipPlacement.Left">Tooltip Left</Tooltip>
OR
Use the below approach on each row when it renders
var exampleTriggerEl = document.getElementById('example')
var tooltip = bootstrap.Tooltip.getOrCreateInstance(exampleTriggerEl)
tooltip.show();
I have trouble correcting an error in my script, the console simply states "uncaught exception: undefined". Could you help me to identify the source of the problem ?
General explanation: I am coding a "copy to clipboard" share-button for a web-app. When the button is pressed, a link get generated and copied to the user clipboard. The link generation is dependent on external factors, reports that are stored in an Oracle database. There is an array named Reports that keeps track of them, but it needs to be up to date to generate a functional sharing-link. The anonymous function getterReports does it in an asynchronous way (JQuery + AJAX).
In order to copy my data to the clipboard, I am using the method described in this link: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
A snackbar (share_bar) is displayed on the screen to indicate that the operation is over.
Here is the raw code:
function copySharingURLToClipboard() {
let share_bar = document.getElementById("modal_share");
//getterReports ensures that the "Reports" array of reports objects is properly updated beforehand (AJAX <-> Database)
getterReports().then(function () { //this ".then" ensurea that the main code is only executed after getterReports is over
let report = getSavedReport(); //finds a report object by iterating the "Reports" array
if (report == undefined) { //no saved report were found
//snackbar related code :
document.getElementById("popup_message").innerHTML = "Temporary reports cannot be shared: please save your report first.";
share_bar.style.backgroundColor = "red";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove('show'); }, 4200);//removing the class after the animation is done
}
else {
let data = BaseURL + "/DisplayReportPage.aspx?id=" + report.id; //generating the string that we want to copy in the clipboard
navigator.clipboard.writeText(data).then(function () {
/* success */
//snackbar related code :
document.getElementById("popup_message").innerHTML = "URL copied to the clipboard successfully";
share_bar.style.backgroundColor = "lightgreen";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove("show"); }, 4200); //removing the class after the animation (it takes 4s + eventual minor latency)
}, function () {
/* failure case */
});
}
});
}
During the execution of copySharingURLToClipboard(), the "uncaught exception: undefined" error happens in the following line, at ".then":
navigator.clipboard.writeText(data).then(function () {
My Web-app doesn't crash though, but the code is simply skipped. The error is only displayed when going step by step in devtool (otherwise the portion of the code is simply skipped without any error message). My Firefox browser is up to date.
All involved function are working fine in other part of the code. I am confident in the fact that getterReports() is working fine on its own. The same is true for the copy to clipboard part of copySharingURLToClipboard(), without the getterReports() updating the "Reports" array, it works fine (but its results are not up to date obviously):
function copySharingURLToClipboard() {
let share_bar = document.getElementById("modal_share");
//getterReports ensures that the "Reports" array is properly updated beforehand (AJAX <-> Database)
let report = getSavedReport(); //finds a report object by iterating the "Reports" array
if (report == undefined) { //no saved report were found
//snackbar related code :
document.getElementById("popup_message").innerHTML = "Temporary reports cannot be shared: please save your report first.";
share_bar.style.backgroundColor = "red";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove('show'); }, 4200);//removing the class after the animation is done
}
else {
let data = BaseURL + "/DisplayReportPage.aspx?id=" + report.id; //generating the string that we want to copy in the clipboard
navigator.clipboard.writeText(data).then(function () {
/* success */
//snackbar related code :
document.getElementById("popup_message").innerHTML = "URL copied to the clipboard successfully";
share_bar.style.backgroundColor = "lightgreen";
share_bar.classList.add('show');//launching the fade-in fade-out animation from the associated css class
setTimeout(function () { share_bar.classList.remove("show"); }, 4200); //removing the class after the animation (it takes 4s + eventual minor latency)
}, function () {
/* failure case */
});
}
}
Am I missing something simple ? A syntax error maybe ?
Firefox does not allow async copy-to-clipboard functionality.
Calling navigator.clipboard.writeText() in a promise resolve or an async function does not work intentionally. The code is working properly on other browsers.
After some research, it appears that there is still no available alternative implementation on Firefox. The logic of the project as a whole needs to be adapted in order to not use asynchronous functions when copying something in the clipboard.
I want my extension to add elements to a page. But the site uses React which means every time there's a change the page is re-rendered but without the elements that I added.
I'm only passingly familiar with React from some tutorial apps I built a long time ago.
I implemented #wOxxOm's comment to use MutationObserver and it worked very well.
static placeAndObserveMutations (insertionBoxSelector) {
let placer = new Placement ();
placer.place(insertionBoxSelector);
placer.observeMutations(insertionBoxSelector);
}
place (insertionBoxSelector) {
let box = $(insertionBoxSelector)
this.insertionBox = box; //insertionBox is the element that the content
// will be appended to
this.addedBox = EnterBox.addInfo(box); //addedBox is the content
// Worth noting that at this point it's fairly empty. It'll get filled by
// async ajax calls while this is running. And all that will still be there
// when it's added back in the callback later.
}
observeMutations(insertionBoxSelector) {
let observer = new MutationObserver (this.replaceBox.bind(this));
// this.insertionBox is a jQuery object and I assume `observe` doesn't accept that
let insertionBox = document.querySelector(insertionBoxSelector);
observer.observe(title, {attributes: true});
}
replaceBox () {
this.insertionBox.append(this.addedBox);
_position (this.addedBox);
}
That being said someone suggested adding the content above the React node (i.e. the body) and just positioning the added content absolutely relative to the window. And as this will actually solve a separate problem I was having on some pages of a site I'll probably do that. Also, it's far simpler.
But I thought this was still an interesting solution to a rare problem so I wanted to post it as well.
I use a page object model to store locators and business functions specific to a certain page of the application under test. You can see a sample page object pattern style here
https://github.com/bhreinb/SYSTAC
All the page objects go through a function called pageLoaded() method which can be found in application under test page object. The pageLoaded is used within the base page. Each page object must implement that method or the framework throws an exception to force the user to implement this. The pageLoaded checks attributes of the page belonging to the application under test (for example the page title, a unique element on the page etc) to verify we are on the target page.
I found this works ok in most cases but when a page navigation occurs with multiple re-directs I have to await for elements to exist and be visible as TestCafe prematurely interacts with the application under test. Here is my code sample
pageLoaded() {
this.userNameLabel = Selector('[data-automation-id="userName"]').with({ visibilityCheck: true });
this.passWordLabel = Selector('[data-automation-id="password"]').with({ visibilityCheck: true });
logger.info('Checking Elements That Contain UserName & PassWord Labels For Existence To Confirm We Are On Tenant\'s Target Page');
const promises = [
this.userNameLabel.exists,
this.passWordLabel.exists,
];
return Promise.all(promises).then((elementsExists) => {
const userNameVisible = elementsExists.pop();
const passWordVisible = elementsExists.shift();
if ((userNameVisible) && (passWordVisible)) {
return true;
}
logger.warn(`PageLoaded Method Failure -> Element Containing UserName Is Visible '[${userNameVisible}]'`);
logger.warn(`PageLoaded Method Failure -> Element Containing PassWord Is Visible '[${passWordVisible}]'`);
return false;
});
}
this will fail but will pass if I change the promises to be like this
const promises = [
t.expect(this.userNameLabel.exists).ok({ timeout: 20000 }),
t.expect(this.passWordLabel.exists).ok({ timeout: 20000 }),
];
which is not a pattern I particularly like as I don't want assertions logic to be in the page object pattern plus I want the outcome of the promises to return a true or false if that makes sense. Is their anyway I change the expectation to operate on the method for instance
t.expect(pageLoaded()).ok({ timeout: 20000 })
or any other alternative. In addition how can you specify TestCafe to wait for window.onload event to be fired rather than Domcontentloaded in the case where multiple page redirects happen? Thanks in advance as to any help with the above.
As #AlexKamaev mentioned in his comment, you can specify the timeout option in the Selector constructor. It'll allow you to avoid using the timeout value as an assertion argument. In addition, have a look at the setPageLoadTimeout method, which will allow you to wait until the page is fully loaded.
I am currently building a custom task presenter for my PYBOSSA project. I have almost implemented it, but am stuck at the following javascript function -
pybossa.taskLoaded(function(task, deferred) {
if ( !$.isEmptyObject(task) ) {
console.log("Hello from taskLoaded");
// load image from flickr
var img = $('<img />');
img.load(function() {
// continue as soon as the image is loaded
deferred.resolve(task);
pybossaNotify("", false, "loading");
});
img.attr('src', task.info.url).css('height', 460);
img.addClass('img-thumbnail');
task.info.image = img;
console.log("Task ##"+task.id);
}
else {
deferred.resolve(task);
}
});
According to the docs -
The pybossa.taskLoaded method will be in charge of adding new items to the JSON task object and resolve the deferred object once the data has been loaded (i.e. when an image has been downloaded), so another task for the current user can be pre-loaded.
But notice my function. I have logged the task ids, the function loads. It loads 2 tasks. After logging, the console shows -
Task ##256
Task ##257
Also I have tried various other statements. They also execute twice. What I think is that if now I try to insert question of the current task, the function of the next task will also be put along with its respective image. How do I resolve this?
You are seeing double for a good reason :-) PYBOSSA preloads the next task, so the final user does thinks that the next task loads really fast (actually instantly).
While for some projects this might not be a problem, in some cases the user needs to download big images, check other APIs, etc. so it takes 2 or 3 seconds (or even more) to get everything before presenting the task to the user.
PYBOSSA.JS handles this scenario, as soon as the data has been downloaded, it requests a new task, but instead of presenting it, you have it in your window. As you are building your own template, you will have to add that data into the dom (via hidden elements) and then in the pybossa.presentTask method, you will check which task is being loaded, and show/hide the previous one.
In pybossa.saveTask, you can delete the previous DOM elements.
I hope this is now more clear. If you don't want this, you can use jQuery or Axios to request a task, save it and load the next one when you want ;-)