Project 4: Crypto (part 1)

Project 4: Crypto (part 1)

November 29 2021

The goal of this tutorial is to go over building an interactive cryptocurrency logging tool using HTMX and Django. We are using this project to learn common UX patterns that usually need a SPA Javascript library or framework. This tool could be extended further to track taxes owed, or log transactions programmatically by linking to an exchange API. We will only focus on the UX parts in this tutorial though.

The UX patterns we would learn are,

  • Complex forms
  • Assisted form entry
  • Using toasts
  • Click to load more
  • Inline editing
  • Deleting by row
  • Bulk updating  

Bear in mind that this is an advanced project, and doing the previous tutorials is highly recommended. Also, I will be splitting the project into two parts to keep the length reasonable. Lastly, we are adding a few lines of hyperscript to our project, as it complements the patterns of HTMX nicely. No prior knowledge of hyperscript is needed, but knowing basic Javascript DOM events will help.

Here are our user stories:

  1. I want to be able to log my cryptocurrency transactions
  2. I want to see a full history of these transactions via click to load
  3. I want to be able to edit my transaction inline
  4. I want to delete my transaction with a confirmation prompt
  5. I want to mark and unmark specific transactions as taxable events

So let's get going

Configuration

To start, let's go ahead and clone the starter code, start our docker container then migrate.

git clone https://github.com/Jonathan-Adly/htmx-crypto/tree/startercode
docker compose up -d --build
docker compose exec web python manage.py migrate

As in the previous tutorials, the starter code has three apps. A user application, where we are using the all-auth package to manage our users. A config application that holds our settings and utilities. The last one is an application named "crypto" that returns a basic home template at this point.

In our templates, we have a base template, a home template, and a component directory that only holds a navbar. You will notice that our navbar has an "hx-boost" on it, to make our links function like a SPA.

Also, for this project, we are using two packages. Crispy forms for styling our forms, and this package to make debugging htmx a little bit easier. Both are already installed for you.

To make the debugging work with the django-htmx package, we added this script tag in our base template.

{% load django_htmx %}
{% django_htmx_script %}

Logging Transactions

Our first user story calls for a user to be able to enter his transactions manually. The typical Django pattern would be to create a model representing the transaction. Then a form to handle data entry with the HTML components needed. Lastly a view for data processing.

Let's go ahead and build these parts,

Model

Database Schema for financial transactions can get complicated quickly. The basic idea is to use double-entry accounting. Where each transaction has two equal and corresponding sides, debit and credit (or bought/sold to be explicit in our example).

Take for example a user who wants to log a transaction where they exchanged $60,000 of USD for 1 BTC. Our model would have on it, "-60,000" in "USD" and "+1" for "BTC". If they later sold that 1 BTC for $65,000, our model would represent that with "-1" BTC, and "+65000" and so on.

Here is that model for our project:

# in crypto/models.py
from django.db import models
from django.contrib.auth import get_user_model
from datetime import date


class Transaction(models.Model):

    user = models.ForeignKey(
        get_user_model(), related_name="transactions", on_delete=models.CASCADE
    )

    exchange = models.CharField(max_length=250, blank=True)
    date = models.DateField(default=date.today)

    sold_currency = models.CharField(max_length=3)
    sold_currency_amount = models.DecimalField(
        max_digits=19,
        decimal_places=10,
    )

    sold_currency_fee = models.DecimalField(
        max_digits=19,
        decimal_places=10,
    )
    bought_currency = models.CharField(max_length=3)
    bought_currency_amount = models.DecimalField(max_digits=19, decimal_places=10)

    bought_currency_fee = models.DecimalField(
        max_digits=19,
        decimal_places=10,
    )

    price = models.DecimalField(max_digits=19, decimal_places=10)

    class Meta:
        ordering = ["-date"]

    def __str__(self):
        return f"{self.user.email} on {self.exchange} - {self.date}"

    def save(self, *args, **kwargs):
        self.price = (self.sold_currency_amount + self.sold_currency_fee) / (
            self.bought_currency_amount + self.bought_currency_fee
        )
        if not self.bought_currency_fee:
            self.bought_currency_fee = 0
        if not self.sold_currency_fee:
            self.sold_currency_fee = 0
        super(Transaction, self).save(*args, **kwargs)

