vue js large array to table performance advice - javascript

I am building an internal app that displays data in an HTML table. The API returns me an array of up to 20K objects which are stored in data() and the table is populated by each cell calling a method to find the correct object. The object is located via a 3-part key (2 for the row and 1 for the column). Initial rendering performance on my laptop (i7 7thGen and 8GB ram) is acceptable [about 1 minute] for initial render (excel app it is replacing takes almost 3), however an update to a single cell (object in the array) triggers the change detection and an update takes another minute. Sometimes the user will want to update a single cell, sometimes a row and sometimes propagate a change on all rows(or some selected rows) for a single column. Is there a recommended strategy for performance enhancement. I was thinking of restructuring the data that comes back into an object per row (2-part key) with a collection of objects for the column (1-part key). This would mean that the method call will only have to iterate over 2500 'row' objects and then over 8 'column' objects which feels logically like it should be faster but will now consume more memory. I need to be able to identify which objects have changed at a table cell level as I need to write the changes back to the database. Should I discard the original API results and re-hydrate on save or would an option be to use Object.freeze to prevent reactivity on the original array and write changes to a separate array which then takes precedence if a record exists.
[
{
key1:value,
key2:value,
key3:valueA,
displayValue:value
},
{
key1:value,
key2:value,
key3:valueB,
displayValue:value2
},
{
key1:value,
key2:value,
key3:valueC,
displayValue:value3
}...
{
key1:value2,
key2:value2,
key3:valueA,
displayValue:value
},
{
key1:value2,
key2:value2,
key3:valueB,
displayValue:value2
},
{
key1:value2,
key2:value2,
key3:valueC,
displayValue:value3
}...
]
becomes
[
{
key1:value,
key2:value,
key3collection:[
{key3:valueA, displayvalue:value},
{key3:valueB, displayvalue:value2},
{key3:valueC, displayvalue:value3}
]
},
{
key1:value2,
key2:value2,
key3collection:[
{key3:valueA, displayvalue:value},
{key3:valueB, displayvalue:value2},
{key3:valueC, displayvalue:value3}
]
},
]

