Pagination Ideas for Angular2 and ReactiveX - pagination

I'm learning ReactiveX. I've snipped the error checking, logging, and other bits out to make this easier to read.
I have a service that returns a collection of objects as JSON:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json());
}
My component calls the service method and stores the data in an array:
panels: Panel[] = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe(data => this.panels = data);
}
The goal is to display this data in groups in my template:
<ol>
<li *ngFor="#panel of panels">
<h3>{{panel.startDate}}</h3>
</li>
</ol>
Now I want to add pagination and display only three or four panels at a time.
My first idea was to use bufferCount to emit the objects in groups:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json())
.bufferCount(3,3);
}
Now I have a multidimensional array, so I have to update the component accordingly:
panels: Array<Panel[]> = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe( data => this.panels = data );
}
I thought I would have a nice and tidy array with each index having three members of the collection. I was wrong, and the entire collection is now stored in data[0]. Next, I tried switching the chain up a bit:
getNextPanel() {
return this.http.get(this._nextPanelUrl)
.bufferCount(3,3)
.map(res => <Panel[]> res.map(r => <Panel> r.json()));
}
Whoa. I obviously need someone to save me from myself at this point. Look at my lambdas! Data isn't even going to flow all the way back to the component at this point. This is when I started thinking maybe I don't need to learn how to do this the ReactiveX way … .
My next course was to try and iterate through the values with Angular. I tried using a few variables with the slice pipe:
<ol>
<li *ngFor="#panel of (panels | slice:start:items)">
<h3>{{panel.startDate}}
</li>
</ol>
<button (click)="start = start + start"></button>
Even though Angular 2 is still in beta, I could tell that I was getting tired when the parser kept barking at me for using operators and expressions where they don't belong.
I'm ready to learn from these mistakes so I can make bigger ones. Any suggestions?
[EDIT]
I've decided to use ng2-pagination because it does exactly what I want it to do. I'm not going to post that as the answer, however, because I still want to try and implement it with rxjs.
So if you've come this far, and you just need something that works, ng2-pagination (in beta 2 as of this writing) works very well.

Really late, but I hope this might help someone else with this issue.
I think the problem with your implementation is that you are overwriting the this.panel variable with each onNext event on the subscriber.
With this change should work as expected:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json())
.bufferCount(3)
.toArray();
}
and then:
panels: Panel[][] = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe( data => { this.panels = data } );
}
The idea is to merge all onNext events into an array (using the toArray method) that will be emited as the single onNext of that new Observer, and will contain all events.

Related

How do I prevent a re-render of a large list?

I have a 60x30 grid for a game editor and as cells are updated, a new array is created to hold the state.
The problem is that when I update that grid array, this changes the property and it causes render() to recreate the grid. This seems almost obvious but then what do my options become?
If this is overly specific, imagine just a huge list of items and you have an immutable array in which one of the items properties must change.
render() {
return html`
${this.data?.cells.map((row) => {
return row.map((cell) => {
return html`<editor-cell .data="${cell}"></editor-cell>`;
});
})}
`;
}
Coincidentally, I had the same problem on Angular with a for loop only it had trackBy which used the index or item.id to prevent the recreation of a list of items. I just accepted the unicorns for that but here it is the same issue.
Question:
What am I missing about immutable states here? I totally understand why this is happening in that, its a new array and so lit element just renders what it deems a new array. I want that, but once the grid has been rendered, I don't understand the separation between rendering and data updates. I'm either missing a key lifecycle understanding, or my approach to state is just totally whack.
If you update the complete array this change the memory reference and then lit-html has to re-render the whole array because it doesn't know what item changes.
In the lit-html documentation you have a section about Repeating templates that explain that very well.
In your case you should use repeat directive, that performs efficient updates of lists based on user-supplied keys:
render() {
return html`
${repeat(this.data?.cells, row => row.id,
row => html`${repeat(row, cell => cell.id,
cell => html`<editor-cell .data="${cell}"></editor-cell>`
)}`
)}
`;
}
Notice the importance of the second argument, that it's the guaranteed unique key for each item.