Other things to note, is that we are ordering our model by date. This has a performance penalty if we ever want to retrieve all transactions (typically in the admin). In our case, it is okay for most users since they would never request all transactions, just their own.

Additionally, we are overriding our save method to calculate the price automatically and set a fee of zero if the user didn't provide an amount.

Keep in mind that there is lots of room to optimize that model. We are keeping things super simple to focus on the HTMX UX patterns instead of the ideal database schema.

Model Form

Next, we will build our Django modelform. This part should be straightforward. The only difference is that we are substituting labels with placeholders for a better look since our form is going to be quite big.

from django import forms
from .models import Transaction

#this is to get a calendar for our date field
class DateInput(forms.DateInput):
    input_type = "date"


class TransactionForm(forms.ModelForm):
    class Meta:
        model = Transaction
        fields = (
            "date",
            "exchange",
            "sold_currency_amount",
            "sold_currency",
            "sold_currency_fee",
            "bought_currency_amount",
            "bought_currency",
            "bought_currency_fee",
        )
        
        #blank label, removes the label
        labels = {
            "date": "Date of transaction",
            "exchange": "Exchange",
            "sold_currency_amount": "",
            "sold_currency": "",
            "sold_currency_fee": "",
            "bought_currency_amount": "",
            "bought_currency": "",
            "bought_currency_fee": "",
        }
        
        #we are using widgets and attrs to update the placeholder
        widgets = {
            "date": DateInput(),
            "exchange": forms.TextInput(
                attrs={
                    "placeholder": "Exchange"
                }
            ),
            "sold_currency_amount": forms.TextInput(
                attrs={
                    "placeholder": "Total Amount",
                }
            ),
            "sold_currency": forms.TextInput(
                attrs={
                    "placeholder": "Fiat or Crypto",
                }
            ),
            "sold_currency_fee": forms.TextInput(
                attrs={
                    "placeholder": "Fee Amount",
                }
            ),
            "bought_currency_amount": forms.TextInput(
                attrs={
                    "placeholder": "Total Amount",
                }
            ),
            "bought_currency": forms.TextInput(
                attrs={
                    "placeholder": "Fiat or Crypto",
                }
            ),
            "bought_currency_fee": forms.TextInput(
                attrs={
                    "placeholder": "Fee Amount",
                }
            ),
        }

Form Component

Since we know we will use HTMX, let's go ahead and put the form in its own component. Previously, we handled forms with a Django {{form|crispy}} tag. We have done that in previous tutorials and the results were certainly good enough for most use cases. But, one of the great advantages of HTMX is that it allows for teams to work together on complicated UX.

For example, a backend developer can hand off the form component to a frontend developer. The front-end developer can then work on a custom layout for a great user experience. For more complex and long forms, you don't have to render the form as a big whole unit.

Part of our goals for this project is to be able to build complex forms. So, we will do that.

To start, let's add the component

touch templates/components/transaction_form.html

We don't want to render the form as a whole unit, since that would limit our layout plans.  Instead, we will render the fields individually. This allows us to build complex layouts. To render an individual field, we access it via "form.field" - to style it using crispy forms, we add the tag "|as_crispy_field".

Here is our form with a complex layout inspired by the cointracker.io transaction page.

