need help w/ lazy loading mat-accordion content - angular-material

I'm using Angular 5 and mat-accordion to show a list of authors. Each author has written multiple books and articles. The author's name appears in the panel-header and the content of the panel shows all of the books, articles, etc.
Because I want to display 100+ authors each with 50+ entries, I don't want to populate the entire accordion and content at once. What I'd like to have happen is that when the user clicks on an author, it kicks off a service that queries the database and then fills the panel content as appropriate. If the panel is closed, the content should remain so re-expanding the panel doesn't kick off another database query.
So when I visit the page, I see the authors Alice, Bob, and Eve. When click on Alice, the app queries the database, gets back Alice's entries, renders the content, then the accordion expands. When I click on Eve, the app should close Alice's panel, query the db, get Eve's entries, render the content, and finally expand the panel.
If I click on Alice again Eve's panel closes, but since the content is already there for Alice, there is no db query or rendering. It just expands. The docs say to use ng-template, but I'm not sure how to do that, and really not sure how to do it so the content remains after the panel is closed. I'm not worried about there being a change to the data that would require getting Alice's data again in case there was a change.
Any examples out there of the best way to handle this?
thanks!
G. Tranter's answer was correct, I was on the right path. If anyone else ends up on this page, here is what I ended up doing.
ngOnInit(){
this.authorsRetrieved.subscribe( authors => {
this.allAuthors = authors as Array;
this.authorsRetrieved = new Array(
Math.max.apply(Math, this.allTrainers.map(function(t){ return t.trainer_id; }))
);
// as authors are added and deleted, the author_id won't equal the number of
// authors, so get the highest id number, create an array that long
// then fill it with blanks so the keys have some value
this.authorsRetrieved.fill([{}]);
});
showAuthorsWorks(authorID: Number = -1){
if(authorID > this.authorsRetrieved.length){
const tempArray = new Array(authorID - this.authorsRetrieved.length + 1);
tempArray.fill([{}]);
this.authorsRetrieved = this.authorsRetrieved.concat(tempArray);
}
// only make the network call if we have to
// because we filled the id array, we can't just use length
if(typeof(this.authorsRetrieved[authorID][0]['manuscript_id']) === 'undefined'){
this.authorWorksService.getAuthorWorks(authorID).subscribe( works => {
this.worksRetrieved.splice(authorID, 0, works as Array<any>);
});
}
I added a check for the almost impossible situation where the array length is less than the max author_id. You have to create an empty array of N elements, then fill that array. If you don't, the length of the empty array is 0, and you can't push data to an array element that doesn't exist. Even though at the chrome console it says the length is N and the elements are there, just empty.
Thanks again!

If you are referring to the MatExpansionPanelContent directive used with ng-template, all that does is delay loading content until the panel is opened. It doesn't know whether or not it has already been loaded. So if you are using a bound expression for content such as {{lazyContent}} that will be evaluated every time the tab is opened. You need to manage content caching yourself. One easy way to do that is via a getter.
In your component:
_lazyContent: string;
get lazyContent() {
if (!this._lazyContent) {
this._lazyContent = fetchContent();
}
return this._lazyContent;
}
Plus in your HTML:
<mat-expansion-panel>
...
<ng-template matExpansionPanelContent>
{{lazyContent}}
</ng-template>
....
</mat-expansion-panel>
So the ng-template takes care of the lazy loading, and the getter takes care of caching the content.

Related

How do you get the column order for the Grid in Vaadin 14?

In Vaadin 8 you could just do the following for example to get a list of the columns in the order displayed on the screen.
String columnOrderPreference = ((List<Grid.Column>)grid.getColumns()).stream()
.map(Grid.Column::getId)
.collect(Collectors.joining(VALUE_SEPARATOR));
This was especially handy because you could then save that string and then call:
grid.setColumnOrder(columnOrderPreference.split(VALUE_SEPARATOR));
In Vaadin 14 (ignoring that getId() should now use getKey()) this is no longer possible because the getColumns() list is now always returned in the order they were added and not the order in which they are ordered. You can still call setColumnOrder(...) (with different parameters - list of grid.getColumnByKey(columnKey)) but I cannot figure out how to get the list of columns in the order they are displayed.
This is especially useful for trying to save the column order the user has set/changed when they come back to the same page (with the Grid).
You can listen for
ColumnReorderEvent
on the grid.
Registration addColumnReorderListener(ComponentEventListener<ColumnReorderEvent<T>> listener)
The event holds:
/* Gets the new order of the columns. */
List<Grid.Column<T>> getColumns()
Unfortunately in Vaadin 14 getColumns does not return the columns in the right order. You can get the order when with the event GridReorderEvent as said before and you need to store it. There is a feature request here ( that will give you some technical reasons if you're interested): https://github.com/vaadin/flow-components/issues/1315
You can add a comment or vote for it, because it makes the migration from Vaadin 8 harder.

How to Let Chrome History Ignore Part of URL

As my work involves viewing many items from a website, I need to know which items have been visited and which not, so as to avoid repeated viewing.
The problem is that the URL of these items include some garbage parameters that are dynamically changing. This means the browser's history record is almost useless in identifying which items have already been viewed.
This is an example of the URL:
https://example.com/showitemdetail/?item_id=e6de72e&hitkey=true&index=234&cur_page=1&pageSize=30
Only the "item_id=e6de72e" part is useful in identifying each item. The other parameters are dynamic garbage.
My question is: how to let Chrome mark only the "example.com/showitemdetail/?item_id=e6de72e" part as visited, and ignore the rest parameters?
Please note that I do NOT want to modify the URLs, because that might alarm the website server to suspect that I am abusing their database. I want the garbage parameters to be still there, but the browser history mechanism to ignore them.
I know this is not easy. I am proposing a possible solution, but do not know whether it can be implemented. It's like this:
Step: 1) An extension background script to extract the item_id from each page I open, and then store it in a collection of strings. This collection of strings should be saved in a file somewhere.
Step: 2) Each time I open a webpage with a list of various items, the background script verifies whether each URL contains a string which matches any one in the above collection. If so, that URL would be automatically added to history. Then that item will naturally be shown as visited.
Does the logic sound OK? And if so how to implementable it by making a simple extension?
Of course, if you have other more neat solutions, I'd be very interested to learn.
Assuming that the link to the items always have the item_id, that would work, yes.
You would need the following steps:
Recording an element
content_script that adds a code to the product pages and tracks it.
On accessing the product page:
i. You can extract the current product id by checking the URL parameters (see one of these codes).
ii. You use storage api to retrieve a certain stored variable, say: visited_products. This variable you need to implement it as a Set since it's the best data type to handle unique elements.
iii. You check whether the current element is on the list with .has(). If yes, then you skip it. If all is good, it should always be new, but no harm in checking. If not, then you use add() to add the new product id (although Set will not allow you to add a repeated items, so you can skip the check and just save add it directly). Make sure you store it to Chrome.
Now you have registered a visit to a product.
Checking visited elements
You use a content_script again to be inserted on product pages or all pages if desired.
You get all the links of the page with document.querySelectorAll(). You could apply a CSS selector like: a[href*="example.com/showitemdetail/?item_id="] which would select all the links whose href contains that URL portion.
Then, you iterate the links with a for loop. On each iteration, you extract the item_id. Probably, the easiest way is: /(?:item_id=)(.*?)(?:&|$)/. This matches all characters preceded by item_id= (not captured) until it finds an & or end of the string (whichever happens first, and not captured).
With the id captured, you can check the Set of the first part with .has() to see whether it's on the list.
Now, about how to handle whether it's on the list, depends on you. You could hide visited elements. Or apply different CSS classes or style to them so you differentiate them easily.
I hope this gives you a head start. Maybe you can give it a try and, if you cannot make it work, you can open a new question with where you got stuck.
Thanks a lot, fvbuendia. After some trial and error elbow grease, I made it.
I will not post all the codes here, but will give several tips for other users' reference:
1) To get the URL of newly opened webpage and extract the IDs, use chrome.tabs.onUpdated.addListener and extractedItemId = tab.url.replace(/..../, ....);
2) Then save the IDs to storage.local, using chrome.storage.local.set and chrome.storage.local.get. The IDs should be saved to an object array.
1) and 2) should be written in the background script.
3) Each time the item list page is opened, the background calls a function in the content script, asking for all the URLs in the page. Like this:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(changeInfo.status == "complete") {
if(tab.url.indexOf("some string typical of the item list page URL") > -1) {
chrome.tabs.executeScript(null, { code: 'getalltheurls();' });
} }
});
4) The function to be executed in content script:
function getalltheurls() {
var urls = [];
var links = document.links;
for (var i = 0; i < links.length; i++) {
if(links[i].href.indexOf("some string typical of the item list URLs") > -1) { urls.push(links[i].href);}
}
chrome.runtime.sendMessage({ urls: urls });
};
5) Background receives the URLs, then converts them to an array of IDs, using
idinlist = urls[i].replace(........)
6) Then background gets local storage, using chrome.storage.local.get, and checks if these IDs are in the stored array. If so, add the URL to history.
for (var i = 0; i < urls.length; i++) {
if (storedIDs.indexOf(idinlist) > -1 ) { chrome.history.addUrl({ url: urls[i] }); }
}

