Skip to content

Commit

Permalink
initial template include support (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogrodnek authored Sep 4, 2024
1 parent 68f7458 commit e7eb2cd
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 4 deletions.
8 changes: 8 additions & 0 deletions examples/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
from starlette.staticfiles import StaticFiles
from starlette.routing import Route
from pyview import PyView, defaultRootTemplate
from pyview.vendor import ibis
from pyview.vendor.ibis.loaders import FileReloader
from markupsafe import Markup
from .format_examples import ExampleEntry, format_examples
import os

from .views import (
CountLiveView,
Expand All @@ -21,6 +24,7 @@
MapLiveView,
FileUploadDemoLiveView,
KanbanLiveView,
IncludesLiveView,
)

app = PyView()
Expand Down Expand Up @@ -62,6 +66,9 @@ def content_wrapper(_context, content: Markup) -> Markup:

app.rootTemplate = defaultRootTemplate(css=Markup(css), content_wrapper=content_wrapper)

current_file_dir = os.path.dirname(os.path.abspath(__file__))
ibis.loader = FileReloader(os.path.join(current_file_dir, "views"))

routes = [
("/count", CountLiveView),
("/count_pubsub", CountLiveViewPubSub),
Expand All @@ -86,6 +93,7 @@ def content_wrapper(_context, content: Markup) -> Markup:
("/maps", MapLiveView),
("/file_upload", FileUploadDemoLiveView),
("/kanban", KanbanLiveView),
("/includes", IncludesLiveView),
]


Expand Down
2 changes: 2 additions & 0 deletions examples/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .kanban import KanbanLiveView
from .count_pubsub import CountLiveViewPubSub
from .count import CountLiveView
from .includes import IncludesLiveView

__all__ = [
"CountLiveView",
Expand All @@ -30,4 +31,5 @@
"MapLiveView",
"FileUploadDemoLiveView",
"KanbanLiveView",
"IncludesLiveView",
]
1 change: 1 addition & 0 deletions examples/views/includes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .includes import IncludesLiveView
22 changes: 22 additions & 0 deletions examples/views/includes/_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions examples/views/includes/_navbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<nav class="navbar">
<div class="navbar-left">
<div class="logo">
{% include "includes/_logo.svg" %}
</div>
<div class="company-name">MyCompany</div>
</div>
<div class="navbar-center">
{% for page in pages %}
<a data-phx-link="patch" data-phx-link-state="push" href="/includes?page={{page}}"
{% if current_page == page %}aria-current="page" {%endif%}>{{page}}</a>
{% endfor %}
</div>
<div class="navbar-right">
<div class="user-profile">
<img src="{{avatar_url}}" alt="User Avatar" class="avatar">
</div>
</div>
</nav>
99 changes: 99 additions & 0 deletions examples/views/includes/includes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
body > a:first-of-type {
display: none;
}

.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.navbar-left {
display: flex;
align-items: center;
}


.company-name {
font-size: 18px;
font-weight: bold;
margin-left: 10px;
color: #333;
}

.navbar-center {
display: flex;
align-items: center;
gap: 20px;
}

.navbar-center a {
text-decoration: none;
color: #333;
font-size: 18px;
padding: 5px 10px;
border-radius: 4px;
text-transform: capitalize;
transition: background-color 0.3s ease, color 0.3s ease;
}

.navbar-center a:hover {
background-color: #f1f1f1;
}

.navbar-center a[aria-current="page"] {
background-color: #007BFF;
color: #ffffff;
font-weight: bold;
pointer-events: none;
}

.navbar-right {
display: flex;
align-items: center;
}

.user-profile {
display: flex;
align-items: center;
cursor: pointer;
}

.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #ccc;
}

.content {
padding: 20px;
max-width: 900px;
margin: 20px auto;
background-color: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}

.content h1 {
margin-bottom: 20px;
font-size: 32px;
text-transform: capitalize;
}

.content p {
margin-bottom: 10px;
}

code {
background-color: #f5f7fa;
color: #2e3a59;
padding: 1em;
border-radius: 8px;
border: 1px solid #d1d5db;
margin: 1em 0;
display: block;
}
22 changes: 22 additions & 0 deletions examples/views/includes/includes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% include "includes/_navbar.html" with avatar_url=user.avatar_url %}

