|
| 1 | +# Components |
| 2 | + |
| 3 | +## Component State |
| 4 | + |
| 5 | +The state is defined in a separate class. The state must include parameters, passed to the component as keyword arguments, so that the component gets all necessary information to re-render itself on partial render. |
| 6 | + |
| 7 | +For example, given the template the alert component: |
| 8 | + |
| 9 | +```html |
| 10 | +<alert>{{ message }}</alert> |
| 11 | +``` |
| 12 | + |
| 13 | +that you want to use as |
| 14 | + |
| 15 | +```html |
| 16 | +{% component "alert" message="Hello, world!" %} |
| 17 | +``` |
| 18 | + |
| 19 | +Assuming that the component will be re-rendered on partial render, the state must include the "message" parameter: |
| 20 | + |
| 21 | +```python |
| 22 | +from pydantic import BaseModel |
| 23 | +from livecomponents.component import LiveComponent |
| 24 | +from livecomponents.manager.manager import InitStateContext |
| 25 | + |
| 26 | +class AlertState(BaseModel): |
| 27 | + message: str = "" |
| 28 | + |
| 29 | + |
| 30 | +class Alert(LiveComponent): |
| 31 | + |
| 32 | + template_name = "alert.html" |
| 33 | + |
| 34 | + |
| 35 | + def init_state(self, context: InitStateContext) -> AlertState: |
| 36 | + return AlertState(**context.component_kwargs) |
| 37 | +``` |
| 38 | + |
| 39 | +Component states don't need to be stored if components are not expected to be re-rendered independently, and only |
| 40 | +as part of the parent component. For example, components for buttons are rarely re-rendered independently, so |
| 41 | +you get away without the state model. |
| 42 | + |
| 43 | +## Serializing Component State |
| 44 | + |
| 45 | +When the page is rendered for the first time, a new session is created, and each component is initialized with its |
| 46 | +state by calling the `init_state()` method. |
| 47 | + |
| 48 | +The state is then serialized and stored in the session store, and as long as the session is the same (in other words, |
| 49 | +while the page is not loaded), the state is reused. |
| 50 | + |
| 51 | +The state is serialized using the `StateSerializer` class and saved in Redis. By default, the `PickleStateSerializer` |
| 52 | +is used. The serializer uses custom pickler and is optimized to store effectively the most common types of data, used |
| 53 | +in a Django app. More specifically: |
| 54 | + |
| 55 | +- When serializing a Django model, only the model's name and primary key are stored. The serializer takes advantage of |
| 56 | + the persistent_id/persistent_load pickle mechanism. |
| 57 | +- When serializing a Pydantic model, only the model's name and the values of the fields are stored. |
| 58 | +- When serializing a Django form, only the form's class name, as well as initial data and data, are stored. |
| 59 | + |
| 60 | + |
| 61 | +## Stateless components |
| 62 | + |
| 63 | +If the component doesn't store any state, you can inherit it from the StatelessLiveComponent class. You may find it |
| 64 | +helpful for rendering the hierarchy of components where the shared state is stored in the root components. |
| 65 | + |
| 66 | +```python |
| 67 | +from livecomponents.component import StatelessLiveComponent |
| 68 | + |
| 69 | +class StatelessAlert(StatelessLiveComponent): |
| 70 | + |
| 71 | + template_name = "alert.html" |
| 72 | + |
| 73 | + def get_extra_context_data( |
| 74 | + self, extra_context_request: "ExtraContextRequest[State]" |
| 75 | + ) -> dict: |
| 76 | + state_manager = extra_context_request.state_manager |
| 77 | + root_addr = extra_context_request.state_addr.must_find_ancestor("root") |
| 78 | + root_state = state_manager.get_component_state(root_addr) |
| 79 | + return {"message": root_state.message} |
| 80 | +``` |
| 81 | + |
| 82 | +## Returning results from command handlers |
| 83 | + |
| 84 | +Here's the signature of the Livecomponent function: |
| 85 | + |
| 86 | +```python |
| 87 | +from livecomponents import LiveComponent, CallContext, command |
| 88 | +from livecomponents.manager.execution_results import IExecutionResult |
| 89 | + |
| 90 | +class MyComponent(LiveComponent): |
| 91 | + |
| 92 | + @command |
| 93 | + def my_command_handler(self , call_context: CallContext, **kwargs) -> list[IExecutionResult] | IExecutionResult | None : |
| 94 | + ... |
| 95 | +``` |
| 96 | + |
| 97 | +Notice the type of the returned value for the handler. If set to something other than None, it can shape the |
| 98 | +partial HTTP response. |
| 99 | + |
| 100 | +More specifically here's what you can do: |
| 101 | + |
| 102 | +- Return ComponentDirty() to mark the component as dirty. This will result in the component being re-rendered and sent to the client. This is the default behavior. If you don't return anything, the component will be marked as dirty. |
| 103 | +- Return ComponentDirty(component_id) to mark a different component as dirty. |
| 104 | +- Return ComponentClean() to mark the current component as clean (not needing re-rendering). |
| 105 | +- Return ParentDirty() to mark the parent component as dirty. |
| 106 | +- Return RefreshPage(). If the command returns RefreshPage(), a "HX-Refresh: true" header will be sent to the client. |
| 107 | +- Return RedirectPage(url). If the command returns Redirect(), a "HX-Redirect: url" header will be sent to the client. |
| 108 | +- Return ReplaceUrl(url). If the command returns ReplaceUrl(), a "HX-Replace: url" header will be sent to the client. This will replace the current URL in the browser without reloading the page. |
| 109 | + |
| 110 | +## Raising exceptions from command handlers |
| 111 | + |
| 112 | +In some rare scenarios, you may need to cancel rendering the component and instruct the command handler to return an empty string to the client. |
| 113 | + |
| 114 | +If this is the case, you can raise a `livecomponents.exceptions.CancelRendering()` exception. |
| 115 | + |
| 116 | +The exception can be raised directly from a command handler or from one of the methods that it will call, such as `get_extra_context_data()`. |
| 117 | + |
| 118 | +```python |
| 119 | +from livecomponents.exceptions import CancelRendering |
| 120 | +... |
| 121 | + |
| 122 | +class MyComponent(LiveComponent): |
| 123 | + |
| 124 | + @command |
| 125 | + def my_command_handler(self, call_context: CallContext, **kwargs): |
| 126 | + if not self.pre_condition_met(call_context): |
| 127 | + raise CancelRendering() |
| 128 | + ... |
| 129 | +``` |
| 130 | + |
| 131 | +We encountered this situation at least once, where a race condition caused the pre-condition that was true when we started executing a command to no longer be true when we rendered a sub-component. In this case, we couldn't render the sub-component but also didn't want to return a partially rendered component. The best solution was to return an empty string, effectively making the command have no effect. |
| 132 | + |
| 133 | + |
| 134 | +## Calling component methods from others |
| 135 | + |
| 136 | +There are several ways to call component methods from other components: |
| 137 | + |
| 138 | +**Using the component ID.** For example, if you have a component with ID "|message.0" and a method "set_message", you can call it like this: |
| 139 | + |
| 140 | +```python |
| 141 | +from livecomponents import LiveComponent, command, CallContext |
| 142 | + |
| 143 | +class MyComponent(LiveComponent): |
| 144 | + |
| 145 | + @command |
| 146 | + def do_something(self, call_context: CallContext): |
| 147 | + call_context.find_one("|message:0").set_message("Hello, world!") |
| 148 | +``` |
| 149 | + |
| 150 | +**Using the "parent" reference.** |
| 151 | + |
| 152 | +```python |
| 153 | +from livecomponents import LiveComponent, command, CallContext |
| 154 | + |
| 155 | +class MyComponent(LiveComponent): |
| 156 | + |
| 157 | + @command |
| 158 | + def do_something(self, call_context: CallContext): |
| 159 | + call_context.parent.set_message("Hello, world!") |
| 160 | +``` |
0 commit comments