Vue 2 - Flexgrid - Show multiiple column headers and row data in one column

Posted by: eric.devwise on 18 March 2019, 10:26 pm EST

  • Posted 18 March 2019, 10:26 pm EST - Updated 3 October 2022, 8:07 pm EST

    I have attached a screenshot of what I am trying to achieve. I have multiple columns that I would like to condense and show in one column. In the image you can see the column headers stack as well as the corresponding data.

    Is this possible with Flexgrid or is there any similar functionality?

  • Posted 19 March 2019, 8:03 am EST

    Hi Eric,

    If you need to show the data only i.e read-only mode then you may simply handle the formatItem event and display the required data. However, if you also need to let the user edit the data then you need to use the multirow control instead of FlexGrid. Please refer to the following sample to get started with the multiRow: https://demos.wijmo.com/5/PureJS/MultiRowIntro/MultiRowIntro/

    ~Sharad

  • Posted 19 March 2019, 10:31 pm EST

    How would I implement this as a vue js component? Could I still explicitly write columns as or is it different? Also when I mount the component do I still assign the data to a new wjCore.CollectionView(response.data)?

    Is it possible to show me through a js fiddle example?

  • Posted 20 March 2019, 6:29 am EST

    Please refer to the following sample for Vue equivalent:

    https://codesandbox.io/s/0p21zwpxv

    In case of MultiRow, columns definition is provided using the layoutDefinition property and writing columns as is not supported.

  • Posted 20 March 2019, 5:36 pm EST

    Thanks I have multirow working, however what about all my flex-grid events like grid.hitTest() or formatItem()? Can I still refer to ```

    e.panel.columns[e.col].binding

  • Posted 22 March 2019, 1:52 am EST

    Hi Eric,

    MultiRow extends from FlexGrid so all of grid’s events like formatItem and methods like hitTest are still available in MultiRow.

    Please let us know if you face any issues with their usage.

  • Posted 22 March 2019, 4:05 pm EST

    I am having some issues:

    For starters my column filtering is gone. I had both global search bar filtering & column based filtering. Global search bar is working, but column is not.

    Another bigger issue is that I am experiencing some weird behavior when layout def is set a certain way where cells colspan is 1, 1, and 2 for the bottom row of the group. On the bottom cell I have hitTest() and formatItem…but with this layout those are not working.

    What’s weird too is when I set up the bottom row of the group to be on top i.e. cells colspan 2,1,1…then the hitTest() and formatItem() works however this takes up colspan 2 like it should but then wraps over to the cell on the bottom of the group.

    I will see if I can put together some example code of this, but yes I need help in understanding why this is happening.

  • Posted 25 March 2019, 1:03 am EST

    Further, please refer to the ‘MultiRow Layout Definition’ section of the following sample which explains how layoutDefinition works:

    https://demos.wijmo.com/5/angular/MultiRowIntro/MultiRowIntro/

  • Posted 25 March 2019, 1:03 am EST

    Hi Eric,

    We are sorry but we are unable to replicate the issue at our end with the column filters. Column filters seem to be working fine in our test. Could you please have a look at the following sample and let us know if we are missing something in order to replicate the issue:

    https://codesandbox.io/s/jvkoozlly

    Also, could you please explain the exact issue you are facing with the usage of hitTest()/formatItem? Please attach some code snippets and snapshots for a better understanding of the issue.

    Regards

  • Posted 25 March 2019, 11:32 pm EST

    I am still can’t get the column to show up with multirow.

    I even stripped the code down to the bare minimum and tested, but still no column filters.

    Also the hitTest() and formatItem issue is still occuring where the content will take up grid cell colspan 2 like it shoud, but will then overflow and wrap to be the next cell. This next cell should not contain the top cell’s content. I then moved the top colspan 2 cell below the two colpan 1 cells and the formatItem and hitTest() stops working.

    Here is the full code:

    
    <template>
      <el-container>
        <el-header class="formatHeader">
          <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item><a href="/createpo">food List</a></el-breadcrumb-item>
            <el-breadcrumb-item>Product list</el-breadcrumb-item>
          </el-breadcrumb>
            <el-tag size="medium">{{ foodName }} PO# {{ numOfFoods }}</el-tag>
        </el-header>
          <el-main>
            <div class="toolBar">
              <!-- Search Input -->
              <div>
                <el-input 
                  type="text" 
                  class="gobalSearchBar" 
                  id="filter" 
                  clearable
                  placeholder="search"
                  v-model="filterText">
                  <i slot="prefix" class="el-input__icon el-icon-search"></i>
                </el-input>
              </div>
              <el-button class="exportExcel saveBtn" type="info" @click="saveColumnLayout()">Save Column Layout</el-button>
              <el-button class="exportExcel loadBtn" type="primary" @click="loadColumnLayout()">Load Column Layout</el-button>
              <!-- Export As Excel -->
              <el-button class="exportExcel" type="primary" icon="el-icon-document" @click="exportExcel()">Export Excel</el-button>
              <!-- Create PO Instructions-->
              <el-alert
                title="Create a PO:"
                type="info"
                description="Fill in Order Qty(s), and Click Save PO Button"
                show-icon
                :closable="false">
              </el-alert>
              <div class="savePo">
              <!-- Save PO To DB -->
                <el-button type="info" v-show="!save" @click="savePo" :disabled="btnClicked">Save PO</el-button>
                <el-button type="success" v-show="save" @click="updatePo">Update PO</el-button>
              </div>
            </div>
    
              <!-- Product Modal -->
              <el-dialog v-for="info in foodInfo" :key="info.id" :title="info.title" :visible.sync="dialogFormVisible">
                <wj-flex-grid
                  class="grid"
                  id="modalGrid"
                  control="grid"
                  :items-source="foodInfo"
                  :format-item="formatModalGrid"
                  selection-mode="None"
                  header-visibility="All">
                  
                  <wj-flex-grid-column binding="oranges" header="Oranges"></wj-flex-grid-column>
                  <wj-flex-grid-column binding="apples" header="Apples"></wj-flex-grid-column>
                  <wj-flex-grid-column binding="crackers" header="Crackers"></wj-flex-grid-column>
                  <wj-flex-grid-column binding="proteins" header="Proteins" :data-map="proteins"></wj-flex-grid-column>
    
                  </wj-flex-grid>
                  <span slot="footer" class="dialog-footer">
                    <el-button type="primary" @click="dialogFormVisible=false">Close</el-button>
                  </span>
              </el-dialog>
              <!-- END OF MODAL -->
    
            <!-- DataTable Grid -->
            <wj-multi-row
              class="grid expanded-groups"
              id="theGrid"
              control="grid"
              :items-source="data"
              :layout-definition="def"
              :collapsed-headers="null"
              :initialized="initGrid"
              :allow-sorting="true"
              :format-item="formatProduct"
              selection-mode="Cell"
              header-visibility="All">
    
              <!-- Each Column Has A Filter -->
              <wj-flex-grid-filter :initialized="initFil"></wj-flex-grid-filter>
    
            </wj-multi-row>
            <!-- End Grid -->
    
            <!-- Pagination -->
            <div class="btn-group pagination">
              <button type="button" class="btn btn-default"
                  @click="data.moveToFirstPage()"
                  :disabled="data.pageIndex <= 0">
                  <span class="fas fa-fast-backward"></span>
              </button>
              <button type="button" class="btn btn-default"
                  @click="data.moveToPreviousPage()"
                  :disabled="data.pageIndex <= 0">
                  <span class="fas fa-step-backward"></span>
              </button>
              <button type="button" class="btn btn-default" disabled style="width:100px">
                  {{data.pageIndex + 1}}/{{data.pageCount}}
              </button>
              <button type="button" class="btn btn-default"
                  @click="data.moveToNextPage()"
                  :disabled="data.pageIndex >= data.pageCount - 1">
                  <span class="fas fa-step-forward"></span>
              </button>
              <button type="button" class="btn btn-default"
                  @click="data.moveToLastPage()"
                  :disabled="data.pageIndex >= data.pageCount - 1">
                  <span class="fas fa-fast-forward"></span>
              </button>
              <select name="pagesize" id="pageSize" v-model="pageSize">
                <option :value=25>25</option>
                <option value=50>50</option>
                <option value=100>100</option>
              </select>
          </div>
        </el-main>         
      </el-container>
    </template>
    
    <script>
    
    import * as wjCore from 'wijmo/wijmo'
    import * as wjGrid from 'wijmo/wijmo.grid'
    import * as wjGridXlsx from 'wijmo/wijmo.grid.xlsx';
    import * as wjInput from 'wijmo/wijmo.input'
    import 'wijmo/wijmo.vue2.input'
    import 'wijmo/wijmo.vue2.grid.filter'
    import 'wijmo/wijmo.vue2.grid.multirow'
    import * as wjMultirow from "wijmo/wijmo.grid.multirow"
    import { CollectionView } from 'wijmo/wijmo';
    
    
    
    export default {
      name: 'PoProducts',
      data () {
        let foodsMap = [
          { value: 1, text: "example1" },
          { value: 2, text: "example2" },
          { value: 3, text: "example3" },
          { value: 4, text: "example4" }
        ]
        let proteinsMap = [
          { value: 0, text: "test1" },
          { value: 1, text: "test2" },
          { value: 2, text: "test3" },
          { value: 3, text: "test4" }
        ]
        return {
          grid: null,
          data: new wjCore.CollectionView(),
          // Define Grouped Columns Layout
          def: [
            {
              header: '', colspan: 1, cells: [
                {binding: 'oranges', header: 'Oranges', colspan: 1}
              ]
            },
            {
              header: 'Food Info', colspan: 2, cells: [
                {binding: 'lemons', header: 'lemons', colspan: 1},
                {binding: 'apples', header: 'apples', colspan: 1},
                {binding: 'bananas', header: 'Banans', colspan: 2, wordWrap: true, multiLine: true},
              ]
            },
            {
              header: 'Food Details', colspan: 2, cells: [
                {binding: 'crackers', header: 'Crackers', colspan: 1},
                {binding: 'kiwis', header: 'Kiwis', colspan: 1},
                {binding: 'strawberries', header: 'Strawberries', colspan: 2},
              ]
            },
            {
              header: 'Category', colspan: 2, cells: [
                {binding: 'fruit', header: 'Fruit', colspan: 2},
                {binding: 'vegetables', header: 'Veggies', colspan: 1},
                {binding: 'carbs', header: 'Carbs', colspan: 1},
              ]
            }
          ],
          foods: new wjGrid.DataMap(foodsMap, "value", "text"),
          foodName: 'Food Name Will Go Here',
          numOfFoods: 0,
          foodFormData: [],
          isVegetable: false,
          save: false,
          btnClicked: false,
          pageSize: 100,
          filterText: '',
          filterRx: null,
          proteins: new wjGrid.DataMap(proteinsMap, "value", "text"),
          dialogFormVisible: false,
          foodInfo: []  
        }
      },
      mounted(){
        // Get All Products From Clicked food Name
        axios.get('/get_foods/'+food)
        .then((response) => {
          (this.data = new wjCore.CollectionView(response.data.food_items, {
            pageSize: this.pageSize
          }), this.data.filter = this.filter)
        .then(this.numOfFoods = response.data.numOfFoods);
        });
      },
    
      methods: {
        initGrid(grid) { 
    
          // grid.autoSizeColumns();
          this.grid = grid;
    
          // SET food NAME
          let foodNum = food;
          let foodNameIndex = this.food.collectionView.sourceCollection.filter(o=>Object.values(o).includes(foodNum));
          let foodName = foodNameIndex[0].text;
          this.foodName = foodName;
    
    
          // CLICK ON PRODUCT TITLE TO OPEN MODAL
          grid.hostElement.addEventListener('click',  (e)=>{
            
            let htInfo = grid.hitTest(e);
            let panel = htInfo.panel,
              col = htInfo.panel.columns[htInfo.col],
              row = htInfo.panel.rows[htInfo.row];
            
            if(!htInfo.panel || htInfo.panel.cellType!=wjGrid.CellType.Cell){
              return;
            }
    
            if(col.binding != 'oranges'){
              return;
            }
    
            this.foodInfo = [{title: row.dataItem.oranges, uad: row.dataItem.uad, apples: row.dataItem.apples, crackers: row.dataItem.crackers}];
            // trigger Product Modal
            this.dialogFormVisible = true;
    
          });
          
    
          // ONLY ALLOW NUMERIC VALUES FOR Oranges 
          grid.prepareCellForEdit.addHandler((s, e) => {
    
            let col = e.panel.columns[e.col];
    
            if (col.binding != "apples") {
              return;
            }
    
            wjCore.setAttribute(s.activeEditor, "type", "number");
            s.activeEditor.addEventListener("keydown", e => {
              if (e.keyCode == 69) {
                e.preventDefault();
              }
            })
            
            // set the value again, since prev value was of text type
            let val = e.panel.getCellData(e.row, e.col);
            s.activeEditor.value = val;
    
          });
    
          // UPDATE COMBO STYLING FLAG
          this.updateFlags(grid.collectionView.items, 'lemons');
          
          
          // BUILD PO FORM DATA
          grid.cellEditEnded.addHandler((s, e) => {
    
            let col = e.panel.columns[e.col];
            let row = e.panel.rows[e.row];
            let qtys =  [];
            let poData = [];
    
            if ( col.binding != "oranges" && col.binding != "apples" ) {
              return;
            }
    
            this.data.sourceCollection.forEach(row => {
              if(row.oranges != undefined && row.oranges > 0) {
                // build condition to allow combo
                qtys.push(row.oranges);
                // add or remove row data to and from foodFormData array
                foodData.push({
                  food_number: this.numOfFoods, 
                  food: row.food, 
                  food_lemons: row.food_lemons, 
                  seller_lemons: row.lemons, 
                  price: row.crackers, 
                  crate: row.oranges,
                  bucket: row.lemons});
              } else {
                foodData.splice(row, 0);
              }
            });
    
            this.foodFormData = poData;
    
            // sort desc when order qty is entered
            if (col.binding == 'oranges') {
              this.applySort();
            }
    
            // only show make combo column if Milwaukee
            if( row.dataItem.food == 4 && qtys.length > 1 ) {
              this.isVegetable = true;
            } else {
              this.isVegetable = false;
            }
    
            // if lemmons made add cell styling
            this.updateFlags(s.collectionView.items, 'lemmons');
          // end cell edit ended event 
          });
    
          // IF FLAG THEN ADD CLASS FOR lemmon CELL STYLING
          grid.formatItem.addHandler((s, e) => {
            if (e.panel != s.cells) {
              return;
            }
    
            let col = s.columns[e.col],
              row = s.rows[e.row];
    
            if (col.binding != "lemmons") {
              return;
            }
    
            let dataItem = row.dataItem;
            // compare to other values too
            if (dataItem.lemmons != undefined && dataItem.lemmons !== '' && dataItem["_flag"] == "green") {
              wjCore.addClass(e.cell, "green-cell");
            } else if (dataItem.lemmons != undefined && dataItem.lemmons !== '') {
              wjCore.addClass(e.cell, "red-cell");
            }
          });
    
    
          // COLUMN SELECTION
          // format top left to display edit icon
          grid.formatItem.addHandler((s,e)=>{
            if(s.topLeftCells == e.panel){
              e.cell.innerHTML = '<span class="wj-glyph-pencil col-picker-icon" style="color: orange"></span>';
            }
          });
    
          // create the column picker
          var theColumnPicker = new wjInput.ListBox(document.createElement('div'), {
            itemsSource: grid.columns,
            checkedMemberPath: 'visible',
            displayMemberPath: 'header',
            lostFocus() {
              wjCore.hidePopup(theColumnPicker.hostElement);
            }
          });
          wjCore.addClass(theColumnPicker.hostElement, 'col-picker');
    
          // show column picker
          let ref = grid.hostElement.querySelector('.wj-topleft');
          grid.hostElement.addEventListener('mousedown', function (e) {
            if (wijmo.hasClass(e.target, 'col-picker-icon')) {
              wjCore.showPopup(theColumnPicker.hostElement, ref, false, true, false);
              theColumnPicker.focus();
              e.preventDefault();
            }
          }, true);
    
        // end initGrid method
        },
        applySort() {
          if (!this.grid) {
            return;}
    
          // create sort
          let sd = new wjCore.SortDescription('oranges', false);
          // clear previous sort
          this.grid.collectionView.sortDescriptions.clear();
          // push collectionView's sortDescription property
          this.grid.collectionView.sortDescriptions.push(sd);
        },
        updateFlags(data, prop) {
          data.forEach(item => {
            item["_flag"] = "red";
          });
          for (let i = 0; i < data.length - 1; i++) {
            for (let j = i + 1; j < data.length; j++) {
              if (data[i][prop] == data[j][prop]) {
                data[i]["_flag"] = data[j]["_flag"] = "green";
              }
            }
          }
        },
        saveColumnLayout() {
            let grid = wijmo.Control.getControl("#theGrid");
            localStorage['columns'] = grid.columnLayout;
            this.$message({
              message: 'Column layout saved.',
              type: 'success'
            })
        },
        loadColumnLayout() {
            let grid = wijmo.Control.getControl("#theGrid"),
                columnLayout = localStorage['columns'];
            if (columnLayout) {
                grid.columnLayout = columnLayout;
            }
        },
        formatProduct (s, e) {
          if( e.panel == s.cells && s.columns[e.col].binding == 'uad' ) {
            // display food images
            e.cell.innerHTML = wijmo.format(
              '<img src="{uad}">',
              s.rows[e.row].dataItem);
          } else if( e.panel == s.cells && s.columns[e.col].binding == 'crackers' ) {
            // display food name as a link
            e.cell.innerHTML = wijmo.format(
              '<a href="#">{crackers}</a>',
              s.rows[e.row].dataItem);
          } else if( e.panel == s.cells && s.columns[e.col].binding == 'lemmons' ) {
            if( s.rows[e.row].dataItem.oranges == undefined 
              || s.rows[e.row].dataItem.oranges == '' 
              || s.rows[e.row].dataItem.oranges == 0 ) {
                e.cell.innerText = wijmo.format( '', s.rows[e.row].dataItem );
            } 
          } 
        },
        formatModalGrid (s,e) {
          if ( e.panel == s.cells && s.columns[e.col].binding == 'uad' ) {
            // display food image 
            e.cell.innerHTML = wijmo.format(
              '<img src="{uad}">',
              s.rows[e.row].dataItem);
    
            let grid = wijmo.Control.getControl(document.getElementById('modalGrid'));
            grid.rows.forEach(row => {
              return row.size = 80;
            })
          }
        },
        savePo() {
          this.btnClicked = true;
          axios.post('/make_food_list', this.foodFormData)
            .then(response => {
                if(response.status == 200) {
                  this.foodMade()
                  return this.save = true;
                } 
            })
            .catch(function (error) {              
              alert(error);
            });
        },
        updateFoodForm() {
          axios.post('/update_food_list', this.foodFormData)
                .then(response => {
                    if(response.status == 200) {
                      this.foodMade()
                    } 
                })
                .catch(function (error) {              
                  alert(error);
                });
        },
        foodMade() {
            this.$notify({
            title: 'Success',
            message: 'Food List Saved',
            type: 'success'
          });      
        },
        foodListError() {
          this.$notify({
            title: 'Error',
            message: 'List not Saved',
            type: 'error'
          });      
        },
        exportExcel() {
          if(this.grid){
            wjGridXlsx.FlexGridXlsxConverter.save(this.grid, null, 'sample.xlsx');
          }
        },
        initFil(fil) {
          //save filter instance for later use
          this.flexFilter = fil;
    
          fil.filterApplied.addHandler((s,e)=>{
            setTimeout(() => {
              fil.grid.collectionView.filter = this.filter;
            });
          });
        },
        filter(item) {
          // passes both filters
          return this.itemPassesDefaultFilter(item) && this.itemPassesCustomFilter(item);
        },
        itemPassesDefaultFilter(item) {
          // no filter, thus filter passed
          if (!this.flexFilter) {
            return true;
          }
    
          let filCols = this.flexFilter.filterColumns;
          if (!filCols) {
            filCols = this.flexFilter.grid.columns.map(col => col.binding);
          }
          for (let i = 0; i < filCols.length; i++) {
            let colBinding = filCols[i];
            let colFil = this.flexFilter.getColumnFilter(colBinding);
            if (!colFil.apply(item)) {
              // failed FlexGridFilter
              return false;
            }
          }
          // all column filters passed 
          return true;
        },
        itemPassesCustomFilter(item) {
          return this.filterRx == null || this.filterRx.test(item.apples) || this.filterRx.test(item.lemons);
        }
      },
    
      created: function() {
        this.data.filter = this.filter;
    
        this.$watch('filterText', function() {
          this.filterRx = this.filterText ? new RegExp(this.filterText, 'i') : null;
          this.data.refresh();
        })
      },
    
      watch: {
        pageSize(val,oldVal) {
          this.data.pageSize=parseInt(val);
        }
      }
    }
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
      .el-header { 
        display: flex;
        align-items: center;
        font-size: 14px;
        height: 30px !important;}
    
      .el-breadcrumb {
        margin-right: 20px;}
    
      .el-button {
        font-weight: 200;}
    
      .el-tag {
        background-color: #546F87;
        border-color: #546F87;
        color: #FFF;
        font-weight: 200;
        font-size: 14px;}
    
      .el-main .wj-control {  
        display: inline-block;  
        vertical-align: top;  
        width: 100%;  
        height: auto;}
    
      .toolBar {
        display: flex;
        align-items: baseline;}
    
      .el-alert {
        margin-left: 20px;
        max-width: 500px;}
    
      .el-input {
        min-width: 300px;}
    
      .wj-cell.no-padding-cell {
        padding: 0px!important;}
    
      .hidden { display: none; }
    
      .exportExcel {
        margin: 0 0 0 20px;
        margin-bottom: 20px;}
    
    s  .grid {
          font-size: 14px;
          height: auto;
          max-height: 600px;}
    
      h1, h2 {
        font-weight: normal;}
    
      ul {
        list-style-type: none;
        padding: 0;}
    
      li {
        display: inline-block;
        margin: 0 10px;}
    
      .fa-fast-forward,
      .fa-step-forward,
      .fa-fast-backward,
      .fa-step-backward {
        color: #333;}
    </style>
    <style>
      .expanded-groups .wj-cell.wj-group-header {
        font-weight: 200;
        background-color: #546F87;
        border-color: transparent;
        color: #FFF;
      }  
      .wj-state-multi-selected{
        background: #546F87 !important;
        opacity: 0.885;
      }
      .wj-state-selected{
        background: #0074D9 !important;
      }
      /* div[wj-part="div-valuses"]{
        max-height:200px;
      } */
      .wj-cell.green-cell:not(.wj-header):not(.wj-group):not(.wj-state-selected):not(.wj-state-multi-selected) {
      background-color: rgba(46,139,87,0.2);
      /* color: #FFF; */
      }
      .wj-cell.red-cell:not(.wj-header):not(.wj-group):not(.wj-state-selected):not(.wj-state-multi-selected) {
        background-color: rgba(255,0,1,0.9);
        color: #FFF;
      }
      .pagination, .pagination button {
        font-size: 13px;}
    </style>
    
    
  • Posted 26 March 2019, 1:13 am EST

    Hi Eric,

    Thanks for the code snippet. However, we are still unable to replicate the issue with the flex-grid-filter. It seems to be working fine. Could you please share a small sample replicating the issue so that we may investigate it further.

    As for the formatItem and hitTest, the issue was that you were directly accessing the columns using like s.columns[e.col].binding, now, this approach works perfectly fine with the normal grid but in case of multi row grid actual rendered columns and created columns are different so to get the correct column we should use the getBindingColumn() method of the multi-row. Please refer to the following code snippet and let us know if you face any further issues:

    formatProduct(s, e) {
          if (
            e.panel == s.cells &&
            s.getBindingColumn(e.panel, e.row, e.col).binding == "uad"
          ) {
            // display food images
            e.cell.innerHTML = wijmo.format(
              '<img src="{uad}">',
              s.rows[e.row].dataItem
            );
          } else if (
            e.panel == s.cells &&
            s.getBindingColumn(e.panel, e.row, e.col).binding == "crackers"
          ) {
            // display food name as a link
            e.cell.innerHTML = wijmo.format(
              '<a href="#">{crackers}</a>',
              s.rows[e.row].dataItem
            );
          } else if (
            e.panel == s.cells &&
            s.getBindingColumn(e.panel, e.row, e.col).binding == "lemmons"
          ) {
            if (
              s.rows[e.row].dataItem.oranges == undefined ||
              s.rows[e.row].dataItem.oranges == "" ||
              s.rows[e.row].dataItem.oranges == 0
            ) {
              e.cell.innerText = wijmo.format("", s.rows[e.row].dataItem);
            }
          }
        },
    

    You may also refer to the following updated sample: https://codesandbox.io/s/m5kyrn0okx

Need extra support?

Upgrade your support plan and get personal unlimited phone support with our customer engagement team

Learn More

Forum Channels