Angular material virtural repeat ajax requests 2nd page altough there is no 2nd page

I have an angular virtual repeater which works fine most of the time but sometimes, based on the item count I guess, it makes a secondary paging call to the server altough there are is no second page.
For example, my page size is set to 50. The ajax call returns 36 items, I update the internals like this:
$http.get(url).success(function (data) {
this.numItems = data.result.pagination.totalRecords;
var items = data.result.departments;
....// (I also tried changing the order of these lines)
}).bind(this)
Then, the list gets displayed for a split second followed by a second ajax call to the next page. Which returns nothing and due to my server side code the pagination is also 0 which eventually deletes the existing data.
Now, I could modify the server side to return the row count even if an out of bound page number is requested but I'd rather like to troubleshoot the issue of firing a second ajax pagination call, when there is clearly no more data to display.
This isssue apears about half the time when there is only one page. I mean, I keep clicking a button that refreshes the list and sometimes it shows OK and sometimes not.
I am using 1.1.1/angular-material, the repeater is in a full height flex div.
Thanks
Your this.numItems is bound to your callback function (data){}); and not the parent function. Try to declare something like var self = this:
var self = this;
$http.get(url).success(function (data) {
self.numItems = data.result.pagination.totalRecords;
var items = data.result.departments;
you still may need to add some logic to check the numItems somewhere outside.

Vaadin - Table column order

Anybody know how to/or it is possible - create a Table with column specific order; configuration order which was before save - example in DB,
and uploaded at specific view on? also I wonder how to take generate this columns headers and content from POJOS class - beans.
Any good ideas?
setVisibleColumns
The Table::setVisibleColumns does double-duty:
Controls which columns are visible, and
Sets the order in which the columns appear.
Call Table::getVisibleColumns to see current ordering.
Doc
This is well described in:
Book of Vaadin > Table
Sampler > User Interface > Data Presentation > Table
Table API JavaDoc
Example Code
Basically, you need the code like this to control columns order and also set list of bean instances as datasource.
Code is not tested, just a demonstration. Valid for Vaadin 6, but I guess no significant changes comparing to Vaadin 7.
table = new Table();
// Wrap your beans collection into vaadin data container. There are many
// types of them , check Book of Vaadin.
BeanItemContainer<Bean> container = new BeanItemContainer<Bean>(Bean.class)
container.addBean(new Bean());
// Set collection of your beans as data source.
// Columns will be created for each property, vaadin uses reflection.
table.setContainerDataSource( container );
// You can select to display only properties you want, not all.
// Order matters. You can get columns list from any source - for example
// store in your DB.
table.setVisibleColumns( new Object[] {"prop1", "prop2"} );
// You can set column headers (by default vaadin will set them same as bean
// properties names). Order matters, should match above order.
table.setColumnHeaders( new String[] {"Property 1", "Property2"} );
The answer by Sergey Makarov is correct. This answer provides further information.
User’s Reordering
You may allow the user to drag-and-drop columns in a table to re-order them at runtime. To enable this feature, call isColumnReorderingAllowed.
You can use a listener to be informed when such a reorder event occurs.
The user’s re-ordering lasts only for this work session. If you want to maintain the user’s order in future work sessions, you must somehow persist their desired order and apply the order when instantiating the table again.
Losing The Order
If you replace the data source of the table, your column order will be reset. You can get the current order before the change, then restore. Example code follows.
// Visible status & ordering.
java.lang.Object[] visibleColumns = this.exampleTable.getVisibleColumns();
// ------| Fresh Data |--------------
// Put fresh data into the Table.
this.exampleTable.setContainerDataSource( freshBeanItemContainer );
// ------| Restore Config |--------------
// Visible status & ordering.
this.exampleTable.setVisibleColumns( visibleColumns ); // Assumes the table has the same properties.
By the way, as with visibility and order, being collapsed will also be reset with fresh data. You may want to track and restore column collapsing as well as ordering.

Dynamically Loading LI's in JQueryMobile 1.0

I've just updated my project from jquerymobile 1.0a1 to version 1.0.
I've encountered a problem with dynamic content. Based on an ajax search I populate an unordered list with list items. Previous the following code refreshed the list so that all the styling appeared correctly:
$('#myContent').find("ul").listview();
$('#myContent').find("ul").listview('refresh');
However as of 1.0 this no longer seems to work.
The list appears but the styling is all wrong and the data-theme on all the elements gets ignored.
Has anyone come across a similar issue with updating and come across the solution.
Updating lists If you add items to a listview, you'll need to call the refresh() method on it to update the styles and create
any nested lists that are added. For example:
$('#mylist').listview('refresh');
Note that the refresh() method only affects new nodes appended to a
list. This is done for performance reasons. Any list items already
enhanced will be ignored by the refresh process. This means that if
you change the contents or attributes on an already enhanced list
item, these won't be reflected. If you want a list item to be updated,
replace it with fresh markup before calling refresh.
http://jquerymobile.com/demos/1.0/docs/lists/docs-lists.html
if #myContent is the listview you can do this:
$('#myContent').listview('refresh');
if #myContent is the page you can do something like this:
$('#myContent').trigger('create');
Create vs. refresh: An important distinction Note that there is an important difference between the create event and refresh method
that some widgets have. The create event is suited for enhancing raw
markup that contains one or more widgets. The refresh method should be
used on existing (already enhanced) widgets that have been manipulated
programmatically and need the UI be updated to match.
For example, if you had a page where you dynamically appended a new
unordered list with data-role=listview attribute after page creation,
triggering create on a parent element of that list would transform it
into a listview styled widget. If more list items were then
programmatically added, calling the listview’s refresh method would
update just those new list items to the enhanced state and leave the
existing list items untouched.
http://jquerymobile.com/demos/1.0/docs/pages/page-scripting.html
What you want can be achieved by replacing your 2 lines of code with the following:
$('#myContent ul').listview('create');
Hope this helps...
I've had this issue. The reason you are getting things all messed up is you are initalizing and refreshing the element multiple times. I noticed I had 2 different functions running that would call .listview('refresh') on the same element. After I took one out the themes and data went back to looking normal. Also are you getting any JS errors?
EDIT:
To be more specific you are calling .listview() somewhere in your code 2 times which is initializing it twice. I would wait to before you page is loaded to run the refresh so you only call it once.
Another thing you could do is check if the element is initialized already or not so you don't do it twice. Just check the element or in some cases the parent to see if the class ui-listview is present.
var element = $('#myContent').find('ul');
if ($(element).hasClass('ui-listview')) {
//Element is already initialized
$(element).listview('refresh');
} else {
//Element has not been initiliazed
$(element).listview().listview('refresh');
}
Just an FYI you can chain those events to look like $('#myContent').find('ul').listview().listview('refresh');
It cand be achived through.
$('#myContent').listview('refresh');
The below snippet shows you to load data from xml and dynamically create a list view.
function loadData()
{
$.ajax({
url:"BirthdayInvitations.xml",
dataType: "xml",
success: function(xml)
{
$(xml).find("event").each(function()
{
$("#mymenu").append('<li>' + this.textContent + ' </li>');
});
$("#mymenu").listview('refresh');
}
});
}
See if this is related to ur question http://www.amitpatil.me/demos/jquery-mobile-twitter-app/ and this one also http://www.amitpatil.me/demos/ipad-online-dictionary-app/
In first example i am using listview('refresh'); method and in second example i am using
$(document).page("destroy").page();

Resources