mirror of
https://github.com/Django-LiveView/docs.git
synced 2024-11-10 02:45:42 +01:00
Add nav
This commit is contained in:
parent
7e50167ab5
commit
825d642738
@ -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,51 +119,52 @@ a, .link {
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code__block {
|
||||
.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 {
|
||||
.code__line {
|
||||
color: var(--color-brown);
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
.details {
|
||||
margin-block: 1rem;
|
||||
border: 2px solid var(--color-brown);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.details__title {
|
||||
.details__title {
|
||||
border-bottom: 2px solid var(--color-brown);
|
||||
}
|
||||
}
|
||||
|
||||
.details__summary {
|
||||
.details__summary {
|
||||
background-color: var(--color-brown);
|
||||
color: var(--color-white);
|
||||
padding: 1rem;
|
||||
list-style-type: "🐱 ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.details[open] > .details__summary {
|
||||
.details[open] > .details__summary {
|
||||
list-style-type: "😺 ";
|
||||
}
|
||||
}
|
||||
|
||||
.details__content {
|
||||
.details__content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
/* Hero */
|
||||
|
||||
.hero__hgroup {
|
||||
.hero__hgroup {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: 1fr auto 1fr;
|
||||
@ -145,9 +173,9 @@ a, .link {
|
||||
"subtitle image"
|
||||
"nav image";
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 600px) {
|
||||
@media (width < 600px) {
|
||||
.hero__hgroup {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(3, auto);
|
||||
@ -157,32 +185,120 @@ a, .link {
|
||||
"subtitle"
|
||||
"nav";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero__logo {
|
||||
.hero__logo {
|
||||
grid-area: image;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
.hero__title {
|
||||
grid-area: title;
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__subtitle {
|
||||
.hero__subtitle {
|
||||
grid-area: subtitle;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-docs {
|
||||
.nav-docs {
|
||||
grid-area: nav;
|
||||
}
|
||||
}
|
||||
|
||||
/* Home */
|
||||
.nav-home__list {
|
||||
/* Home */
|
||||
.nav-home__list {
|
||||
justify-content: center;
|
||||
gap: var(--gap-l);
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
/* 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; }
|
||||
|
BIN
assets/img/quickstart/minimal-template.webp
Normal file
BIN
assets/img/quickstart/minimal-template.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
285
one.org
285
one.org
@ -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
|
||||
|
23
onerc.el
23
onerc.el
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user