Guest User

htmx CRUD example

a guest
Mar 17th, 2025
257
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.59 KB | Source Code | 0 0
  1. Yeah sure so using flask all you need is an [app.py](http://app.py)
  2.  
  3. I use flask with flask-sqlalchemy and jinaj2 templates. In a templates folder you both have the index.html as well as templates/partials/ that contains all the snippets you'll return from the htmx-y endpoints. All you do with HTMX is return HTML snippets from some endpoints, you can hardcode html strings but I prefer using a templating system like jinja2 since this allows you to get some really powerful HTML generation.
  4.  
  5. So for a CRUD app, on an edit you probably want to return an html snippet containing your entity with the updated values, delete doesn't have to return something, (you are going to remove the entity in the html directly).
  6. ===================================
  7.     ├── main.py
  8.     └── templates
  9.         ├── index.html
  10.         └── partials
  11.             ├── task_row.html
  12.             ├── task_detail.html
  13.             └── task_edit_form.html
  14. ===================================
  15.     from flask import Flask, render_template, request, redirect, url_for, jsonify
  16.     from flask_sqlalchemy import SQLAlchemy
  17.     from datetime import datetime
  18.    
  19.     app = Flask(__name__)
  20.     app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
  21.     app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
  22.     db = SQLAlchemy(app)
  23.    
  24.     # Define the Task model
  25.     class Task(db.Model):
  26.         id = db.Column(db.Integer, primary_key=True)
  27.         title = db.Column(db.String(100), nullable=False)
  28.         description = db.Column(db.Text, nullable=True)
  29.         completed = db.Column(db.Boolean, default=False)
  30.         created_at = db.Column(db.DateTime, default=datetime.utcnow)
  31.    
  32.         def __repr__(self):
  33.             return f'<Task {self.title}>'
  34.    
  35.     # Create the database tables
  36.     with app.app_context():
  37.         db.create_all()
  38.    
  39.     # Routes
  40.     app.route('/')
  41.     def index():
  42.         tasks = Task.query.order_by(Task.created_at.desc()).all()
  43.         return render_template('index.html', tasks=tasks)
  44. ===================================
  45. The index.html would be liek this (water.css for styling, I like it and it's simple)
  46. ===================================
  47.    <!DOCTYPE html>
  48.    <html lang="en">
  49.      <head>
  50.        <meta charset="UTF-8">
  51.        <meta name="viewport" content="width=device-width, initial-scale=1.0">
  52.        <title>Flask HTMX CRUD App</title>
  53.        <!-- HTMX -->
  54.        <script src="https://unpkg.com/[email protected]"></script>
  55.        <!-- Water.css -->
  56.        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
  57.      </head>
  58.      <body>
  59.        <h1>Task Manager</h1>
  60.        <div class="task-form">
  61.          <h2>Add New Task</h2>
  62.          <form hx-post="/tasks" hx-target="#task-list" hx-swap="afterbegin">
  63.    <div>
  64.              <input type="text" name="title" placeholder="Task Title" required>
  65.    </div>
  66.    <div>
  67.              <textarea name="description" placeholder="Description"></textarea>
  68.    </div>
  69.    <div>
  70.              <button type="submit">Add Task</button>
  71.    </div>
  72.          </form>
  73.        </div>
  74.        <!-- Task List -->
  75.        <div class="task-list">
  76.          <h2>Tasks</h2>
  77.          <div id="task-list">
  78.            {% for task in tasks %}
  79.            {% include "partials/task_row.html" %}
  80.            {% endfor %}
  81.          </div>
  82.        </div>    
  83.      </body>
  84.    </html>
  85. ===================================
  86.  
  87. And then add the routes for the CRUD handling
  88. ===================================
  89.    # Create
  90.    @app.route('/tasks', methods=['POST'])
  91.    def create_task():
  92.        title = request.form.get('title')
  93.        description = request.form.get('description')
  94.        
  95.        if not title:
  96.            return "Title is required", 400
  97.        
  98.        new_task = Task(title=title, description=description)
  99.        db.session.add(new_task)
  100.        db.session.commit()
  101.        
  102.        # Return just the new task row for htmx to insert
  103.        return render_template('partials/task_row.html', task=new_task)
  104.    
  105.    # Read (single task)
  106.    @app.route('/tasks/<int:task_id>')
  107.    def get_task(task_id):
  108.        task = Task.query.get_or_404(task_id)
  109.        return render_template('partials/task_detail.html', task=task)
  110.    
  111.    # Update form
  112.    @app.route('/tasks/<int:task_id>/edit', methods=['GET'])
  113.    def edit_task_form(task_id):
  114.        task = Task.query.get_or_404(task_id)
  115.        return render_template('partials/task_edit_form.html', task=task)
  116.    
  117.    # Update
  118.    @app.route('/tasks/<int:task_id>', methods=['PUT', 'POST'])
  119.    def update_task(task_id):
  120.        task = Task.query.get_or_404(task_id)
  121.        
  122.        # Handle the form data
  123.        task.title = request.form.get('title', task.title)
  124.        task.description = request.form.get('description', task.description)
  125.        
  126.        db.session.commit()
  127.        
  128.        # Return the updated task row
  129.        return render_template('partials/task_row.html', task=task)
  130.    
  131.    # Update completion status
  132.    @app.route('/tasks/<int:task_id>/toggle', methods=['POST'])
  133.    def toggle_completed(task_id):
  134.        task = Task.query.get_or_404(task_id)
  135.        task.completed = not task.completed
  136.        db.session.commit()
  137.        
  138.        # Return just the updated task row
  139.        return render_template('partials/task_row.html', task=task)
  140.    
  141.    # Delete
  142.    @app.route('/tasks/<int:task_id>/delete', methods=['DELETE', 'POST'])
  143.    def delete_task(task_id):
  144.        task = Task.query.get_or_404(task_id)
  145.        db.session.delete(task)
  146.        db.session.commit()
  147.        
  148.        # For htmx, return empty 200 response
  149.        return "", 200
  150. ===================================
  151.  
  152. And don't forget to have the individual partials to render:
  153. ===================================
  154.     <!-- templates/partials/task_row.html -->
  155.     <div id="task-{{ task.id }}" class="task {% if task.completed %}completed{% endif %}">
  156.         <h3>{{ task.title }}</h3>
  157.         <p>{{ task.description }}</p>
  158.         <div class="task-buttons">
  159.             <button hx-get="/tasks/{{ task.id }}/edit"
  160.                     hx-target="#task-{{ task.id }}"
  161.                     hx-swap="innerHTML">
  162.                 Edit
  163.             </button>
  164.             <button hx-post="/tasks/{{ task.id }}/toggle"
  165.                     hx-target="#task-{{ task.id }}"
  166.                     hx-swap="outerHTML">
  167.                 {% if task.completed %}Mark Incomplete{% else %}Mark Complete{% endif %}
  168.             </button>
  169.             <button hx-post="/tasks/{{ task.id }}/delete"
  170.                     hx-target="#task-{{ task.id }}"
  171.                     hx-swap="outerHTML"
  172.                     hx-confirm="Are you sure you want to delete this task?">
  173.                 Delete
  174.             </button>
  175.             <button hx-get="/tasks/{{ task.id }}"
  176.                     hx-target="#task-{{ task.id }} .task-detail"
  177.                     hx-swap="innerHTML">
  178.                 Show Details
  179.             </button>
  180.             <div class="task-detail"></div>
  181.         </div>
  182.     </div>
  183.    
  184.     <!-- templates/partials/task_detail.html -->
  185.     <div class="task-detail-info">
  186.         <p><strong>Created at:</strong> {{ task.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
  187.         <p><strong>Status:</strong> {% if task.completed %}Completed{% else %}Pending{% endif %}</p>
  188.         <button hx-get="/tasks" hx-target=".task-detail" hx-swap="innerHTML">Close</button>
  189.     </div>
  190.    
  191.     <!-- templates/partials/task_edit_form.html -->
  192.     <div class="task-form">
  193.         <h3>Edit Task</h3>
  194.         <form hx-post="/tasks/{{ task.id }}" hx-target="#task-{{ task.id }}" hx-swap="outerHTML">
  195.             <div>
  196.                 <input type="text" name="title" value="{{ task.title }}" required>
  197.             </div>
  198.             <div>
  199.                 <textarea name="description">{{ task.description }}</textarea>
  200.             </div>
  201.             <div>
  202.                 <button type="submit">Save Changes</button>
  203.                 <button type="button" hx-get="/tasks/{{ task.id }}" hx-target="#task-{{ task.id }}" hx-swap="outerHTML">
  204.                     Cancel
  205.                 </button>
  206.             </div>
  207.         </form><!-- templates/partials/task_row.html -->
  208.     <div id="task-{{ task.id }}" class="task {% if task.completed %}completed{% endif %}">
  209.         <h3>{{ task.title }}</h3>
  210.         <p>{{ task.description }}</p>
  211.         <div class="task-buttons">
  212.             <button hx-get="/tasks/{{ task.id }}/edit"
  213.                     hx-target="#task-{{ task.id }}"
  214.                     hx-swap="innerHTML">
  215.                 Edit
  216.             </button>
  217.             <button hx-post="/tasks/{{ task.id }}/toggle"
  218.                     hx-target="#task-{{ task.id }}"
  219.                     hx-swap="outerHTML">
  220.                 {% if task.completed %}Mark Incomplete{% else %}Mark Complete{% endif %}
  221.             </button>
  222.             <button hx-post="/tasks/{{ task.id }}/delete"
  223.                     hx-target="#task-{{ task.id }}"
  224.                     hx-swap="outerHTML"
  225.                     hx-confirm="Are you sure you want to delete this task?">
  226.                 Delete
  227.             </button>
  228.             <button hx-get="/tasks/{{ task.id }}"
  229.                     hx-target="#task-{{ task.id }} .task-detail"
  230.                     hx-swap="innerHTML">
  231.                 Show Details
  232.             </button>
  233.             <div class="task-detail"></div>
  234.         </div>
  235.     </div>
  236.    
  237.     <!-- templates/partials/task_detail.html -->
  238.     <div class="task-detail-info">
  239.         <p><strong>Created at:</strong> {{ task.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
  240.         <p><strong>Status:</strong> {% if task.completed %}Completed{% else %}Pending{% endif %}</p>
  241.         <button hx-get="/tasks" hx-target=".task-detail" hx-swap="innerHTML">Close</button>
  242.     </div>
  243.    
  244.     <!-- templates/partials/task_edit_form.html -->
  245.     <div class="task-form">
  246.         <h3>Edit Task</h3>
  247.         <form hx-post="/tasks/{{ task.id }}" hx-target="#task-{{ task.id }}" hx-swap="outerHTML">
  248.             <div>
  249.                 <input type="text" name="title" value="{{ task.title }}" required>
  250.             </div>
  251.             <div>
  252.                 <textarea name="description">{{ task.description }}</textarea>
  253.             </div>
  254.             <div>
  255.                 <button type="submit">Save Changes</button>
  256.                 <button type="button" hx-get="/tasks/{{ task.id }}" hx-target="#task-{{ task.id }}" hx-swap="outerHTML">
  257.                     Cancel
  258.                 </button>
  259.             </div>
  260.         </form>
  261. ===================================
Tags: htmx
Advertisement
Add Comment
Please, Sign In to add comment