<!-- in components/transaction_form.html -->
 {% load crispy_forms_tags %}
        
 <form class="container">
      <div class="form-row row-eq-spacing-sm">
                <div class="col-sm">
                    {{form.date|as_crispy_field}}
                </div>
                <div class= "col-sm">
                 {{form.exchange|as_crispy_field}}
                </div>
       </div>
             
           
       <div class="form-row row-eq-spacing">

                <div class="col-6 mt-5">
                   <label class="required font-weight-bold">
                   Paid / Sent / Withdrawn</label>      
                </div>

                <div class="col-6 mt-5">
                    <label class="required font-weight-bold">
                    Bought / Received / Deposited</label>
                </div>

                <div class="col-6 mt-5">
                {{form.sold_currency_amount|as_crispy_field}}
                </div>
                
                <div class="col-6 mt-5">
                {{form.bought_currency_amount|as_crispy_field}}
                </div>
            
                <div class="col-6 mt-5">
                {{form.sold_currency|as_crispy_field}}
                </div>

                <div class="col-6 mt-5">
                {{form.bought_currency|as_crispy_field}}
                </div>
         
                <div class="col-6 mt-5">
                {{form.sold_currency_fee|as_crispy_field}}
                </div>

                <div class="col-6 mt-5">
                {{form.bought_currency_fee|as_crispy_field}}
                </div>
           
           </div>
            
           <div class="text-left">
                <button class="btn btn-primary"> Submit </button>
           </div>
 </form>

Note that we can build complex layouts using the FormHelper class from django-crispy-forms. But, the point here is, that in small/medium teams, being able to compartmentalize parts of your application and separate concerns is a big plus.

When using React and Django, a backend developer would build the endpoint and the view (through DRF). A front-end developer would work on the form itself. This pattern separates the concerns but lets go of Django forms. We essentially have to reinvent the wheels that Django forms already invented for us 15 years ago.

Using htmx, a backend developer would own the form data (inputs), where we may end up using htmx attributes. While a frontend developer can own the layout of the form. This way, we get the best of both worlds.

The last thing we need to do is give our home view and template access to our form. Here is our new home view and template.

# in crypto/views.py

from .forms import TransactionForm

def home(request):
    form = TransactionForm()
    return render(request, "home.html", {"form": form})

Our home.html template would look like this,

{% block content %}

<div class="content">
{% if request.user.is_authenticated %}
<!-- new -->
<div class="card" id= "transaction_form">
{% include 'components/transaction_form.html'%}

</div>

{% else %}
<div hx-boost="true">
To start please <a href= "{% url 'account_signup' %}"> make an account </a> or <a href="{% url 'account_login' %}"> login</a>.

</div>
{% endif %}
</div>
{% endblock %}

Go ahead a create a superuser and log in. Our form should look like this.

 

Hyperscript

This looks great, but let's go ahead and take it to a whole new level by sprinkling a little bit of hyperscript. Hyperscript is a scripting language meant to work alongside HTMX.

As I said before, this form is inspired by the cointracker transaction page. One nice feature that they have is that when a user clicks on the currency input, it brings up a list of potential currencies that they can pick from.

Let's go ahead and try to get this feature for our form using HTMX and hyperscript.

Here is our big picture approach to make this work, we will add an empty div right under our sold_currency input. When a user clicks on the input, we would fill the div with a list of currencies via HTMX. Then, when a user clicks on one of these currencies, we would use hyperscript to populate our input.

Here are the steps and the code:

First, we will add an empty div under our sold_currency input.

<div class="col-6 mt-5">
  {{form.sold_currency|as_crispy_field}}
  <div id="results_sold"> </div> <!-- currency suggestions would be here --> 
</div>

Then, we will add htmx attributes to our input to populate that empty div with a component when the user clicks.

#crypto/forms.py
# in the form widgets
...
"sold_currency": forms.TextInput(
                attrs={
                    "placeholder": "Fiat or Crypto",
                    "autocomplete": "off", #new
                    "hx-trigger": "click", #new
                    "hx-get": reverse_lazy("currencies"), #new
                    "hx-target": "#results_sold", #new
                }
            ),

Next, we will build the endpoint and view.

