Skip to content

Commit

Permalink
Make accessing other components from get_extra_context_data() easier
Browse files Browse the repository at this point in the history
BACKWARD INCOMPATIBLE CHANGES

- Modify get_extra_context_data() to accept an instance of
  ExtraContextRequest(). This instance includes the component
  state, component_kwargs, current request, component state address,
  and the state manager. By including the state manager and address,
  it becomes possible to access stores for other components.

- Remove the passing of component kwargs to init_state() and
  update_state(). These kwargs are redundant since both
  InitStateContext and UpdateStateContext already have them.

- Update the example project to reflect these changes.

- Add StatelessLiveComponent as a base class for components that
  do not need to store state.
  • Loading branch information
imankulov committed Dec 18, 2023
1 parent 78fe4e4 commit 7754257
Show file tree
Hide file tree
Showing 15 changed files with 164 additions and 116 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added helper method StateAddress.with_component_id() to create a StateAddress with the same session ID, but a different component ID.
- Added reference to the state manager to InitStateContext and UpdateStateContext.
- Added "find_ancestor(ancestor_type)" method to StateAddress and CallContext.
- 🚨 **Breaking Change**. Modified get_extra_context_data() to accept an instance of ExtraContextRequest(). This instance includes the component state, component_kwargs, current request, component state address, and the state manager. By including the state manager and address, it becomes possible to access stores for other components.
- 🚨 **Breaking Change**. Removed the passing of component kwargs to init_state() and update_state(). These kwargs are redundant since both InitStateContext and UpdateStateContext already have them.
- Added StatelessLiveComponent as a base class for components that do not need to store state.


## 1.7.0 (2023-12-13)

Expand Down
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Assuming that the component will be re-rendered on partial render, the state mus
```python
from pydantic import BaseModel
from livecomponents.component import LiveComponent
from livecomponents.manager.manager import InitStateContext

class AlertState(BaseModel):
message: str = ""
Expand All @@ -179,19 +180,42 @@ class Alert(LiveComponent):
template_name = "alert.html"


def init_state(self, **component_kwargs) -> AlertState:
return AlertState(**component_kwargs)
def init_state(self, context: InitStateContext) -> AlertState:
return AlertState(**context.component_kwargs)
```

Component states don't need to be stored if components are not expected to be re-rendered independently, and only
as part of the parent component. For example, components for buttons are rarely re-rendered independently, so
you get away without the state model.

## On calling component methods from others

There are two ways to call component methods from other components:
## Stateless components

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:
If the component doesn't store any state, you can inherit it from the StatelessLiveComponent class. You may find it
helpful for rendering the hierarchy of components where the shared state is stored in the root components.

```python
from livecomponents.component import StatelessLiveComponent

class StatelessAlert(StatelessLiveComponent):

template_name = "alert.html"

def get_extra_context_data(
self, extra_context_request: "ExtraContextRequest[State]"
) -> dict:
state_manager = extra_context_request.state_manager
root_addr = extra_context_request.state_addr.must_find_ancestor("root")
root_state = state_manager.get_component_state(root_addr)
return {"message": root_state.message}
```


## Calling component methods from others

There are several ways to call component methods from other components:

**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:

```python
from livecomponents import LiveComponent, command, CallContext
Expand All @@ -203,7 +227,7 @@ class MyComponent(LiveComponent):
call_context.find_one("|message:0").set_message("Hello, world!")
```

Using the "parent" reference.
**Using the "parent" reference.**

```python
from livecomponents import LiveComponent, command, CallContext
Expand Down Expand Up @@ -385,9 +409,9 @@ class SampleState(BaseModel):
class Sample(LiveComponent):
...

def init_state(self, context: InitStateContext, **component_kwargs) -> SampleState:
def init_state(self, context: InitStateContext) -> SampleState:
var = context.outer_context.get("var", "unset")
effective_kwargs = {**component_kwargs, "var": var}
effective_kwargs = {**context.component_kwargs, "var": var}
return SampleState(**effective_kwargs)
```

