Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2: Add merge props #60

Merged
merged 2 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,34 @@ def example(request):

In the example above, the `data1`, and `data2` props will be fetched in one request, while the `data` prop will be fetched in a separate request in parallel. Group names are arbitrary strings and can be anything you choose.

### Merge Props

By default, Inertia overwrites props with the same name when reloading a page. However, there are instances, such as pagination or infinite scrolling, where that is not the desired behavior. In these cases, you can merge props instead of overwriting them.

```python
from inertia import merge, inertia

@inertia('ExampleComponent')
def example(request):
return {
'name': lambda: 'Brandon',
'data': merge(Paginator(objects, 3)),
}
```

You can also combine deferred props with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded.

```python
from inertia import defer, inertia

@inertia('ExampleComponent')
def example(request):
return {
'name': lambda: 'Brandon',
'data': defer(lambda: Paginator(objects, 3), merge=True),
}
```

### Json Encoding

Inertia Django ships with a custom JsonEncoder at `inertia.utils.InertiaJsonEncoder` that extends Django's
Expand Down
2 changes: 1 addition & 1 deletion inertia/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .http import inertia, render, location
from .utils import lazy, optional, defer
from .utils import lazy, optional, defer, merge
from .share import share
23 changes: 20 additions & 3 deletions inertia/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from json import dumps as json_encode
from functools import wraps
import requests
from .utils import DeferredProp, LazyProp
from .prop_classes import IgnoreOnFirstLoadProp, DeferredProp, MergeableProp

INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history"
INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history"
Expand Down Expand Up @@ -37,7 +37,7 @@ def build_props():
if key not in partial_keys():
del _props[key]
else:
if isinstance(_props[key], LazyProp) or isinstance(_props[key], DeferredProp):
if isinstance(_props[key], IgnoreOnFirstLoadProp):
del _props[key]

return deep_transform_callables(_props)
Expand All @@ -52,7 +52,20 @@ def build_deferred_props():
_deferred_props.setdefault(prop.group, []).append(key)

return _deferred_props


def build_merge_props():
reset_keys = request.headers.get('X-Inertia-Reset', '').split(',')

return [
key
for key, prop in props.items()
if (
isinstance(prop, MergeableProp)
and prop.should_merge()
and key not in reset_keys
)
]

