PyBladePyBlade

Forms

Because forms are the backbone of most web applications, PyBlade Live provides loads of helpful utilities for building them. From handling simple input elements to complex things like real-time validation or file uploading, PyBlade Live has simple, well-documented tools to make your life easier and delight your users.

Let's dive in.

Basic usage

Let's start by looking at a very simple form in a PostCreate component. This form will have two simple text inputs and a submit button, as well as some code on the backend to manage the form's state and submission:

from pyblade import LiveComponent
from app.models import Post

class PostCreate(LiveComponent):

    title: str = ""
    content: str = ""

    def save(self):
        Post.objects.create(title=self.title, content=self.content)
        # self.flash('status', 'Post successfully created.') Comming soon feature
        return self.redirect('/posts')

    def render(self):
        return self.render_template("live.post-create")
<form pb:submit="save">
    <input type="text" pb:model="title">

    <input type="text" pb:model="content">

    <button type="submit">Save</button>
</form>

As you can see, we are "binding" the public title and content properties in the form above using pb:model. This is one of the most commonly used and powerful features of PyBlade Live.

In addition to binding title and content, we are using pb:submit to capture the submit event when the "Save" button is clicked and invoking the save() action. This action will persist the form input to the database.

After the new post is created in the database, we redirect the user to the ShowPosts component page and show them a "flash" message that the new post was created.

PyBlade Live-updating fields

By default, PyBlade Live only sends a network request when the form is submitted (or any other action is called), not while the form is being filled out.

Take the CreatePost component, for example. If you want to make sure the "title" input field is synchronized with the title property on the backend as the user types, you may add the .live modifier to pb:model like so:

<input type="text" pb:model.live="title">

Now, as a user types into this field, network requests will be sent to the server to update title. This is useful for things like a real-time search, where a dataset is filtered as a user types into a search box.

Debouncing input

When live-updating with pb:model.debounce on a text input, you may want more fine-grained control over how often a network request is sent. By default, a debounce of "250ms" is applied to the input; however, you can customize this using the .Xms modifier, X representing the number of milliseconds:

<input type="text" pb:model.debounce.150ms="title" >

Now that .debounce.150ms has been added to the field, a shorter debounce of "150ms" will be used when handling input updates for this field. In other words, as a user types, a network request will only be sent if the user stops typing for at least 150 milliseconds.

Throttling input

As stated previously, when an input debounce is applied to a field, a network request will not be sent until the user has stopped typing for a certain amount of time. This means if the user continues typing a long message, a network request won't be sent until the user is finished.

Sometimes this isn't the desired behavior, and you would rather send a request as the user types, not when they've finished or taken a break.

In these cases, you can instead use .throttle to signify a time interval to send network requests:

<input type="text" pb:model.throttle.150ms="title" >

In the above example, as a user is typing continuously in the "title" field, a network request will be sent every 150 milliseconds until the user is finished.

PyBlade Live-updating only on blur

For most cases, pb:model.live is fine for real-time form field updating; however, it can be overly network resource-intensive on text inputs.

If instead of sending network requests as a user types, you want to instead only send the request when a user "tabs" out of the text input (also referred to as "blurring" an input), you can use the .blur modifier instead:

<input type="text" pb:model.blur="title" >

Now the component class on the server won't be updated until the user presses tab or clicks away from the text input.

Showing a loading indicator

By default, PyBlade Live will automatically disable submit buttons and mark inputs as readonly while a form is being submitted, preventing the user from submitting the form again while the first submission is being handled.

However, it can be difficult for users to detect this "loading" state without extra affordances in your application's UI.

Here's an example of adding a small loading spinner to the "Save" button via pb:loading so that a user understands that the form is being submitted:

<button type="submit">
    Save

    <div pb:loading>
        <svg>...</svg> <!-- SVG loading spinner -->
    </div>
</button>

Now, when a user presses "Save", a small, inline spinner will show up.

You can learn more about the pb:loading directive and other PyBlade Live directives in the PyBlade Live Directives documentation.

Real-time form saving

If you want to automatically save a form as the user fills it out rather than wait until the user clicks "submit", you can do so using PyBlade Live's updated() hook:

