Closed JuroOravec closed 1 month ago
A whole new API for defining components a slightly different way? I see where it comes from, but I don't like having two ways of doing the same thing. It adds mental load for people writing components, "which style do I use?" similar too how things work in the React community. React is an especially bad offender, with a community that's split between different standards, so code examples you find only can be in way too many different "styles".
PEP 20: There should be one-- and preferably only one --obvious way to do it.
Fair enough, my motivation was mostly because I thought I could trim a couple of lines per component, but actually not really. And this is like 20 lines of code, so I can use it just in my project.
There are a few points I want to discuss on the template caching and template-related API, but I'll continue that over at https://github.com/EmilStenstrom/django-components/issues/589.
Btw, just for completeness, if anyone comes across this thread and wants to implement a similar feature in their project, here's how I did it:
from typing import Any, Type, Protocol, Union
from django.template import Context, Template
from django_components import Component, ArgsType, KwargsType, SlotsType, DataType, SlotResult, register
class ComponentFn(Protocol):
def __call__(
self,
context: Context,
*args: Any,
**kwargs: Any,
) -> Union[SlotResult, Template]: ...
def component_func(fn: ComponentFn) -> Type[Component[ArgsType, KwargsType, SlotsType, DataType]]:
def get_context_data(self: Component, *args: Any, **kwargs: Any) -> Any:
self.template = lambda ctx: fn(ctx, *args, **kwargs)
# Dynamically create a Component subclass
# See https://stackoverflow.com/a/3915434/9788634
subclass = type(
fn.__name__, # type: ignore[attr-defined]
(Component, object),
{"get_context_data": get_context_data}
)
return subclass
# Example usage
@register("my_comp")
@component_func
def my_comp(context: Context, one: int, two: str, three: int = 1) -> Template:
# NOTE:
# - IF returns str or SafeString, assume it's rendered
# - IF returns Template or CachedTemplate, render it with given context
# VARIANT A: Let the rendering happing in the background
context["abc"] = one
return Template("""
{{ one }}
""")
# VARIANT B: Explicit rendering
context["abc"] = 123
return Template("""
{{ one }}
""").render(context)
Background
Another inspo from Vuetify (the UI component library) is that, altho it's built on Vue, it uses JSX for rendering components. Their reason for that was likely that it allows for multiple components to be defined in a single file, while still having proper syntax support for the rendered template.
This would also make my work a bit easier, as the Django templates could look more similar to the original.
Because currently, if I there is a template like this (VDivider):
Then I cannot succintly express
if (!slots.default) return divider;
. The only way I can do it is by putting the entirety of the content in a single if / else:Moreover, django-web-components also supports a React-style render function:
So I'd like to have something similar (I already have the demo).
API
The example from django-web-components doesn't include parameters, so I don't know if they allow that. But IMO I'd go with React style, where the component props are declared as function args/kwargs.
Input
However, there's two difference for Django:
Context
object that's passed around at render time. So IMO it should be available inside the render function too.Component
, and this function gets called insideget_context_data()
. So it could be also useful for people to be able to access the component itself.So taking these into the consideration, the input to the functional components could look like this:
So when it comes to the parameters, after the first two, the rest would be the same as defining parameters for
get_context_data
.Output
Here I'm considering between two approaches:
Return already-rendered content
In this case, the body and output would be the same as in django-web-components. So it would be up to the user to create and render a template. They would be expected to return
str | SafeString
.However, I don't like how it is then up to the user to manage template caching.
Return Template or template string.
On the other hand, we could ask users to return only the raw template or string thereof. And template caching would be automatically handled by django_components:
However, the issue with this latter approach is that if the user would want to add some variables to the context, there would have to be some workaround for that, e.g.:
Personally I like the 2nd approach more, I find it simpler / cleaner.
CachedTemplate
Idea for
CachedTemplate
comes from django-web-components. Basically it's a stand-in for a regularTemplate
class, with the distinction that those Django templates that are defined throughCachedTemplates
are - you guessed it - cached! In the same cache as configured by thetemplate_cache_size
settings.