#in crypto/urls.py
urlpatterns = [
    path("", views.home, name="home"),
    path("currencies", views.currencies, name="currencies"), #new
]
# in crypto/views.py
...
def currencies(request):
    return render(request, "components/currencies.html")

Lastly, we will build our currencies component. Note, I am hard coding the currencies in the template and keeping the styling minimal. But nothing is stopping us from populating the currencies from our server, where we can keep a model with more details on each currency.

<div id= "currencies">

<p class="text-muted"> Fiat </p>

<span class="btn"> USD </span>
<span class="btn"> EUR </span>

<p class="text-muted"> Crypto  </p>

<span class="btn"> BTC </span>
<span class="btn"> ETH </span>
<span class="btn"> SOL </span>

</div>

Now, clicking on sold_currency input will bring out our currency component.

Our next task would be putting those currency symbols in the input field when a user clicks on one. For this, we would use hyperscript. Hyperscript is currently in alpha, which means you don't want to heavily lean on it in production. It is a really elegant solution though for small stuff like this feature.

First, let's add the CDN to our base.html template

 <!-- hyperscript -->
    <script src="https://unpkg.com/hyperscript.org@0.8.3"></script>

Then, let's add this line to just one of our buttons to test it out.

<span class="btn"  _= "on click put my.innerText into #id_sold_currency.value then remove #currencies"> BTC </span>

Hyperscript is an event-driven language, so you almost always start your lines with an event (on click, on load, etc.). Then, you write out what you want to be done on that event using Javascript terminology. You get access to some magic words like "my", "it", and "you". It takes some getting used to, but when combined with HTMX can be quite powerful.

Now, go ahead and test whatever button you added the hyperscript to. It should work (we are only doing the sold input for now).

Finally, to avoid repeating ourselves and writing the same line on all our buttons, we would write our hyperscript in the parent element. Again, we want to be strategic about how we use hyperscript as it can be like vanilla Javascript and get out of hand quickly.

<!-- in components/currencies.html -->
<div 
id= "currencies" 
_= "on click put event.target.innerText into #id_sold_currency.value then remove me">

<p class="text-muted"> Fiat </p>

<span class="btn"> USD </span>
<span class="btn"> EUR </span>

<p class="text-muted"> Crypto  </p>

<span class="btn"> BTC </span>
<span class="btn"> ETH </span>
<span class="btn"> SOL </span>

</div>

Now, all our buttons should work in the sold_currency column.

The last piece of this feature is making the feature work for both the bought and sold column.  For this, we need to keep track of what input did the user click.

We can use hyperscript for that as well. As a rule of thumb though if we can do something with HTMX or hyperscript, we will go with the HTMX route as it is more stable and cleaner.

To keep track of which input the user clicked, we would pass it to the backend via a URL parameter. Here is how we will change our code,

# crypto/forms.py
...
"sold_currency": forms.TextInput(
                attrs={
                    "placeholder": "Fiat or Crypto",
                    "autocomplete": "off",
                    "hx-trigger": "click",
                    "hx-get": reverse_lazy(
                        "currencies", kwargs={"input_clicked": "sold"}
                    ), #new
                    "hx-target": "#results_sold",
                }
            ),
...
"bought_currency": forms.TextInput(
                attrs={
                    "placeholder": "Fiat or Crypto",
                    "autocomplete": "off",
                    "hx-trigger": "click",
                    "hx-get": reverse_lazy(
                        "currencies", kwargs={"input_clicked": "bought"}
                    ), #new
                    "hx-target": "#results_bought",
                }
            ),

Then in our view,

def currencies(request, input_clicked):
    return render(
        request, "components/currencies.html", {"input_clicked": input_clicked}
    )

Finally, we will update our component and it's hyperscript line as such,

<!-- in components/currencies.html -->
<!-- nothing else changes -->
<div id= "currencies" 
_= "on click put event.target.innerText into #id_{{input_clicked}}_currency.value then remove me">

