Ag-Grid component with input support

@C45513 , @imClumsyPanda @maarten-betman here is my preliminar solution:
1- copy the class DatePicker from the aggrid documentation Cell Editors, last example, look at the main.js ( JavaScript Data Grid: Cell Editors (ag-grid.com))

DatePicker = JsCode("""
class DatePicker {
  // gets called once before the renderer is used
  init(params) {
    // create the cell
    this.eInput = document.createElement('input');
    this.eInput.value = params.value;
    this.eInput.classList.add('ag-input');
    this.eInput.style.height = '100%';
    
    
    // https://jqueryui.com/datepicker/    
    $(this.eInput).datepicker({
      dateFormat: 'dd/mm/yy',
      onSelect: () => {
        this.eInput.focus();
      },
    });
  }

  // gets called once when grid ready to insert the element
  getGui() {
    return this.eInput;
  }

  // focus and select can be done after the gui is attached
  afterGuiAttached() {
    this.eInput.focus();
    this.eInput.select();
  }

  // returns the new value after editing
  getValue() {
    return this.eInput.value;
  }

}
""")

2- configure your column and set the cellEditor: gb.configure_column('Date_Column', cellEditor=DatePicker, cellEditorPopup=True)

3- Edit the content of the index.html in your library (Lib\site-packages\st_aggrid\frontend\build\index.html) to reference the JQUERY API:

<!doctype html>
<html lang="en">
<head>
  <title>Streamlit-AgGrid</title>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
  <meta name="theme-color" content="#000000"/>
  <meta name="description" content="Streamlit AgGrid"/>
  <link rel="stylesheet" href="bootstrap.min.css"/>
  <link href="./static/css/2.7fd92936.chunk.css" rel="stylesheet">
  <link href="./static/css/main.a8536b63.chunk.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css"/>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js">
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js">
  </script>
</head>

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <script>!function(e){function r(r){for(var n,f,l=r[0],i=r[1],a=r[2],c=0,s=[];c<l.length;c++)f=l[c],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="./";var l=this.webpackJsonpfrontend=this.webpackJsonpfrontend||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var a=0;a<l.length;a++)r(l[a]);var p=i;t()}([])
  </script>
  <script src="./static/js/2.40163c18.chunk.js"></script>
  <script src="./static/js/main.506285f1.chunk.js"></script>


</body>
</html>

4- for a Prod environment you may need to write a command in Docker to copy this file and replace the default package file

5- run your Streamlit UI and you should see the calendar in the grid:
image

Note: The only problem is that my calendar is not working properly. Cannot assign the value to the cell and cannot change the month. But is a step closer.

If you have any suggestion or manage to make it work, please share it with the rest of the community.

@PablocFonseca any advice to not overwrite or edit the st_aggrid files to include the JQUERY URL? … any option to do it programmatically as the functions are passed in the index.html body section?

references:

2 Likes

This looks really good. have you deployed it on streamlit? I would love to have look at app and code.

@Shinigami86 , what do you embed inside imageElement.src? I tried to do ./images/a.png from my relative path and it didnt work.

I was able to get delete and add buttons for each row within a table by inserting two columns each with their own rendered button and click on cell action. This functionality allows users to add or delete rows within the current table.

Implementing this consists of following three steps for the add button:

  1. create add row string
    string_to_add_row = "\n\n function(e) { \n \
    let api = e.api; \n \
    let rowIndex = e.rowIndex + 1; \n \
    api.applyTransaction({addIndex: rowIndex, add: [{}]}); \n \
        }; \n \n"
  1. create a string to render the button and put inside JsCode function:
cell_button_add = JsCode('''
    class BtnAddCellRenderer {
        init(params) {
            this.params = params;
            this.eGui = document.createElement('div');
            this.eGui.innerHTML = `
             <span>
                <style>
                .btn_add {
                  background-color: limegreen;
                  border: none;
                  color: white;
                  text-align: center;
                  text-decoration: none;
                  display: inline-block;
                  font-size: 10px;
                  font-weight: bold;
                  height: 2.5em;
                  width: 8em;
                  cursor: pointer;
                }

                .btn_add :hover {
                  background-color: #05d588;
                }
                </style>
                <button id='click-button' 
                    class="btn_add" 
                    >&CirclePlus; Add</button>
             </span>
          `;
        }

        getGui() {
            return this.eGui;
        }

    };
    ''')
  1. now add column to GridOptionsBuilder object:
        gb.configure_column('', headerTooltip='Click on Button to add new row', editable=False, filter=False,
                            onCellClicked=JsCode(string_to_add_row), cellRenderer=cell_button_add,
                            autoHeight=True, wrapText=True, lockPosition='left')

In case you would like to get the delete button you have to do following steps:

  1. create delete row string
    string_to_delete = "\n\n function(e) { \n \
    let api = e.api; \n \
    let sel = api.getSelectedRows(); \n \
    api.applyTransaction({remove: sel}); \n \

  1. create a string to render the button and put inside JsCode function
cell_button_delete = JsCode('''
    class BtnCellRenderer {
        init(params) {
            console.log(params.api.getSelectedRows());
            this.params = params;
            this.eGui = document.createElement('div');
            this.eGui.innerHTML = `
             <span>
                <style>
                .btn {
                  background-color: #F94721;
                  border: none;
                  color: white;
                  font-size: 10px;
                  font-weight: bold;
                  height: 2.5em;
                  width: 8em;
                  cursor: pointer;
                }

                .btn:hover {
                  background-color: #FB6747;
                }
                </style>
                <button id='click-button'
                    class="btn"
                    >&#128465; Delete</button>
             </span>
          `;
        }

        getGui() {
            return this.eGui;
        }

    };
    ''')

  1. Now add column to GridOptionsBuilder object
gb.configure_column('Delete', headerTooltip='Click on Button to remove row',
                                    editable=False, filter=False, onCellClicked=JsCode(string_to_delete),
                                    cellRenderer=cell_button_delete,
                                    autoHeight=True, suppressMovable='true')

If anyone is interesting I could also share some additional features we implemented with these buttons:

  • Instead of adding an empty row it is possible to define default values for the columns.

  • When clicking on delete a pop-up will appear asking for a confirmation from the user before deleting the row.

6 Likes

Such a great solution! I’m also looking for a method to add buttons to add or delete rows, have managed to add aggrid buttons above the table like this?

Thanks for sharing the solution! I’ve tried the code you’ve shared, I’m wondering why the buttons in first column doesnt have a circle corner button look.

You can make it a circle by adding the following code in the .btn_add { } section.

border-radius: 50%

This would give you the below button:
image

There are many more options have a look at How To Create Round Buttons (w3schools.com) or have a search on google.

Thanks a lot!

How can i add a drop down to a cell of a table created with or without using aggird inside a streamlite environment (python). Can somebody help on this. I need a drop down as “Long” or “Short” in the “Long/Short” column of the table rather than a manual text input. I’ve been struggling for hours, can somebody help. Thanks in advance.

Hi @asehmi ,

Can you elaborate a bit more on what you mean by “Simply build and then start the component, instead of running it in dev mode. Check your packages.json for for the commands.”?

Does this imply that by default we are using the streamlit-aggrid component in dev mode?

(Context: I’m deploying on Google Cloud Run and the app performance degrades over time when using streamlit-aggrid component. Searching for the underlying cause.)

Thanks a lot in advance! :slight_smile:

I think that’s a question for the component author, however I’d image it’s built in release mode for the pip install.

If you download the git repo for streamlit-aggrid you can set the release flag to false and start the component using something like npm start from its root directory, and then start your Streamlit app separately. Just make sure the component is mounted using the dev url parameter e.g. url=http://localhost:3000, not the path parameter. Then you can debug the component on its own.

1 Like

