gks
gks

Reputation: 261

How to create a dynamic form in htmx and Django?

I'm looking to create a dynamic website in Django and htmx that has a search bar where the user can enter keywords and get search results without having to reload the page. For this I intend to use htmx . Submitting the search bar is not done from a submit button but rather from the Enter key on the keyboard. When this search bar is submitted it sends the keywords entered to Django's views.py file via the POST method which will take care of returning the results to the address localhost:8000/search and on the page of the search bar. Here is the content of my views.py file:

from django.shortcuts import render 
import requests 
from bs4 import BeautifulSoup as bs


from search.models import MyForm



def index(request):
    return render(request, 'index.html')

def search(request):
    if request.method == 'POST':
        search = request.POST['search']
        form = MyForm(request.POST)
        
        max_pages_to_scrap = 15
        final_result = []
        for page_num in range(1, max_pages_to_scrap+1):
            url = "https://www.ask.com/web?q=" + search + "&qo=pagination&page=" + str(page_num)
            res = requests.get(url)
            soup = bs(res.text, 'lxml')
            result_listings = soup.find_all('div', {'class': 'PartialSearchResults-item'})

            for result in result_listings:
                result_title = result.find(class_='PartialSearchResults-item-title').text
                result_url = result.find('a').get('href')
                result_desc = result.find(class_='PartialSearchResults-item-abstract').text
           
                final_result.append((result_title, result_url, result_desc))

        context = {'final_result': final_result, 'form':form}
        

        return render(request, 'index.html', context)

    else:
        return render(request, 'index.html')


it is a scraper that scrapes the Ask.com site. For the submission form that will be sent to the user, I first tried this:

<form method="POST" action="search">
                        ...
                        <input  type="search" name="search"  hx-post="/search" hx-target="#results"  hx-trigger="keyup changed delay:500ms"   value="{{form.search.value}}" placeholder="Search here..."  autofocus x-webkit-speech/>
                       
                </form>

When the user submits this form, the keywords entered are sent to Django views.py file via the POST method and this is responsible for returning the results.

I tried to replace :

<form method="POST" action="search">
                        ...
                        <input  type="search" name="search"  hx-post="/search" hx-target="#results"  hx-trigger="keyup changed delay:500ms"   value="{{form.search.value}}" placeholder="Search here..."  autofocus x-webkit-speech/>
                       
                </form>

by :


                        <input  type="search" name="search"  hx-post="/search" hx-target="#results" hx-sync="closest form:abort"  hx-trigger="keyup changed delay:500ms"   value="{{form.search.value}}" placeholder="Search here..."  autofocus x-webkit-speech/>
                       
                </form>

I also tried this:

<! DOCTYPE html>
<html> 
        <head> 
                <meta charset="utf-8"/>
                <script src="https://unpkg.com/[email protected]" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script>

                     . . .
                <title>Search page</title>
        
        </head>

        <body> 
        <div class="search">
        <div class="header">
               
        <header >
                
                        {% csrf_token %}
                        <input  type="search" name="search" hx-sync="closest form:abort" hx-post="/search" hx-target="#results"  hx-trigger="keyup changed delay:500ms"   value="{{form.search.value}}" placeholder="Search here..."  autofocus x-webkit-speech/>
                       
                
        
        </header>
        </div>
        </div>
        <div id="results">

        {% if final_result %}
    <b style="padding-left: 50px; padding-right: 50px; color: #646464;">Web Results</b>
    {% for result in final_result %}
    <div style="padding-left: 50px; padding-right: 50px;">
            <h3 style="color: blue;  font-size:20px; "><a href="{{ result.1 }}">{{ result.0 }}</a></h3>
            <h3 style="color: green; font-size:14px;">{{ result.1 }}</h3>
            <h3 style="font-size:17.25px; " >{{ result.2 }}</h3>
    </div><br><br>
    {% endfor %}
    {% endif %}
    
</div>
    </body>

</html>

but without success.

Upvotes: 0

Views: 2696

Answers (1)

SamSparx
SamSparx

Reputation: 5257

Updated answer based on increased information.

You need to think about what your pages are asking for and returning.

HTMX is a way of requesting information via ajax without excess javascript etc. We're putting our response into the existing page, so we want html but we don't want the entire page worth of HTML - just the new section.

Your django template, index.html is delivered by the server. It doesn't know what is being returned by ajax, as that is all happening after the server has delivered index.html, so you can't use it format new HTML (unless you reload the entire page, which defeats the point to using HTMX)

Let's say you've got a page, we'll call it /index for reference. It calls the htmx code so we can use it. Place in the <head> section of index.html.

<script src="https://unpkg.com/[email protected]" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script>

Beneath that, and also in the <head> seaction (from various docs) we'll put in the following to ensure we have the encessary CSRF token linked to the htmx ajax post

<script>
  document.body.addEventListener('htmx:configRequest', (event) => {
    event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
  })
</script>

On that page in the also you've got a htmx search field. We'll use yours

<input  type="search" name="search"  
 hx-post="/search" hx-target="#results"  
 hx-trigger="keyup changed delay:500ms"   
 value="{{form.search.value}}" placeholder="Search here..."  
 autofocus x-webkit-speech/>

Your hx trigger says, on either keyup or change (after typing, or clicking out or hitting enter, and after waiting half a second) submit to the view domain/search and put the result in <div id="results"></div>

Obviously, we need to make sure domain/search has an entry in urls.py so the view can be found.

Now - because we want this to all happen without loading the page, we can't expect django's index.html template to handle the formatting (the server has already done its work delivering the page to the browser, and we don't want to reload the entire page and index.html is a whole page worth of HTML). So our /search has to render the html using another template. We'll call it search.html

search.html

    {% if final_result %}
<b style="padding-left: 50px; padding-right: 50px; color: #646464;">Web Results</b>
{% for result in final_result %}
<div style="padding-left: 50px; padding-right: 50px;">
        <h3 style="color: blue;  font-size:20px; "><a href="{{ result.1 }}">{{ result.0 }}</a></h3>
        <h3 style="color: green; font-size:14px;">{{ result.1 }}</h3>
        <h3 style="font-size:17.25px; " >{{ result.2 }}</h3>
</div><br><br>
{% endfor %}
{% endif %}

and we call this instead of index.html in views.py search

return render(request, 'search.html', context) #context includes final_result

So, in this case, we are returning an HTML fragment not the entire page and this is what should be displayed in the results div.

Upvotes: 1

Related Questions