What I would do is to convert API response (directly on server if you control API or on the client) to something like this:
{
columns: ['ValueA', 'ValueB', 'ValueC'] // same for every row, right ?
rows: [
{
key1:value,
key2:value,
values:[ 'value', 'value2', 'value3' ]
}, {
key1:value2,
key2:value2,
values:[ 'value', 'value2', 'value3' ]
}, ....
]
I'm working with an assumption that every row has same set of columns so why to keep duplicity in the data.
That way you can render the table by following template (pseudo code) without any additional data lookups:
<table>
<thead>
<tr>
<th v-for="column in data.columns">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data.rows">
<td v-for="(rowItem, columnIndex) in row.values">{{ rowItem }}</td>
</tr>
</tbody>
</table>
For change tracking (so you can later send it back to API) just add simple array alongside values like isDirty: [ fasle, false, false ] and change it to true every time you update some cell. When posting just iterate over data structure and recreate updated objects in the format your API expects it....

Related

For sibling communication between many identical components, how should I store the data in the lowest-common ancestor?

Background:
I'm a Python/Vue developer; I've been using Vue since 2016.
I have a client who runs a weight loss / meal planning business: clients pay her to prepare weekly single-page PDF menus that tell them (the clients) exactly what to eat for breakfast, lunch, and dinner of every day of the week. (image of an example menu)
Each meal is shown as a list of ingredients.
Right now she's preparing these menus in Excel, and she hired me to reproduce and extend the functionality of what she has in Excel, but in a Python/Vue app.
The app I'm building for her has many "pages" ("top-level" components) to allow her to add/modify/delete objects like clients, ingredients, and recipes (image), but the most complicated part of the UI is the component in which she can define the meals for every meal of every day of the week (image). That component is named WeeklyMenu.vue.
WeeklyMenu.vue itself contains seven DailyMenu.vue children, one for each day of the week (Monday, Tuesday, etc.). (image)
Each DailyMenu.vue component itself contains four Meal.vue components, one for each of four meal types: Breakfast, Lunch, Dinner, and Snacks. (image)
Important: At the moment, the DailyMenu.vue and Meal.vue components themselves contain their data rather than accessing it from the Vuex store.
For example, the list of ingredients for each meal is contained within the Meal.vue component as a mealIngredients variable within the component's data attribute. (image)
Side-note: This means that there are lots of HTTP requests being sent to the back-end when the page loads as all of the meals are requesting their own data, rather than a single request being sent via a Vuex action (for example). This seems like it can't be best practice.
The problem:
The problem is that she is now asking me to add features in which a change to the data in one subcomponent should update the data in a different subcomponent.
For example, she wants the app to work so that when she has the same recipe in several different Meals of the week, then a change to an ingredient in one of the meals will propagate to the other meals that have the same recipe. (image explanation)
My question:
What is the best practice for handling a situation like this? Should I move the ingredient data into the Vuex store or (in the same vein) the lowest-common-ancestor WeeklyMenu.vue component? If so, how exactly should it work? Should there be a separate variable for each meal? Or should I have an object that contains data for all of the different meals? If I use a single object, do I need to worry that a watcher on that object in the Meal.vue component would be triggering even when a change was made to a different meal's data?
If I store all the meal ingredients in separate variables, I would need to pass all of those to every meal (so every meal would need to receive every other meal's ingredients as separate props). So that doesn't seem like the right way to go.
If a user is making a particular change to a particular meal, how would I only have the other meals with the same name react?
Related links:
Communication between sibling components in VueJs 2.0
I'm looking into whether it would make sense to move the ingredient data up to the level of the WeeklyMenu.vue component as described in the "Lowest Common Ancestor" approach (here and here).
Simplified example of the situation I'm trying to handle:
Without Vuex: https://codepen.io/NathanWailes/pen/zYBGjME
Using Vuex: https://codepen.io/NathanWailes/pen/WNxWxWe
With everything working (including the state being kept in Vuex) except the propagation: https://codepen.io/NathanWailes/pen/KKMYNVZ
Yes, problem domain seems complex enough to more than justify use of Vuex. I would not go with keeping data in components and sharing by props - that doesn't scale well
Keep each Recipe as an object in single object recipes - you don't need to worry about watchers. If one particular Recipe object will change, Vue will re-render only components using same Recipe object (and if done properly you don't even need watchers for that)
Create a "weekly menu" object inside the store
In leaf nodes (Meals) of that object just use some kind of reference (by name or unique ID if you have one) into recipes. As a result multiple Meal.vue components on a menu will use same object in the store and update automatically
I ended up getting it working in a simple example in CodePen, which I'm going to use as a guide when trying to get it working on the actual site.
The summary of my findings with this solution is, "Vue will actually update when the nested entries of a Vuex state object are updated; you don't need to worry about it not detecting those changes. So it's OK to just keep all the data in a single big Vuex store object when you have many duplicate sibling components that need to react to each other."
Here's the CodePen: https://codepen.io/NathanWailes/pen/NWRNgNz
Screenshot
Summary of what the CodePen example does
The data used to populate the menu all lives in the Vuex store in a single weeklyMenu object, which has child objects to break up the data into the different days / meals.
The individual meals have computed properties with get and set functions so that it can both get changes from the store and also update the store.
The DailyMenu and WeeklyMenu components get their aggregate data by simply having computed properties that iterate over the Vuex weeklyMenu object, and it "just works".
I have same-named meals update to match each other by iterating over the meals in the Vuex mutation and looking for meals with the same "Ingredient Name".
The code
HTML
<html>
<body>
<div id='weekly-menu'></div>
<h3>Requirements:</h3>
<ul>
<li>Each row should have all the numbers in it summed and displayed ('total daily calories').</li>
<li>The week as a whole should have all the numbers summed and displayed ('total weekly calories').</li>
<li>If two or more input boxes have the same text, a change in one numerical input should propagate to the other same-named numerical inputs.</li>
<li>Ideally the data (ingredient names and calories) should be stored in one place (the top-level component or a Vuex store) to make it more straightforward to populate it from the database with a single HTTP call (which is not simulated in this example).</li>
</ul>
</body>
</html>
JavaScript
const store = new Vuex.Store(
{
state: {
weeklyMenu: {
Sunday: {
Breakfast: {
name: 'aaa',
calories: 1
},
Lunch: {
name: 'bbb',
calories: 2
},
},
Monday: {
Breakfast: {
name: 'ccc',
calories: 3
},
Lunch: {
name: 'ddd',
calories: 4
},
}
}
},
mutations: {
updateIngredientCalories (state, {dayOfTheWeekName, mealName, newCalorieValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['calories'] = newCalorieValue
const ingredientNameBeingUpdated = state.weeklyMenu[dayOfTheWeekName][mealName]['name']
for (const dayOfTheWeekName of Object.keys(state.weeklyMenu)) {
for (const mealName of Object.keys(state.weeklyMenu[dayOfTheWeekName])) {
const mealToCheck = state.weeklyMenu[dayOfTheWeekName][mealName]
const ingredientNameToCheck = mealToCheck['name']
if (ingredientNameToCheck === ingredientNameBeingUpdated) {
mealToCheck['calories'] = newCalorieValue
}
}
}
},
updateIngredientName (state, {dayOfTheWeekName, mealName, newValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['name'] = newValue
}
}
}
)
var Meal = {
template: `
<td>
<h4>{{ mealName }}</h4>
Ingredient Name: <input v-model="ingredientName" /><br/>
Calories: <input v-model.number="ingredientCalories" />
</td>
`,
props: [
'dayOfTheWeekName',
'mealName'
],
computed: {
ingredientCalories: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['calories']
},
set (value) {
if (value === '' || value === undefined || value === null) {
value = 0
}
this.$store.commit('updateIngredientCalories', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newCalorieValue: value
})
}
},
ingredientName: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['name']
},
set (value) {
this.$store.commit('updateIngredientName', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newValue: value
})
}
}
}
};
var DailyMenu = {
template: `
<tr>
<td>
<h4>{{ dayOfTheWeekName }}</h4>
Total Daily Calories: {{ totalDailyCalories }}
</td>
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Breakfast" />
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Lunch" />
</tr>
`,
props: [
'dayOfTheWeekName'
],
data: function () {
return {
}
},
components: {
meal: Meal
},
computed: {
totalDailyCalories () {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[this.dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[this.dayOfTheWeekName][mealName]['calories']
}
return totalDailyCalories
}
}
};
var app = new Vue({
el: '#weekly-menu',
template: `<div id="weekly-menu" class="container">
<div class="jumbotron">
<h2>Weekly Menu</h2>
Total Weekly Calories: {{ totalWeeklyCalories }}
<table class="table">
<tbody>
<daily_menu day-of-the-week-name="Sunday" />
<daily_menu day-of-the-week-name="Monday" />
</tbody>
</table>
</div>
</div>
`,
data: function () {
return {
}
},
computed: {
totalWeeklyCalories () {
let totalWeeklyCalories = 0
for (const dayOfTheWeekName of Object.keys(this.$store.state.weeklyMenu)) {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[dayOfTheWeekName][mealName]['calories']
}
totalWeeklyCalories += totalDailyCalories
}
return totalWeeklyCalories
}
},
components: {
daily_menu: DailyMenu
},
store: store
});

