I’ve been working on a project recently where I utilize Ag-Grid. One of the columns that should be editable is also datetime formatted like 2023-04-01T08:00:00 Using agDateStringCellEditor worked like a charm for date editing as it opens datepicker in every edited cell.
columnDefs = []
for col in data.columns:
if col == 'Date':
columnDefs.append({
"field": col,
"editable": True,
"cellEditor": "agDateStringCellEditor"
})
else:
columnDefs.append({"field": col})
However, I found it challenging when it came to editing both date and hour simultaneously. I’m thinking of spliting (by T) date and hour - date column would be edited with DateStringCellEditor while hour would be just a string in format hh:mm. Has anybody came across a better solution to do this?
Here is one possible solution: using a custom date picker. I am using a datetime format of '%Y-%m-%d %H:%M:%S' in this example.
app.py
Note the external scripts and stylesheets. We will also use a custom JS function as the editor "cellEditor": {"function": "DatePicker"} rather than the agDateStringCellEditor
import dash_ag_grid as dag
from dash import Dash, html
import plotly.express as px
import pandas as pd
external_scripts = ["https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js",
'https://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.js']
external_stylesheets = ['https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css']
app = Dash(__name__, external_scripts=external_scripts, external_stylesheets=external_stylesheets)
df = px.data.medals_wide()
df['date'] = pd.date_range('2023-04-29', periods=len(df), freq='h').strftime('%Y-%m-%d %H:%M:%S')
app.layout = html.Div(
[
dag.AgGrid(
id="basic-editing-example",
columnDefs=[{"field": i} if i != 'date' else {'field': i, "cellEditor": {"function": "DatePicker"},
'cellDataType': 'dateString'}
for i in df.columns],
rowData=df.to_dict("records"),
columnSize="sizeToFit",
defaultColDef={"editable": True, "cellDataType": False},
dashGridOptions={"dataTypeDefinitions": {"function": "dataTypeDefinitions"}}
),
]
)
if __name__ == "__main__":
app.run(debug=True)
dashAgGridFunctions.js
add our custom functions
var dagfuncs = (window.dashAgGridFunctions = window.dashAgGridFunctions || {});
dagfuncs.dataTypeDefinitions = {
dateString: {
baseDataType: 'dateString',
extendsDataType: 'dateString',
valueParser: (params) =>
params.newValue != null &&
params.newValue.match('\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}')
? params.newValue
: null,
valueFormatter: (params) => (params.value == null ? '' : params.value),
dataTypeMatcher: (value) =>
typeof value === 'string' && !!value.match('\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}'),
dateParser: (value) => {
if (value == null || value === '') {
return undefined;
}
const dateParts = value.split('/');
return dateParts.length === 3
? new Date(
parseInt(dateParts[2]),
parseInt(dateParts[1]) - 1,
parseInt(dateParts[0])
)
: undefined;
},
dateFormatter: (value) => {
if (value == null) {
return undefined;
}
const date = String(value.getDate());
const month = String(value.getMonth() + 1);
return `${date.length === 1 ? '0' + date : date}/${
month.length === 1 ? '0' + month : month
}/${value.getFullYear()}`;
},
},
};
dagfuncs.DatePicker = class {
// 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 = 'var(--ag-row-height)';
this.eInput.style.fontSize = 'calc(var(--ag-font-size) + 1px)';
// https://trentrichardson.com/examples/timepicker/
$(this.eInput).datetimepicker({
timeFormat: 'HH:mm:ss',
dateFormat: 'yy-mm-dd'
});
}
// 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;
}
// any cleanup we need to be done here
destroy() {
// but this example is simple, no cleanup, we could
// even leave this method out as it's optional
}
// if true, then this editor will appear in a popup
isPopup() {
// and we could leave this method out also, false is the default
return false;
}
}