Our form now looks great with the ability to pull in currencies and guide the user to use our list of currencies.

Form Processing

The next part is handling the form submission. Now, part of the complexity of that form is the double-entry accounting. We have to make sure that the "sold" side always gets saved with a negative value (as well as the fees). We can't rely on our users to know the intricacies of double-entry accounting and enter the data the correct way.

To convert the values they enter to a negative value, we have to use the magic of Django forms to "clean" the entered data. We want to match that data to our double-entry accounting format.

For example, to clean the "sold_currency_amount" field, we define a function called "clean_sold_currency_amount". This function then can perform whatever operations we need on the user entered data. The awesomeness of Django forms will handle the rest for us. Any function with the format clean_{field} would clean the specified field for us.

Here is the code.


class TransactionForm(forms.ModelForm):
    class Meta:
    #nothing changes here
    
    """
    if the user entered a value and its higher > 0, we will convert to a negative value
    """
    def clean_sold_currency_amount(self):
        data = self.cleaned_data["sold_currency_amount"]
        if data and data > 0:
            data = data * -1
        return data

    def clean_sold_currency_fee(self):
        data = self.cleaned_data["sold_currency_fee"]
        if data and data > 0:
            data = data * -1
        return data

    def clean_bought_currency_fee(self):
        data = self.cleaned_data["bought_currency_fee"]
        if data and data > 0:
            data = data * -1
        return data

We can even use the same idea to have a custom validator on our fields. For example, if we don't want our user to put a future date for the transaction - we can do something like this.

from datetime import date
from django.core.exceptions import ValidationError
    ...
    
    def clean_date(self):
        data = self.cleaned_data["date"]
        if data > date.today():
            raise ValidationError("You can't enter a future transaction!")
        return data #you have to always return the data!

I am spending a lot of time here because Django forms are a powerful feature of the framework. We shouldn't have to choose between using Django forms or a great UX experience. This is why HTMX makes a lot of sense.

Now - for some use cases cleaning the fields is enough. In our case though, we need to make sure that no bad data enters our database. so, we will put some validators there as well. We would use the Django MaxValueValidator to not allow values higher than 0 to be saved to our database. Also, we are going to go ahead and make fees optional.

# in crypto/models.py
from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import MaxValueValidator #new

from datetime import date


class Transaction(models.Model):

    user = models.ForeignKey(
        get_user_model(), related_name="transactions", on_delete=models.CASCADE
    )

    exchange = models.CharField(max_length=250, blank=True)
    date = models.DateField(default=date.today)

    sold_currency = models.CharField(max_length=3)
    sold_currency_amount = models.DecimalField(
        max_digits=19, decimal_places=10, validators=[MaxValueValidator(0)]
    ) #new

    sold_currency_fee = models.DecimalField(
        max_digits=19, decimal_places=10, blank=True, validators=[MaxValueValidator(0)] 
    ) #new
    bought_currency = models.CharField(max_length=3)
    bought_currency_amount = models.DecimalField(max_digits=19, decimal_places=10)

    bought_currency_fee = models.DecimalField(
        max_digits=19, decimal_places=10, blank=True, validators=[MaxValueValidator(0)]
    ) #new

    price = models.DecimalField(max_digits=19, decimal_places=10)

    class Meta:
        ordering = ["-date"]

    def __str__(self):
        return f"{self.user.email} on {self.exchange} - {self.date}"
    
    # we rearranged this to deal with a situation when there are no fees
    def save(self, *args, **kwargs):
        if not self.bought_currency_fee:
            self.bought_currency_fee = 0
        if not self.sold_currency_fee:
            self.sold_currency_fee = 0

        self.price = (self.sold_currency_amount + self.sold_currency_fee) / (
            self.bought_currency_amount + self.bought_currency_fee
        )

        super(Transaction, self).save(*args, **kwargs)