Access Table Header Row Key React

I was working with tables and I came across this issue: I want to access the data-row-key attribute (shown in the image below) in the table header row at a child row and I'm stuck. Code:
class App extends React.Component {
render() {
const columns = [
// sample of how the JSON API is read
{
title: "Title", dataIndex: "title", key: "title",
},
// the one that actually matters. becomes the actions column eventually
{
title: "Action", dataIndex: "", key: "x", width: "12%",
render: () => (
<Popconfirm
placement="topRight"
title="Are you sure to delete this task?"
// retrieve the data here as a parameter into the confirm(n) call
onConfirm={() => confirm(43)} okText="Yes" cancelText="No"
>
<a>delete</a>
</Popconfirm>
)
}
];
return (
<Table columns={columns} dataSource={this.state.data}/>
);
}
}
Right now I have the actual number (43) in there, but I want it to be dynamic as to be able to retrieve the data from the <tr data-row-key=...> tag, shown in the image below.
As a note, there is not a leading id column at the start of the table. The keys are provided through Django's rest framework -- which is in JSON format, in the very last image. Rendered results:
JSON format:
Can anyone please help me? Thanks in advance.
You can use the querySelector for it.
let value = document.querySelector('data-row-key')

How to format a list of string into columns of react bootstrap table 2 such that data doesn't flows out of actual columns