Loop with break or continue based on user input

I loosely know how to use a loop. I've used simple ones and understand how it works. However, I have a situation where I think a loop would be useful, but I'm not quite certain how to get the desired result. I am self-taught in javascript and I've tried poking around some other posts, but I don't recall seeing anything that helped in my case. This may very well be one of my gaps in understanding.
What I'd like this to do: This code is going to be taking the top card off a deck of cards for a TCG. Each card has a "type". I want the person to input a command into Discord like "!flipcard 10". I would like the bot to make a loop to flip cards off the top of the deck until 10 is reached OR a certain card type is flipped over. The card "types" I am working with are "action", "evo", "flip" and "hero". I only want the bot to react differently to the "Flip" card type. I have gotten working code to where I can do this if the user inputs !flipcard but does it one at a time. The maximum amount of cards that can ever be flipped is 34 cards or so, but this is less likely to happen than lower numbers.
I just wrote the code up from what I think I need to do, but I get stuck with not knowing exactly where to go next. So it isn't exactly functioning code yet. I do get an "illegal break statement" error currently, so I can't progress much more.
for (i = 0; i < damageToTake; i++) {
cardRemoved = deckAarray.shift()
//Bunch of stuff edited out for Embed mentioned below
if (cardType == "Flip"){
message.channel.send(flipEmbed)
const filter = m => m.author.id === message.author.id
author.send(`You revealed a Flip card! Do you wish to use it? Please reply with 'yes' or 'no'.`).then((newmsg) => {
newmsg.channel.awaitMessages(filter, {
max: 1,
}).then((collected) => {
reply = collected.first();
if (reply.content == 'yes'){
console.log("Yes reply.")
break;
} else if (reply.content == 'no'){
console.log("No reply")
}
})
})
} else if (cardType != "Flip"){
message.channel.send(nonflipEmbed)
continue;
}
}
})
}
Like I mentioned, the code isn't working to a point I can try it and get a problem because I am getting an "illegal break statement" error.
I think I am on the right track, but I'm uncertain how to make it work exactly how I'm hoping.
So, to conclude...
I would like this code to to the following:
1)User inputs command and specifies how many cards to flip over.
2)Loop should flip cards (displaying the correct embed based on card type)
3)When a "flip" card type is revealed, I would like the code to pause, essentially, to ask the author whether or not they would like to use the Flip card that was revealed. If yes, stop the loop. If no, continue the loop from where it left off until the desired number are flipped OR if another Flip card type is revealed.
Thanks for the help in advance!
What you are trying to do is impossible without code modification. The reason for this is that for loops are synchronous, while promises are not. This means that .then() will only be reached after the loop completes.
One way around this is to wrap everything inside an async function and run your loop in there. Then, use try-catch clauses to break out of the loop when needed. Consider the toy example below:
( async () => {
for (i = 0; i < 100; i++) {
console.log('i:', i);
try {
const val = await randomPromise();
console.log('should resolve and break!');
console.log(val);
break;
} catch (e) {
console.log('moving on...');
}
}
})();
function randomPromise() {
return new Promise( (resolve, reject) => {
const val = Math.random();
if (val > 0.9) {
resolve(val);
} else {
reject(0);
}
})
}
You've changed contexts -- as in you have two functions that know nothing about each other's control flow (e.g.: if/then, for loops, while loops, etc). They cannot influence each other in the way you're wanting. What you're doing is the equivalent of this pseudo code:
function printAllNumbers() {
for(currentNumber : allNumbers) {
printNumber(currentNumber);
}
}
function printNumber(num) {
print(num);
break; // This won't work because it has no clue about for/loop in other func
}
printNumber is scoped so that it only knows what its own function (those 2 lines) are doing. It has no clue about the for/each in the printAllNumbers call, hence the error you're seeing.
And what I said above is only partially true -- you're actually maybe 2 functions deep at this point because you've called .then() and then after your .newMessage() call another .then(). You're reading the code sequentially, but you should try and look at it as branching off paths in this way:
for {
call async functions and (.then) call these other functions
}
...some time passes
<branched off path> The other functions within .then(functions) get called
I recommend reading up on how asynchronous functions work, and also how promises work to better understand in what order your code will execute.
To do what you want, you'll need to write separate code to handle these scenarios:
1) Receiving the number of cards from user and just flat out looping through and printing those cards to chat
2) Receiving further input based on the user (will need to keep track of user) and the current state of their cards that have been flipped -- probably want to number the cards and have them send a command like USE 1 or DISCARD ALL
The code won't pause in the middle of your loop to wait for input, these each have to be different pieces of code.

