diff --git a/one.org b/one.org index f2bbbbd..8b57bb4 100644 --- a/one.org +++ b/one.org @@ -177,38 +177,150 @@ We strongly recommend that you follow the [[#/quick-start/][Quick start]] to see Handlers are Python functions that respond to WebSocket messages from the client. They contain your business logic and can render HTML, update the database, broadcast to multiple users, and more. -** Creating Handlers +** Your First Handler -Use the ~@liveview_handler~ decorator to register functions that can be called from the client: +Let's start with the simplest possible handler. This handler will be called when a button is clicked and will update a section of the page. + +Python code: #+BEGIN_SRC python from liveview import liveview_handler, send -from django.template.loader import render_to_string @liveview_handler("say_hello") def say_hello(consumer, content): - """ - Args: - consumer: WebSocket consumer instance - content: dict with: - - function: str - the function name - - data: dict - custom data from data-data-* attributes - - form: dict - form input values - - lang: str - current language - - room: str - room identifier - """ - name = content.get("form", {}).get("name", "World") - - html = render_to_string("hello_message.html", { - "message": f"Hello, {name}!" + send(consumer, { + "target": "#greeting", + "html": "

Hello, World!

" }) +#+END_SRC + +HTML code: + +#+BEGIN_SRC html +
+
+ +
+#+END_SRC + +When you click the button: +1. The frontend sends a WebSocket message with ~function: "say_hello"~ +2. Django LiveView calls the ~say_hello~ handler +3. The handler sends back HTML to replace the content of ~#greeting~ +4. The page updates instantly without a full reload + +** Working with Form Data + +Most handlers need to receive data from the user. All form inputs within the same container are automatically sent to your handler. + +Python code: + +#+BEGIN_SRC python +@liveview_handler("greet_user") +def greet_user(consumer, content): + # Get the name from form data + name = content["form"].get("name", "Anonymous") send(consumer, { "target": "#greeting", + "html": f"

Hello, {name}! Welcome to Django LiveView.

" + }) +#+END_SRC + +HTML code: + +#+BEGIN_SRC html +
+
+ + +
+#+END_SRC + +The ~content["form"]~ dictionary contains all input values with their ~name~ attribute as the key. + +** Using Templates for Complex HTML + +For anything beyond simple strings, use Django templates to render your HTML. + +Python code: + +#+BEGIN_SRC python +from django.template.loader import render_to_string + +@liveview_handler("show_profile") +def show_profile(consumer, content): + user_id = content["form"].get("user_id") + + # Get user from database + from .models import User + user = User.objects.get(id=user_id) + + # Render template with context + html = render_to_string("profile_card.html", { + "user": user, + "is_online": True + }) + + send(consumer, { + "target": "#profile-container", "html": html }) #+END_SRC +HTML code: + +#+BEGIN_SRC html +
+
+ + +
+#+END_SRC + +Template (~profile_card.html~): + +#+BEGIN_SRC html +
+ {{ user.name }} +

{{ user.name }}

+

{{ user.bio }}

+ {% if is_online %} + Online + {% endif %} +
+#+END_SRC + +** Understanding the Content Parameter + +Every handler receives two parameters: ~consumer~ and ~content~. The ~content~ dictionary contains all the information from the client. + +#+BEGIN_SRC python +@liveview_handler("example") +def example(consumer, content): + # content structure: + # { + # "function": "example", # Handler name + # "form": {...}, # All form inputs + # "data": {...}, # Custom data-data-* attributes + # "lang": "en", # Current language + # "room": "user_123" # WebSocket room identifier + # } + pass +#+END_SRC + ** Auto-discovery Django LiveView automatically discovers handlers in ~liveview_components/~ directories within your installed apps: @@ -229,80 +341,242 @@ Handlers are loaded on startup with this output: ✓ Imported: my_app.liveview_components.comments #+END_SRC -** Using the send() function +** Using Custom Data Attributes -The ~send()~ function sends data back to the client with many options: +Sometimes you need to send additional data that isn't part of a form. Use ~data-data-*~ attributes for this. -*** Replace HTML (Basic Update) +Python code: #+BEGIN_SRC python -@liveview_handler("update_content") -def update_content(consumer, content): +@liveview_handler("delete_comment") +def delete_comment(consumer, content): + # Access custom data from data-data-* attributes + comment_id = content["data"]["comment_id"] + post_id = content["data"]["post_id"] + + # Delete from database + from .models import Comment + Comment.objects.filter(id=comment_id).delete() + send(consumer, { - "target": "#my-element", - "html": "

New content

" + "target": f"#comment-{comment_id}", + "remove": True # Remove the element from DOM }) #+END_SRC -*** Append HTML +HTML code: + +#+BEGIN_SRC html +
+

This is a comment

+ +
+#+END_SRC + +The attribute ~data-data-comment-id~ becomes ~comment_id~ (with underscores, not camelCase) within ~content["data"]~. + +** Appending Content to Lists + +When building dynamic lists (like infinite scroll or chat messages), you want to add items without replacing the entire list. + +Python code: #+BEGIN_SRC python -@liveview_handler("add_item") -def add_item(consumer, content): +@liveview_handler("load_more_posts") +def load_more_posts(consumer, content): + page = int(content["form"].get("page", 1)) + + # Get next page of posts + from .models import Post + posts = Post.objects.all()[(page-1)*10:page*10] + + # Render new posts + html = render_to_string("posts_list.html", { + "posts": posts + }) + send(consumer, { - "target": "#items-list", - "html": "
  • New item
  • ", - "append": True # Adds to the end + "target": "#posts-container", + "html": html, + "append": True # Add to the end instead of replacing }) #+END_SRC -*** Remove Element +HTML code: + +#+BEGIN_SRC html +
    + +
    + + + +#+END_SRC + +Template (~posts_list.html~): + +#+BEGIN_SRC html +{% for post in posts %} +
    +

    {{ post.title }}

    +

    {{ post.content }}

    +
    +{% endfor %} +#+END_SRC + +** Removing Elements from the DOM + +Instead of hiding elements with CSS, you can completely remove them from the page. + +Python code: #+BEGIN_SRC python -@liveview_handler("delete_item") -def delete_item(consumer, content): - item_id = content["data"]["id"] - # Delete from database... +@liveview_handler("archive_notification") +def archive_notification(consumer, content): + notification_id = content["data"]["notification_id"] + # Archive in database + from .models import Notification + Notification.objects.filter(id=notification_id).update(archived=True) + + # Remove from page send(consumer, { - "target": f"#item-{item_id}", + "target": f"#notification-{notification_id}", "remove": True }) #+END_SRC -*** Update URL and Title +HTML code: + +#+BEGIN_SRC html +
    +

    You have a new message

    + +
    +#+END_SRC + +** Updating the URL Without Page Reload + +Create SPA-like navigation by updating both content and the browser URL. + +Python code: #+BEGIN_SRC python -@liveview_handler("navigate") -def navigate(consumer, content): +@liveview_handler("navigate_to_profile") +def navigate_to_profile(consumer, content): + user_id = content["data"]["user_id"] + + # Get user data + from .models import User + user = User.objects.get(id=user_id) + + # Render profile page + html = render_to_string("profile_page.html", { + "user": user + }) + send(consumer, { - "target": "#content", - "html": render_to_string("new_page.html"), - "url": "/new-page/", - "title": "New Page Title" + "target": "#main-content", + "html": html, + "url": f"/profile/{user.username}/", # Update browser URL + "title": f"{user.name} - Profile" # Update page title }) #+END_SRC -*** Scroll Management +HTML code: + +#+BEGIN_SRC html +
    +

    Home Page

    + +
    +#+END_SRC + +The browser's back/forward buttons will work correctly, and users can bookmark or share the URL. + +** Scrolling to Elements + +After updating content, you often want to scroll to a specific element or to the top of the page. + +Python code: #+BEGIN_SRC python -@liveview_handler("show_section") -def show_section(consumer, content): - send(consumer, { - "target": "#content", - "html": render_to_string("section.html"), - "scroll": "#section-2" # Smooth scroll to element +@liveview_handler("show_product_details") +def show_product_details(consumer, content): + product_id = content["data"]["product_id"] + + from .models import Product + product = Product.objects.get(id=product_id) + + html = render_to_string("product_details.html", { + "product": product }) -@liveview_handler("back_to_top") -def back_to_top(consumer, content): send(consumer, { - "target": "#content", - "html": render_to_string("content.html"), + "target": "#product-details", + "html": html, + "scroll": "#product-details" # Smooth scroll to this element + }) + +@liveview_handler("search_products") +def search_products(consumer, content): + query = content["form"].get("q") + + from .models import Product + products = Product.objects.filter(name__icontains=query) + + html = render_to_string("products_list.html", { + "products": products + }) + + send(consumer, { + "target": "#products-list", + "html": html, "scrollTop": True # Scroll to top of page }) #+END_SRC +HTML code: + +#+BEGIN_SRC html +
    + + + + + +
    +#+END_SRC + * Frontend Integration :PROPERTIES: :ONE: one-custom-default-doc