Files
raven-client/ayanova/src/components/gz-data-table.vue
2020-01-31 20:05:48 +00:00

837 lines
32 KiB
Vue

<template>
<div>
<v-data-table
:headers="headers"
:items="records"
v-model="selected"
:options.sync="dataTablePagingOptions"
:server-items-length="totalRecords"
:loading="loading"
:disable-sort="true"
:show-select="showSelect"
:single-select="singleSelect"
:hide-default-header="narrowFormat"
:footer-props="{
showCurrentPage: true,
showFirstLastPage: true,
itemsPerPageOptions: rowsPerPageItems,
itemsPerPageText: lt('RowsPerPage'),
pageText: lt('PageOfPageText')
}"
:loading-text="lt('Loading')"
:no-data-text="lt('NoData')"
class="elevation-1"
>
<!-- /*From server: UiDataTypes
NoType = 0,v-on:item-selected="handleSelect"
DateTime = 1,
Date = 2,
Time = 3,
Text = 4,
Integer = 5,
Bool = 6,
Decimal = 7,
Currency = 8,
Tags = 9,
Enum = 10,
EmailAddress = 11,
HTTP = 12
*/ -->
<template v-slot:body="{ items }">
<tbody>
<template v-if="!narrowFormat">
<tr v-for="item in items" :key="item.id">
<template v-if="showSelect">
<td>
<v-checkbox
v-model="selected"
:value="item"
primary
hide-details
></v-checkbox>
</td>
</template>
<td v-for="c in item.columns" :key="c.key">
<!-- Handle all plain text types right up to and including enums -->
<!--TODO when get to coloured stuff will need to add that as a prop to column data but leaving out for now-->
<template v-if="c.t < 11">
<template v-if="c.i">
<!-- openable object with an ID -->
<v-btn depressed small @click="btnClick(c.key, c.i)">{{
c.v
}}</v-btn>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-if="c.t == 11">
<a :href="'mailto:' + c.v">{{ c.v }}</a>
</template>
<template v-if="c.t == 12">
<!-- Expects full url with protocol etc in c.v so might need to add to record builder -->
<a :href="c.v" target="_blank">{{ c.v }}</a>
</template>
</td>
</tr>
</template>
<template v-else>
<!-- Narrow width template -->
<tr
class="v-data-table__mobile-table-row"
v-for="item in items"
:key="item.id"
>
<!-- HEADER TO THE LEFT -->
<!-- FIELD VALUE TO THE RIGHT -->
<template v-if="showSelect">
<td class="v-data-table__mobile-row">
<div class="v-data-table__mobile-row__header"></div>
<div class="v-data-table__mobile-row__cell">
<v-checkbox
v-model="selected"
:value="item"
primary
hide-details
></v-checkbox>
</div>
</td>
</template>
<td
class="v-data-table__mobile-row"
v-for="c in item.columns"
:key="c.key"
>
<div class="v-data-table__mobile-row__header">
{{ getHeaderText(c.key) }}
</div>
<div class="v-data-table__mobile-row__cell">
<!-- Handle all plain text types right up to and including enums -->
<!--TODO when get to coloured stuff will need to add that as a prop to column data but leaving out for now-->
<template v-if="c.t < 11">
<template v-if="c.i">
<!-- openable object with an ID -->
<v-btn depressed small @click="btnClick(c.key, c.i)">{{
c.v
}}</v-btn>
</template>
<template v-else>
{{ c.v }}
</template>
</template>
<template v-if="c.t == 11">
<a :href="'mailto:' + c.v">{{ c.v }}</a>
</template>
<template v-if="c.t == 12">
<!-- Expects full url with protocol etc in c.v so might need to add to record builder -->
<a :href="c.v" target="_blank">{{ c.v }}</a>
</template>
</div>
</td>
</tr>
</template>
</tbody>
</template>
</v-data-table>
<!-- <hr />
<div>Selected: {{ selected }}</div>
<div>Headers: {{ headers }}</div>
<div>Records: {{ records }}</div>
<div>TotalRecords: {{ totalRecords }}</div>
<div>caption: {{ caption }}</div>
<div>apiBaseUrl: {{ apiBaseUrl }}</div>
<div>formKey: {{ formKey }}</div>
<div>dataListKey: {{ dataListKey }}</div>
<div>dataFilterId: {{ dataFilterId }}</div>
<div>viewPort is XS: {{ mini }}</div> -->
</div>
</template>
<script>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Xeslint-disable */
////////////////////////////////////////////////////////////////////////////////////////////////////////////
export default {
data() {
return {
loading: true,
dataTablePagingOptions: {},
headers: [],
serverColumns: [],
totalRecords: 0,
records: [],
rowsPerPageItems: [5, 10, 25, 50, 100],
selected: [],
narrowFormat: false
};
},
props: {
apiBaseUrl: {
type: String,
default: "DataList/List"
},
formKey: String,
dataListKey: String,
dataFilterId: {
type: Number,
default: 0
},
showSelect: {
type: Boolean,
default: false
},
singleSelect: {
type: Boolean,
default: false
}
},
watch: {
dataTablePagingOptions: {
handler() {
this.getDataFromApi();
},
deep: true
},
selected: function(newValue, oldValue) {
if (newValue.length != oldValue.length) {
this.handleSelectChange();
}
},
"$vuetify.breakpoint.xs": function(value) {
this.narrowFormat = value;
}
},
methods: {
//Used by narrow view to get the "header" text for a column based on the column key
getHeaderText(key) {
//key format is row-column e.g."500-2"
var columnIndex = key.split("-")[1];
var header = this.headers[columnIndex - 1];
if (header && header.text) {
return header.text;
}
return "";
},
handleSelectChange() {
//due to making own template for items need to handle singleselect which only affects if select all checkbox at top is visible when making own item template
if (this.singleSelect) {
this.selected.splice(0, this.selected.length - 1);
}
//emit event to parent form of selected rows
//this.$emit("update:selected", this.selected);
//Note that this bubbles up all the columns of all the selected rows
//could be more efficient if it just sends the ID's instead:
this.$emit("update:selected", window.$gz._.map(this.selected, "id"));
},
btnClick(key, i) {
//translate key to actual object type from header data
//key format is row-column e.g."500-2"
//get the datatype of the column which matches the server columns array index
var typeToOpen = this.serverColumns[key.split("-")[1]].ay;
//i is the actual AyaNova index of that record so we have all we need to open that object
window.$gz.eventBus.$emit("openobject", { type: typeToOpen, id: i });
},
lt(ltKey) {
return window.$gz.locale.get(ltKey);
},
ltFormat() {
return window.$gz.locale.format();
},
// mini() {
// //https://vuetifyjs.com/en/customization/breakpoints#breakpoint-service-object
// return this.$vuetify.breakpoint.xs;
// },
getDataFromApi() {
//debugger;
var that = this;
//DataList/list?DataListKey=TestWidgetDataList&Offset=0&Limit=999&DataFilterId=1&mini=true
//start with defaults
var listOptions = {
DataListKey: that.dataListKey,
Limit: 100,
Offset: 0
};
//calculate paging based on settings
const { page, itemsPerPage } = that.dataTablePagingOptions;
if (itemsPerPage && itemsPerPage > 0) {
listOptions.Offset = (page - 1) * itemsPerPage;
listOptions.Limit = itemsPerPage;
}
//is there a filter?
if (that.dataFilterId != 0) {
listOptions["DataFilterID"] = that.dataFilterId;
}
//Mini?
//OK, seems as though the datatable has a built in ability to handle small viewports by displaying each record vertically instead so maybe I only need
//mini for picklists
// if (that.$vuetify.breakpoint.xs) {
// listOptions["Mini"] = true;
// }
that.loading = true;
var listUrl =
that.apiBaseUrl + "?" + window.$gz.api.buildQuery(listOptions);
window.$gz.api.get(listUrl).then(res => {
//NOTE: This is how to call an async function and await it from sync code
(async function() {
//Save a copy of the server columns data for handling button clicks etc later
that.serverColumns = res.columns;
//Make sure the locale keys are fetched
await fetchLocalizedHeaderNames(res.columns); //Note can use await here because it's wrapped inside an async function call, it will wait then resume next stuff below
//build that.headers here
that.headers = buildHeaders(res.columns);
//Post process data here and then set that.records
that.records = buildRecords(
res.data,
res.columns,
that.$options.filters //this is the VUE filters collection, nothing to do with the local data structure
);
that.loading = false;
that.totalRecords = res.paging.count;
//persist the paging options so user sees same page and list on refresh or leave and return scenario
window.$gz.form.setFormSettings(that.formKey, {
temp: { page: that.dataTablePagingOptions.page },
saved: {
itemsPerPage: that.dataTablePagingOptions.itemsPerPage
}
});
//////////
})();
});
}
},
created() {
//rehydrate last form settings
var formSettings = window.$gz.form.getFormSettings(this.formKey);
if (formSettings.saved && formSettings.saved.itemsPerPage) {
this.dataTablePagingOptions.itemsPerPage =
formSettings.saved.itemsPerPage;
}
if (formSettings.temp && formSettings.temp.page) {
this.dataTablePagingOptions.page = formSettings.temp.page;
}
}
};
//Called by getDataFromApi on retrieval of list with columnData
function buildHeaders(columnData) {
//debugger;
//iterate columns, build headers and return
if (!columnData) {
return [];
}
var ret = [];
//iterate the columns, skip over the first one as it's the df column and not for display
for (var i = 1; i < columnData.length; i++) {
var cm = columnData[i];
var h = {};
h.text = window.$gz.locale.get(cm.cm);
h.value = "columns.c" + i.toString(); //+".v";
ret.push(h);
}
/*
{https://vuetifyjs.com/en/components/data-tables#api see headers property for this info:
text: string
value: string
align?: 'start' | 'center' | 'end'
sortable?: boolean
filterable?: boolean
divider?: boolean
class?: string | string[]
width?: string | number
filter?: (value: any, search: string, item: any) => boolean
sort?: (a: any, b: any) => number
}
*/
return ret;
}
//Called by getDataFromApi on retrieval of list with columnData
function buildRecords(listData, columndefinitions, filters) {
//iterate data, build each object keyed with index name and display set to correct locale filter and then return
if (!listData) {
return;
}
var ret = [];
//comes as an array of arrays, needs to leave as an array of objects representing each row
for (var iRow = 0; iRow < listData.length; iRow++) {
var row = listData[iRow];
//iterate row and build object representing row data keyed to index
//first column is the default column which sets the id for the row
var o = { id: row[0].v, columns: {} };
for (var iColumn = 1; iColumn < row.length; iColumn++) {
var column = row[iColumn];
var dataType = columndefinitions[iColumn].dt;
var display = column.v;
switch (dataType) {
case 1: //datetime format to shortdatetime
display = filters.shortdatelocalized(display);
break;
case 2: //date only
display = filters.shortdateonlylocalized(display);
break;
case 3: //time only
display = filters.shorttimeonlylocalized(display);
break;
case 6: //bool
display = filters.boolastext(display);
break;
case 7: //decimal
display = filters.decimal(display);
break;
case 8: //currency
display = filters.currency(display);
break;
case 10: //enum
display = filters.enum(display, columndefinitions[iColumn].et);
break;
default:
//do nothing, allow it to stay as is
}
//build the row column object that will be used by the datatable
var columnObject = {
v: display,
t: dataType,
key: iRow + "-" + iColumn
};
//is the source dtalist field openable? If so it will have an i property set for it's ID and we already know the types to open from the column headers data
//so between the two we can make a clickable button in the grid that triggers a function with the column index and the id and that in turn will bubble up the event to open that
//object
if (column.i) {
columnObject["i"] = column.i;
}
o.columns["c" + iColumn.toString()] = columnObject;
//Is:
//Headers: [ { "text": "Name", "value": "c1" }, { "text": "Serial #", "value": "c2" },
//Records: [ { "id": 1, "c1": "Incredible Metal Fish 76", "c2": 1, "c3": "$877.8", "c4": "AuthorizationRoles.65536", "c5": "2020-01-30 01:53:57 AM", "c6": "Yup", "c7": "Virgil Strosin 74" }, { "id": 2, "c1": "Practical Plastic Bike 77", "c2": 2, "
//CHANGE TO:
//Headers: [ { "text": "Name", "value": "c1.v" }, { "text": "Serial #", "value": "c2" },
//Records: [ { "id": 1,columns: {"c1": {v:"Incredible Metal Fish 76",t:THETYPE,key:ROWID-COLUMNID e.g. 1-1,1-2,2-1 etc}, "c2": 1, "c3": "$877.8", "c4": "AuthorizationRoles.65536", "c5": "2020-01-30 01:53:57 AM", "c6": "Yup", "c7": "Virgil Strosin 74" }, { "id": 2, "c1": "Practical Plastic Bike 77", "c2": 2, "
//THis way can have v-if for each column that changes based on type and then has it's shit figured out
}
ret.push(o);
}
return ret;
}
/*UiDataTypes
NoType = 0,
DateTime = 1,
Date = 2,
Time = 3,
Text = 4,
Integer = 5,
Bool = 6,
Decimal = 7,
Currency = 8,
Tags = 9,
Enum = 10,
EmailAddress = 11
*/
//////////////////////////////////////////////////////////
//
// Ensures column names are present in locale table
//
async function fetchLocalizedHeaderNames(columnData) {
if (!columnData) {
return;
}
var headerKeys = [];
for (var i = 1; i < columnData.length; i++) {
var cm = columnData[i];
headerKeys.push(cm.cm);
}
//Now fetch all the keys and await the response before returning
await window.$gz.locale
.fetch(headerKeys)
.then(() => {
return;
})
.catch(err => {
that.formState.ready = true; //show the form anyway so we know what's what
window.$gz.errorHandler.handleFormError(err);
});
}
//DataTable component
//https://vuetifyjs.com/en/components/data-tables#paginate-and-sort-server-side
/*
TODO:
This handles fetching and displaying the data, the parent form handles all other aspects:
- error message display
- opening items
- selecting filters and adding editing templates
What is required to be sent to server:
- example full request: DataList/list?DataListKey=TestWidgetDataList&Offset=0&Limit=999&DataFilterId=1
- example mini request: DataList/list?DataListKey=TestWidgetDataList&Offset=0&Limit=999&DataFilterId=1&mini=true
- Offset
- Limit
- DatafilterId
- mini=true if XS otherwise nothing
What it needs from it's parent form:
- form key for saving / getting persistent grid settings
- api list url (maybe, or if all from datalist then no)
- DataListKey value
- filter id
- refresh command
- rows per page
What it needs to provide TO it's parent form
- Custom event for this: https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
- Item click info to trigger opening edit form
- Selected items list for mass ops
- error event and message in case of server error or other error so parent can handle it
- Grid is responsible ONLY for showing data, nothing else
What will be delivered by the server:
Example request: http://localhost:7575/api/v8/DataList/List?Offset=2&Limit=2&DataListKey=TestWidgetDataList
Full response object:
{
"data": [
[
{
"v": 3
},
{
"v": "Handcrafted Fresh Keyboard 27",
"i": 3
},
{
"v": 3
},
{
"v": 697.34
},
{
"v": 128
},
{
"v": "2020-01-28T12:10:46.212435Z"
},
{
"v": true
},
{
"v": "Doug Effertz 17 - OpsAdminFull",
"i": 18
}
],
[
{
"v": 4
},
{
"v": "Unbranded Fresh Shoes 28",
"i": 4
},
{
"v": 4
},
{
"v": 219.09
},
{
"v": 16
},
{
"v": "2020-01-28T12:33:14.347243Z"
},
{
"v": true
},
{
"v": "Samuel Powlowski 14 - CustomerLimited",
"i": 15
}
]
],
"paging": {
"count": 100,
"offset": 2,
"limit": 2,
"first": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&pageNo=1&pageSize=2",
"previous": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&pageNo=1&pageSize=2",
"next": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&pageNo=3&pageSize=2",
"last": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&pageNo=50&pageSize=2"
},
"columns": [
{
"cm": "df",
"dt": 0,
"ay": 2
},
{
"cm": "WidgetName",
"dt": 4,
"ay": 2
},
{
"cm": "WidgetSerial",
"dt": 5
},
{
"cm": "WidgetDollarAmount",
"dt": 8
},
{
"cm": "WidgetRoles",
"dt": 10
},
{
"cm": "WidgetStartDate",
"dt": 1
},
{
"cm": "Active",
"dt": 6
},
{
"cm": "User",
"dt": 4,
"ay": 3
}
]
}
MINI Response object:
{
"data": [
[
{
"v": 3
},
{
"v": "Handmade Soft Towels 27",
"i": 3
},
{
"v": 3
}
],
[
{
"v": 4
},
{
"v": "Handcrafted Cotton Cheese 28",
"i": 4
},
{
"v": 4
}
]
],
"paging": {
"count": 100,
"offset": 2,
"limit": 2,
"first": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&Mini=True&pageNo=1&pageSize=2",
"previous": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&Mini=True&pageNo=1&pageSize=2",
"next": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&Mini=True&pageNo=3&pageSize=2",
"last": "http://localhost:7575/api/v8/DataList/List?DataListKey=TestWidgetDataList&Mini=True&pageNo=50&pageSize=2"
},
"columns": [
{
"cm": "df",
"dt": 0,
"ay": 2
},
{
"cm": "Widget",
"dt": 4,
"ay": 2
}
]
}
What this grid needs to do:
- On created or refresh or change of filter
- set Loading property to true on data object
- send the get request to the server based on filter, viewport etc
- On receipt of successful response
- update the paging values
- update the persistent form settings with the current grid selections for paging etc
- generate the column headers
- They will have locale keys so this grid fetches the keys as necessary at this point as it could change dynamically almost
- Generate the Item slot template (which I've never done in code before but only in the template itself so needs research)
- set the property names in the incoming data to the correct matching headers
- set display filters based on locale settings (i.e. currency, datetime filter which converts to local time offset etc)
- fill the rows
- format?
- On fail
- Do something
- set loading property to false
TODO:
- Start coding grid here with vue component parameters and handlers first and in conjunction with test-widgets.vue
- Once it's parent / component properties are set then get into the details
Headers: [
{
"text": "Name",
"value": "c1" }, {
"text": "Serial #",
"value": "c2" }, { "text": "Price", "value": "c3" }, { "text": "Roles", "value": "c4" }, { "text": "Start", "value": "c5" }, { "text": "Active", "value": "c6" }, { "text": "User", "value": "c7" } ]
Records: [ { "c1": "Awesome Steel Table 25", "c2": 1, "c3": 676.64, "c4": 0, "c5": "2020-01-28T16:50:29.371098Z", "c6": true, "c7": "Cameron Marvin 1 - BizAdminLimited" }, { "c1": "Fantastic Concrete Soap 26", "c2": 2, "c3": 310.6, "c4": 32768, "c5": "2020-01-28T15:56:47.362545Z", "c6": true, "c7": "Becky Parker 9 - TechFull" }, { "c1": "Handmade Soft Towels 27", "c2": 3, "c3": 463.7, "c4": 8, "c5": "2020-01-28T16:55:51.487005Z", "c6": true, "c7": "Zachary Renner 14 - CustomerLimited" }, { "c1": "Handcrafted Cotton Cheese 28", "c2": 4, "c3": 934.95, "c4": 1, "c5": "2020-01-28T16:55:23.324561Z", "c6": true, "c7": "Zachary Renner 14 - CustomerLimited" }, { "c1": "Refined Concrete Salad 29", "c2": 5, "c3": 758.3, "c4": 32, "c5": "2020-01-28T16:00:46.703856Z", "c6": true, "c7": "Terri Paucek 2 - BizAdminFull" }, { "c1": "Licensed Concrete Chips 30", "c2": 6, "c3": 580.73, "c4": 2048, "c5": "2020-01-28T16:44:29.601375Z", "c6": true, "c7": "Virginia Friesen 17 - OpsAdminFull" }, { "c1": "Handmade Cotton Pants 31", "c2": 7, "c3": 880.37, "c4": 1024, "c5": "2020-01-28T16:43:44.380156Z", "c6": true, "c7": "Bernadette Denesik 12 - SubContractorLimited" }, { "c1": "Refined Cotton Sausages 32", "c2": 8, "c3": 615.32, "c4": 1, "c5": "2020-01-28T16:33:10.667606Z", "c6": true, "c7": "Alberta Gleichner 22 - fr" }, { "c1": "Tasty Soft Hat 33", "c2": 9, "c3": 273.68, "c4": 512, "c5": "2020-01-28T16:19:12.318658Z", "c6": true, "c7": "Sadie Stehr 15 - CustomerFull" }, { "c1": "Refined Frozen Bike 35", "c2": 11, "c3": 538.56, "c4": 32, "c5": "2020-01-28T16:46:25.906627Z", "c6": true, "c7": "Sadie Stehr 15 - CustomerFull" } ]
headers: [
{
text: 'Dessert (100g serving)',
align: 'left',
sortable: false,
value: 'name',
},
{ text: 'c2', value: 'c2' },
{ text: 'c3 (g)', value: 'c3' },
{ text: 'c4 (g)', value: 'c4' },
{ text: 'c5 (g)', value: 'c5' },
{ text: 'Iron (%)', value: 'c6' },
],
[
{
name: 'Frozen Yogurt',
c2: 159,
c3: 6.0,
c4: 24,
c5: 4.0,
c6: '1%',
},
{
name: 'Ice cream sandwich',
c2: 237,
c3: 9.0,
c4: 37,
c5: 4.3,
c6: '1%',
},
{
name: 'Eclair',
c2: 262,
c3: 16.0,
c4: 23,
c5: 6.0,
c6: '7%',
},
{
name: 'Cupcake',
c2: 305,
c3: 3.7,
c4: 67,
c5: 4.3,
c6: '8%',
},
{
name: 'Gingerbread',
c2: 356,
c3: 16.0,
c4: 49,
c5: 3.9,
c6: '16%',
},
{
name: 'Jelly bean',
c2: 375,
c3: 0.0,
c4: 94,
c5: 0.0,
c6: '0%',
},
{
name: 'Lollipop',
c2: 392,
c3: 0.2,
c4: 98,
c5: 0,
c6: '2%',
},
{
name: 'Honeycomb',
c2: 408,
c3: 3.2,
c4: 87,
c5: 6.5,
c6: '45%',
},
{
name: 'Donut',
c2: 452,
c3: 25.0,
c4: 51,
c5: 4.9,
c6: '22%',
},
{
name: 'KitKat',
c2: 518,
c3: 26.0,
c4: 65,
c5: 7,
c6: '6%',
},
]
},
},
}
MOBILE VIEW
https://github.com/vuetifyjs/vuetify/blob/088a5093ea6396d1ff12e0d62143f89572c667d6/packages/vuetify/src/components/VDataTable/MobileRow.ts
<div class="v-data-table__wrapper">
<table><colgroup><col class=""><col class=""><col class=""><col class=""><col class=""><col class=""></colgroup>
<thead class="v-data-table-header v-data-table-header-mobile"><tr><th><div class="v-data-table-header-mobile__wrapper"><div class="v-input v-input--hide-details v-input--is-focused theme--light v-text-field v-text-field--is-booted v-select primary--text"><div class="v-input__control"><div role="button" aria-haspopup="listbox" aria-expanded="false" aria-owns="list-6106" class="v-input__slot"><div class="v-select__slot"><label for="input-6106" class="v-label v-label--active theme--light primary--text" style="left: 0px; right: auto; position: absolute;">Sort by</label><div class="v-select__selections"><input id="input-6106" readonly="readonly" type="text" aria-readonly="true" autocomplete="off"></div><div class="v-input__append-inner"><div class="v-input__icon v-input__icon--append"><i aria-hidden="true" class="v-icon notranslate mdi mdi-menu-down theme--light primary--text"></i></div></div><input type="hidden">
</div><div class="v-menu"><div class="v-menu__content theme--light " style="max-height: 304px; min-width: 0px; top: 12px; left: 0px; transform-origin: left top 0px; z-index: 0; display: none;"></div></div></div></div></div></div></th></tr></thead>
<tbody>
<tr class="v-data-table__mobile-table-row">
<td class="v-data-table__mobile-row">
<div class="v-data-table__mobile-row__header">Dessert (100g serving)</div>
<div class="v-data-table__mobile-row__cell">Frozen Yogurt</div>
</td>
<td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Calories</div><div class="v-data-table__mobile-row__cell">159</div></td>
<td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Fat (g)</div><div class="v-data-table__mobile-row__cell">6</div></td>
<td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Carbs (g)</div><div class="v-data-table__mobile-row__cell">24</div></td>
<td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Protein (g)</div><div class="v-data-table__mobile-row__cell">4</div></td>
<td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Iron (%)</div><div class="v-data-table__mobile-row__cell">1%</div></td></tr>
<tr class="v-data-table__mobile-table-row"><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Dessert (100g serving)</div><div class="v-data-table__mobile-row__cell">Ice cream sandwich</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Calories</div><div class="v-data-table__mobile-row__cell">237</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Fat (g)</div><div class="v-data-table__mobile-row__cell">9</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Carbs (g)</div><div class="v-data-table__mobile-row__cell">37</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Protein (g)</div><div class="v-data-table__mobile-row__cell">4.3</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Iron (%)</div><div class="v-data-table__mobile-row__cell">1%</div></td></tr><tr class="v-data-table__mobile-table-row"><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Dessert (100g serving)</div><div class="v-data-table__mobile-row__cell">Eclair</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Calories</div><div class="v-data-table__mobile-row__cell">262</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Fat (g)</div><div class="v-data-table__mobile-row__cell">16</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Carbs (g)</div><div class="v-data-table__mobile-row__cell">23</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Protein (g)</div><div class="v-data-table__mobile-row__cell">6</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Iron (%)</div><div class="v-data-table__mobile-row__cell">7%</div></td></tr><tr class="v-data-table__mobile-table-row"><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Dessert (100g serving)</div><div class="v-data-table__mobile-row__cell">Cupcake</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Calories</div><div class="v-data-table__mobile-row__cell">305</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Fat (g)</div><div class="v-data-table__mobile-row__cell">3.7</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Carbs (g)</div><div class="v-data-table__mobile-row__cell">67</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Protein (g)</div><div class="v-data-table__mobile-row__cell">4.3</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Iron (%)</div><div class="v-data-table__mobile-row__cell">8%</div></td></tr><tr class="v-data-table__mobile-table-row"><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Dessert (100g serving)</div><div class="v-data-table__mobile-row__cell">Gingerbread</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Calories</div><div class="v-data-table__mobile-row__cell">356</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Fat (g)</div><div class="v-data-table__mobile-row__cell">16</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Carbs (g)</div><div class="v-data-table__mobile-row__cell">49</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Protein (g)</div><div class="v-data-table__mobile-row__cell">3.9</div></td><td class="v-data-table__mobile-row"><div class="v-data-table__mobile-row__header">Iron (%)</div><div class="v-data-table__mobile-row__cell">16%</div></td>
</tr>
</tbody>
</table>
</div>
Mobile Checkbox row
<td class="v-data-table__mobile-row">
<div class="v-data-table__mobile-row__header"></div>
<div class="v-data-table__mobile-row__cell">
<div class="v-data-table__checkbox v-simple-checkbox">
<div class="v-input--selection-controls__ripple"></div>
<i aria-hidden="true" class="v-icon notranslate mdi mdi-checkbox-blank-outline theme--light">
</i>
</div>
</div>
</td>
*/
</script>