I've made a component that visualizes an object where array's and nested objects are collapsible.
It works by looping through all the keys/indexes and using the appropriate render function. Right now the collapse and extend actions are purely CSS class-based.
The issue I have right now is that I need to make it so the collapsed/extended state persists. So basically I need a way to store the collapsed state info. and read it later on. I'm not really sure how to even start.
The state should persist even when the object is modified. So when an new item gets pushed in the middle of the array it should still open the same thing. What data structure should I use for this and how would I read it?
The reason I'm not posting any code is that I don't think it's necessary, I'm purely interested in how the data should be structured for the collapsed/extended state of an array/object so it persists when updates are made to the object/array.
I've tried making some type of "breadcrumbs" that follow the structure of the object but then I realized that it won't persist when changes are made.
Any ideas? Is this even possible without some hardcoded unique ids?
Edit 1: point 4 it's not optimal and can be done better - after you finish rendering the main object to the DOM, loop through the state object and for each path, find the DOM node and expand it.
Edit 2: instead of 'index1.nestedIndex2[1].nestedIndex3' you can do 'index1.nestedIndex2.1.nestedIndex3', makes your code simpler
Edit 3: no need to use lodash (edited point 1)
I hope I understood your scenario correctly.
In order to persist the expanded (or collapsed) state, you have to:
create an object that stores the paths to the indexs that you want expanded, like this {'index1.nestedIndex2[1].nestedIndex3': true}. You can do this when clicking to expand. Of course, this means you should know the current index and it's parents when you click - one way to do this is by having a DOM attribute holding the index, on each element. So when you click a node to go up recursively until you reach the top element, and build that string path
Convert the state object to a string using JSON.stringify and store it in localStorage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). localStorage.set('expandedState', JSON.stringify(stateObject)). If you need to store the state for multiple data objects, you have to use some specific key instead of 'expandedState'
In the code, before rendering the main object, convert the string state from localStorage back to an object using JSON.parse
While rendering your main object, you have to build the path for the currently rendered node and check if it can be found in the state object. If yes, expand it by adding your css class on it. To build back the path, it's more optimal to build it as you render each node rather than using the same method used in point 1, but that would also work (but slower, so for really big objects will be a problem, I think)
Related
I have an application that shows and manipulates users' information in a table with many columns. The user model has quite a few deep nested properties, each table column represent a property. To better explain, I created this simplified app at stackbliz. The real application has more columns and is relatively organised into more layers/components.
Basically, each column is a component that has an #Input person. While Location property value is changed by clicking itself, another component LOS needs to be aware of it and change its text colour, if location becomes 'J'.
Normally, this isn't working as the person reference of each #Input is not changed, so the change detection is not firing.
Please ignore the pipe used as it's just a way to show one component has to react if a property is changed in another component. I can mark it as impure but it seems not a good way as it will run too many times unnecessarily.
Also, I know I can make it work by using getter or a function to read the property in another component template, I have included the code as comment in stackblitz app. But it can unnecessarily run many times as well.
Question
I wonder if there is another better, cleaner and intuitive way to notify nested property changes between components. It could be a better way of organising the object and its property, passing them to different components, or other techniques that don't bring performance overheads.
Thanks in advance
Component Structure
You need a a RowComponent to segregate each row and its data and coloring of the LOS column (which may differ between rows).
After adding this component, I would not use LocationComponent or LengthofstayComponent as there is not much to them and your code will become cluttered with passing values between them.
Implementing the color changing
I recommend using a BehaviorSubject, a corresponding Observable and the async pipe. Every time the async pipe, emits, a change detection cycle will be intiated.
In RowComponent add:
showColorSubject = new BehaviorSubject<boolean>(false);
showColor$ = this.showColorSubject.asObservable();
If we call next on the Subject in changeLocation:
this.showColorSubject.next(this.person.location.code === "J");
then showColor$ will emit true/false accordingly in the RowComponent template where we use the async pipe to selectively enable the color class:
[class.color]="showColor$ | async"
Stackblitz
https://stackblitz.com/edit/so-color-row-cell?file=src%2Fapp%2Frow%2Frow.component.html
(Formatting is messed up but I'll leave that to you)
I implemented the following to display a paginated query (this was suggested by Tony O'Hagan in this post: How to get the last document from a VueFire query):
bindUsers: firestoreAction(({ bindFirestoreRef }) => {
return bindFirestoreRef('users',
Firebase.firestore().collection('users').limit(8), { serialize })
}),
bindMoreUsers: firestoreAction(context => {
return context.bindFirestoreRef('users', Firebase.firestore().collection('users').startAfter(context.state.users[context.state.users.length - 1]._doc).limit(8), { serialize })
})
When the user scrolls to the end of the page, I call bindMoreUsers which updates the state.users to the next set of 8 documents. I need to be able to append to the state.users as opposed to overwrite the original set of 8 documents. How can I do this?
Confession: I've not yet implemented pagination on my current app but here's how I'd approach it.
In my previous answer I explained how to keep references to the Firestore doc objects inside each element of the state array that is bound by VuexFire or VueFire. In Solution #1 below we use these doc objects to implement Firestore's recommended cursor based pagination of a query result sets using startAfter(doc) query condition instead of the slower more expensive offset clause.
Keep in mind that since we're using Vuexfire/Vuefire we're saying that we wish to subscribe to live changes to our query so our bound query will define precisely what ends up in our bound array.
Solution #1. Paging forward/backward loads and displays a horizontal slice of the full dataset (our bound array maintains the same size = page size). This is not what you requested but might be a preferred solution given the Cons of other solutions.
Pros: Server: For large datasets, this pagination query will execute with least cost and delay.
Pros: Client: Maintains a small in memory footprint and will render fastest.
Cons: Pagination will likely not feel like scrolling. UI will likely just have buttons to go fwd/backward.
Page Forward: Get the doc object from the last element of our state array and apply a startAfter(doc) condition to our updated view query that binds our array to the next page.
Page Backward: Bit Harder! Get the doc object from the first element of our bound state array. Run our page query with startAfter(doc), limit (1), offset(pagesize-1) and reverse sort order. The result is the starting doc (pageDoc) of the previous page. Now use startAfter(pageDoc) and forward sort order and limit(pageSize) to rebind the state array (same query as Page Forward but with doc = pageDoc).
NOTE: In the general case, I'd argue that we can't just keep the pageDoc values from previous pages (to avoid our reverse query) since we're treating this as a 'live' update filtered list so the number of items still remaining from previous pages could have radically changed since we scrolled down. Your specific application might not expect this rate of change so perhaps keeping past pageDoc values would be smarter.
Solution #2. Paging forward, extends the size of the query result and bound array.
Pros: UX feels like normal scrolling since our array grows.
Pros: Don't need to use serializer trick since we're not using startAfter() or endBefore()
Cons: Server: You're reloading from Firestore the entire array up to the new page every time you rebind to a new page and then getting live updates for growing array. All those doc reads could get pricey!
Cons: Client: Rendering may get slower as you page forward - though shadow DOM may fix this. UI might flicker as you reload each time so more UI magic tricks needed (delay rendering until array is fully updated).
Pros: Might work well if we're using an infinite scrolling feature. I'd have to test it.
Page Forward: Add pageSize to our query limit and rebind - which will re-query Firestore and reload everything.
Page Backward: Subtract pageSize from our query limit and rebind/reload (or not!). May also need to update our scroll position.
Solution #3. Hybrid of Solution #1 and #2. We could elect to use live Vuexfire/Vuefire binding for just a slice of our query/collection (like solution #1) and use a computed function to concat it with an array containing the pages of data we've already loaded.
Pros: Reduces the Firestore query cost and query delay but now with a smooth scrolling look and feel so can use Infinite scrolling UI. Hand me a Koolaid!
Cons: We'll have to try to keep track of which part of our array is displayed and make that part bound and so live updated.
Page Forward/Backward: Same deal as Solution #1 for binding the current page of data, except we now have to copy the previous page of data into our non-live array of data and code a small computed function to concat() the two arrays and then bind the UI list to this computed array.
Solution #3a We can cheat and not actually keep the invisible earlier pages of data. Instead we just replace each page with a div (or similar) of the same height ;) so our scrolling looks we've scrolled down the same distance. As we scroll back we'll need to remove our sneaky previous page div and replace it with the newly bound data. If you're using infinite scrolling, to make the scrolling UX nice and smooth you will need to preload an additional page ahead or behind so it's already loaded well before you scroll to the page break. Some infinite scroll APIs don't support this.
Solution #1 & #3 probably needs a Cookbook PR to VueFire or a nice MIT'd / NPM library. Any takers?
I've a list (immutable.js) in my store containing multiple objects.
This list is displayed in a component as a table with rows. Those rows are subcomponents displaying one single object. One attribute of those objects should be editable. So onChange() i dispatch an action which should change the attribute of that one specific object. As we should never ever change the state, i return a whole new list with just that single object changed. But because the whole list is a new list object, the table component gets updated every single change. this leads to a really slow working app.
I've just looked at the official todo app example and inspected it with the Perf addon. Realising that they also rerender the whole todos-list on every change (mark as completed, unmark). How am I supposed to fix that?
The biggest factors that will impact your list rendering performance are heavy rendering cycles and expensive DOM mutations. Make sure that your list items are as efficient as possible when they re-render. Done properly, this will make a big difference.
You have a couple of straight forward options.
Break your rows out into their own component (if not already done) and optimize the render and update cycle.
Use a library such as react-virtualized to help with list/table/grid performance.
I'm developping an angular app right now for my company, but I reached a point where the app became extremely slow so I tried tunning it by using onetimebind everywhere I can, track by ...but it's faster to load at first but still laggy, it is composed of a pretty much huge nested objects, I've counted the total number of objects, it starts at 680 and can go up to +6000 for normal use of the app, oh yeah I should precise that the app is generating a form and pretty much +90% of the objects in the scope belongs to an input and are updated each time the client click(radio) keyup/change(text).
It also have like 5/6 arrays composed of objects and the array gets bigger/smaller accodring to the clients choice, and that's where it gets laggy, each time I add an object to the array, it takes like a second to render it, so I tried using nested controllers thinking that if the child of an object is updated Angular will render only this child and not all the others, but somehow the app got even slower and laggier :s (it's a bit faster when I use ng-show instead of ng-if but the memory used jumps from ~50Mb to ~150Mb)
I should also precise that the form is in a wizard style, and not all the inputs are displayed at once, the number of inputs that are displayed are between 10%-20% of the total inputs
Has anyone encountred this problem before? does anyone know how to deal with big scopes?
Sad to say, but that's intrinsic of the view rendering in angular.
An update in the model triggers a potential redraw of the entire view. No matter if you have elements hidden or not. The two way data binding can really kill performances. You can consider evaluate if you need to render the view only once, in that case there are optimizations, but I'm assuming that your form change dynamically, therefore a 2 way data binding is necessary.
You can try to work around this limitation but encapsulate sub part of the entire MVC. In this way a contained controllers only update the specific view associated to that scope.
You may want to consider using react (that has as first goal to address exactly your use case)
Have a look at this blog post for a comparison of the rendering pipeline between angular and react Js.
http://www.williambrownstreet.net/blog/2014/04/faster-angularjs-rendering-angularjs-and-reactjs/
Suppose we have two sibling react components called OldContainer and NewContainer. There is a child component inside OldContainer that contains a <video> tag, and the video is currently playing.
The user can now drag the child component (with the video) and drop it in the NewContainer, and they expect the video to keep playing while it's being dragged and after being dropped.
So the video appears to stick to the mouse position, and when dragged and dropped in the new container, it animates to its new position (again, it doesn't get paused).
How would you implement this? Can we implement this in a pure way (in line with the spirit of pure functions)?
Clarification: I could have used some other element instead of a video tag for explaining this problem. A NumberEasing element would be a better example, since it would require the props and state of the component to be preserved during and after the interaction.
Update 1: Code examples obviously would be nice, but what I'm mainly looking for is just a general description of how you would approach this problem in a "functional" way. How do you keep your view code simple and easy to reason about? Who handles the drag-and-drop gesture? How do you model the data that's fed into the views?
Take a look at this library : react-reverse-portal
What is it that you want to preserve? Is it Javascript objects that the component holds as state, or is it state in the DOM (like how long a video has played, or text selection in an input box)?
If it's just Javascript objects as state, you're better of moving the source of that state to another service (something like Flux). That way, it doesn't matter if the component gets recreated because it can be recreated with the state that was there before.
EDIT
The way to keep your view code simple and easy to reason about is to not keep state inside your components. Instead, all data that the component needs should be passed into the component as props. That way, the component is "pure" in that it renders the same output given the same props. That also makes the problem of wanting to reuse a component instance a non-issue, since it doesn't matter when the same input gives the same output.
For drag and drop, I'd suggest looking at: https://github.com/gaearon/react-dnd.
How you model the data you pass to view components is up to you and the needs of your application. The components shouldn't care, they should just expect to get data passed as props, and to render them. But the popular approach to dealing with this is of course Flux, and there are many libraries that implements Flux in different ways.
SECOND EDIT
Regarding if you have a subtree with hundreds of components that you want to move: I'd still start off by making the state external (pure components), and render that tree in a new place. That means that React will probably recreate that entire subtree, which is fine. I wouldn't deviate from that path unless the performance of it turned out to be horrible (just guessing that it might be horrible isn't enough).
If the performance turned out to be horrible, I would wrap that entire subtree in a component that caches the actual DOM tree and reuses it (if it gets passed the same props). But you should only do this when absolutely needed, since it goes against what React tries to do for you.
THIRD EDIT
About gestures: I'd start out with listening to gesture events in componentDidMount, and in the event callback call setState on the component with the coordinates it should have. And then render the component in render with the coordinates given. React won't recreate the component when you call setState but it will re-render it (and diff the output). If the only thing you changed was the coordinates, it should render fast enough.
If that turns out to be too slow, like if the subtree of that component is huge and it becomes a bottleneck to recreate the subtree of vDOM, I'd reposition the DOM node directly in a RAF-loop outside of Reacts control. And I'd also put a huge comment on why that was needed, because it might seem wierd for some other developer later.
Create a new variable using const or var. Put the instance of data using rest spread operator, update the necessary data to pass and send the data to the component without mutating the state of component.
Just like:
const data = {
...this.state.child,
new_data : 'abc'
}