Skip to content

Commit

Permalink
V2: Add merge props (#60)
Browse files Browse the repository at this point in the history
* Add merge props

* add docs
  • Loading branch information
BrandonShar authored Jan 5, 2025
1 parent 36e2974 commit f06c8d2
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 34 deletions.
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)

0 comments on commit f06c8d2

Please sign in to comment.