Go ahead and makemigrations then migrate. That's the hard part. What's left is our view logic.

docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate

Here it is,

def home(request):

    form = TransactionForm()
    #start here
    if request.method == "POST":
        form = TransactionForm(request.POST)
        #form.is_valid will clean the user data as well
        if form.is_valid():
            form = form.save(commit=False)
            form.user = request.user
            form.save()
            #on success we will return a success component with a new form
            return render(request, "components/successful_transaction.html", {"form": TransactionForm()})
        else:
             #on errors, we would return the form back, crispy forms handles displaying the errors for us
            return render(
                request,
                "components/transaction_form.html",
                {"form": form},
            )
    return render(request, "home.html", {"form": form})

The last part is to work on our templates and build the components. The plan is to return an empty form on success or the form with errors on submission. So our target has to be the form or the div containing it. Additionally, on success, we would want to eventually update a transaction list.

Since this involves updating other content in the DOM, we would go ahead and use hx-swap-oob in the response.

First, let's change our home template and make a spot for our future transaction list - our hx-swap-oob target.

<!-- in home.html -->
<div class="content">
{% if request.user.is_authenticated %}
<div class="card" id= "transaction_form">
{% include 'components/transaction_form.html'%}
</div>

<div class= "content" id="transactions"> <!-- new -->

</div>

Then, let's go ahead and change our form to use HTMX to post the data back to our server.

<!-- in components/transaction_form.html -->
         <form 
            class="container"
            hx-post= "{% url 'home' %}"
            hx-target= "#transaction_form"
            >
            <!-- no changes here -->
        </form>
        <!-- hx-trigger= "submit" is default for forms -->
        <!-- hx-swap = "innerHTML" is default -->

Lastly, we are returning a successful_transaction.html component when the form data is saved correctly. Let's go ahead and build it.

touch templates/components/successful_transaction.html

Here is the code for our new component,

{% include 'components/transaction_form.html' %} <!-- new empty form -->

<!-- this will eventually hold a transactions table -->
<!-- it will replace #transactions DOM element that is currently in the DOM -->
<div id="transactions" class="content" hx-swap-oob="true">
Success
</div>

That's it. Our user story is complete. We are now able to log crypto transactions in our database.

Go ahead and test a few entries. On error, you should see this

On success - you should see a new empty form with a success message on the bottom.

Lastly, register your model in the admin and see the transactions there. Make sure that the fields are cleaned correctly (negative values in the sold column and fees).

#in crypto/admin.py
from django.contrib import admin
from .models import Transaction


admin.site.register(Transaction)

You will notice that the price looks unusual in some transactions. Don't worry, we will fix that in part 2.

If all is good, commit your code. Here is my version of the project so far.

Challenge:

Right now, clicking on the currency input pulls up a list of currencies. However, if a user is to "tab" into it, it won't come up. Also, tabbing out or clicking on a different field doesn't dismiss the list.

Here is your challenge:

  1. Bring up the list of currencies, no matter how the user lands (hint: focuses) on the input field.
  2. When the user clicks out of the div or tabs out, the currency list should go away (hint: un-focus/blurs).

If you are stuck, my version of the code has the solution. You may find it helpful to know that you can have both hyperscript and htmx on the same field.

Display Transactions

For our second user story, we want to display the transactions via click to load to our user.

First, we will display them, then work on the click to load.

To start, we need to give our home view the user's transactions list.

def home(request):
    form = TransactionForm()
    transactions = Transaction.objects.filter(user=request.user) #new
    if request.method == "POST":
        form = TransactionForm(request.POST)
        if form.is_valid():
            form = form.save(commit=False)
            form.user = request.user
            form.save()
            #new list of transactions
            transactions = Transaction.objects.filter(user=request.user) 
            return render(
                request,
                "components/successful_transaction.html",
                {"form": TransactionForm(), "transactions": transactions},
            )
        else:
            return render(
                request,
                "components/transaction_form.html",
                {"form": form},
            )

    return render(request, "home.html", {"form": form, "transactions": transactions})

