Customize Flask Admin Template
I recently updated a flask app that was running way too old packages to the newest shiniest versions of its packages. This was painful and I never want to have to jump so many versions at the same time again, thanks for asking.
Apart from functional packages there was a need to upgrade to a newer Bootstrap version too, since the app was running on Bootstrap 3 and due to dependency hell some parts of the app were now running on bootstrap 4. One of those “corners” was the complete backend for that app, which is based purely on flask-admin (the app was an upgrade from a spreadsheet, so having and using the very similar list view of flask-admin is nice). At some point bootstrap changed its behavior regarding focus in the context of combodate editable fields. Before that update the users were able to edit the e.g. time in column C, tab to the save button, push enter to save this field and then tab to jump to the next column D, press enter to open the modal for editing and so on. After updating the python packages pressing tab after saving focused the left uppermost item on the page, i.e. the logo on the navbar of flask-admin. Time to add some custom JavaScript to change this back to old days behavior.
After consulting 1 2 3 garbage producing chatbots and forcing them to understand the task at hand I had a nice little JavaScript function working and ready to go.
Next step was to inject this snippet into all flask-admin pages. Which actually is quite easy and seems to be doable in various manners. However, the official documentation does not explain the whole thing, that’s why you, kind reader, are reading this post right now.
How to add JavaScript or something else to all flask-admin views in some easy steps
It really is not that difficult! We’ll start with the used packages, head on the final directory layout and finish off with the required code snippets and templates for your flask app.
Packages
1Bootstrap-Flask==2.4.1 # for good measure. I think it's not strictly necessary since Flask-Admin will pull in Bootstrap anyway
2Flask==3.1.2
3Flask-Admin==2.0.2
Directory Layout
This already is the final layout including added files:
1.
2└── app/
3 ├── static/
4 │ ├── js/
5 │ │ └── your-function.js
6 │ └── css/
7 │ └── your-style.css
8 ├── templates/
9 │ └── admin/
10 │ └── base_extended.html
11 └── __init__.py
Make sure you store the files you want to inject in the static/ folder. You will most likely have some more files and folders in your project. Everything you see here is needed or modified to make this work and document this well. If you however don’t have a templates/admin/ folder yet, go ahead and create it. In it create an .html file, which will be an extension of sorts for all flask-admin pages. You can name it as you wish, since it won’t be scaffolded (is that the word?) automatically (stay clear however of something like base.html, since this is actually a flask-admin template)(make sure to remember the file’s name).
New Files
Injectables
You can of course inject whatever you want into your Flask-Admin views. I had the dire need to inject some sweet machine generated JavaScript. Therefore, I placed the JavaScript code in static/js/your-function.js:
1# static/js/your-function.js
2alert("Hello! I am an alert box!! For educational purposes only.");
This is just a simple function for showcasing purposes1.
Of course it’s also possible to inject css or any other kind of substance code.
Template
I will call it base_extended.html here. It’s contents are rather simple, since it must only load the desired extra files, i.e. the JavaScript file, and nothing more.
1{% extends 'admin/base.html' %}
2
3{% block tail_js %}
4{{ super() }}
5
6<script type="text/javascript" src="{{ url_for('static', filename='tabfix.js') }}"></script>
7
8{% endblock %}
As you can see it extends flask-admin’s base template and add a <script> block to the base.html’s block tail_js
Overriding Flask-Admin templates
Since we are all set on our new files, this one is a quick one line edit in your projects __init__.py.
Find the line where you create the flask-admin instance. This should be something like admin = Admin(app, name="my app", theme=Bootstrap4Theme())
To use base_extended.html it is necessary to instruct flask-admin to do so in the theme keyword argument:
1admin = Admin(
2 app,
3 name="my app",
4 theme=Bootstrap4Theme(fluid=True, base_template="admin/base_extended.html"),
5 )
We instruct flask-admin to use our base_extended.html as base_template. Since it really just extends the original base.html template and adds some <script> tag to one section of it, this is rather easy.
Make sure to test everything thoroughly and have fun!
The actual script I injected goes like this:
↩︎1document.addEventListener("DOMContentLoaded", function () { 2// state 3let readyForNextModal = false; 4let lastActiveAnchor = null; 5 6// cleanup 7$(document).off(".fix-tab-nav"); 8 9// Remember anchor when user opens an editable popover 10// Covers mouse click 11$(document).on("click.fix-tab-nav", ".editable", function (e) { 12 lastActiveAnchor = $(this); 13}); 14 15// Covers programmatic/keyboard open if x-editable uses bootstrap popover events 16$(document).on("shown.bs.popover.fix-tab-nav", ".editable", function () { 17 lastActiveAnchor = $(this); 18}); 19 20// When user clicks Save or Cancel, mark that next TAB should focus next anchor 21$(document).on( 22 "click.fix-tab-nav", 23 ".editable-submit, .editable-cancel", 24 function () { 25 readyForNextModal = true; 26 } 27); 28 29// Tabbing inside the modal — include submit/cancel in tab order 30$(document).on( 31 "keydown.fix-tab-nav", 32 ".editable-container :input:not([type=hidden])", 33 function (e) { 34 if (e.which !== 9) return; // TAB only 35 36 const $currentInput = $(this); 37 const $container = $currentInput.closest(".editable-container"); 38 39 // include submit + cancel in tab order so they are reachable 40 const $inputs = $container.find(":input:not([type=hidden])"); 41 const currentIndex = $inputs.index($currentInput); 42 const isLast = currentIndex === $inputs.length - 1; 43 44 if (!isLast) { 45 // normal tabbing inside modal (do not interfere) 46 return; 47 } 48 49 // We're tabbing out of the modal => mark for next-modal focus 50 readyForNextModal = true; 51 } 52); 53 54// Global TAB listener: when a modal was just closed / we left it, focus next editable (DON'T open it) 55$(document).on("keydown.fix-tab-nav", function (e) { 56 if (e.which !== 9) return; // TAB only 57 if (!readyForNextModal) return; 58 59 // consume flag 60 readyForNextModal = false; 61 e.preventDefault(); // prevent page nav jump 62 63 // Prefer saved anchor (the one that was last opened). Fallback to .editable-open if present. 64 const $anchor = 65 lastActiveAnchor && lastActiveAnchor.length 66 ? lastActiveAnchor 67 : $(".editable-open"); 68 69 if (!$anchor || $anchor.length === 0) { 70 console.log("No active anchor found for next jump."); 71 return; 72 } 73 74 // Find next editable cell to the right, or first editable in next row 75 let $next = $anchor.closest("td").next("td").find(".editable"); 76 77 if ($next.length === 0) { 78 $next = $anchor.closest("tr").next("tr").find(".editable").first(); 79 } 80 81 if ($next.length > 0) { 82 // Ensure anchor is focusable (anchors with href="#" usually are, but make safe) 83 if (!$next.is("[tabindex]")) { 84 $next.attr("tabindex", 0); 85 } 86 $next.focus(); 87 // keep lastActiveAnchor pointing to the newly focused anchor so further flows can use it 88 lastActiveAnchor = $next; 89 } 90}); 91});