Add tutorials

This commit is contained in:
Andros Fenollosa 2024-03-14 11:02:04 +01:00
parent e11a032cc3
commit 889a5e2767
3 changed files with 448 additions and 280 deletions

View File

@ -90,6 +90,12 @@ a:hover,
grid-gap: 1rem;
}
@media (width < 600px) {
.nav-main__list > li:first-child {
margin-right: 6rem;
}
}
.nav-main__logo {
width: 80px;
margin-bottom: calc(var(--gap-m) * -1);
@ -101,12 +107,6 @@ a:hover,
bottom: 0;
}
@media (width < 600px) {
.nav-main__link--logo {
z-index: -1;
}
}
.center-block {
display: block;
margin-inline: auto;
@ -152,9 +152,11 @@ a:hover,
}
.button:hover {
--translate: 3px;
border-color: var(--color-brown);
box-shadow: 0 5px 0 var(--color-brown);
box-shadow: var(--translate) var(--translate) 0 var(--color-brown);
text-decoration: none;
transform: translate(calc(-1 * var(--translate)), calc(-1 * var(--translate)));
}
.header {
@ -178,7 +180,7 @@ a:hover,
.header .nav__list {
display: grid;
grid-template-columns: 1fr repeat(4, auto);
grid-template-columns: 1fr repeat(5, auto);
}
@media (width < 600px) {
@ -288,6 +290,11 @@ a:hover,
}
}
.nav-docs__list {
list-style: "➔ ";
list-style-position: inside;
}
/* Home */
.nav-home__list {
@ -333,7 +340,7 @@ a:hover,
/* Docs */
.docs {
display: grid;
grid-template-columns: 8rem auto;
grid-template-columns: 10rem auto;
gap: var(--gap-l);
}
@ -348,6 +355,12 @@ a:hover,
}
}
/* Tutorials */
.tutorials ul {
list-style: "📚 ";
list-position: inside;
}
/* Footer */
.footer {
text-align: center;

669
one.org
View File

@ -56,35 +56,23 @@ The same process is repeated for each action, such as clicking a button, submitt
- Everything is asynchronous by default.
- Don't learn anything new. If you know Python, you know how to use Django LiveView.
Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quickstart/][Quickstart]].
Are you ready to create your first Realtime SPA? Let's go to the [[#/tutorials/quickstart/][Quickstart]].
* Quickstart
* Install
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/quickstart/
:TITLE: Quickstart
:DESCRIPTION: Get started with Django LiveView the easy way.
:CUSTOM_ID: /docs/install/
:TITLE: Install
:DESCRIPTION: Install Django LiveView.
:END:
Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python.
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~.
You can 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~.
Then add ~liveview~ to your installed ~INSTALLED_APPS~ in your ~settings.py~.
#+BEGIN_SRC python
INSTALLED_APPS = [
@ -94,264 +82,21 @@ INSTALLED_APPS = [
]
#+END_SRC
Then indicate in which previously created App you want to implement LiveView.
You need to previously have installed ~channels~ and ~daphne~.
Now, in ~settings.py~, indicate in which previously created App you want to integrate LiveView.
#+BEGIN_SRC python
LIVEVIEW_APPS = ["website"]
#+END_SRC
** 4. Migration
Execute the migrations so that the LiveView tables are generated.
Finally, 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 center-block image image--responsive
[[#/img/quickstart/minimal-template.webp][Random number]]
We strongly recommend that you follow the [[#/tutorials/quickstart/][Quickstart]] to see the installation in action.
* Actions
:PROPERTIES:
@ -689,6 +434,54 @@ You can customize the history management system by editing the ~history~ control
If you want to disable it, remove `startHistory();` from ~assets/js/main.js~.
* Internationalization
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/internationalization/
:TITLE: Internationalization
:DESCRIPTION: Internationalization of Django LiveView.
:END:
Django LiveView uses the same internationalization system as Django. You can read more about it in the [[https://docs.djangoproject.com/en/dev/topics/i18n/][Django documentation]]. However, let's go deeper.
Every time that the client sends a request for action, it sends the language that the user has set in the browser.
#+BEGIN_SRC json
{
"action": "blog_list->send_page",
"data": {
"lang": "es"
}
}
#+END_SRC
The ~lang~ attribute is extracted directly from the ~<html>~ tag.
#+BEGIN_SRC html
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
#+END_SRC
You can access the language with the ~lang~ parameter of the action using the ~@enable_lang~ decorator.
#+BEGIN_SRC python
@enable_lang
@loading
async def send_page(consumer, client_data, lang=None):
print(lang)
#+END_SRC
Or you could read it with the ~lang~ key of the ~client_data~ parameter.
#+BEGIN_SRC python
@loading
async def send_page(consumer, client_data):
print(client_data["data"]["lang"])
#+END_SRC
You can read the tutorial [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]] to see how to create a multilingual website with subdomains.
* Deploy
:PROPERTIES:
:ONE: one-custom-default-doc
@ -767,6 +560,348 @@ Yes, you can.
Only me and my free time.
* Tutorials
:PROPERTIES:
:ONE: one-custom-default-tutorials
:CUSTOM_ID: /tutorials/
:TITLE: Tutorials
:DESCRIPTION: Tutorials about Django LiveView.
:END:
** Tutorials
Learn how to create a Django LiveView application step by step.
- [[#/tutorials/quickstart/][Quickstart]]
- [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]]
* Quickstart
:PROPERTIES:
:ONE: one-custom-default-page
:CUSTOM_ID: /tutorials/quickstart/
:TITLE: Quickstart
:DESCRIPTION: Get started with Django LiveView the easy way.
:END:
Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python.
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 when clicked.
#+ATTR_HTML: :class center-block image image--responsive
[[#/img/quickstart/minimal-template.webp][Random number]]
Congratulations! You have created your first Realtime SPA using Django LiveView.
The next step is to create a more complex application. You can read other [[#/tutorials/][tutorials]] or go to the [[#/docs/install/][documentation]].
* Internationalize with subdomains
:PROPERTIES:
:ONE: one-custom-default-page
:CUSTOM_ID: /tutorials/internationalize-with-subdomains/
:TITLE: Internationalize with subdomains
:DESCRIPTION: Create a multilingual website with subdomains.
:END:
Here you will learn how to create a multilingual website using Django LiveView. We recommend using subdomains to define the language (~en.example.com~, ~es.example.com~...), instead of using prefixes in addresses (~example.com/en/blog/~, ~example.com/es/blog/~). They simplify SEO, maintain consistency in the Sitemap and are easy to test.
** 1. Configure the subdomains
** 2. Create the subdomains
** 3. Configure the languages
** 4. Redirection with Middleware
** 5. First text
** 6. Make messages
** 7. Compile messages
** 8. Selector of languages
* Source code
:PROPERTIES:
:ONE: one-custom-default-page

View File

@ -97,13 +97,15 @@
(:li.nav-main__item
(:a.nav-main__link.nav-main__link--logo (@ :href "/") (:img.nav-main__logo (@ :alt "Django LiveView" :src "/img/logo.webp"))))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/docs/quickstart/") "Docs"))
(:a.button.nav-main__link (@ :href "/docs/install/") "Docs"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/source-code/") "Source code"))
(:a.button.nav-main__link (@ :href "/tutorials/") "Tutorials"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/books/") "Books"))))))
(:a.button.nav-main__link (@ :href "/books/") "Books"))
(:li.nav-main__item
(:a.button.nav-main__link (@ :href "/source-code/") "Source code"))))))
,tree-content
(:footer.footer
(:p "Created with " (:i (@ :aria-label "love") "❤️") " by " (:a.link (@ :href "https://andros.dev/" :target "_blank") "Andros Fenollosa") " with " (:a.link (@ :href "https://one.tonyaldon.com/" :target "_blank") "one.el"))
@ -168,7 +170,7 @@
(:nav.nav-docs
(:ul.nav__list.nav__list--docs.nav-docs__list
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/quickstart/") "Quickstart"))
(:a.nav-docs__link (@ :href "/docs/install/") "Install"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/actions/") "Actions"))
(:li.nav-docs__item
@ -177,6 +179,8 @@
(:a.nav-docs__link (@ :href "/docs/routing/") "Routing"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/history/") "History"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/internationalization/") "Internationalization"))
(:li.nav-docs__item
(:a.nav-docs__link (@ :href "/docs/deploy/") "Deploy"))
(:li.nav-docs__item
@ -185,6 +189,22 @@
(:main.main.main--docs
,content))))))
(defun one-custom-default-tutorials (page-tree pages _global)
"Default render function by tutorials page."
(let* ((title (org-element-property :raw-value page-tree))
(description (org-element-property :DESCRIPTION page-tree))
(path (org-element-property :CUSTOM_ID page-tree))
(content (org-export-data-with-backend
(org-element-contents page-tree)
'one-ox nil))
(website-name (one-default-website-name pages))
(nav (one-default-nav path pages)))
(render-layout-html
title
description
(jack-html `(:main.main
(:section.tutorials
(:div.container ,content)))))))
;; Sitemap
(defun make-sitemap (pages tree global)