How to use CellMeasurer correctly?

I have an interactive component inside "react-virtualized" List that acts on clicks. When the component is clicked, the cell transforms i.e. height changes.
My first version of the rowRenderer:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
return <Message key={message.id} text={message.text} />
}
When the message is clicked, a text field appears. This changes the height. What however happens is that component renders over the next message.
This happens because the instance of Message is different in UI and in CellMeasurer as you can see:
<CellMeasurer
cellRenderer={
// Here we return instance 1
({ rowIndex, ...rest }) => this.rowRenderer({ index: rowIndex, ...rest })
}
columnCount={1}
rowCount={messages.length}
>
{({ getRowHeight, resetMeasurementForRow }) => {
this.resetMeasurementForRow = resetMeasurementForRow;
return <List
height={height}
overscanRowCount={50}
rowCount={messages.length}
rowHeight={getRowHeight}
rowRenderer={this.rowRenderer} // Here we create another instance
width={width}
ref={(ref)=>{
this.list = ref;
}}
/>
}}
</CellMeasurer>
The instance created by List will obviously contain correct state but CellMeasurer is not aware of this state.
I tested the following approach but I highly doubt that this is the correct way to do this? I simply cache the UI component instance myself:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
if(!this.componentCache[index]) {
this.componentCache[index] = <Message key={message.id} text={message.text} />
}
return this.componentCache[index];
}
This fixes this problem but probably introduces many other issues. What is the correct way to do this?
(I'm aware that using Flux/Redux/global state could fix this but I'm wondering is there some fundamental react-virtualized feature/aspect that I'm missing here.)
Answer to this is actually in the documentation:
The current implementation of CellMeasurer creates cells on demand,
measures them, and then throws them away. Future versions of this
component may try to clone or in some other way share cells with their
parent Grid in order to improve performance. However until that
happens, be wary of using CellMeasurer to measure stateful components.
Since cells are just-in-time created for measuring purposes they will
only be measured with their default state. To avoid this issue for
now, use controlled props (instead of state) for cell rendering
behavior.
So there is no other solution than handling the state outside the component. In practice, I fixed it as follows:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
let selected = message.id === store.getState().selectedMessage;
return <Message key={message.id} text={message.text} selected={selected} />
}

Is it better to reuse or recreate a reactive source in Meteor