from pyblade import LiveComponent
from app.models import Post

class PostUpdate(LiveComponent):

    post: Post

    def rules(self):
        return {
            "title": "required"
            "content": "required"
        }

    def mount(self, post: Post):
        self.post = post
        self.title = post.title
        self.content = post.content

    def updated(self, property, value): 
        setattr(self.post, property, value)
        self.post.save()

    def render(self):
        return self.render_template('live.post-update')
<form pb:submit>
    <input type="text" pb:model.blur="title">
    <div>
        @error('title') <span class="error">{{ message }}</span> @enderror
    </div>

    <input type="text" pb:model.blur="content">
    <div>
        @error('content') <span class="error">{{ message }}</span> @enderror
    </div>
</form>

In the above example, when a user completes a field (by clicking or tabbing to the next field), a network request is sent to update that property on the component. Immediately after the property is updated on the class, the updated() hook is called for that specific property name and its new value.

We can use this hook to update only that specific field in the database.

Additionally, because we have validation rules attached to those properties, the validation rules will be run before the property is updated and the updated() hook is called.

To learn more about the updated() hook and other livecycle hooks, visit the Lifecycle hooks documentation.

Showing dirty indicators

In the real-time saving scenario discussed above, it may be helpful to indicate to users when a field hasn't been persisted to the database yet.

For example, if a user visits an PostUpdate page and starts modifying the title of the post in a text input, it may be unclear to them when the title is actually being updated in the database, especially if there is no "Save" button at the bottom of the form.

PyBlade Live provides the pb:dirty directive to allow you to toggle elements or modify classes when an input's value diverges from the server-side component:

<input type="text" pb:model.blur="title" pb:dirty.class="border-yellow">

In the above example, when a user types into the input field, a yellow border will appear around the field. When the user tabs away, the network request is sent and the border will disappear; signaling to them that the input has been persisted and is no longer "dirty".

If you want to toggle an entire element's visibility, you can do so by using pb:dirty in conjunction with pb:target. pb:target is used to specify which piece of data you want to watch for "dirtiness". In this case, the "title" field:

<input type="text" pb:model="title">

<div pb:dirty pb:target="title">Unsaved...</div>

Even in a small PyBlade Live component such as the PostCreate example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.

It can be helpful to extract repetitive UI elements such as these into dedicated PyBlade components to be shared across your application.

For example, below is the original PyBlade template from the PostCreate component. We will be extracting the following two text inputs into dedicated PyBlade components:

<form pb:submit="save">
    <input type="text" pb:model="title"> 
    <div>
        @error('title') <span class="error">{{ message }}</span> @enderror
    </div>

    <input type="text" pb:model="content"> 
    <div>
        @error('content') <span class="error">{{ message }}</span> @enderror
    </div>

    <button type="submit">Save</button>
</form>

Here's what the template will look like after extracting a re-usable PyBlade component called text-input:

<form pb:submit="save">
    <pb-text-input name="title" pb:model="title" /> 

    <pb-text-input name="content" pb:model="content" /> 

    <button type="submit">Save</button>
</form>

Next, here's the source for the text-input component:

<!-- templates/components/text-input.html -->

@props(['name'])

<input type="text" name="{{ name }}" {{ attributes }}>

<div>
    @error(name) <span class="error">{{ message }}</span> @enderror
</div>

As you can see, we took the repetitive HTML and placed it inside a dedicated PyBlade component.

For the most part, the PyBlade component contains only the extracted HTML from the original component. However, we have added two things:

  • The @props directive
  • The {{ attributes }} statement on the input

Let's discuss each of these additions:

By specifying name as a "prop" using @props(['name']) we are telling PyBlade: if an attribute called "name" is set on this component, take its value and make it available inside this component through a variable called name.

For other attributes that don't have an explicit purpose, we used the {{ attributes }} statement. This is used for "attribute forwarding", or in other words, taking any HTML attributes written on the PyBlade component and forwarding them onto an element within the component.

This ensures pb:model="title" and any other extra attributes such as disabled, class="...", or required still get forwarded to the actual <input> element.

On this page