Initial commit
This commit is contained in:
1
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/INSTALLER
vendored
Normal file
1
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/INSTALLER
vendored
Normal file
@@ -0,0 +1 @@
|
||||
pip
|
||||
28
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/LICENSE
vendored
Normal file
28
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/LICENSE
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, Tibor Leupold
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
941
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/METADATA
vendored
Normal file
941
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/METADATA
vendored
Normal file
@@ -0,0 +1,941 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: laces
|
||||
Version: 0.1.1
|
||||
Summary: Django components that know how to render themselves.
|
||||
Author-email: Tibor Leupold <tibor@lpld.io>
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
Classifier: Development Status :: 3 - Alpha
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Framework :: Django
|
||||
Classifier: Framework :: Django :: 3.2
|
||||
Classifier: Framework :: Django :: 4.0
|
||||
Classifier: Framework :: Django :: 4.1
|
||||
Classifier: Framework :: Django :: 4.2
|
||||
Classifier: Framework :: Django :: 5.0
|
||||
Requires-Dist: Django>=3.2
|
||||
Requires-Dist: tox==4.12.1 ; extra == "dev"
|
||||
Requires-Dist: tox-gh-actions==3.2.0 ; extra == "dev"
|
||||
Requires-Dist: virtualenv-pyenv==0.4.0 ; extra == "dev"
|
||||
Requires-Dist: coverage==7.3.4 ; extra == "dev"
|
||||
Requires-Dist: pre-commit==3.4.0 ; extra == "dev"
|
||||
Requires-Dist: black==24.1.1 ; extra == "dev"
|
||||
Requires-Dist: blacken-docs==1.16.0 ; extra == "dev"
|
||||
Requires-Dist: isort==5.13.2 ; extra == "dev"
|
||||
Requires-Dist: flake8==7.0.0 ; extra == "dev"
|
||||
Requires-Dist: flake8-bugbear ; extra == "dev"
|
||||
Requires-Dist: flake8-comprehensions ; extra == "dev"
|
||||
Requires-Dist: mypy==1.7.1 ; extra == "dev"
|
||||
Requires-Dist: django-stubs[compatible-mypy]==4.2.7 ; extra == "dev"
|
||||
Requires-Dist: types-requests==2.31.0.20240125 ; extra == "dev"
|
||||
Requires-Dist: dj-database-url==2.1.0 ; extra == "testing"
|
||||
Requires-Dist: coverage==7.3.4 ; extra == "testing"
|
||||
Project-URL: Home, https://github.com/tbrlpld/laces
|
||||
Provides-Extra: dev
|
||||
Provides-Extra: testing
|
||||
|
||||
# Laces
|
||||
|
||||
[](https://opensource.org/licenses/BSD-3-Clause)
|
||||
[](https://badge.fury.io/py/laces)
|
||||
[](https://github.com/tbrlpld/laces/actions/workflows/test.yml)
|
||||
[](https://codecov.io/gh/tbrlpld/laces)
|
||||
|
||||
---
|
||||
|
||||
Django components that know how to render themselves.
|
||||
|
||||
Laces components provide a simple way to combine data (in the form of Python objects) with the Django templates that are meant to render that data.
|
||||
The components can then be simply rendered in any other template using the `{% component %}` template tag.
|
||||
That parent template does not need to know anything about the component's template or data.
|
||||
No need to receive, filter, restructure or pass any data to the component's template.
|
||||
Just let the component render itself.
|
||||
|
||||
Template and data are tied together (sorry, not sorry 😅) in the component, and they can be passed around together.
|
||||
This becomes especially useful when components are nested — it allows us to avoid building the same nested structure twice (once in the data and again in the templates).
|
||||
|
||||
Working with objects that know how to render themselves as HTML elements is a common pattern found in complex Django applications, such as the [Wagtail](https://github.com/wagtail/wagtail) admin interface.
|
||||
The Wagtail admin is also where the APIs provided in this package have previously been discovered, developed and solidified.
|
||||
The purpose of this package is to make these tools available to other Django projects outside the Wagtail ecosystem.
|
||||
|
||||
## Links
|
||||
|
||||
- [Getting started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Creating components](#creating-components)
|
||||
- [Passing context to the component template](#passing-context-to-the-component-template)
|
||||
- [Using components in other templates](#using-components-in-other-templates)
|
||||
- [Adding JavaScript and CSS assets to a component](#adding-javascript-and-css-assets-to-a-component)
|
||||
- [Patterns for using components](#patterns-for-using-components)
|
||||
- [Nesting components](#nesting-components)
|
||||
- [Nested groups of components](#nested-groups-of-components)
|
||||
- [Container components](#container-components)
|
||||
- [Using dataclasses](#using-dataclasses)
|
||||
- [About Laces and components](#about-laces-and-components)
|
||||
- [Contributing](#contributing)
|
||||
- [Changelog](https://github.com/tbrlpld/laces/blob/main/CHANGELOG.md)
|
||||
- [Discussions](https://github.com/tbrlpld/laces/discussions)
|
||||
- [Security](https://github.com/tbrlpld/laces/security)
|
||||
|
||||
## Getting started
|
||||
|
||||
### Installation
|
||||
|
||||
First, install with pip:
|
||||
```sh
|
||||
$ python -m pip install laces
|
||||
```
|
||||
|
||||
Then, add to your installed apps:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
|
||||
INSTALLED_APPS = ["laces", ...]
|
||||
```
|
||||
|
||||
That's it.
|
||||
|
||||
### Creating components
|
||||
|
||||
The simplest way to create a component is to define a subclass of `laces.components.Component` and specify a `template_name` attribute on it.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
```
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/components/welcome.html #}
|
||||
|
||||
<h1>Hello World!</h1>
|
||||
```
|
||||
|
||||
With the above in place, you then instantiate the component (e.g., in a view) and pass it to another template for rendering.
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from my_app.components import WelcomePanel
|
||||
|
||||
|
||||
def home(request):
|
||||
welcome = WelcomePanel() # <-- Instantiates the component
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{"welcome": welcome}, # <-- Passes the component to the view template
|
||||
)
|
||||
```
|
||||
|
||||
In the view template, we `load` the `laces` tag library and use the `{% component %}` tag to render the component.
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/home.html #}
|
||||
|
||||
{% load laces %}
|
||||
{% component welcome %} {# <-- Renders the component #}
|
||||
```
|
||||
|
||||
That's it!
|
||||
The component's template will be rendered right there in the view template.
|
||||
|
||||
Of course, this is a very simple example and not much more useful than using a simple `include`.
|
||||
We will go into some more useful use cases below.
|
||||
|
||||
### Without a template
|
||||
|
||||
Before we dig deeper into the component use cases, just a quick note that components don't have to have a template.
|
||||
For simple cases that don't require a template, the `render_html` method can be overridden instead.
|
||||
If the return value contains HTML, it should be marked as safe using `django.utils.html.format_html` or `django.utils.safestring.mark_safe`.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from django.utils.html import format_html
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
def render_html(self, parent_context):
|
||||
return format_html("<h1>Hello World!</h1>")
|
||||
```
|
||||
|
||||
### Passing context to the component template
|
||||
|
||||
Now back to components with templates.
|
||||
|
||||
The example shown above with the static welcome message in the template is, of course, not very useful.
|
||||
It seems more like an overcomplicated way to replace a simple `include`.
|
||||
|
||||
But, we rarely ever want to render templates with static content.
|
||||
Usually, we want to pass some context variables to the template to be rendered.
|
||||
This is where components start to become interesting.
|
||||
|
||||
The default implementation of `render_html` calls the component's `get_context_data` method to get the context variables to pass to the template.
|
||||
The default implementation of `get_context_data` returns an empty dictionary.
|
||||
To customize the context variables passed to the template, we can override `get_context_data`.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"name": "Alice"}
|
||||
```
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/components/welcome.html #}
|
||||
|
||||
<h1>Hello {{ name }}</h1>
|
||||
```
|
||||
|
||||
With the above we are now rendering a welcome message with the name coming from the component's `get_context_data` method.
|
||||
Nice.
|
||||
But, still not very useful, as the name is still hardcoded — in the component method instead of the template, but hardcoded nonetheless.
|
||||
|
||||
#### Using class properties
|
||||
|
||||
When considering how to make the context of our components more useful, it's helpful to remember that components are just normal Python classes and objects.
|
||||
So, you are basically free to get the context data into the component in any way you like.
|
||||
|
||||
For example, we can pass arguments to the constructor and use them in the component's methods, like `get_context_data`.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"name": self.name}
|
||||
```
|
||||
|
||||
Nice, this is getting better.
|
||||
Now we can pass the name to the component when we instantiate it and pass the component ready to be rendered to the view template.
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from my_app.components import WelcomePanel
|
||||
|
||||
|
||||
def home(request):
|
||||
welcome = WelcomePanel(name="Alice")
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{"welcome": welcome},
|
||||
)
|
||||
```
|
||||
|
||||
So, as mentioned before, we can use the full power of Python classes and objects to provide context data to our components.
|
||||
A couple more examples of how components can be used can be found [below](#patterns-for-using-components).
|
||||
|
||||
#### Using the parent context
|
||||
|
||||
You may have noticed in the above examples that the `render_html` and `get_context_data` methods take a `parent_context` argument.
|
||||
This is the context of the template that is calling the component.
|
||||
The `parent_context` is passed into the `render_html` method by the `{% component %}` template tag.
|
||||
In the default implementation of the `render_html` method, the `parent_context` is then passed to the `get_context_data` method.
|
||||
The default implementation of the `get_context_data` method, however, ignores the `parent_context` argument and returns an empty dictionary.
|
||||
To make use of it, you will have to override the `get_context_data` method.
|
||||
|
||||
Relying on data from the parent context somewhat forgoes some of the benefits of components, which is tying the data and template together.
|
||||
Especially for nested uses of components, you now require that the data in the right format is passed through all layers of templates again.
|
||||
It is usually cleaner to provide all the data needed by the component directly to the component itself.
|
||||
|
||||
However, there may be cases where this is not possible or desirable.
|
||||
For those cases, you have access to the parent context in the component's `get_context_data` method.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"name": parent_context["request"].user.first_name}
|
||||
```
|
||||
|
||||
(Of course, this could have also been achieved by passing the request or user object to the component in the view, but this is just an example.)
|
||||
|
||||
### Using components in other templates
|
||||
|
||||
As mentioned in the [first example](#creating-components), components are rendered in other templates using the `{% component %}` tag from the `laces` tag library.
|
||||
|
||||
Here is that example from above again, in which the view passes an instance of `WelcomePanel` to the context of `my_app/home.html`.
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from my_app.components import WelcomePanel
|
||||
|
||||
|
||||
def home(request):
|
||||
welcome = WelcomePanel()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{
|
||||
"welcome": welcome,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Then, in the `my_app/templates/my_app/home.html` template we render the welcome panel component as follows:
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/home.html #}
|
||||
|
||||
{% load laces %}
|
||||
{% component welcome %}
|
||||
```
|
||||
|
||||
This is the basic usage of components and should cover most cases.
|
||||
|
||||
However, the `{% component %}` tag also supports some additional features.
|
||||
Specifically, the keywords `with`, `only` and `as` are supported, similar to how they work with the [`{% include %}`](https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#std-templatetag-include) tag.
|
||||
|
||||
#### Provide additional parent context variables with `with`
|
||||
|
||||
You can pass additional parent context variables to the component using the keyword `with`:
|
||||
|
||||
```html+django
|
||||
{% component welcome with name=request.user.first_name %}
|
||||
```
|
||||
|
||||
**Note**: These extra variables will be added to the `parent_context` which is passed to the component's `render_html` and `get_context_data` methods.
|
||||
The default implementation of `get_context_data` ignores the `parent_context` argument, so you will have to override it to make use of the extra variables.
|
||||
For more information see the above section on the [parent context](#using-the-parent-context).
|
||||
|
||||
#### Limit the parent context variables with `only`
|
||||
|
||||
To limit the parent context variables passed to the component to only those variables provided by the `with` keyword (and no others from the calling template's context), use `only`:
|
||||
|
||||
```html+django
|
||||
{% component welcome with name=request.user.first_name only %}
|
||||
```
|
||||
|
||||
**Note**: Both, `with` and `only`, only affect the `parent_context` which is passed to the component's `render_html` and `get_context_data` methods. They do not have any direct effect on actual context that is passed to the component's template. E.g. if the component's `get_context_data` method returns a dictionary which always contains a key `foo`, then that key will be available in the component's template, regardless of whether `only` was used or not.
|
||||
|
||||
#### Store the rendered output in a variable with `as`
|
||||
|
||||
To store the component's rendered output in a variable rather than outputting it immediately, use `as` followed by the variable name:
|
||||
|
||||
```html+django
|
||||
{% component welcome as welcome_html %}
|
||||
|
||||
{{ welcome_html }}
|
||||
```
|
||||
|
||||
### Adding JavaScript and CSS assets to a component
|
||||
|
||||
Like Django form widgets, components can specify associated JavaScript and CSS assets.
|
||||
The assets for a component can be specified in the same way that [Django form assets are defined](https://docs.djangoproject.com/en/5.0/topics/forms/media).
|
||||
This can be achieved using either an inner `Media` class or a dynamic `media` property.
|
||||
|
||||
An inner `Media` class definition looks like this:
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
class Media:
|
||||
css = {"all": ("my_app/css/welcome-panel.css",)}
|
||||
```
|
||||
|
||||
The more dynamic definition via a `media` property looks like this:
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from django.forms import Media
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
return Media(css={"all": ("my_app/css/welcome-panel.css",)})
|
||||
```
|
||||
|
||||
**Note**:
|
||||
It is your template's responsibility to output any media declarations defined on the components.
|
||||
|
||||
#### Outputting component media in templates
|
||||
|
||||
Once you have defined the assets on the component in one of the two ways above, you can output them in your templates.
|
||||
This, again, works in the same way as it does for Django form widgets.
|
||||
The component instance will have a `media` property which returns an instance of the `django.forms.Media` class.
|
||||
This is the case, even if you used the nested `Media` class to define the assets.
|
||||
The [string representation of a `Media` objects](https://docs.djangoproject.com/en/5.0/topics/forms/media#s-media-objects) are the HTML declarations to include the assets.
|
||||
|
||||
In the example home template from above, we can output the component's media declarations like so:
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/home.html #}
|
||||
|
||||
{% load laces %}
|
||||
|
||||
<head>
|
||||
{{ welcome.media }}
|
||||
<head>
|
||||
<body>
|
||||
{% component welcome %}
|
||||
</body>
|
||||
```
|
||||
|
||||
#### Combining media with `MediaContainer`
|
||||
|
||||
When you have many components in a page, it can be cumbersome to output the media declarations for each component individually.
|
||||
To make that process a bit easier, Laces provides a `MediaContainer` class.
|
||||
The `MediaContainer` class is a subclass of Python's built-in `list` class which combines the `media` of all it's members.
|
||||
|
||||
In a view we can create a `MediaContainer` instance containing several media-defining components and pass it to the view template.
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
from laces.components import MediaContainer
|
||||
|
||||
from my_app.components import (
|
||||
Dashboard,
|
||||
Footer,
|
||||
Header,
|
||||
Sidebar,
|
||||
WelcomePanel,
|
||||
)
|
||||
|
||||
|
||||
def home(request):
|
||||
components = MediaContainer(
|
||||
[
|
||||
Header(),
|
||||
Sidebar(),
|
||||
WelcomePanel(),
|
||||
Dashboard(),
|
||||
Footer(),
|
||||
]
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{
|
||||
"components": components,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Then, in the view template, we can output the media declarations for all components in the container at once.
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/home.html #}
|
||||
|
||||
{% load laces %}
|
||||
|
||||
<head>
|
||||
{{ components.media }}
|
||||
<head>
|
||||
<body>
|
||||
{% for component in components %}
|
||||
{% component component %}
|
||||
{% endfor %}
|
||||
</body>
|
||||
```
|
||||
|
||||
This will output a combined media declaration for all components in the container.
|
||||
The combination of the media declarations follows the behaviour outlined in the [Django documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#combining-media-objects).
|
||||
|
||||
**Note**:
|
||||
The use of `MediaContainer` is not limited to contain components.
|
||||
It can be used to combine the `media` properties of any kind of objects that have a `media` property.
|
||||
|
||||
## Patterns for using components
|
||||
|
||||
Below, we want to show a few more examples of how components can be used that were not covered in the ["Getting started" section](#getting-started) above.
|
||||
|
||||
### Nesting components
|
||||
|
||||
The combination of data and template that components provide becomes especially useful when components are nested.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component): ...
|
||||
|
||||
|
||||
class Dashboard(Component):
|
||||
template_name = "my_app/components/dashboard.html"
|
||||
|
||||
def __init__(self, user):
|
||||
self.welcome = WelcomePanel(name=user.first_name)
|
||||
...
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"welcome": self.welcome}
|
||||
```
|
||||
|
||||
The template of the "parent" component does not need to know anything about the "child" component, except for which template variable is a component.
|
||||
The child component already contains the data it needs and knows which template to use to render that data.
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/components/dashboard.html #}
|
||||
|
||||
{% load laces %}
|
||||
|
||||
<div class="dashboard">
|
||||
{% component welcome %}
|
||||
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
The nesting also provides us with a nice data structure we can test.
|
||||
|
||||
```python
|
||||
dashboard = Dashboard(user=request.user)
|
||||
|
||||
assert dashboard.welcome.name == request.user.first_name
|
||||
```
|
||||
|
||||
### Nested groups of components
|
||||
|
||||
The nesting of components is not limited to single instances.
|
||||
We can also nest groups of components.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class WelcomePanel(Component): ...
|
||||
|
||||
|
||||
class UsagePanel(Component): ...
|
||||
|
||||
|
||||
class TeamPanel(Component): ...
|
||||
|
||||
|
||||
class Dashboard(Component):
|
||||
template_name = "my_app/components/dashboard.html"
|
||||
|
||||
def __init__(self, user):
|
||||
self.panels = [
|
||||
WelcomePanel(name=user.first_name),
|
||||
UsagePanel(user=user),
|
||||
TeamPanel(groups=user.groups.all()),
|
||||
]
|
||||
...
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"panels": self.panels}
|
||||
```
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/components/dashboard.html #}
|
||||
|
||||
{% load laces %}
|
||||
|
||||
<div class="dashboard">
|
||||
{% for panel in panels %}
|
||||
{% component panel %}
|
||||
{% endfor %}
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### Container components
|
||||
|
||||
The [above example](#nested-groups-of-components) is relatively static.
|
||||
The `Dashboard` component always contains the same panels.
|
||||
|
||||
You could also imagine passing the child components in through the constructor.
|
||||
This would make your component into a dynamic container component.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
class Section(Component):
|
||||
template_name = "my_app/components/section.html"
|
||||
|
||||
def __init__(self, children: list[Component]):
|
||||
self.children = children
|
||||
...
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return {"children": self.children}
|
||||
|
||||
|
||||
class Heading(Component): ...
|
||||
|
||||
|
||||
class Paragraph(Component): ...
|
||||
|
||||
|
||||
class Image(Component): ...
|
||||
```
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/components/section.html #}
|
||||
|
||||
{% load laces %}
|
||||
<section>
|
||||
{% for child in children %}
|
||||
{% component child %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
```
|
||||
|
||||
The above `Section` component can take any kind of component as children.
|
||||
The only thing that `Section` requires is that the children can be rendered with the `{% component %}` tag (which all components do).
|
||||
|
||||
In the view, we can now instantiate the `Section` component with any children we want.
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from my_app.components import (
|
||||
Heading,
|
||||
Image,
|
||||
Paragraph,
|
||||
Section,
|
||||
)
|
||||
|
||||
|
||||
def home(request):
|
||||
content = Section(
|
||||
children=[
|
||||
Heading(...),
|
||||
Paragraph(...),
|
||||
Image(...),
|
||||
]
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{"content": content},
|
||||
)
|
||||
```
|
||||
|
||||
```html+django
|
||||
{# my_app/templates/my_app/home.html #}
|
||||
|
||||
{% load laces %}
|
||||
|
||||
<body>
|
||||
{% component content %}
|
||||
...
|
||||
</body>
|
||||
```
|
||||
|
||||
### Using dataclasses
|
||||
|
||||
Above, we showed how to [use class properties](#using-class-properties) to add data to the component's context.
|
||||
This is a very useful and common pattern.
|
||||
However, it is a bit verbose, especially when you have many properties and directly pass the properties to the template context.
|
||||
|
||||
To make this a little more convenient, we can use [`dataclasses`](https://docs.python.org/3.12/library/dataclasses.html#module-dataclasses).
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
@dataclass
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
name: str
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return asdict(self)
|
||||
```
|
||||
|
||||
With dataclasses we define the name and type of the properties we want to pass to the component in the class definition.
|
||||
Then, we can use the `asdict` function to convert the dataclass instance to a dictionary that can be directly as the template context.
|
||||
|
||||
The `asdict` function only adds keys to the dictionary that were defined as the properties defined in the dataclass.
|
||||
In the above example, the dictionary returned by `asdict` would only contain the `name` key.
|
||||
It would not contain the `template_name` key, because that is set on the class with a value but without a type annotation.
|
||||
If you were to add the type annotation, then the `template_name` key would also be included in the dictionary returned by `asdict`.
|
||||
|
||||
### Custom constructor methods
|
||||
|
||||
When a component has many properties, it can be a pain to pass each property to the constructor individually.
|
||||
This is especially true when the component is used in many places and the data preparation would need to be repeated in each use case.
|
||||
Custom constructor methods can help with that.
|
||||
|
||||
In case of our `WelcomePanel` example, we might want to show some more user information, including a profile image and link to the user's profile page.
|
||||
We can add a `classmethod` that takes the user object and returns an instance of the component with all the data needed to render the component.
|
||||
We can also use this method to encapsulate the logic for generating additional data, such as the profile URL.
|
||||
|
||||
```python
|
||||
# my_app/components.py
|
||||
|
||||
from django import urls
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from laces.components import Component
|
||||
|
||||
|
||||
@dataclass
|
||||
class WelcomePanel(Component):
|
||||
template_name = "my_app/components/welcome.html"
|
||||
|
||||
first_name: str
|
||||
last_name: str
|
||||
profile_url: str
|
||||
profile_image_url: str
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user):
|
||||
profile_url = urls.reverse("profile", kwargs={"pk": user.pk})
|
||||
return cls(
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
profile_url=profile_url,
|
||||
profile_image_url=user.profile.image.url,
|
||||
)
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
return asdict(self)
|
||||
```
|
||||
|
||||
Now, we can instantiate the component in the view like so:
|
||||
|
||||
```python
|
||||
# my_app/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from my_app.components import WelcomePanel
|
||||
|
||||
|
||||
def home(request):
|
||||
welcome = WelcomePanel.from_user(request.user)
|
||||
return render(
|
||||
request,
|
||||
"my_app/home.html",
|
||||
{"welcome": welcome},
|
||||
)
|
||||
```
|
||||
|
||||
The constructor method allows us to keep our view very simple and clean as all the data preparation is encapsulated in the component.
|
||||
|
||||
As in the example above, custom constructor methods pair very well with the use of dataclasses, but they can of course also be used without them.
|
||||
|
||||
## About Laces and components
|
||||
|
||||
### Why "Laces"?
|
||||
|
||||
"Laces" is somewhat of a reference to the feature of tying data and templates together.
|
||||
The components are also "self-rendering," which could be seen as "self-reliance," which relates to "bootstrapping."
|
||||
And aren't "bootstraps" just a long kind of "(shoe)laces?"
|
||||
|
||||
Finally, it is a nod to [@mixxorz](https://github.com/mixxorz)'s fantastic [Slippers package](https://github.com/mixxorz/slippers), which also takes a component focused approach to improve the experience when working with Django templates, but in a quite different way.
|
||||
|
||||
### Supported versions
|
||||
|
||||
- Python >= 3.8
|
||||
- Django >= 3.2
|
||||
|
||||
## Contributing
|
||||
|
||||
### Install
|
||||
|
||||
To make changes to this project, first clone this repository:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/tbrlpld/laces.git
|
||||
$ cd laces
|
||||
```
|
||||
|
||||
With your preferred virtualenv activated, install the development dependencies:
|
||||
|
||||
#### Using pip
|
||||
|
||||
```sh
|
||||
$ python -m pip install --upgrade pip>=21.3
|
||||
$ python -m pip install -e '.[dev]' -U
|
||||
```
|
||||
|
||||
#### Using flit
|
||||
|
||||
```sh
|
||||
$ python -m pip install flit
|
||||
$ flit install
|
||||
```
|
||||
|
||||
### pre-commit
|
||||
|
||||
Note that this project uses [pre-commit](https://github.com/pre-commit/pre-commit).
|
||||
It is included in the project testing requirements. To set up locally:
|
||||
|
||||
```shell
|
||||
# initialize pre-commit
|
||||
$ pre-commit install
|
||||
|
||||
# Optional, run all checks once for this, then the checks will run only on the changed files
|
||||
$ git ls-files --others --cached --exclude-standard | xargs pre-commit run --files
|
||||
```
|
||||
|
||||
### How to run tests
|
||||
|
||||
Now you can run all tests like so:
|
||||
|
||||
```sh
|
||||
$ tox
|
||||
```
|
||||
|
||||
Or, you can run them for a specific environment:
|
||||
|
||||
```sh
|
||||
$ tox -e python3.11-django4.2
|
||||
```
|
||||
|
||||
Or, run only a specific test:
|
||||
|
||||
```sh
|
||||
$ tox -e python3.11-django4.2 laces.tests.test_file.TestClass.test_method
|
||||
```
|
||||
|
||||
To run the test app interactively, use:
|
||||
|
||||
```sh
|
||||
$ tox -e interactive
|
||||
```
|
||||
|
||||
You can now visit `http://localhost:8020/`.
|
||||
|
||||
#### Testing with coverage
|
||||
|
||||
`tox` is configured to run tests with coverage.
|
||||
The coverage report is combined for all environments.
|
||||
This is done by using the `--append` flag when running coverage in `tox`.
|
||||
This means it will also include previous results.
|
||||
|
||||
You can see the coverage report by running:
|
||||
|
||||
```sh
|
||||
$ coverage report
|
||||
```
|
||||
|
||||
To get a clean report, you can run `coverage erase` before running `tox`.
|
||||
|
||||
#### Running tests without `tox`
|
||||
|
||||
If you want to run tests without `tox`, you can use the `testmanage.py` script.
|
||||
This script is a wrapper around Django's `manage.py` and will run tests with the correct settings.
|
||||
|
||||
To make this work, you need to have the `testing` dependencies installed.
|
||||
|
||||
```sh
|
||||
$ python -m pip install -e '.[testing]' -U
|
||||
```
|
||||
|
||||
Then you can run tests with:
|
||||
|
||||
```sh
|
||||
$ ./testmanage.py test
|
||||
````
|
||||
|
||||
To run tests with coverage, use:
|
||||
|
||||
```sh
|
||||
$ coverage run ./testmanage.py test
|
||||
```
|
||||
|
||||
### Python version management
|
||||
|
||||
Tox will attempt to find installed Python versions on your machine.
|
||||
|
||||
If you use `pyenv` to manage multiple versions, you can tell `tox` to use those versions.
|
||||
To ensure that `tox` will find Python versions installed with `pyenv` you need [`virtualenv-pyenv`](https://pypi.org/project/virtualenv-pyenv/) (note: this is not `pyenv-virtualenv`).
|
||||
`virtualenv-pyenv` is part of the development dependencies (just like `tox` itself).
|
||||
Additionally, you have to set the environment variable `VIRTUALENV_DISCOVERY=pyenv`.
|
||||
|
||||
### Publishing
|
||||
|
||||
This project uses the [Trusted Publisher model for PyPI releases](https://docs.pypi.org/trusted-publishers/).
|
||||
This means that publishing is done through GitHub Actions when a [new release is created on GitHub](https://github.com/tbrlpld/laces/releases/new).
|
||||
|
||||
To create a release, you need a Git tag.
|
||||
The tag can either be created on the command line and pushed or in the "create release" interface on GitHub.
|
||||
The tag name should be the version number prefixed with a `v` (e.g. `v0.1.0`).
|
||||
|
||||
Before publishing a new release, make sure to update
|
||||
|
||||
- the changelog in `CHANGELOG.md`, and
|
||||
- the version number in `laces/__init__.py`.
|
||||
|
||||
To manually test publishing the package, you can use `flit`.
|
||||
Be sure to configure the `testpypi` repository in your `~/.pypirc` file according to the Flit [documentation](https://flit.pypa.io/en/stable/upload.html#controlling-package-uploads).
|
||||
If your PyPI account is using 2FA, you'll need to create a [PyPI API token](https://test.pypi.org/help/#apitoken) and use that as your password and `__token__` as the username.
|
||||
|
||||
When you're ready to test the publishing, run:
|
||||
|
||||
```shell
|
||||
$ flit build
|
||||
$ flit publish --repository testpypi
|
||||
```
|
||||
|
||||
30
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/RECORD
vendored
Normal file
30
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/RECORD
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
laces-0.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
laces-0.1.1.dist-info/LICENSE,sha256=CbPYGqv5GGCHyw--Pf0l6JxHeZpKfVOG4jlFO93tWx8,1500
|
||||
laces-0.1.1.dist-info/METADATA,sha256=aGg_GwTkrtdpIn_EdC8WUfycT3koZjdLnTdaP2z4YEc,30152
|
||||
laces-0.1.1.dist-info/RECORD,,
|
||||
laces-0.1.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
||||
laces/__init__.py,sha256=mQjmpyBUamXG8IKFjQ-ykXuygVvV-riKtK0wGg_zPxo,62
|
||||
laces/__pycache__/__init__.cpython-310.pyc,,
|
||||
laces/__pycache__/apps.cpython-310.pyc,,
|
||||
laces/__pycache__/components.cpython-310.pyc,,
|
||||
laces/__pycache__/typing.cpython-310.pyc,,
|
||||
laces/apps.py,sha256=80KCVR0eddL9pu3qLVVOqoYHuA9q6FWn4hxWFLSEY5k,135
|
||||
laces/components.py,sha256=9tRddZICFXA6qQelfCxcnHkNN2E-jL8XHSIvUuPu8uE,3825
|
||||
laces/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
laces/migrations/__pycache__/__init__.cpython-310.pyc,,
|
||||
laces/templates/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
laces/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
laces/templatetags/__pycache__/__init__.cpython-310.pyc,,
|
||||
laces/templatetags/__pycache__/laces.cpython-310.pyc,,
|
||||
laces/templatetags/laces.py,sha256=qKuk1YBxFflHyNZDR_aF7G6rWHpMhUUYRANMJdnuEco,5443
|
||||
laces/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
laces/tests/__pycache__/__init__.cpython-310.pyc,,
|
||||
laces/tests/__pycache__/test_components.cpython-310.pyc,,
|
||||
laces/tests/__pycache__/utils.cpython-310.pyc,,
|
||||
laces/tests/test_components.py,sha256=tNLClgH52P0XB7Pgo7Sd1c79dau28vFCJ96bnVDmaSs,10274
|
||||
laces/tests/test_templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
laces/tests/test_templatetags/__pycache__/__init__.cpython-310.pyc,,
|
||||
laces/tests/test_templatetags/__pycache__/test_laces.cpython-310.pyc,,
|
||||
laces/tests/test_templatetags/test_laces.py,sha256=UMLGTQfvCSJtWUPO7gL0PKtjg4QPZnnXgHcAHeYHSA8,15918
|
||||
laces/tests/utils.py,sha256=vMVE0rJSsfQaqOQ5slDJKhi-Pqvijg0tr7XmPSndmF0,770
|
||||
laces/typing.py,sha256=pwnCVasfgVID6sqr1UENuuOSDG2hvjuBQyNJmpRLnJI,760
|
||||
4
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/WHEEL
vendored
Normal file
4
env/lib/python3.10/site-packages/laces-0.1.1.dist-info/WHEEL
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.9.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
Reference in New Issue
Block a user