Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use components in python #11

Merged
merged 2 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/assets/component-in-py.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/component-in-template.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ It is inspired by Rails [ViewComponent](https://viewcomponent.org/), which built

For more insights into the problem it addresses and its design philosophy, check out [this video by GitHub Staff member Joel Hawksley](https://youtu.be/QoetqsBCsbE?si=28PCFCD4N4CyfKY7&t=624)

## Use Component in Django Template

You can create components and use them in Django templates.

![Use Component in Django Template](.github/assets/component-in-template.png)

## Use Component in Python

Or you can create components and use them in pure Python code.

![Use Component in Python](.github/assets/component-in-py.png)

## Why use django-viewcomponent

### Single responsibility
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Topics
templates.md
context.md
namespace.md
use_components_in_python.md
preview.md
testing.md
articles.md
Expand Down
50 changes: 50 additions & 0 deletions docs/source/use_components_in_python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Use Components in Python

With django-viewcomponent, you can also create components and use them in pure Python code.

```python
class Div(component.Component):
template_name = "layout/div.html"
css_class = None

def __init__(self, *fields, dom_id=None, css_class=None):
self.fields = list(fields)
if self.css_class and css_class:
self.css_class += f" {css_class}"
elif css_class:
self.css_class = css_class
self.dom_id = dom_id

def get_context_data(self):
context = super().get_context_data()
self.fields_html = " ".join(
[
child_component.render_from_parent_context(context)
for child_component in self.fields
]
)
return context
```

This is a `Div` component, it will accept a list of child components and set them in `self.fields`

In `get_context_data`, it will pass `context` to each child component and render them to HTML using `render_from_parent_context` method.

Then in `layout/div.html`, the child components will be rendered using `{{ self.fields_html|safe }}`

You can find more examples in the `tests` folder.

With this approach, you can use components in Python code like this

```python
Layout(
Fieldset(
"Basic Info",
Field("first_name"),
Field("last_name"),
Field("password1"),
Field("password2"),
css_class="fieldset",
),
)
```
53 changes: 46 additions & 7 deletions src/django_viewcomponent/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Component:
template_name: ClassVar[Optional[str]] = None
template: ClassVar[Optional[str]] = None

# if pass HTML to the component without fill tags, it will be stored here
# if pass HTML to the component without calling slot fields, it will be stored here
# and you can get it using self.content
content = ""

Expand All @@ -32,18 +32,18 @@ class Component:
component_target_var = None

# the context of the component, generated by get_context_data
component_context: Dict["str", Any] = {}
component_context: Context = Context({})

# the context of the outer
outer_context: Dict["str", Any] = {}
outer_context: Context = Context({})

def __init__(self, *args, **kwargs):
pass

def __init_subclass__(cls, **kwargs):
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)

def get_context_data(self, **kwargs) -> Dict[str, Any]:
def get_context_data(self, **kwargs) -> Context:
# inspired by rails viewcomponent before_render method
# developers can add extra context data by overriding this method
self.outer_context["self"] = self
Expand Down Expand Up @@ -71,18 +71,57 @@ def get_template(self) -> Template:
f"Note: this attribute is not required if you are overriding the class's `get_template*()` methods."
)

def render(
def prepare_context(
self,
context_data: Union[Dict[str, Any], Context, None] = None,
) -> str:
) -> Context:
"""
Prepare the context data for rendering the component.

https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render
"""
context_data = context_data or {}
if isinstance(context_data, dict):
context = Context(context_data)
else:
context = context_data

return context

def render(
self,
context_data: Union[Dict[str, Any], Context, None] = None,
) -> str:
template = self.get_template()
return template.render(context)
return template.render(self.prepare_context(context_data))

def render_from_parent_context(self, parent_context=None):
"""
If developers build components in Python code, then slot fields can be ignored, this method
help simplify rendering the child components

Div(
Fieldset(
"Basic Info",
Field('first_name'),
Field('last_name'),
Field('email'),
css_class='fieldset',
),
Submit('Submit'),
dom_id="main",
)
"""
parent_context = parent_context or {}
# create isolated context for component
if isinstance(parent_context, Context):
copied_context = Context(parent_context.flatten())
else:
copied_context = Context(dict(parent_context))

self.outer_context = self.prepare_context(copied_context)
self.component_context = self.get_context_data()
return self.render(self.component_context)

def check_slot_fields(self):
# check required slot fields
Expand Down
7 changes: 7 additions & 0 deletions tests/templates/layout/button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<button
class="{{ self.field_classes }}"
type="{{ self.button_type }}"
{% if self.id %}id="{{ self.id }}"{% endif %}
>
{{ self.text_html }}
</button>
3 changes: 3 additions & 0 deletions tests/templates/layout/div.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div {% if self.dom_id %}id="{{ self.dom_id }}"{% endif %} {% if self.css_class %}class="{{ self.css_class }}"{% endif %} >
{{ self.fields_html|safe }}
</div>
43 changes: 43 additions & 0 deletions tests/test_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from tests.testapp.layout import HTML, Button, Div

from .utils import assert_select


class TestLayoutComponents:
def test_html(self):
html = HTML("{% if saved %}Data saved{% endif %}").render_from_parent_context(
{"saved": True}
)
assert "Data saved" in html

# step_field and step0 not defined
html = HTML(
'<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />'
).render_from_parent_context()
assert_select(html, "input")

def test_div(self):
html = Div(
Div(
HTML("Hello {{ value_1 }}"),
HTML("Hello {{ value_2 }}"),
css_class="wrapper",
),
dom_id="main",
).render_from_parent_context({"value_1": "world"})

assert_select(html, "div#main")
assert_select(html, "div.wrapper")
assert "Hello world" in html

def test_button(self):
html = Div(
Div(
Button("{{ value_1 }}", css_class="btn btn-primary"),
),
dom_id="main",
).render_from_parent_context({"value_1": "world"})

assert_select(html, "button.btn")
assert_select(html, "button[type=button]")
assert "world" in html
58 changes: 58 additions & 0 deletions tests/testapp/layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.template import Template

from django_viewcomponent import component


class Div(component.Component):
template_name = "layout/div.html"
css_class = None

def __init__(self, *fields, dom_id=None, css_class=None):
self.fields = list(fields)
if self.css_class and css_class:
self.css_class += f" {css_class}"
elif css_class:
self.css_class = css_class
self.dom_id = dom_id

def get_context_data(self):
context = super().get_context_data()
self.fields_html = " ".join(
[
child_component.render_from_parent_context(context)
for child_component in self.fields
]
)
return context


class HTML(component.Component):
def __init__(self, html, **kwargs):
self.html = html

def get_template(self) -> Template:
return Template(self.html)


class Button(component.Component):
template_name = "layout/button.html"
field_classes = "btn"
button_type = "button"

def __init__(self, text, dom_id=None, css_class=None, template=None):
self.text = text

if dom_id:
self.id = dom_id

self.attrs = {}

if css_class:
self.field_classes += f" {css_class}"

self.template_name = template or self.template_name

def get_context_data(self):
context = super().get_context_data()
self.text_html = Template(str(self.text)).render(context)
return context
Loading