Hi, not sure where post to post so I’m putting this here. I am trying to use pre_selected_rows in GridOptionsBuilder to select a random row for my aggrid component to display some information about the current selected row (trying to mimic Google’s I’m Feeling Lucky by choosing an item at random vs. presenting an interactive table). I am using something like row_idx = df.head(15).sample().index.tolist()[0] to randomly pick from the first 15 rows and pass this into pre_selected_rows but streamlit keeps refreshing the table nonstop unless I hard code the index or pass in a random_state to sample. Here are my grid options and aggrid calls:

gb = GridOptionsBuilder.from_dataframe(df)
gb.configure_pagination(enabled=True)
gb.configure_column('link',  hide='True')
gb.configure_column('image', hide='True')

gb.configure_selection(selection_mode='single',
                       use_checkbox=False,
                       pre_selected_rows=[row_idx])

aggrid_table = AgGrid(df, fit_columns_on_grid_load=True,
                      gridOptions=aggrid_options,
                      update_mode=GridUpdateMode.SELECTION_CHANGED,
                      allow_unsafe_jscode=True,
                      theme='streamlit')

Any ideas? I don’t want the user to get the same selected row on every run so I don’t want to save this in a session state, but I’m not sure how to prevent the table from spazzing out. Sorry, I’m still very new to streamlit and aggrid and learning.

Hi @nimaim, why not randomize row_idx between the row limits of your data frame?

Hi Shawn, so my row limits are different based on which site I am scraping. Could be 15, could be 50, could be 500. That’s why I randomize something only out of the first 15, so the user can see what it chose (if it picked row 300, some sites would not return that much and the user would not see the row it chose as it’d be on the 3rd or 4th page of the table). But that is not the issue and does not solve the problem of the ag grid component constantly reloading due to randomization of row_idx.

I’ll have to take a video of this and post more of my code so others can better understand what is going on. Essentially streamlit will go into a constant RUNNING loop and table will keep reloading with row_idx changing (I expect it to only do it once but it does not). I tried different ways of generating a random # and it is the same behavior.

EDIT: Nevermind, got it working, assigned random.randint(0,14) to a session state variable and update it on the button press callback.

@Max_Wittemanfor this is a great idea! For some reason I cannot get your code to work :frowning: Can you publish the full code for your example, please? Thank you! Daniel

@PablocFonseca I would love to use more complex cell renderers but am restricted to using Python 3.7 or below so cannot use your latest releases. Is there any possible workaround? Or would there be a way to modify your latest releases for use with Python 3.7 or use an earlier one with complex cell renders? Thanks as always :pray:

I am having the same issue, @Max_Witteman it would possibly help to know which version of Streamlit and streamlit-aggrid you’re using? Thanks!

Hey @rjh124, @dmaniel,

I’m using streamlit-aggrid version 0.3.2, streamlit 1.11.1 and Python 3.9.
If the code still doesn’t work let me know and I can prepare a simple example.

Thanks to @MarceTU we managed to finally add a DatePicker to the UI:
image
image

To use the DatePicker follow the below instructions:

  1. Edit the content of the index.html in your library (Lib\site-packages\st_aggrid\frontend\build\index.html) to reference the JQUERY API:
<!doctype html><html lang="en">
<head>
    <title>Streamlit-AgGrid</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Streamlit AgGrid" />
    <link rel="stylesheet" href="bootstrap.min.css" />
    <link href="./static/css/2.7fd92936.chunk.css" rel="stylesheet">
    <link href="./static/css/main.a8536b63.chunk.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/flatpickr.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/themes/material_blue.css" />
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/flatpickr.min.js"></script>
</head>
<body>
    <noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div>
    <script>!function (e) { function r(r) { for (var n, f, l = r[0], i = r[1], a = r[2], c = 0, s = []; c < l.length; c++)f = l[c], Object.prototype.hasOwnProperty.call(o, f) && o[f] && s.push(o[f][0]), o[f] = 0; for (n in i) Object.prototype.hasOwnProperty.call(i, n) && (e[n] = i[n]); for (p && p(r); s.length;)s.shift()(); return u.push.apply(u, a || []), t() } function t() { for (var e, r = 0; r < u.length; r++) { for (var t = u[r], n = !0, l = 1; l < t.length; l++) { var i = t[l]; 0 !== o[i] && (n = !1) } n && (u.splice(r--, 1), e = f(f.s = t[0])) } return e } var n = {}, o = { 1: 0 }, u = []; function f(r) { if (n[r]) return n[r].exports; var t = n[r] = { i: r, l: !1, exports: {} }; return e[r].call(t.exports, t, t.exports, f), t.l = !0, t.exports } f.m = e, f.c = n, f.d = function (e, r, t) { f.o(e, r) || Object.defineProperty(e, r, { enumerable: !0, get: t }) }, f.r = function (e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, f.t = function (e, r) { if (1 & r && (e = f(e)), 8 & r) return e; if (4 & r && "object" == typeof e && e && e.__esModule) return e; var t = Object.create(null); if (f.r(t), Object.defineProperty(t, "default", { enumerable: !0, value: e }), 2 & r && "string" != typeof e) for (var n in e) f.d(t, n, function (r) { return e[r] }.bind(null, n)); return t }, f.n = function (e) { var r = e && e.__esModule ? function () { return e.default } : function () { return e }; return f.d(r, "a", r), r }, f.o = function (e, r) { return Object.prototype.hasOwnProperty.call(e, r) }, f.p = "./"; var l = this.webpackJsonpfrontend = this.webpackJsonpfrontend || [], i = l.push.bind(l); l.push = r, l = l.slice(); for (var a = 0; a < l.length; a++)r(l[a]); var p = i; t() }([])</script>
    <script src="./static/js/2.40163c18.chunk.js"></script>
    <script src="./static/js/main.506285f1.chunk.js"></script>
</body>
</html>
  1. Create DatePicker variable:
DatePicker = JsCode("""
class DatePicker {
  init(params) {
      const template = `
              <input type="text" data-input style="width: 100%;" />
              <a class="input-button" title="clear" data-clear>
                  <i class="fa fa-times"></i>
              </a>`;

      this.params = params;
      this.eGui = document.createElement('div');
      this.eGui.classList.add('ag-input');
      this.eGui.style.height = '100%';
      this.eGui.innerHTML = template;


      this.eInput = this.eGui.querySelector('input');

      this.picker = flatpickr(this.eGui, {
        onChange: this.onDateChanged.bind(this),
        dateFormat: 'Y-m-d',
        wrap: true,
        defaultDate: this.params.value,
      });

      this.picker.calendarContainer.classList.add('ag-custom-component-popup');

      this.date = this.params.value;
    }

    getGui() {
      return this.eGui;
    }

    onDateChanged(selectedDates) {
      this.date = selectedDates[0] || this.params.value;
      this.params.onDateChanged();
    }

    getValue() {
        return this.eInput.value;
      }
  }
""")

  1. Configure column on which you want to use the datepicker:
gb.configure_column(column_def, cellEditor=DatePicker)
  1. (Optional) we noticed that the DatePicker sometimes went outside the view area. To solve this problem we increased the height of the AgGrid to 600 (height=600)

  2. In case you have a DockerFile you can add the JQUERY api with the following Code:

# Edit st-aggrid package to reference JQUERY API
ARG var='<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/flatpickr.min.css"/><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/themes/material_blue.css"/><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"/><script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.5.2/flatpickr.min.js"></script></head>'
RUN sed -i "s~</head>~${var}~g" /usr/local/lib/python3.7/site-packages/st_aggrid/frontend/build/index.html

Versions used:

  • streamlit 1.11.1
  • streamlit_aggrid 0.3.2
  • Python 3.9 (Also tested on 3.7)

@C45513 @imClumsyPanda @maarten-betman

2 Likes

Hi @Max_Witteman,

I thought that would be the reason. It turns out I can use Python 3.9, so I got the cell renderers working by installing the latest version of Streamlit AgGrid - @dmaniel, hopefully this is your issue too.

However, @Max_Witteman, the delete buttons do not work for me. So it would be great if you could share an example and perhaps those cool additional features from your original post (pre-filled rows and warnings) - this functionality you’ve helped with here is crucial so thank you very much!