def render_ssr():
data = json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER)
response = requests.post(
Expand Down Expand Up @@ -88,6 +101,10 @@ def page_data():
if _deferred_props:
_page['deferredProps'] = _deferred_props

_merge_props = build_merge_props()
if _merge_props:
_page['mergeProps'] = _merge_props

return _page

if 'X-Inertia' in request.headers:
Expand Down
33 changes: 33 additions & 0 deletions inertia/prop_classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
import warnings

class CallableProp(ABC):
def __init__(self, prop):
self.prop = prop

def __call__(self):
return self.prop() if callable(self.prop) else self.prop

class MergeableProp(ABC):
@abstractmethod
def should_merge(self):
pass

class IgnoreOnFirstLoadProp(ABC):
pass

class OptionalProp(CallableProp, IgnoreOnFirstLoadProp):
pass

class DeferredProp(CallableProp, MergeableProp, IgnoreOnFirstLoadProp):
def __init__(self, prop, group, merge=False):
super().__init__(prop)
self.group = group
self.merge = merge

def should_merge(self):
return self.merge

class MergeProp(CallableProp, MergeableProp):
def should_merge(self):
return True
11 changes: 10 additions & 1 deletion inertia/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def page(self):

def props(self):
return self.page()['props']

def merge_props(self):
return self.page()['mergeProps']

def deferred_props(self):
return self.page()['deferredProps']

def template_data(self):
context = self.mock_render.call_args.args[2]
Expand All @@ -53,7 +59,7 @@ def assertHasExactTemplateData(self, template_data):
def assertComponentUsed(self, component_name):
self.assertEqual(component_name, self.component())

def inertia_page(url, component='TestComponent', props={}, template_data={}, deferred_props=None):
def inertia_page(url, component='TestComponent', props={}, template_data={}, deferred_props=None, merge_props=None):
_page = {
'component': component,
'props': props,
Expand All @@ -65,6 +71,9 @@ def inertia_page(url, component='TestComponent', props={}, template_data={}, def

if deferred_props:
_page['deferredProps'] = deferred_props

if merge_props:
_page['mergeProps'] = merge_props

return _page

Expand Down
33 changes: 33 additions & 0 deletions inertia/tests/test_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,36 @@ def test_only_deferred_props_in_group_are_included_when_requested(self):
self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
inertia_page('defer-group', props={'grit': 'intense'})
)

class MergePropsTestCase(InertiaTestCase):
def test_merge_props_are_included_on_initial_load(self):
self.assertJSONResponse(
self.inertia.get('/merge/'),
inertia_page('merge', props={
'name': 'Brandon',
'sport': 'Hockey',
}, merge_props=['sport', 'team'], deferred_props={'default': ['team']})
)


def test_deferred_merge_props_are_included_on_subsequent_load(self):
self.assertJSONResponse(
self.inertia.get('/merge/', HTTP_X_INERTIA_PARTIAL_DATA='team', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
inertia_page('merge', props={
'team': 'Penguins',
}, merge_props=['sport', 'team'])
)

def test_merge_props_are_not_included_when_reset(self):
self.assertJSONResponse(
self.inertia.get(
'/merge/',
HTTP_X_INERTIA_PARTIAL_DATA='sport,team',
HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent',
HTTP_X_INERTIA_RESET='sport,team'
),
inertia_page('merge', props={
'sport': 'Hockey',
'team': 'Penguins',
})
)
3 changes: 2 additions & 1 deletion inertia/tests/testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
path('lazy/', views.lazy_test),
path('optional/', views.optional_test),
path('defer/', views.defer_test),
path('defer-group/', views.defer_group_test),
path('defer-group/', views.defer_group_test),
path('merge/', views.merge_test),
path('complex-props/', views.complex_props_test),
path('share/', views.share_test),
path('inertia-redirect/', views.inertia_redirect_test),
Expand Down
10 changes: 9 additions & 1 deletion inertia/tests/testapp/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.http.response import HttpResponse
from django.shortcuts import redirect
from django.utils.decorators import decorator_from_middleware
from inertia import inertia, render, lazy, optional, defer, share, location
from inertia import inertia, render, lazy, merge, optional, defer, share, location
from inertia.http import INERTIA_SESSION_CLEAR_HISTORY, clear_history, encrypt_history

class ShareMiddleware:
Expand Down Expand Up @@ -77,6 +77,14 @@ def defer_group_test(request):
'grit': defer(lambda: 'intense')
}

@inertia('TestComponent')
def merge_test(request):
return {
'name': 'Brandon',
'sport': merge(lambda: 'Hockey'),
'team': defer(lambda: 'Penguins', merge=True),
}

@inertia('TestComponent')
def complex_props_test(request):
return {
Expand Down
39 changes: 12 additions & 27 deletions inertia/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db import models
from django.db.models.query import QuerySet
from django.forms.models import model_to_dict as base_model_to_dict
from .prop_classes import OptionalProp, DeferredProp, MergeProp
import warnings

def model_to_dict(model):
Expand All @@ -17,35 +18,19 @@ def default(self, value):

return super().default(value)

class LazyProp:
def __init__(self, prop):
warnings.warn(
"lazy and LazyProp are deprecated and will be removed in a future version. Please use optional instead.",
DeprecationWarning,
stacklevel=2
)
self.prop = prop

def __call__(self):
return self.prop() if callable(self.prop) else self.prop

class OptionalProp(LazyProp):
def __init__(self, prop):
self.prop = prop

class DeferredProp:
def __init__(self, prop, group):
self.prop = prop
self.group = group

def __call__(self):
return self.prop() if callable(self.prop) else self.prop

def lazy(prop):
return LazyProp(prop)
warnings.warn(
"lazy is deprecated and will be removed in a future version. Please use optional instead.",
DeprecationWarning,
stacklevel=2
)
return optional(prop)

def optional(prop):
return OptionalProp(prop)

def defer(prop, group="default"):
return DeferredProp(prop, group)
def defer(prop, group="default", merge=False):
return DeferredProp(prop, group=group, merge=merge)

def merge(prop):
return MergeProp(prop)
Loading