This commit is contained in:
Andros Fenollosa 2024-02-29 17:08:43 +01:00
parent 7e50167ab5
commit 825d642738
4 changed files with 498 additions and 94 deletions

View File

@ -17,6 +17,7 @@ body {
color: var(--color-white);
line-height: 1.5;
scroll-behavior: smooth;
margin-top: 6rem;
}
/* Components */
@ -42,7 +43,7 @@ a, .link {
padding-block: .3rem;
}
.link:hover {
a:hover, .link:hover {
text-decoration: underline;
}
@ -69,6 +70,10 @@ a, .link {
object-position: center;
}
.block-center {
margin-inline: auto;
}
.button {
display: inline-block;
min-width: 4rem;
@ -85,6 +90,28 @@ a, .link {
.button:hover {
border-color: var(--color-brown);
box-shadow: 0 5px 0 var(--color-brown);
text-decoration: none;
}
.header {
position: fixed;
inset-inline: 0;
top: 0;
border-bottom: 2px solid var(--color-brown);
background-color: var(--color-black);
overflow-x: auto;
}
.header .nav__list {
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
}
@media (width < 600px) {
.header .nav__list {
justify-content: flex-start;
}
}
.nav__list {
@ -92,97 +119,186 @@ a, .link {
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
}
.code__block {
background-color: var(--color-gray);
color: var(--color-black);
padding: 1rem;
border-radius: .5rem;
font-family: 'Fira Code', monospace;
overflow-x: auto;
}
.code__line {
color: var(--color-brown);
font-family: 'Fira Code', monospace;
}
.details {
margin-block: 1rem;
border: 2px solid var(--color-brown);
}
.details__title {
border-bottom: 2px solid var(--color-brown);
}
.details__summary {
background-color: var(--color-brown);
color: var(--color-white);
padding: 1rem;
list-style-type: "🐱 ";
}
.details[open] > .details__summary {
list-style-type: "😺 ";
}
.details__content {
padding: 1rem;
}
/* Hero */
.hero__hgroup {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr auto 1fr;
grid-template-areas:
"title image"
"subtitle image"
"nav image";
grid-gap: 1rem;
}
@media (width < 600px) {
.hero__hgroup {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, auto);
grid-template-areas:
"title"
"image"
"subtitle"
"nav";
.code__block {
background-color: var(--color-gray);
color: var(--color-black);
padding: 1rem;
border-radius: .5rem;
font-family: 'Fira Code', monospace;
overflow-x: auto;
}
}
.hero__logo {
grid-area: image;
}
.code__line {
color: var(--color-brown);
font-family: 'Fira Code', monospace;
}
.hero__title {
grid-area: title;
align-self: end;
}
.details {
margin-block: 1rem;
border: 2px solid var(--color-brown);
}
.hero__subtitle {
grid-area: subtitle;
}
.nav-docs {
grid-area: nav;
}
.details__title {
border-bottom: 2px solid var(--color-brown);
}
/* Home */
.nav-home__list {
.details__summary {
background-color: var(--color-brown);
color: var(--color-white);
padding: 1rem;
list-style-type: "🐱 ";
}
.details[open] > .details__summary {
list-style-type: "😺 ";
}
.details__content {
padding: 1rem;
}
/* Hero */
.hero__hgroup {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr auto 1fr;
grid-template-areas:
"title image"
"subtitle image"
"nav image";
grid-gap: 1rem;
}
@media (width < 600px) {
.hero__hgroup {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, auto);
grid-template-areas:
"title"
"image"
"subtitle"
"nav";
}
}
.hero__logo {
grid-area: image;
}
.hero__title {
grid-area: title;
align-self: end;
}
.hero__subtitle {
grid-area: subtitle;
}
.nav-docs {
grid-area: nav;
}
/* Home */
.nav-home__list {
justify-content: center;
gap: var(--gap-l);
}
}
/* Footer */
.footer {
text-align: center;
}
/* Footer */
.footer {
text-align: center;
}
/* ------- '.one' classes used by 'one-ox' org backend ------- */
.one-hl {
font-family: 'Fira Mono', monospace;
font-size: 80%;
border-radius: 6px;
}
.one-hl-inline {
background: #31424a;
padding: 0.2em 0.4em;
margin: 0;
white-space: break-spaces;
}
.one-hl-block {
background: #161f22;
color: #c5c5c5;
display: block;
overflow: auto;
padding: 16px;
line-height: 1.45;
}
.one-blockquote {
background: #202d31;
border-left: 0.3em solid #31424a;
margin: 0px auto 16px;
padding: 1em 1em;
width: 90%;
}
.one-blockquote > p:last-child {
margin-bottom: 0;
}
.one-hl-results {
background: #202d31 ;
border-left: 2px solid #c5c5c5;
display: block;
margin: auto;
padding: 0.5em 1em;
overflow: auto;
width: 98%;
}
.one-hl-negation-char { color: #ff6c60} /* font-lock-negation-char-face */
.one-hl-warning { color: #fd971f} /* font-lock-warning-face */
.one-hl-variable-name { color: #fd971f} /* font-lock-variable-name-face */
.one-hl-doc { color: #d3b2a1} /* font-lock-doc-face */
.one-hl-doc-string { color: #d3b2a1} /* font-lock-doc-string-face */
.one-hl-string { color: #d3b2a1} /* font-lock-string-face */
.one-hl-function-name { color: #02d2da} /* font-lock-function-name-face */
.one-hl-builtin { color: #b2a1d3} /* font-lock-builtin-face */
.one-hl-type { color: #457f8b} /* font-lock-type-face */
.one-hl-keyword { color: #f92672} /* font-lock-keyword-face */
.one-hl-preprocessor { color: #f92672} /* font-lock-preprocessor-face */
.one-hl-comment-delimiter { color: #8c8c8c} /* font-lock-comment-delimiter-face */
.one-hl-comment { color: #8c8c8c} /* font-lock-comment-face */
.one-hl-constant { color: #f5ebb6} /* font-lock-constant-face */
.one-hl-reference { color: #f5ebb6} /* font-lock-reference-face */
.one-hl-regexp-grouping-backslash { color: #966046} /* font-lock-regexp-grouping-backslash */
.one-hl-regexp-grouping-construct { color: #aa86ee} /* font-lock-regexp-grouping-construct */
.one-hl-number { color: #eedc82} /* font-lock-number-face */
.one-hl-sh-quoted-exec { color: #62bd9c} /* sh-quoted-exec */
.one-hl-ta-colon-keyword {color: #62b5e0;} /* ta-colon-keyword-face */
.one-hl-org-code { color: #dedede; background: #31424a; }
.one-hl-org-block { color: #c5c5c5 ; background: #31424a; }
.one-hl-org-block-begin-line { color: #c3957e; }
.one-hl-org-block-end-line { color: #c3957e; }
.one-hl-org-meta-line { color: #8c8c8c;}
.one-hl-org-quote { color: #c5c5c5}
.one-hl-org-drawer { color: #d3b2a1; font-size: 0.9em; }
.one-hl-org-special-keyword { color: #c3957e; font-size: 0.9em; }
.one-hl-org-property-value { color: #d2934a; font-size: 0.9em; }
.one-hl-org-level-1 { font-size: 1.7em; text-decoration: underline; }
.one-hl-org-level-2 { font-size: 1.4em; text-decoration: underline; }
.one-hl-org-level-3 { font-size: 1.2em; text-decoration: underline; }
.one-hl-org-level-4 { font-size: 1.1em; text-decoration: underline; }
.one-hl-org-level-5 { font-size: 1.0em; text-decoration: underline; }
.one-hl-org-level-6 { font-size: 1.0em; text-decoration: underline; }
.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; }
.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

285
one.org
View File

@ -34,6 +34,291 @@ Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quicks
:DESCRIPTION: Get started with Django LiveView the easy way.
:END:
All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]].
** 1. Install Django
Install Django, create a project and an app.
** 2. Install LiveView
Install django-liveview with ~pip~.
#+BEGIN_SRC sh
pip install django-liveview
#+END_SRC
** 3. Modify the configuration
Add ~liveview~ to your installed ~INSTALLED_APPS~.
#+BEGIN_SRC python
INSTALLED_APPS = [
"daphne",
"channels",
"liveview",
]
#+END_SRC
Then indicate in which previously created App you want to implement LiveView.
#+BEGIN_SRC python
LIVEVIEW_APPS = ["website"]
#+END_SRC
** 4. Migration
Execute the migrations so that the LiveView tables are generated.
#+BEGIN_SRC python
python manage.py migrate
#+END_SRC
** 5. ASGI
Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different.
#+BEGIN_SRC python
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
django.setup()
from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from liveview.consumers import LiveViewConsumer
application = ProtocolTypeRouter(
{
# Django's ASGI application to handle traditional HTTP requests
"http": get_asgi_application(),
# WebSocket handler
"websocket": AuthMiddlewareStack(
AllowedHostsOriginValidator(
URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())])
)
),
}
)
#+END_SRC
** 6. Create your first Action
Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it.
Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~.
#+BEGIN_SRC python
# my-app/actions/home.py
from liveview.context_processors import get_global_context
from core import settings
from liveview.utils import (
get_html,
update_active_nav,
enable_lang,
loading,
)
from channels.db import database_sync_to_async
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext as _
from random import randint
template = "pages/home.html"
# Database
# Functions
async def get_context(consumer=None):
context = get_global_context(consumer=consumer)
# Update context
context.update(
{
"url": settings.DOMAIN_URL + reverse("home"),
"title": _("Home") + " | Home",
"meta": {
"description": _("Home page of the website"),
"image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}",
},
"active_nav": "home",
"page": template,
}
)
return context
@enable_lang
@loading
async def send_page(consumer, client_data, lang=None):
# Nav
await update_active_nav(consumer, "home")
# Main
my_context = await get_context(consumer=consumer)
html = await get_html(template, my_context)
data = {
"action": client_data["action"],
"selector": "#main",
"html": html,
}
data.update(my_context)
await consumer.send_html(data)
async def random_number(consumer, client_data, lang=None):
my_context = await get_context(consumer=consumer)
data = {
"action": client_data["action"],
"selector": "#output-random-number",
"html": randint(0, 10),
}
data.update(my_context)
await consumer.send_html(data)
#+END_SRC
There are several points in the above code to keep in mind.
- ~template~ is the name of the template that will be rendered.
- ~get_context()~ is a function that returns a dictionary with the context of the page.
- ~send_page()~ is the function that will be executed when the page is loaded.
- ~random_number()~ is the function that will be executed when the button is clicked.
** 7. Create the base template
Now we will create the base template, which will be the one that will be rendered when the page is loaded.
Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~.
#+BEGIN_SRC html
{# my-app/templates/layouts/base.html #}
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
>
<meta
name="description"
content="{{ meta.description }}"
>
<meta
property="og:image"
content="{{ meta.image }}"
>
<script type="module" src="{% static 'js/main.js' %}" defer></script>
</head>
<body
data-host="{{ request.get_host }}"
data-debug="{{ DEBUG }}"
>
<section id="loading"></section>
<section id="notifications" class="notifications"></section>
<section id="no_connection"></section>
<div class="container">
<header id="content-header">
{% include 'components/header.html' %}
</header>
<main id="main" class="main-container">{% include page %}</main>
<footer id="content-footer">
{% include 'components/footer.html' %}
</footer>
</div>
</body>
</html>
#+END_SRC
In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client.
** 8. Create the page template
We will create the home page template, which will be the one that will be rendered when the page is loaded.
Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~.
#+BEGIN_SRC html
{# my-app/templates/pages/home.html #}
{% load static %}
<main data-controller="home">
<p>
<button data-action="click->home#randomNumber">Random number</button>
</p>
<h2 id="output-random-number"></h2>
</main>
#+END_SRC
As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~).
** 9. Create frontend
Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions.
Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~.
** 10. Create View
We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application).
In a normal Django application we would create a view, ~views.py~, similar to the following:
#+BEGIN_SRC python
# my-app/views.py
from django.shortcuts import render
# Create your views here.
def home(request):
return render(request, "pages/home.html")
#+END_SRC
With LiveView, on the other hand, you will have the following structure.
#+BEGIN_SRC python
# my-app/views.py
from django.shortcuts import render
from .actions.home import get_context as get_home_context
from liveview.utils import get_html
async def home(request):
return render(request, "layouts/base.html", await get_home_context())
#+END_SRC
** 11. Create URL
Finally, we will create the URL that will render the page.
#+BEGIN_SRC python
# my-app/urls.py
from django.urls import path
from .views import home
urlpatterns = [
path("", home, name="home"),
]
#+END_SRC
** 12. Run the server
Run the server.
#+BEGIN_SRC sh
python manage.py runserver
#+END_SRC
And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number.
#+ATTR_HTML: :class block-center
[[#/img/quickstart/minimal-template.webp][Random number]]
* Source code
:PROPERTIES:
:ONE: one-custom-default-doc

View File

@ -24,6 +24,18 @@
(:link (@ :rel "stylesheet" :type "text/css" :href "https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"))
(:link (@ :rel "stylesheet" :type "text/css" :href "/css/main.css")))
(:body
(:header.header
(:div.container
(:nav.nav-main
(:ul.nav__list.nav-main__list
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/docs/quickstart/") "Docs"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/tutorials/") "Tutorials"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "https://github.com/Django-LiveView/" :target "_blank") "Source code"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo"))))))
,tree-content
(:footer.footer
(:p "Created with ❤️ by " (:a.link (@ :href "https://andros.dev/" :target "_blank") "Andros Fenollosa"))
@ -49,15 +61,6 @@
(:h1.hero__title "Django LiveView")
(:h2.hero__subtitle "Framework for creating Realtime SPAs using HTML over the Wire technology")
(:img.image.hero__logo (@ :alt "pet" :src "img/pet.webp")))))
(:nav.nav-home
(:div.container
(:ul.nav__list.nav-home__list
(:li.nav-home__item
(:a.button.nav-home__link (@ :href "/docs/quickstart/") "Docs"))
(:li.nav-home__item
(:a.button.nav-home__link (@ :href "/school/make-a-blog/") "Tutorials"))
(:li.nav-home__item
(:a.button.nav-home__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo")))))
(:section
(:div.container ,content)))))))
@ -71,7 +74,7 @@
'one-ox nil))
(website-name (one-default-website-name pages))
(nav (one-default-nav path pages)))
(render-layout-html
(render-layout-html
title
description
(jack-html `(:main.main