diff --git a/README.md b/README.md index 209f9400..7e097468 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Read on to learn about the details! ## Release notes +🚨📢 **Version 0.81** Aligned the `render_to_response` method with the (now public) `render` method of `Component` class. Moreover, slots passed to these can now be rendered also as functions. + +- BREAKING CHANGE: The order of arguments to `render_to_response` has changed. + **Version 0.80** introduces dependency injection with the `{% provide %}` tag and `inject()` method. 🚨📢 **Version 0.79** @@ -313,7 +317,7 @@ This makes it easy to create small components without having to create a separat Note that the `t.django_html`, `t.css`, and `t.js` types are used to specify the type of the template, CSS, and JS files, respectively. This is not necessary, but if you're using VSCode with the [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS. -## Use the component in a template +## Use components in templates First load the `component_tags` tag library, then use the `component_[js/css]_dependencies` and `component` tags to render the component to the page. @@ -358,13 +362,143 @@ The output from the above template will be: This makes it possible to organize your front-end around reusable components. Instead of relying on template tags and keeping your CSS and Javascript in the static directory. +## Use components outside of templates + +_New in version 0.81_ + +Components can be rendered outside of Django templates, calling them as regular functions ("React-style"). + +The component class defines `render` and `render_to_response` class methods. These methods accept positional args, kwargs, and slots, offering the same flexibility as the `{% component %}` tag: + +```py +class SimpleComponent(component.Component): + template = """ + {% load component_tags %} + hello: {{ hello }} + foo: {{ foo }} + kwargs: {{ kwargs|safe }} + slot_first: {% slot "first" required %}{% endslot %} + """ + + def get_context_data(self, arg1, arg2, **kwargs): + return { + "hello": arg1, + "foo": arg2, + "kwargs": kwargs, + } + +rendered = SimpleComponent.render( + args=["world", "bar"], + kwargs={"kw1": "test", "kw2": "ooo"}, + slots={"first": "FIRST_SLOT"}, + context={"from_context": 98}, +) +``` + +Renders: + +``` +hello: world +foo: bar +kwargs: {'kw1': 'test', 'kw2': 'ooo'} +slot_first: FIRST_SLOT +``` + +### Inputs of `render` and `render_to_response` + +Both `render` and `render_to_response` accept the same input: + +```py +Component.render( + context: Mapping | django.template.Context | None = None, + args: List[Any] | None = None, + kwargs: Dict[str, Any] | None = None, + slots: Dict[str, str | SafeString | SlotRenderFunc] | None = None, + escape_slots_content: bool = True +) -> str: +``` + +- _`args`_ - Positional args for the component. This is the same as calling the component + as `{% component "my_comp" arg1 arg2 ... %}` + +- _`kwargs`_ - Keyword args for the component. This is the same as calling the component + as `{% component "my_comp" key1=val1 key2=val2 ... %}` + +- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component. + Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string + or [`SlotRenderFunc`](#slotrenderfunc). + +- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input. + +- _`context`_ - A context (dictionary or Django's Context) within which the component + is rendered. The keys on the context can be accessed from within the template. + - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via + component's args and kwargs. + +#### `SlotRenderFunc` + +When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function. + +The function has following signature: + +```py +def render_func( + context: Context, + data: Dict[str, Any], + slot_ref: SlotRef, +) -> str | SafeString: + return nodelist.render(ctx) +``` + +- _`context`_ - Django's Context available to the Slot Node. +- _`data`_ - Data passed to the `{% slot %}` tag. See [Scoped Slots](#scoped-slots). +- _`slot_ref`_ - The default slot content. See [Accessing original content of slots](#accessing-original-content-of-slots). + - NOTE: The slot is lazily evaluated. To render the slot, convert it to string with `str(slot_ref)`. + +Example: +```py +def footer_slot(ctx, data, slot_ref): + return f""" + SLOT_DATA: {data['abc']} + ORIGINAL: {slot_ref} + """ + +MyComponent.render_to_response( + slots={ + "footer": footer_slot, + }, +) +``` + +### Response class of `render_to_response` + +While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`. + +If you want to use a different Response class in `render_to_response`, set the `Component.response_class` attribute: + +```py +class MyResponse(HttpResponse): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + # Configure response + self.headers = ... + self.status = ... + +class SimpleComponent(component.Component): + response_class = MyResponse + template: types.django_html = "HELLO" + +response = SimpleComponent.render_to_response() +assert isinstance(response, MyResponse) +``` + ## Use components as views _New in version 0.34_ Components can now be used as views. To do this, `Component` subclasses Django's `View` class. This means that you can use all of the [methods](https://docs.djangoproject.com/en/5.0/ref/class-based-views/base/#view) of `View` in your component. For example, you can override `get` and `post` to handle GET and POST requests, respectively. -In addition, `Component` now has a `render_to_response` method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object. +In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object. Here's an example of a calendar component defined as a view: @@ -393,7 +527,7 @@ class Calendar(component.Component): slots = { "header": "Calendar header", } - return self.render_to_response(context, slots) + return self.render_to_response(context=context, slots=slots) ``` Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view: