How to use CellMeasurer correctly? - react-virtualized

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} />
}

Related

Is it possible to recolor a lottie animation programmatically?

If I have a lottie animation in the form of a json file, is there a way to recolor it in code or even within the json itself?
(To be clear, I hope there's a way to do it without involving After Effects. For instance if I decide to change my app's primary color, the whole app will change except the animation unless there's a way to do that.)
I figured it out. For this example, let's say I want to recolor a specific layer to Color.RED.
You'll need your LottieAnimationView, a KeyPath, and a LottieValueCallback
private LottieAnimationView lottieAnimationVIew;
private KeyPath mKeyPath;
private LottieValueCallback<Integer> mCallback;
Then in your onCreate (or onViewCreated for a fragment) you'll get the animation with findViewById, as well as "addLottieOnCompositionLoadedListener" to the lottieAnimationView, in which you will setup the "mKeyPath" and "mCallback":
lottieAnimationVIew = findViewById(R.id.animationView);
lottieAnimationView.addLottieOnCompositionLoadedListener(new LottieOnCompositionLoadedListener() {
#Override
public void onCompositionLoaded(LottieComposition composition) {
mKeyPath = getKeyPath(); // This is your own method for getting the KeyPath you desire. More on that below.
mCallback = new LottieValueCallback<>();
mCallback.setValue(Color.RED);
checkBox.addValueCallback(mKeyPath, LottieProperty.COLOR, mCallback);
}
});
The argument "LottieProperty.COLOR" specifies which property I am changing.
There's probably a better way to do this, but here's my "getKeyPath" method for finding the specific thing I want to change. It will log every KeyPath so you can see which one you want. Then it returns it once you've supplied the correct index. I saw that the one I want is the 5th in the list, hence the hard-coded index of 4.
private KeyPath getKeyPath() {
List<KeyPath> keyPaths = lottieAnimationView.resolveKeyPath(new KeyPath("Fill", "Ellipse 1", "Fill 1"));
for (int i = 0; i < keyPaths.size(); i++) {
Log.i("KeyPath", keyPaths.get(i).toString());
}
if (keyPaths.size() == 5) {
return keyPaths.get(4);
}
else {
return null;
}
}
Note that the "Fill", "Ellipse 1", "Fill 1" are strings I supplied to narrow the list down to just the ones that have those keys, because I know that the layer I want will be among those. There's likely a better way to do this as well.
There is another thread on this topic with the same approach but a bit simplified:
How to add a color overlay to an animation in Lottie?
Here's directly an example (Kotlin):
yourLottieAnimation.addValueCallback(
KeyPath("whatever_keypath", "**"),
LottieProperty.COLOR_FILTER
) {
PorterDuffColorFilter(
Color.CYAN,
PorterDuff.Mode.SRC_ATOP
)
}
You can find the names of the keypaths also in the Lottie editor.

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.

AngularJS 1.5 component $postLink and data binding

I am building an Angular 1.5 component that wraps Chosen list, which needs to be initialized by calling .chosen() on the jQuery element. I can do that using the $postLink lifecycle callback, and something like $('.chosen-select').chosen(), which works fine. However, I can anticipate someone using multiple instances of the component on the same page, so using a class selector would not necessarily get the component you want.
I tried using an id selector instead, by adding a prefix to whatever id someone assigns to the component in HTML. For example, I may use the component like <chosen-select id="roles"></chosen-select> and in the template if have <select id="cs-{{$ctrl.id}}"> (in the controller, I bind id: '#'). This all works as expected EXCEPT that in $postLink, the select element has been created (and other bindings, such as the one that lists options, resolved) but id is still "cs-{{$ctrl.id}}". At what point does that become "cs-roles" (which is what it is in the DOM when everything has been set up)? What is the best way to ensure that I am accessing the object that belongs to this component?
Here is the component code, which works:
template:
<select id="cs-{{$ctrl.id}}" class="chosen-select"
ng-options="(option.name || option) for option in $ctrl.options track by (option.id || option)"
ng-model="$ctrl.result"
>
</select>
component:
mymod.component('chosenSelect', {
templateUrl: 'shared/components/chosenSelectComponent.html',
controller: chosenSelectController,
bindings: {
id: '#',
options: '<',
config: '<?',
selected: '<?',
doChange: '&?'
}
});
function chosenSelectController() {
var vm = this;
vm.result = vm.selected || vm.options[0];
vm.$postLink = function() {
// would like to use ("#cscomp-" + vm.id) to make sure it is unique,
// but id doesn't seem to have been resolved yet in select element
$(".chosen-select")
.chosen(vm.config)
.on('change', function(evt, params) {
// parms.selected also holds result
vm.doChange({ value: vm.result });
});
};
}
I realized I could use a hierarchical selector to solve the problem. In the $postLink function, referencing $("#" + vm.id + " .chosen-select") does exactly what I want it to by narrowing the selection to only elements that are descendants of the element with the specified id.

Pagination Ideas for Angular2 and ReactiveX

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.

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