Creating Tables in Reflex
Describing main uses of the Table component with a Database
Tom Gotsman
·
The table component is one of the most useful components for visualizing, editing and working with data. In Reflex we have a built in rx.table
component. This table efficiently displays data and allows embedding of various Reflex components, such as buttons, dropdowns, checkboxes, or forms, directly within table cells.
In this blog we will link the data in our rx.table
to an external database. The live version of the app we create in this blog can be found here: https://customer-data-app.reflex.run. The first thing that it is necessary to understand are Tables.
Defining the Customer Table
Tables
are database objects that contain all the data in a database. To create a table in Reflex make a class that inherits from rx.Model
.
A Customer
table is defined below that inherits from rx.Model
. It has fields such as name
and email
.
To learn more about databases in Reflex check out the documentation here.
Loading Data into the Table
Now that we have our database created it is necessary to load our data from the database. The load_entries
event handler in State
does a database query and returns all the data in the database to the State
variable self.users
.
The show_customer
function takes in a user
and renders it in a rx.table.row
. All the attributes of the Customer
class are rendered out as rx.table.cell
components in the row. The user.status
uses an rx.match
to handle multiple conditions and their corresponding components. Learn more about rx.match
here. The status_badge
function takes in a status and renders it out in an rx.badge
.
The main_table
function renders an rx.table
component. The _header_cell
function renders a text
and an icon
in an rx.table.column_header_cell
. This renders out the names of the Customer
model fields as the titles of the table columns.
The State.users
var is iterated through using rx.foreach
in this code rx.table.body(rx.foreach(State.users, show_customer))
. The show_customer
function is used to render each item in the State.users
var.
Lastly the State.load_entries
event handler is called when the rx.table
is rendered using the on_mount
event trigger.
Adding a New Customer
Next let's explore how to add a user to our database and therefore to our table. We have a function add_customer_button
, which is a UI element that is rendered in the main_table
function defined in the section above.
In this add_customer_button
function there is an rx.dialog
which opens when the rx.button
"Add New Customer" is pressed.
When the dialog is opened there is an rx.form.root
with several form_field
functions called inside. The form_field
function returns an rx.form.field
. It takes in several arguments, but most importantly takes in a name
argument. The name
prop is needed to submit with its owning form as part of a name/value pair. The content typed into the rx.input
is submitted as the value
in that name/value pair when the rx.form
is submitted.
The same is done for the rx.radio
component, where the name
is set and the radio item selected is passed as the value
when the rx.form
is submitted.
To submit the rx.form
with all the name/value pairs added, the "Submit Customer" button is pressed which is inside of an rx.dialog.close
and an rx.form.submit
, so it closes the dialog and it submits the form.
When the form is submitted it runs the code on_submit=State.add_customer_to_db
, running the event handler add_customer_to_db
. This event handler sets the State
var current_user
to equal the form data just submitted in the rx.form
, as a type Customer
.
This event handler also does a database query to add the current self.current_user
, which we just set to be the form response, to the database.
Calling session.refresh
after the submit ensures that the local model gets updated with the inserted ID (in more advanced cases, if any fields are lazy loaded, it would also fetch those so the object is less dependent on the active session and can be serialized).
Before this a check is done to see if the email
the user has added is already an email
in the database, and if it is then an rx.window_alert
is thrown.
Finally once the new user is added to the database, the load_entries
event handler is run again which loads all the data from the database and fills the table.
Updating and Deleting a Customer
Updating
Let's now explore how to update and delete customers from our table. To update a customer is very similar to adding a customer. We have a function update_customer_dialog
, which is a UI element that is rendered in the show_customer
function.
In this update_customer_dialog
function there is an rx.dialog
which opens when the rx.button
"Edit" is pressed. In addition to opening the rx.dialog
, the on_click=lambda: State.get_user(user)
runs the event handler get_user
which sets the self.current_user
as the user
clicked on.
You might be wondering how the user
is actually being passed through to be edited. All our users are in our state var users
. This is being passed to rx.table.body(rx.foreach(State.users, show_customer))
in the main_table
function. So the show_customer
function is taking in each user
separately and it renders an edit button for each user from the code update_customer_dialog(user)
in show_customer
(Check the full code example below to follow this).
The rest of the update_customer_dialog
function is the same as the add_customer_button
, except that now we also pass through a default_value
to the form_field
, such as user.email
in the example code below. This shows the current value for that user that we are now able to edit.
The update_customer_to_db
event handler is run when the form is submitted which updates the self.current_user
and updates that user in the database.
Deleting
To delete a customer from our table there is a button in show_customer
where on_click=lambda: State.delete_customer(getattr(user, "id"))
. This runs the event handler delete_customer
, which takes in an id
from the user
, queries this id
in the database and then deletes the user from the database.
Sorting and Filtering
Now that we are able to add, edit and delete data from our table, let's explore how to sort and filter this data as needed.
Sorting
For sorting the rx.select
component is used. The data is sorted based on the attributes of the Customer
class. When a select item is selected, as the on_change
event trigger is hooked up to the sort_values
event handler, the data is sorted based on the state variable sort_value
attribute selected. This works by setting the state var sort_value
and then running self.load_entries
.
The sorting query gets the sort_column
based on the state variable sort_value
. It gets the order using the asc
function from sql and finally uses the order_by
function. If the sort_value
is not payments
, as this is a float
type, then the items in the sort column are also put to lower case using func.lower
, so capitalisation of words will not affect the sorting.
Filtering
For filtering the rx.input
component is used. The data is filtered based on the search query entered into the rx.input
component. When a search query is entered, as the on_change
event trigger is hooked up to the filter_values
event handler, the data is filtered based on if the state variable search_value
is present in any of the data in that specific Customer
.
The filter_values
event handler sets the state var search_value
and then runs self.load_entries
.
In the self.load_entries
event handler the %
character before and after search_value
makes it a wildcard pattern that matches any sequence of characters before or after the search_value
. query.where(...)
modifies the existing query to include a filtering condition. The or_
operator is a logical OR operator that combines multiple conditions. The query will return results that match any of these conditions dynamically generated by iterating over the fields returned by Customer.get_fields()
, excluding the id
and payments
fields.
The output for the name
field is Customer.name.ilike(search_value)
, which checks if the name
column of the Customer
table matches the search_value
pattern in a case-insensitive manner (ilike
stands for "case-insensitive like").
Additionally, the payments
field, which is not included in the initial list, is explicitly cast to a string before the ilike
operator is applied, ensuring it can be correctly compared in a case-insensitive manner.
Conclusion
And that is it. We have set up our table in a database, learnt how to add, edit and delete users, and finally how to sort and filter them. This is just a basic use case for the rx.table
and there are many more advanced use cases like setting up Machine Learning job workflows etc.
The live app can be found here: https://customer-data-app.reflex.run and the full code can be found here: https://github.com/reflex-dev/reflex-examples/tree/main/customer_data_app.