I am trying to render some data in tabular form using react-bootstrap-table but the data of one column is overlapping with the data of other columns. i wanted to keep my layout fixed and thus have added the css layout:fixed which is actually a requirement as well. But the final result is:
Actually for this column i'm getting an array of string from backend. e.g. ["DEPT","OLD","CUSTOM_FUNCTION",...] which is getting converted into a single string internally by react and i'm not sure how to further format it.
I also searched in react table docs at : https://react-bootstrap-table.github.io/react-bootstrap-table2/docs/basic-celledit.html#rich-editors but didn't find anything.
My ultimate goal is to visualize the data in a much better way like drop down or each element of array in new line within the same column expandable on some mouse click.
The above image can be considered as sample requirement where only the first element of list will be displayed on load and after clicking on arrow button it will show all the list items below one another in the same column as shown below.
I am not able to figure out which column prop will help me or whether it's even possible or not. The goal is exactly the same but a simple new line separated data will also do.
Column Definition Code:
{
dataField: 'data',
text: 'DATA',
editable: false,
filter: textFilter(),
headerStyle: () =>
{
return { width: '100px', textAlign: 'center'};
}
}
Table Creation Code:
<BootstrapTable
keyField='serialNo'
data={ this.state.data }
columns={ this.state.columns }
filter={ filterFactory() }
pagination={ paginationFactory({sizePerPage: 4}) }
cellEdit={ cellEditFactory({ mode: 'click'}) }
striped
hover
/>
Kindly help or suggest something appropriate.
Thanks
Check this sandbox.
https://codesandbox.io/s/competent-rain-2enlp
const columns = [{
dataField: 'id',
text: 'Product ID',
}, {
dataField: 'name',
text: 'Product Name'
}, {
dataField: 'labels',
text: 'Labels',
formatter: (cell) => {
return <>{cell.map(label => <li>{label}</li>)}</>
},
}];
You need to define your own formatter in order to include "complex" html inside your table cell.

How to Paginate dynamic AngularJS table?