Expand Down
4 changes: 2 additions & 2 deletions example/coffee/components/coffee/row/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class Meta:
class RowComponent(LiveComponent[RowState]):
template_name = "coffee/row/row.html"

def init_state(self, context: InitStateContext, **component_kwargs) -> RowState:
return RowState(**component_kwargs)
def init_state(self, context: InitStateContext) -> RowState:
return RowState(**context.component_kwargs)

@command
def edit_on(self, call_context: CallContext[RowState]):
Expand Down
14 changes: 3 additions & 11 deletions example/coffee/components/coffee/search/search.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
from django_components import component
from pydantic import BaseModel

from livecomponents import CallContext, InitStateContext, LiveComponent, command


class SearchState(BaseModel):
pass
from livecomponents import CallContext, StatelessLiveComponent, command


@component.register("coffee/search")
class SearchComponent(LiveComponent[SearchState]):
class SearchComponent(StatelessLiveComponent):
template_name = "coffee/search/search.html"

def init_state(self, context: InitStateContext, **component_kwargs) -> SearchState:
return SearchState(**component_kwargs)

@command
def update_search(self, call_context: CallContext[SearchState], search: str):
def update_search(self, call_context: CallContext, search: str):
call_context.parent.update_search(search=search)
19 changes: 14 additions & 5 deletions example/coffee/components/coffee/table/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
from django_components import component

from coffee.models import CoffeeBean
from livecomponents import CallContext, InitStateContext, LiveComponent, command
from livecomponents.utils import LiveComponentsModel
from livecomponents import (
CallContext,
ExtraContextRequest,
InitStateContext,
LiveComponent,
LiveComponentsModel,
command,
)


class TableState(LiveComponentsModel):
Expand All @@ -14,7 +20,10 @@ class TableState(LiveComponentsModel):
class TableComponent(LiveComponent[TableState]):
template_name = "coffee/table/table.html"