Next, we will have a component to display those transactions.

touch templates/components/transactions.html

Here is the code inside that component.

{% for transaction in transactions %}
<div class="card row">   
    <div class="col-3">
    <p> Transacation date: {{transaction.date}} </p>
    {% if transaction.exchange %}  
    <small> Exchange: {{transaction.exchange}} </small> 
    {% endif %}
    </div>

    <div class="col-3">
    <p> {{transaction.sold_currency_amount|floatformat:2}}
    {{transaction.sold_currency}} </p>
    
    {% if transaction.sold_currency_fee %}  
    <small> Fees: {{transaction.sold_currency_fee|floatformat:2}} 
    {{transaction.sold_currency}}</small>  
    {% endif %}
    </div>

    <div class="col-3">
    <p>{{transaction.bought_currency_amount|floatformat:2}}
    {{transaction.bought_currency}} </p>
    {% if transaction.bought_currency_fee %} 
    <small> {{transaction.bought_currency_fee|floatformat:2}} 
    {{transaction.bought_currency}}  </small> 
    {% endif %}
    </div>

     <div class="col-3">
    <p> {{transaction.price|floatformat:9}} 
    {{transaction.sold_currency}} per 1 {{transaction.bought_currency}} </p>
    </div>
    
</div>
        {% empty %}
        <div class="row card">
            <p> No transactions yet </p>
        </div>
    {% endfor %}

Here we are using the Django template tag "floatformat" to display our currencies with only 2 decimals numbers.

Next, we will include our component in our home template as well as our successful transaction response.

In home.html

<div class= "content" id="transactions">
   {% include 'components/transactions.html' %}
</div>

And, in our successful_transaction.html,

{% include 'components/transaction_form.html' %}

<div id="transactions" class="content" hx-swap-oob="true">
    {% include 'components/transactions.html' %}
</div>

That's it. We should see something like this for our transactions.

When we submit a new transaction, the table gets updated with the new transaction as part of the response.

Toasts

Now ideally, we would like to display to the user that their submission was successful. For that, I like using halfmoon toasts or sticky alerts.

Toasts are alerts that stick to the top right of the page and are only displayed for a few seconds. You can really put anything on there and use them as notifications, alerts, or just a replacement for the Django messages system.

We can make them on the fly with Javascript (see the docs) or have them pre-compiled in advance. Usually, pre-compiling them is a better choice which we will go with.

Let's go ahead and make an "alert.html" component.

touch templates/components/alert.html

In there, let's put a simple message.

<div class="alert alert-primary filled" role="alert" id=alert-1">
<p> Transacation added successfully </p>      
</div>

Then, we should include this component to our sticky alerts div in our base.html.

<!-- in base.html -->
    <div class="sticky-alerts">
    {% include 'components/alert.html' %} <!-- new -->
    </div>

Finally, we will use hyperscript to trigger the alert when our transactions load.

<!-- in successful_transaction.html -->
{% include 'components/transaction_form.html' %}

<div id="transactions" 
class="content" 
hx-swap-oob="true" 
_="on load call halfmoon.toastAlert('alert-1', 3500)">
    {% include 'components/transactions.html' %}
</div>

Now, our user is alerted when a transaction is logged with an alert that will go away in 3.5 seconds. Toasts are very flexible and you can put anything in there (links, tables, forms!) - so, it's a good tool to have in our toolbox.

The last thing we will do for this part of our project is adding click to load to our transactions.

Click to Load

Click to load relies heavily on Django paginations. If you are unfamiliar with pagination, I highly recommend reading the docs. The big picture is, we are going to turn our transactions query into multiple pages using the Django Paginator class. Click to load simply calls the next page using HTMX.

First thing we need to do is to separate the view logic of form retrieval/submission and our query retrieval. We don't want to call the form every time we click to load new transactions.