I have a template called 'contacts'. Inside is an #each which renders the template 'contact'. The user can press the 'edit' button which sets a session variable with the mongo id of the edited row. The row then reactively re-renders into "edit" mode.
Template.contact.viewEditing = function() {
return Session.get("contactViewEditingId") === this._id;
}
The html uses the viewEditing helper a few times, for instance:
{{#if viewEditing}}
<div class="panel-heading">EDITING!</div>
{{/if}}
I need to bind some javascript in the .rendered(). I would like to check again if we are editing. I can think of 2 options:
Should I call Template.content.viewEditing() inside my template.rendered() ? Does this save on reactivity calculations?
Or should I just copy pasta the if statement. This option seems to violate DRY.
Option 1:
Template.contact.rendered = function() {
if( Template.contact.viewEditing.call(this.data) ) {
// Bind some fancy jQuery
bindEditInPlace(this.data);
}
}
Option 2:
Template.contact.rendered = function() {
if( Session.get("contactViewEditingId") === this._id ) {
// Bind some fancy jQuery
bindEditInPlace(this.data);
}
}
I think that putting {{viewEditing}} multiple times in your template doesn't "cost" anything extra. So logically I would think that using this helper elsewhere is better. Maybe I need more help understanding reactivity calculations. Thanks!
Helpers are run inside a Deps.Computation, which means that every time a reactive variable is referenced and modified in a helper, it will re-run.
Template.rendered is a callback that runs each time the template is re-rendered (which usually happens when a helper in the template is re-run reactively), but it is not itself a reactive computation.
So it doesn't matter using either the template helper or copy-pasting its code inside your rendered callback : both ways won't trigger a reactive computation invalidation because we are not inside one.
As far as DRY is concerned, you could refactor your code like this :
function isContactViewEditing(contactView){
return Session.equals("contactViewEditingId",contactView._id);
}
Template.contact.helpers({
viewEditing:isContactViewEditing
});
Template.contact.rendered=function(){
if(isContactViewEditing(this.data)){
//
}
};
I think saimeunt's answer is correct, especially if you have more complex logic in the function which you don't want to replicate.
Create a local function which you can re-use in both the helper and the .rendered callback.
If you had a case where you wanted to use a reactive source minus the reactivity you could make it non-reactive by wrapping it in a Deps.nonreactive function likes so:
Deps.nonreactive(function(){
//Reactive stuff here
});
Regarding reactivity concerns, pay attention to his change from using Session.get to Session.equals. Session.get will cause any reactive computation it is used in to re-calculate on every change of the session variable. So if you use this helper in multiple places with different ids, and you change the session variable, every single one will re-calculate and re-render the templates they are used in. Session.equals only invalidates a computation when the equality changes. So changing the session variable from one non-equal id to another non-equal id will not cause the computation/template to re-run when you use Session.equals.
For your specific example where the helper is only returning the result of a Session.equals you might consider creating a global handlebars helper that can do this for you with any session variable and any value. Like the following.
Handlebars.registerHelper('sessionEquals', function (key, value) {
return Session.equals(key, value);
});
Then in the template use it like so:
{{#if sessionEquals 'contactViewEditingId' _id}}
<div class="panel-heading">EDITING!</div>
{{/if}}
In the template when rendering an item that is editable add a unique class name to mark the item as editable. Then in your Template.rendered callback when binding the javascript use a selector which looks for that class and only binds to elements with that special class.

dijit.Tree search and refresh

I can't seem to figure out how to search in a dijit.Tree, using a ItemFileWriteStore and a TreeStoreModel. Everything is declarative, I am using Dojo 1.7.1, here is what I have so far :
<input type="text" dojoType="dijit.form.TextBox" name="search_fruit" id="search_fruit" onclick="search_fruit();">
<!-- store -->
<div data-dojo-id="fruitsStore" data-dojo-type="dojo.data.ItemFileWriteStore" clearOnClose="true" urlPreventCache="true" data-dojo-props='url:"fruits_store.php"'></div>
<!-- model -->
<div data-dojo-id="fruitsModel" data-dojo-type="dijit.tree.TreeStoreModel" data-dojo-props="store:fruitsStore, query:{}"></div>
<!-- tree -->
<div id="fruitsTree" data-dojo-type="dijit.Tree"
data-dojo-props='"class":"container",
model:fruitsModel,
dndController:"dijit.tree.dndSource",
betweenThreshold:5,
persist:true'>
</div>
The json returned by fruits_store.php is like this :
{"identifier":"id",
"label":"name",
"items":[{"id":"OYAHQIBVbeORMfBNZXFGOHPdaRMNUdWEDRPASHSVDBSKALKIcBZQ","name":"Fruits","children":[{"id":"bSKSVDdRMRfEFNccfTZbWHSACWbLJZMTNHDVVcYGcTBDcIdKIfYQ","name":"Banana"},{"id":"JYDeLNIGPDBRMcfSTMeERZZEUUIOMNEYYcNCaCQbCMIWOMQdMEZA","name":"Citrus","children":[{"id":"KdDUfEDaKOQMFNJaYbSbAcAPFBBdLALFMIPTFaYSeCaDOFaEPbJQ","name":"Orange"},{"id":"SDWbXWbTWKNJDIfdAdJbbbRWcLZFJHdEWASYDCeFOZYdcZUXJEUQ","name":"Lemon"}]},{"id":"fUdQTEZaIeBIWCHMeBZbPdEWWIQBFbVDbNFfJXNILYeBLbWUFYeQ","name":"Common ","children":[{"id":"MBeIUKReBHbFWPDFACFGWPePcNANPVdQLBBXYaTPRXXcTYRTJLDQ","name":"Apple"}]}]}]}
Using a grid instead of a tree, my search_fruit() function would look like this :
function search_fruit() {
var grid = dijit.byId('grid_fruits');
grid.query.search_txt = dijit.byId('search_fruit').get('value');
grid.selection.clear();
grid.store.close();
grid._refresh();
}
How to achieve the same using the tree ? Thanks !
The refreshing of a dijit.Tree becomes a little more complicated, since there is a model involved (which in grid afaik is inbuilt, the grid component implements query functionality)
Performing search via store
But how to search, thats incredibly easy whilst using the ItemFileReadStore. Syntax is as such:
myTree.model.store.fetch({
query: {
name: 'Oranges'
},
onComplete: function(items) {
dojo.forEach(items, function(item) {
console.log(myTree.model.store.getValue(item, "ID"));
});
}
});
Displaying search results only
As shown above, the store will fetch, the full payload is put into its _allItemsArray and the store queryengine then filters out what its told by query argument to the fetch method. At any time, we could call fetch on store, even without sending an XHR for json contents - fetch with query argument can be considered as a simple filter.
It becomes slightly more interesting to let the Model know about this query.. If you do so, it will only create treeNodes to fill the tree, based on the returned results from store.fetch({query:model.query});
So, instead of sending store.fetch with a callback, lets _try to set model query and update the tree.
// seing as we are working with a multi-parent tree model (ForestTree), the query Must match a toplevel item or else nothing is shown
myTree.model.query = { name:'Fruits' };
// below method must be implemented to do so runtime
// and note, that the DnD might become invalid
myTree.update();
Refreshing tree with new xhr-request from store
You need to do exactly as you do with regards to the store. Close it but then rebuild the model. Model contains all the TreeNodes (beneath its root-node) and the Tree itself maps an itemarray which needs to be cleared to avoid memory leakage.
So, performing following steps will rebuild the tree - however this sample does not take in account, if you have DnD activated, the dndSource/dndContainer will still reference the old DOM and thereby 'keep-alive' the previous DOMNode hierachy (hidden ofc).
By telling the model that its rootNode is UNCHECKED, the children of it will be checked for changes. This in turn will produce the subhierachy once the tree has done its _load()
Close the store (So that the store will do a new fetch()).
this.model.store.clearOnClose = true;
this.model.store.close();
Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
Destroy the widget
this.rootNode.destroyRecursive();
Recreate the model, (with the model again)
this.model.constructor(this.model)
Rebuild the tree
this.postMixInProperties();
this._load();
Creds; All together as such, scoped onto the dijit.Tree:
new dijit.Tree({
// arguments
...
// And additional functionality
update : function() {
this.model.store.clearOnClose = true;
this.model.store.close();
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
this.rootNode.destroyRecursive();
this.model.constructor(this.model)
this.postMixInProperties();
this._load();
}
});

Resources