def get_extra_context_data(self, state: TableState, **component_kwargs):
def get_extra_context_data(
self, extra_context_request: ExtraContextRequest[TableState]
):
state = extra_context_request.state
if state.search:
beans = CoffeeBean.objects.filter(
Q(name__icontains=state.search)
Expand All @@ -26,8 +35,8 @@ def get_extra_context_data(self, state: TableState, **component_kwargs):
beans = CoffeeBean.objects.all()
return {"beans": beans}

def init_state(self, context: InitStateContext, **component_kwargs) -> TableState:
return TableState(**component_kwargs)
def init_state(self, context: InitStateContext) -> TableState:
return TableState(**context.component_kwargs)

@command
def update_search(self, call_context: CallContext[TableState], search: str):
Expand Down
18 changes: 11 additions & 7 deletions example/counters/components/clickcounter/clickcounter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from django_components import component
from pydantic import BaseModel

from livecomponents import CallContext, InitStateContext, LiveComponent, command
from livecomponents import (
CallContext,
ExtraContextRequest,
InitStateContext,
LiveComponent,
command,
)
from livecomponents.const import HIER_SEP, TYPE_SEP


Expand All @@ -17,14 +23,12 @@ class ClickCounter(LiveComponent[ClickCounterState]):
template_name = "clickcounter/clickcounter.html"

def get_extra_context_data(
self, state: ClickCounterState, **component_kwargs
self, extra_context_request: ExtraContextRequest[ClickCounterState]
) -> dict[str, Any]:
return {"value_str": f"{state.value:,}"}
return {"value_str": f"{extra_context_request.state.value:,}"}

def init_state(
self, context: InitStateContext, **component_kwargs
) -> ClickCounterState:
return ClickCounterState(**component_kwargs)
def init_state(self, context: InitStateContext) -> ClickCounterState:
return ClickCounterState(**context.component_kwargs)

@command
def increment(self, call_context: CallContext[ClickCounterState], value: int = 1):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ class IncrementButtonState(BaseModel):
class IncrementButton(LiveComponent[IncrementButtonState]):
template_name = "incrementbutton/incrementbutton.html"

def init_state(
self, context: InitStateContext, **component_kwargs
) -> IncrementButtonState:
return IncrementButtonState(**component_kwargs)
def init_state(self, context: InitStateContext) -> IncrementButtonState:
return IncrementButtonState(**context.component_kwargs)

@command
def increment_parent_counter(
Expand Down
4 changes: 2 additions & 2 deletions example/counters/components/message/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ class MessageState(BaseModel):
class MessageComponent(LiveComponent[MessageState]):
template_name = "message/message.html"

def init_state(self, context: InitStateContext, **component_kwargs) -> MessageState:
def init_state(self, context: InitStateContext) -> MessageState:
initialized_from = context.request.META["REMOTE_ADDR"]

if context.request.GET.get("forbidden"):
raise PermissionDenied()

return MessageState(initialized_from=initialized_from, **component_kwargs)
return MessageState(initialized_from=initialized_from)

@command
def set_message(self, call_context: CallContext, message: str):
Expand Down
4 changes: 2 additions & 2 deletions example/counters/components/sample/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class SampleState(BaseModel):
class SampleComponent(LiveComponent[SampleState]):
template_name = "sample/sample.html"

def init_state(self, context: InitStateContext, **component_kwargs) -> SampleState:
def init_state(self, context: InitStateContext) -> SampleState:
var = context.outer_context.get("var", "unset")
effective_kwargs = {**component_kwargs, "var": var}
effective_kwargs = {**context.component_kwargs, "var": var}
return SampleState(**effective_kwargs)
23 changes: 3 additions & 20 deletions example/modals/components/emailsender/emailsender.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
from typing import Any

from django_components import component
from pydantic import BaseModel

from livecomponents import CallContext, InitStateContext, LiveComponent, command


class EmailSenderState(BaseModel):
pass
from livecomponents import CallContext, StatelessLiveComponent, command


@component.register("emailsender")
class EmailSender(LiveComponent[EmailSenderState]):
class EmailSender(StatelessLiveComponent):
template_name = "emailsender/emailsender.html"

def get_extra_context_data(
self, state: EmailSenderState, **component_kwargs
) -> dict[str, Any]:
return {}

def init_state(
self, context: InitStateContext, **component_kwargs
) -> EmailSenderState:
return EmailSenderState()

@command
def send_email(self, call_context: CallContext[EmailSenderState], email: str):
def send_email(self, call_context: CallContext, email: str):
print("Sending email to", email)
11 changes: 2 additions & 9 deletions example/modals/components/modal/modal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Any

from django_components import component
from pydantic import BaseModel

Expand All @@ -14,13 +12,8 @@ class ModalState(BaseModel):
class Modal(LiveComponent[ModalState]):
template_name = "modal/modal.html"

def get_extra_context_data(
self, state: ModalState, **component_kwargs
) -> dict[str, Any]:
return {}

def init_state(self, context: InitStateContext, **component_kwargs) -> ModalState:
return ModalState(**component_kwargs)
def init_state(self, context: InitStateContext) -> ModalState:
return ModalState(**context.component_kwargs)

@command
def close(self, call_context: CallContext[ModalState]):
Expand Down
13 changes: 11 additions & 2 deletions livecomponents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from livecomponents.component import LiveComponent, command
from livecomponents.component import (
ExtraContextRequest,
LiveComponent,
StatelessLiveComponent,
command,
)
from livecomponents.manager.manager import (
CallContext,
InitStateContext,
UpdateStateContext,
)
from livecomponents.utils import LiveComponentsModel

__all__ = [
"LiveComponent",
"command",
"CallContext",
"InitStateContext",
"UpdateStateContext",
"ExtraContextRequest",
"LiveComponent",
"LiveComponentsModel",
"StatelessLiveComponent",
]
Loading

0 comments on commit 7754257

Please sign in to comment.