How do I get pagination with ng-table-dynamic and $http working?
HTML specification of the table is
<table class="table-bonds table table-bordered table-hover table-striped"
export-csv="csv"
separator=","
show-filter="true"
ng-table-dynamic="bondsTable.bondsDataParams with bondsTable.bondsDataCols">
<tr ng-repeat="row in $data">
<td class="hand"
ng-repeat="col in $columns">{{::row.node[col.field]}}</td>
</tr>
The table creation code is:
self.bondsDataParams = new NgTableParams({
page: 1, // show first page
count: 5 // count per page
}, {
filterDelay: 0,
total: 0,
getData: function (params) {
return $http(bondsDataRemote).then(function successCallback(response) {
// http://codepen.io/christianacca/pen/mJoGPE for total setting example.
params.total(response.data.nodes.length);
return response.data.nodes;
}, function errorCallback(response) {
});
}
});
AngularJS 1.5.8
This is an excellent directive for pagination have a look at it . It has lots of options and its easy to use.
The main problem was mixing up loading the data via ajax and not supporting the filtering/pagination on the server side of the request.
Either provide all the data up-front so that the table can filter, or fully support the pagination, sorting and filtering on the server side.
Option 1. Load the data before hand. I used this option because my dataset is not that big and it seemed like the easiest way to allow people to use all the permutations of filtering sorting and downloading.
No total value is required here. The data is all loaded.
var Api = $resource('/green-bonds.json');
// Or just load all the data at once to enable in-page filtering, sorting & downloading.
Api.get({page: "1", count: "10000"}).$promise.then(function (data) {
self.bondsDataParams = new NgTableParams({count: 25}, {
dataset: data.results
})
});
Or fully support the lazy loading data API and set total. Uses getData: rather than just setting dataset.
var Api = $resource('/green-bonds.json');
this.bondsDataParams = new NgTableParams({}, {
getData: function (params) {
return Api.get(params.url()).$promise.then(function (data) {
params.total(data.count);
return data.results;
});
}
});
Note 1: By default $resource expects an object .get() for object, .query() for array. Also see isArray:. I didn't get this to work.
Note 2: params.url() provides $resource with the ng-table params. e.g. {page: "1", count: "10"}

Kendo grid columns with dynamic data

Need help with Kendo Grid, where in I have dynamic columns on Kendo Grid.
dynamicCols- Object is a object which has list of title and value properties which could be dynamic where it could have any number of objects in the list with title, value pair.
Kendo grid works well if JSON has a flat structure which has all properties at same level and I haven't come across this kind of hierarchial/JSON structure until now.
This grid also needs to support server side sorting and filtering with C# Web API, with Kendo Datasource API for server side sorting and filtering.
Existing kendo column mapping
var cols = [
{ field: 'name', title: 'Name', encoded: false },
{ field: 'id', title: 'Id' },
{ field: 'age', title: 'Age }
]
json = [{
name:'XYZ', id:123, age:45,
dynamicCols: [{title:'Gender',value:'Male'},
{title:'Veteran',value:'Yes'}]
}, {
name:'Jim', id:555, age:24,
dynamicCols: [{title:'Gender',value:'Male'},
{title:'Veteran',value:'No'}]
}, {
name:'Nick', id:557, age:78,
dynamicCols: [{title:'Gender',value:'Female'},
{title:'Veteran',value:'No'}]
}]
**Expected Grid**
Name Id Age Gender Veteran
XYZ 123 45 Male Yes
Jim 555 24 Male No
For Json2
json2 = [
{name:'XYZ', id:123, age:45,
dynamicCols: [{title:'SSN',value:'xx-xx-7891'}]
},
{name:'Jim', id:555, age:24,
dynamicCols: [{title:'SSN',value:'xx-xx-7892'}]
},
{name:'Nick', id:557, age:78,
dynamicCols: [{title:'SSN',value:'xx-xx-7895'}]
}];
**Expected Grid**
Name Id Age Gender SSN
XYZ 123 45 Male xx-xx-7891
Jim 555 24 Male xx-xx-7892
You have two options:
When you are done fetching the data and before creating the new Grid, resolve the JSON object and create flat columns object that the Grid accepts
Your second option is to forget the idea of creating dynamic columns and instead have a template column that dynamically resolves what it needs to display. In such cases you create an external function that you can call from your template. This way you do not end up with complicated and crappy templates. How to invoke external function from a template is covered here.
The easiest solution for me has been to make all columns and then end it by hiding those columns I did not need.
Even hiding around 50 columns did not take any noticable time.
(I had the luxury of knowing all potential columns that could appear)

Categories

Resources