<div class="content">
<h1>{{current_page}}</h1>
<p>
This is a simple example of how to re-use template code in PyView.
</p>
<p>
This page includes a navbar that is defined in a separate file, and passes the user's avatar as a parameter:
</p>
<code>
{{ '{% include "includes/_navbar.html" with avatar_url=user.avatar_url %}' }}
</code>
<p>
Templates do have access to the parent template context, but it can be useful to have template parameters
(e.g. using a template in a for loop and passing each element as a parameter).
</p>
<p>
You read more about the <a href="http://www.dmulholl.com/docs/ibis/master/tags.html#include">include tag</a>
feature in the template engine documentation.
</p>
</div>
34 changes: 34 additions & 0 deletions examples/views/includes/includes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pyview import LiveView
from dataclasses import dataclass, field
import random


@dataclass
class User:
user_id: int = field(default_factory=lambda: random.randint(1, 100))

@property
def avatar_url(self):
return f"https://avatar.iran.liara.run/public/{self.user_id}"


@dataclass
class IncludesContext:
user: User = field(default_factory=User)
pages: list[str] = field(default_factory=lambda: ["home", "about", "contact"])
current_page: str = "home"


class IncludesLiveView(LiveView):
"""
Template Includes
This example shows how to include templates in other templates.
"""

async def mount(self, socket, session):
socket.context = IncludesContext()

async def handle_params(self, url, params, socket):
if "page" in params:
socket.context.current_page = params["page"][0]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ packages = [
{ include = "pyview" },
]

version = "0.0.21"
version = "0.0.22"
description = "LiveView in Python"
authors = ["Larry Ogrodnek <[email protected]>"]
license = "MIT"
Expand Down
19 changes: 16 additions & 3 deletions pyview/vendor/ibis/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ def tree_parts(self, context) -> PartsTree:
resp.add_static(child.token.text if child.token else "")
elif isinstance(child, PrintNode):
resp.add_dynamic(child.wrender(context))
elif isinstance(child, IncludeNode):
resp.add_dynamic(child.tree_parts(context))
else:
resp.add_dynamic(child.tree_parts(context))

Expand Down Expand Up @@ -624,17 +626,16 @@ def process_token(self, token):
else:
raise errors.TemplateSyntaxError("Malformed 'include' tag.", token)

def wrender(self, context):
def visit_node(self, context, visitor: NodeVisitor):
template_name = self.template_expr.eval(context)
if isinstance(template_name, str):
if ibis.loader:
template = ibis.loader(template_name)
context.push()
for name, expr in self.variables.items():
context[name] = expr.eval(context)
rendered = template.root_node.render(context)
visitor(context, template.root_node)
context.pop()
return rendered
else:
msg = f"No template loader has been specified. "
msg += f"A template loader is required by the 'include' tag in "
Expand All @@ -645,6 +646,18 @@ def wrender(self, context):
msg += f"The variable '{self.template_arg}' should evaluate to a string. "
msg += f"This variable has the value: {repr(template_name)}."
raise errors.TemplateRenderingError(msg, self.token)

def wrender(self, context):
output = []
self.visit_node(context, lambda ctx, node: output.append(node.render(ctx)))
return "".join(output)

def tree_parts(self, context) -> PartsTree:
output = []
def visitor(ctx, node):
output.append(node.tree_parts(ctx))
self.visit_node(context, visitor)
return output[0]


# ExtendsNodes implement template inheritance. They indicate that the current template inherits
Expand Down
22 changes: 22 additions & 0 deletions tests/vendor/ibis/test_template_include.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pyview.vendor.ibis import Template
import pyview.vendor.ibis as ibis
from pyview.vendor.ibis.loaders import DictLoader
import pytest

@pytest.fixture
def template_loader():
ibis.loader = DictLoader(
{
"header.html": "<p>{{gr}}, World!</p>",
}
)

def test_template_include(template_loader):
a = Template("<div>{% include 'header.html' with gr=greeting %}</div>")
d = {"greeting": "Hello"}

assert a.tree(d) == {
"s": ["<div>", "</div>"],
"0": {"s": ["<p>", ", World!</p>"], "0": "Hello"},
}
assert a.render(d) == "<div><p>Hello, World!</p></div>"

0 comments on commit e7eb2cd

Please sign in to comment.