So, we need a new view and a URL endpoint for our transactions.

from django.core.paginator import Paginator #new

...
def transactions(request):
    """
    Give Paginator a list of objects, plus the number of items you’d like to have on each page, and it gives you methods for accessing the items for each page
    """
    transactions_list = Transaction.objects.filter(user=request.user)
    paginator = Paginator(transactions_list, 3)
    page_number = request.GET.get("page")
    transactions = paginator.get_page(page_number)
    return render(
        request,
        "components/transactions.html",
        {"transactions": transactions},
    )

And here is our urls.py

from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("currencies/<str:input_clicked>", views.currencies, name="currencies"),
    path("transactions", views.transactions, name="transactions"), #new
]

Next, let's add the "Load more" button to our transactions component.

<!-- in components/transactions.html -->
<!-- all the way at the bottom of the page -->
    {% if transactions.has_next %}
    <button
        class= "btn btn-primary"
        hx-get="/transactions?page={{ transactions.next_page_number }}"
        hx-swap="outerHTML"
        >
        Load More
            <p class="htmx-indicator"> Loading... </p>
</button>
    {% endif %}
    <!-- hx-target = "this" - is the default. -->
    <!-- hx-trigger = "click" - default for buttons -->

Here we are combining the magic of pagination with htmx to replace our button. On click, we are getting a new transactions component which has a new set of transactions plus a new button.

Note, that we used a new class "htmx-indicator". This is an invisible piece of the DOM initially, that shows up while the request is being processed. Once the request is complete, it disappears again. It's useful if the user connection is slow and the request is taking a while to load.

I used a simple "loading..." here, but we can use any DOM element we want. The HTMX docs recommend using SVG images (see here). Which is even better UX pattern,

<img class="htmx-indicator" src="/img/bars.svg"/>

Now, the last thing we need to do is adjust our home view logic. Right now, we are giving our home view access to all the transactions. We will take this away and have our transactions endpoint handle it.

#in crypto/views.py
# GET requests no longer have access to our transactions list
def home(request):
    form = TransactionForm()
    if request.method == "POST":
        form = TransactionForm(request.POST)
        if form.is_valid():
            form = form.save(commit=False)
            form.user = request.user
            form.save()
           
            #Successful POST requests returns the last 3 transactions
            transactions = Transaction.objects.filter(user=request.user)[:3]
            return render(
                request,
                "components/successful_transaction.html",
                {"form": TransactionForm(), "transactions": transactions},
            )
        else:
            return render(
                request,
                "components/transaction_form.html",
                {"form": form},
            )

    return render(request, "home.html", {"form": form})

Finally, our home template would change as such,

<!-- nothing else changes -->
<div class= "content" id="transactions" 
hx-trigger= "load"
hx-get = "{% url 'transactions' %}"
>
<p class="htmx-indicator"> Loading... </p>
</div>
<!-- hx-target = "this" is default -->
<!-- hx-swap = "innerHTML" is default -->

We could have given our home view access to the last 3 transactions, then used our "click to load" button to get subsequent transactions. But then our button logic would get messier. This way is cleaner. On page load, we get the first page, then we have the "click to load" button getting subsequent pages.

Go ahead and add a few transactions then try our new button. You should see something like this.

That's our user story is done. Go ahead and commit your code. You can see my version of the project here.

That's it for part 1 of this project. In part 2, we will focus on our transaction row and implement a couple of extra features.

Conclusion

In this project, we built a tool to log cryptocurrencies transactions. We focused on building a complex form using Django forms. Guiding the user with additional data, and using the "click to load" to display the user transactions.

Additionally, we introduced hyperscript and how can we use it in conjunction with htmx to build several dynamic UX patterns.

Get notified about new HTMX/Django tutorials
You will only get an email whenever a post or a course is published!

Copyright © HTMX-Django 2021.

Built by Jonathan Adly. Contact for opportunities.