Importing Text From File Into a textarea HTML Form Field
Introduction
There is often a need to create a text field in a model that stores information you may have saved elsewhere as a plain text file such as configuration files or CSV values that you might want to render as an HTML table, but you only want the contents, not to upload the file to your web server.
The first part of this article takes you through the necessary front-end code to enable this from two different events: from a file input button change event, and from a (mouse) drop event.
In the second part, we'll implement this in a custom Wagtail FieldPanel
that you can use with a standard Django textfield
.
The next article will show you how to use the same front end process in a reusable StructBlock
.
Note
For clarity, the aim of this article is to augment atextarea
form field with the ability to read plain text from a local file, not to create a file upload tool.
Reading a File With JavaScript
The FileReader Object
The JavaScript FileReader
object provides a way to read the contents of a file asynchronously. To read a file, you first create a new instance of the FileReader
object then attach an event listener to the object's load
event, which fires when the file is successfully loaded.
Inside the event listener, you can access the file contents using the result
property of the FileReader
object. The reading process by calling the readAsText()
, readAsDataURL()
or readAsArrayBuffer()
method, passing in the file you want to read as the parameter. Since this example is dealing with plain text files, we'll be using readAsText()
.
The FileReader
object asynchronously reads the file, and when completed, triggers the load
event, allowing you to access and work with the file's contents. Here, we'll be taking that result and setting it to the textarea
value
attribute.
Since the process for reading a file from either an input element or a drag/drop event, we'll create a single method readFile()
and pass in the filename from either event.
Reading a file from an input element
Using <input type="file"/>
gives us the familiar 'Choose File' button. We'll add an listener to trigger on the change
event and pass the returned file name into the readFile()
method. Once that has passed, in this case, it's important to clear the value
attribute of the input
control. The reason for this is that if the editor notices a mistake in the file contents, updates the file and reselects the file from the input control, the change
event will not be triggered again if that value has been left after the last event. Clearing the value ensures the change
event is triggered each time. We'll also blur()
the control to take the focus off the file chooser button after the read operation.
Using preventDefault to perform custom actions
By calling event.preventDefault()
, the default behaviour of the change
event is suppressed, preventing the file selection dialog from opening.
The reason preventDefault()
is used in this context is to handle the file selection process manually using the readFile()
function and custom logic. By preventing the default behaviour, you can take control of how the selected file is processed, read, and passed to the readFile()
function.
Reading a file from a drag/drop event
To enable this on the textarea
control, we add event listeners for the dragover
and drop
events.
The dragover
event is just to provide some visual feedback that the control is available for drag/drop by setting event.dataTransfer.dropEffect
to copy.
The drop
event provides us with a list of files via the dataTransfers.files
attribute of the event. For this purpose, we will only take the first file in the list in the case that multiple files have been selected for one drop.
Using stopPropagation to prevent event bubbling.
The stopPropagation()
method is used in JavaScript to prevent the propagation of events from reaching further elements in the DOM tree. When applied to the dragover
and drop
events, it allows you to control the event flow during drag and drop interactions. By calling stopPropagation()
on these events, you prevent them from triggering any event listeners on parent or ancestor elements. This ensures that only the intended element receives and handles the drag and drop events, avoiding unwanted side effects or conflicting behaviours caused by event bubbling.
Initialising the Form Controls
Because this text field control could be used multiple times on one form, we need to make sure there are unique events for each instance. We'll create a function that receives an ID each for the file input and text area and initialises everything for both.
// static/js/import_text_field_panel.js
const initialiseImportTextFieldPanel = (fileInputId, textAreaId) => {
const fileInput = document.getElementById(fileInputId);
const textArea = document.getElementById(textAreaId);
const textInitialHeight = textArea.style.height
if (textArea.style.maxHeight == '') {textArea.style.maxHeight = '30em';}
textArea.style.overflowY='auto';
const readFile = (source, target) => {
const reader = new FileReader();
reader.addEventListener('load', (event) => {
target.value = event.target.result;
target.style.height = textInitialHeight;
target.style.height = target.scrollHeight
+ parseFloat(getComputedStyle(target).paddingTop)
+ parseFloat(getComputedStyle(target).paddingBottom) + 'px';
});
reader.readAsText(source);
}
fileInput.addEventListener('change', (event) => {
event.preventDefault();
const input = fileInput.files[0];
readFile(input, textArea)
fileInput.value = '';
fileInput.blur();
});
textArea.parentElement.addEventListener('dragover', (event) => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
});
textArea.parentElement.addEventListener('drop', (event) => {
event.stopPropagation();
event.preventDefault();
const input = event.dataTransfer.files[0];
readFile(input, textArea)
});
}
As well as the event listeners described previously, the function performs the following tasks:
- It receives the ID's for the file
input
andtextarea
controls and retrieves those objects from the DOM, stored asfileInput
andtextField
. textInitialHeight
stores the initial height of thetextField
element, used in dynamically setting thetextarea
height after a read file event..- It checks for a
maxHeight
attribute on thetextField
element. If there is none, it sets themaxHeight
property to '30em'. This ensures that thetextField
element has a maximum height to prevent the control from growing oversized after reading a large file. - It sets the
overflowY
property of thetextField
element's style toauto
. This enables vertical scrolling when the content exceeds themaxHeight
of thetextField
element. - It defines a function called
readFile
that takes two parameters:source
andtarget
. This function is responsible for reading the contents of a file selected by the user and displaying the content in thetarget
element. It uses theFileReader
API to read the file contents asynchronously.
ThereadFile
function creates a newFileReader
object, attaches an event listener to it and sets up a callback function to handle theload
event. When the file is successfully loaded, the callback function is executed.
In the callback function, it assigns the loaded file's contents to thevalue
property of thetarget
element (in this case, thetextarea
element). The height of thetarget
element is adjusted dynamically to fit the content properly based on thescrollHeight
of thetarget
element plus the top and bottom padding.
Finally, it calls thereadAsText
method of theFileReader
object, passing in thesource
file object. This initiates the process of reading the file as text.
Example textarea with File Reading Enabled
Create a simple form with the following HTML (assumes you have the initialiseImportTextFieldPanel
function loaded and Bootstrap enabled):
<style>
.textarea-fileinput, .textarea-fileinput::file-selector-button {
width: 10em;
}
</style>
<form class="bg-light p-3 rounded mx-md-5">
<textarea id="txtField" class="overflow-auto form-control mb-2 w-100"></textarea>
<input type="file" id="readFile" class="form-control textarea-fileinput">
</form>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
initialiseImportTextFieldPanel("readFile", "txtField")
});
</script>
Note
I'm setting the input element and the file selector button to the same width with.textarea-fileinput, .textarea-fileinput::file-selector-button {width: 10em;}
in order to hide selected file text that the input element would normally display. Because we set the input value to to empty after each file read operation, this will always display "No file selected" and would possibly be confusing for the editor.
This gives you a form similar to the following.
This is a working form, feel free to see it in action.
Creating a Custom Wagtail Field Panel
I'll assume you have a working knowledge of Wagtail's FieldPanel
for this, it's beyond the scope of this article to cover that in depth.
If not, in a nutshell, this is the class that provides the relevant form control for most field types in Wagtail's CMS. You can easily sublass this and add keyword attributes via the clone_kwargs()
method which will then be available to the BoundPanel
. BoundPanel
is, as the name suggests, where the form control is bound to the named field name in the model. The render_html()
method is where the magic happens to render the control on the editor form.
You can find more complete information on Wagtail's Panel API documentation.
Important
This article assumes you are using Wagtail 3.x or higher. The FieldPanel is not present in earlier versions.
Adding a file type filter
The file input element can take an accept attribute to limit the types of files the editor can choose from the pop out file dialog. See the Mozilla Developer docs for a full explanation of these.
We'll add this an option to the FieldPanel
kwargs
and check for this when rendering the form control HTML.
Note, this only affects the file input control. It does not prevent files of other types being drag/dropped onto the text area.
ImportTextFieldPanel
# import_text_field_panel.py
from bs4 import BeautifulSoup
from django.core.exceptions import ImproperlyConfigured
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel
class ImportTextFieldPanel(FieldPanel):
"""
TextArea form field with option to import from file or drag/drop.
file_type_filter: any valid accept string
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
"""
def __init__(self, field_name, file_type_filter=None, *args, **kwargs):
self.file_type_filter = file_type_filter
super().__init__(field_name, *args, **kwargs)
def clone_kwargs(self):
kwargs = super().clone_kwargs()
kwargs.update(
file_type_filter=self.file_type_filter,
)
return kwargs
class BoundPanel(FieldPanel.BoundPanel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.accepts = (
f'accept="{self.panel.file_type_filter}" ' if self.panel.file_type_filter else ""
)
msg = {
'file_label': _("Use 'Choose File' or drag/drop to import data from file."),
}
def render_html(self, parent_context=None):
html = super().render_html(parent_context)
soup = BeautifulSoup(html, 'html.parser')
text_area = soup.find('textarea')
if not text_area:
raise ImproperlyConfigured(_("ImportTextFieldPanel should only be used with TextFields"))
else:
text_area['class'] = text_area.get('class', []) + ['import_text_area']
return mark_safe(
str(soup) + self.import_text_field_button() + self.initialise_panel()
)
def import_text_field_button(self):
file_input_id = f'{self.field_name}File'
return '''
<div class="textarea-fileinput-container">
<input
type="file"
class="textarea-fileinput"
id="''' + file_input_id + '''"
''' + self.accepts + '''
/>
<span class="help">
''' + self.msg['file_label'] + '''
</span>
</div>
'''
def initialise_panel(self):
return '''
<script>
window.addEventListener('DOMContentLoaded', (event) => {
initialiseImportTextFieldPanel(
"''' + f'{self.field_name}File' + '''",
"''' + self.id_for_label() + '''"
)
});
</script>
'''
file_type_filter
is added to theFieldPanel
kwargs
.- The
BoundPanel
initialistion checks for the presence of this filter and assigns it to theaccepts
variable. render_html()
calls the inheritedFieldPanel.render_html()
method, stores the result and passes this into aBeautifulSoup
object.- If no
textarea
element is found in the passed HTML, an error is raised using Django'sImproperlyConfigured
method, otherwise we add an additional CSS class to thetextarea
to allow some additional formatting. import_text_field_button()
adds the HTML for the fileinput
element. Note theid
for this element comes from the field name to ensure it is unique on the page. There is also some basic help text to assist the editor. Thediv
container handles how to wrap these elements on narrow screen sizes (see CSS below).initialise_panel()
as aDOMContentLoaded
event listener to initialise theinitialiseImportTextFieldPanel
JavaScript method we created above. Note that thetextarea
id
comes from theBoundPanel
id_for_label()
method.- These two methods are called from
render_html()
, added to the HTML from theBeautifulSoup
object and returned to render the complete panel.
Adding custom CSS to the panel
We need to add come CSS to control the layout and behaviour of the panel.
Aside from some basic styling, we set:
- The file input and the file selector button to the same width (for reasons explained above).
- The file input container properties to display
flex
and set theflex-wrap
to handle positioning the elements on small screen sizes. - Set the text area
maxHeight
and vertical scroll properties. - Note we need to use
!important
on those attributes that Wagtail's built-in CSS would normally have precedence over.
/* static/css/import_text_field.css */
.textarea-fileinput {
width: 10em !important;
border: 0 !important;
padding: 0 !important;
margin-right: 1rem;
}
.textarea-fileinput::file-selector-button {
width: 10em;
padding: 0.3rem;
border: 0;
border-radius: 5px;
}
.textarea-fileinput-container {
display: flex;
flex-wrap: wrap;
}
.import_text_area {
max-height: 30em !important;
overflow-y: auto !important;
width: 100%;
}
Important:
Depending on Wagtail version and/or any custom admin CSS you have in place, you may need to adjust this CSS to meet your needs.
Registering the custom JavaScript and CSS in the Admin interface
We'll make use of two hooks to do this: insert_global_admin_js
and insert_global_admin_css
In wagtail_hooks.py
:
# wagtail_hooks.py
from django.templatetags.static import static
from django.utils.safestring import mark_safe
from wagtail import hooks
@hooks.register('insert_global_admin_js')
def register_admin_js():
import_text_field_panel_js = static('js/import_text_field_panel.js')
return mark_safe(
f'<script src="{import_text_field_panel_js}"></script>'
)
@hooks.register('insert_global_admin_css')
def register_admin_css():
import_text_field = static('css/import_text_field.css')
return mark_safe(
f'<link rel="stylesheet" href="{import_text_field}">'
)
Using the ImportTextFieldPanel
Use this in any Page
or Django model where you have a TextField
.
In the content_panels
declaration, use ImportTextFieldPanel
in place of FieldPanel
and, optionally, specify a file type filter.
For example, if you had a Page
model with an air_quality
field used to store data from comma separated value files (.csv), you might use the following:
from wagtail.models import Page
from django.db import models
from core.panels import ImportTextFieldPanel
class SomePage(Page):
air_quality = models.TextField(verbose_name="Air Quality")
content_panels = Page.content_panels + [
ImportTextFieldPanel('air_quality', file_type_filter=".csv"),
]
The rendered panel would be:

Conclusion
Importing text files into HTML form fields using the JavaScript FileReader
object offers a powerful solution for incorporating external data seamlessly. Use of this technique facilitates efficient data transfer, enhances data input capabilities and improves user experience.
In this article, we covered the following points:
- How to import text from file on your web form without the need to upload files to your web server.
- Using the file type
<input>
element, including customising its appearance and adding a file type filter with theaccept
attribute. - Using the
change
event on that element to perform custom behaviour by sending the chosen file to aFileReader
object to read the file contents and pass the result to another element. - Adding drag/drop capability to a standard
<textarea>
element by adding event listeners for thedragover
anddrop
events and adding custom actions to thedrop
event to read the file contents into thetextarea
.
In addition, we covered creating a custom FieldPanel
to use this technique in Wagtail's CMS including using hooks to register custom JavaScript and CSS.
In the next article, I'll cover how to use this technique in a reusable StructBlock
for use in Wagtail's StreamField
editor interface.