Skip to content

Commit 385b700

Browse files
mrfolksyYour NameBrandonShar
authored
Support Inertia 2.0 deferred props feature (#56)
* support deferred props * fixed typo * fixed formatting on utils, set the default group to be default on deferred props and added additional tests cases * set indentation to 2 spaces on build_deffered_props --------- Co-authored-by: Your Name <[email protected]> Co-authored-by: Brandon Shar <[email protected]>
1 parent e01b216 commit 385b700

File tree

8 files changed

+157
-26
lines changed

8 files changed

+157
-26
lines changed

README.md

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
![image](https://user-images.githubusercontent.com/6599653/114456558-032e2200-9bab-11eb-88bc-a19897f417ba.png)
22

3-
43
# Inertia.js Django Adapter
54

65
## Installation
76

87
### Backend
98

109
Install the following python package via pip
10+
1111
```bash
1212
pip install inertia-django
1313
```
1414

1515
Add the Inertia app to your `INSTALLED_APPS` in `settings.py`
16+
1617
```python
1718
INSTALLED_APPS = [
1819
# django apps,
@@ -22,6 +23,7 @@ INSTALLED_APPS = [
2223
```
2324

2425
Add the Inertia middleware to your `MIDDLEWARE` in `settings.py`
26+
2527
```python
2628
MIDDLEWARE = [
2729
# django middleware,
@@ -36,27 +38,29 @@ Now you're all set!
3638

3739
### Frontend
3840

39-
Django specific frontend docs coming soon. For now, we recommend installing [django_vite](https://github.com/MrBin99/django-vite)
41+
Django specific frontend docs coming soon. For now, we recommend installing [django_vite](https://github.com/MrBin99/django-vite)
4042
and following the commits on the Django Vite [example repo](https://github.com/MrBin99/django-vite-example). Once Vite is setup with
4143
your frontend of choice, just replace the contents of `entry.js` with [this file (example in react)](https://github.com/BrandonShar/inertia-rails-template/blob/main/app/frontend/entrypoints/application.jsx)
4244

43-
44-
You can also check out the official Inertia docs at https://inertiajs.com/.
45+
You can also check out the official Inertia docs at https://inertiajs.com/.
4546

4647
### CSRF
4748

48-
Django's CSRF tokens are tightly coupled with rendering templates so Inertia Django automatically handles adding the CSRF cookie for you to each Inertia response. Because the default names Django users for the CSRF headers don't match Axios (the Javascript request library Inertia uses), we'll need to either modify Axios's defaults OR Django's settings.
49+
Django's CSRF tokens are tightly coupled with rendering templates so Inertia Django automatically handles adding the CSRF cookie for you to each Inertia response. Because the default names Django users for the CSRF headers don't match Axios (the Javascript request library Inertia uses), we'll need to either modify Axios's defaults OR Django's settings.
4950

5051
**You only need to choose one of the following options, just pick whichever makes the most sense to you!**
5152

5253
In your `entry.js` file
54+
5355
```javascript
54-
axios.defaults.xsrfHeaderName = "X-CSRFToken"
55-
axios.defaults.xsrfCookieName = "csrftoken"
56+
axios.defaults.xsrfHeaderName = "X-CSRFToken";
57+
axios.defaults.xsrfCookieName = "csrftoken";
5658
```
59+
5760
OR
5861

5962
In your Django `settings.py` file
63+
6064
```python
6165
CSRF_HEADER_NAME = 'HTTP_X_XSRF_TOKEN'
6266
CSRF_COOKIE_NAME = 'XSRF-TOKEN'
@@ -102,7 +106,7 @@ from .models import User
102106

103107
def inertia_share(get_response):
104108
def middleware(request):
105-
share(request,
109+
share(request,
106110
app_name=settings.APP_NAME,
107111
user_count=lambda: User.objects.count(), # evaluated lazily at render time
108112
user=lambda: request.user, # evaluated lazily at render time
@@ -114,7 +118,7 @@ def inertia_share(get_response):
114118

115119
### External Redirects
116120

117-
It is possible to redirect to an external website, or even another non-Inertia endpoint in your app while handling an Inertia request.
121+
It is possible to redirect to an external website, or even another non-Inertia endpoint in your app while handling an Inertia request.
118122
This can be accomplished using a server-side initiated `window.location` visit via the `location` method:
119123

120124
```python
@@ -124,10 +128,11 @@ def external():
124128
return location("http://foobar.com/")
125129
```
126130

127-
It will generate a `409 Conflict` response and include the destination URL in the `X-Inertia-Location` header.
131+
It will generate a `409 Conflict` response and include the destination URL in the `X-Inertia-Location` header.
128132
When this response is received client-side, Inertia will automatically perform a `window.location = url` visit.
129133

130134
### Lazy Props
135+
131136
On the front end, Inertia supports the concept of "partial reloads" where only the props requested
132137
are returned by the server. Sometimes, you may want to use this flow to avoid processing a particularly slow prop on the intial load. In this case, you can use `Lazy props`. Lazy props aren't evaluated unless they're specifically requested by name in a partial reload.
133138

@@ -142,10 +147,46 @@ def example(request):
142147
}
143148
```
144149

150+
### Deferred Props
151+
152+
As of version 2.0, Inertia supports the ability to defer the fetching of props until after the page has been initially rendered. Essentially this is similar to the concept of `Lazy props` however Inertia provides convenient frontend components to automatically fetch the deferred props after the page has initially loaded, instead of requiring the user to initiate a reload. For more info, see [Deferred props](https://inertiajs.com/deferred-props) in the Inertia documentation.
153+
154+
To mark props as deferred on the server side use the `defer` function.
155+
156+
```python
157+
from inertia import defer, inertia
158+
159+
@inertia('ExampleComponent')
160+
def example(request):
161+
return {
162+
'name': lambda: 'Brandon', # this will be rendered on the first load as usual
163+
'data': defer(lambda: some_long_calculation()), # this will only be run after the frontend has initially loaded and inertia requests this prop
164+
}
165+
```
166+
167+
#### Grouping requests
168+
169+
By default, all deferred props get fetched in one request after the initial page is rendered, but you can choose to fetch data in parallel by grouping props together.
170+
171+
```python
172+
from inertia import defer, inertia
173+
174+
@inertia('ExampleComponent')
175+
def example(request):
176+
return {
177+
'name': lambda: 'Brandon', # this will be rendered on the first load as usual
178+
'data': defer(lambda: some_long_calculation()),
179+
'data1': defer(lambda: some_long_calculation1(), group='group'),
180+
'data2': defer(lambda: some_long_calculation1(), 'group'),
181+
}
182+
```
183+
184+
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.
185+
145186
### Json Encoding
146187

147-
Inertia Django ships with a custom JsonEncoder at `inertia.utils.InertiaJsonEncoder` that extends Django's
148-
`DjangoJSONEncoder` with additional logic to handle encoding models and Querysets. If you have other json
188+
Inertia Django ships with a custom JsonEncoder at `inertia.utils.InertiaJsonEncoder` that extends Django's
189+
`DjangoJSONEncoder` with additional logic to handle encoding models and Querysets. If you have other json
149190
encoding logic you'd prefer, you can set a new JsonEncoder via the settings.
150191

151192
### History Encryption
@@ -191,9 +232,11 @@ class LogoutView(auth_views.LogoutView):
191232
### SSR
192233

193234
#### Backend
235+
194236
Enable SSR via the `INERTIA_SSR_URL` and `INERTIA_SSR_ENABLED` settings
195237

196238
#### Frontend
239+
197240
Coming Soon!
198241

199242
## Settings
@@ -225,22 +268,22 @@ class ExampleTestCase(InertiaTestCase):
225268

226269
# check the component
227270
self.assertComponentUsed('Event/Index')
228-
271+
229272
# access the component name
230273
self.assertEqual(self.component(), 'Event/Index')
231-
274+
232275
# props (including shared props)
233276
self.assertHasExactProps({name: 'Brandon', sport: 'hockey'})
234277
self.assertIncludesProps({sport: 'hockey'})
235-
278+
236279
# access props
237280
self.assertEquals(self.props()['name'], 'Brandon')
238-
281+
239282
# template data
240283
self.assertHasExactTemplateData({name: 'Brian', sport: 'basketball'})
241284
self.assertIncludesTemplateData({sport: 'basketball'})
242-
243-
# access template data
285+
286+
# access template data
244287
self.assertEquals(self.template_data()['name'], 'Brian')
245288
```
246289

@@ -255,6 +298,6 @@ for you to simulate an inertia response. You can access and use it just like the
255298

256299
A huge thank you to the community members who have worked on InertiaJS for Django before us. Parts of this repo were particularly inspired by [Andres Vargas](https://github.com/zodman) and [Samuel Girardin](https://github.com/girardinsamuel). Additional thanks to Andres for the Pypi project.
257300

258-
*Maintained and sponsored by the team at [bellaWatt](https://bellawatt.com/)*
301+
_Maintained and sponsored by the team at [bellaWatt](https://bellawatt.com/)_
259302

260303
[![bellaWatt Logo](https://user-images.githubusercontent.com/6599653/114456832-5607d980-9bab-11eb-99c8-ab39867c384e.png)](https://bellawatt.com/)

inertia/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .http import inertia, render, location
2-
from .utils import lazy
2+
from .utils import lazy, defer
33
from .share import share

inertia/http.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from json import dumps as json_encode
66
from functools import wraps
77
import requests
8-
from .utils import LazyProp
8+
from .utils import DeferredProp, LazyProp
99

1010
INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history"
1111
INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history"
@@ -37,11 +37,22 @@ def build_props():
3737
if key not in partial_keys():
3838
del _props[key]
3939
else:
40-
if isinstance(_props[key], LazyProp):
40+
if isinstance(_props[key], LazyProp) or isinstance(_props[key], DeferredProp):
4141
del _props[key]
4242

4343
return deep_transform_callables(_props)
4444

45+
def build_deferred_props():
46+
if is_a_partial_render():
47+
return None
48+
49+
_deferred_props = {}
50+
for key, prop in props.items():
51+
if isinstance(prop, DeferredProp):
52+
_deferred_props.setdefault(prop.group, []).append(key)
53+
54+
return _deferred_props
55+
4556
def render_ssr():
4657
data = json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER)
4758
response = requests.post(
@@ -64,7 +75,7 @@ def page_data():
6475
if not isinstance(clear_history, bool):
6576
raise TypeError(f"Expected boolean for clear_history, got {type(clear_history).__name__}")
6677

67-
return {
78+
_page = {
6879
'component': component,
6980
'props': build_props(),
7081
'url': request.build_absolute_uri(),
@@ -73,6 +84,12 @@ def page_data():
7384
'clearHistory': clear_history,
7485
}
7586

87+
_deferred_props = build_deferred_props()
88+
if _deferred_props:
89+
_page['deferredProps'] = _deferred_props
90+
91+
return _page
92+
7693
if 'X-Inertia' in request.headers:
7794
return JsonResponse(
7895
data=page_data(),

inertia/test.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def assertHasExactTemplateData(self, template_data):
5353
def assertComponentUsed(self, component_name):
5454
self.assertEqual(component_name, self.component())
5555

56-
def inertia_page(url, component='TestComponent', props={}, template_data={}):
57-
return {
56+
def inertia_page(url, component='TestComponent', props={}, template_data={}, deferred_props=None):
57+
_page = {
5858
'component': component,
5959
'props': props,
6060
'url': f'http://testserver/{url}/',
@@ -63,6 +63,11 @@ def inertia_page(url, component='TestComponent', props={}, template_data={}):
6363
'clearHistory': False,
6464
}
6565

66+
if deferred_props:
67+
_page['deferredProps'] = deferred_props
68+
69+
return _page
70+
6671
def inertia_div(*args, **kwargs):
6772
page = inertia_page(*args, **kwargs)
6873
return f'<div id="app" data-page="{escape(dumps(page))}"></div>'

inertia/tests/test_rendering.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,40 @@ def test_that_csrf_is_included_even_on_initial_page_load(self):
109109
response = self.client.get('/props/')
110110

111111
self.assertIsNotNone(response.cookies.get('csrftoken'))
112+
113+
class DeferredPropsTestCase(InertiaTestCase):
114+
def test_deferred_props_are_set(self):
115+
self.assertJSONResponse(
116+
self.inertia.get('/defer/'),
117+
inertia_page(
118+
'defer',
119+
props={'name': 'Brian'},
120+
deferred_props={'default': ['sport']})
121+
)
122+
123+
def test_deferred_props_are_grouped(self):
124+
self.assertJSONResponse(
125+
self.inertia.get('/defer-group/'),
126+
inertia_page(
127+
'defer-group',
128+
props={'name': 'Brian'},
129+
deferred_props={'group': ['sport', 'team'], 'default': ['grit']})
130+
)
131+
132+
def test_deferred_props_are_included_when_requested(self):
133+
self.assertJSONResponse(
134+
self.inertia.get('/defer/', HTTP_X_INERTIA_PARTIAL_DATA='sport', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
135+
inertia_page('defer', props={'sport': 'Basketball'})
136+
)
137+
138+
139+
def test_only_deferred_props_in_group_are_included_when_requested(self):
140+
self.assertJSONResponse(
141+
self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='sport,team', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
142+
inertia_page('defer-group', props={'sport': 'Basketball', 'team': 'Bulls'})
143+
)
144+
145+
self.assertJSONResponse(
146+
self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
147+
inertia_page('defer-group', props={'grit': 'intense'})
148+
)

inertia/tests/testapp/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
path('props/', views.props_test),
99
path('template_data/', views.template_data_test),
1010
path('lazy/', views.lazy_test),
11+
path('defer/', views.defer_test),
12+
path('defer-group/', views.defer_group_test),
1113
path('complex-props/', views.complex_props_test),
1214
path('share/', views.share_test),
1315
path('inertia-redirect/', views.inertia_redirect_test),

inertia/tests/testapp/views.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.http.response import HttpResponse
22
from django.shortcuts import redirect
33
from django.utils.decorators import decorator_from_middleware
4-
from inertia import inertia, render, lazy, share, location
4+
from inertia import inertia, render, lazy, defer, share, location
55
from inertia.http import INERTIA_SESSION_CLEAR_HISTORY, clear_history, encrypt_history
66

77
class ShareMiddleware:
@@ -52,6 +52,23 @@ def lazy_test(request):
5252
'grit': lazy(lambda: 'intense'),
5353
}
5454

55+
@inertia('TestComponent')
56+
def defer_test(request):
57+
return {
58+
'name': 'Brian',
59+
'sport': defer(lambda: 'Basketball')
60+
}
61+
62+
63+
@inertia('TestComponent')
64+
def defer_group_test(request):
65+
return {
66+
'name': 'Brian',
67+
'sport': defer(lambda: 'Basketball', 'group'),
68+
'team': defer(lambda: 'Bulls', 'group'),
69+
'grit': defer(lambda: 'intense')
70+
}
71+
5572
@inertia('TestComponent')
5673
def complex_props_test(request):
5774
return {

inertia/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ def __init__(self, prop):
2323
def __call__(self):
2424
return self.prop() if callable(self.prop) else self.prop
2525

26+
class DeferredProp:
27+
def __init__(self, prop, group):
28+
self.prop = prop
29+
self.group = group
30+
31+
def __call__(self):
32+
return self.prop() if callable(self.prop) else self.prop
2633

2734
def lazy(prop):
2835
return LazyProp(prop)
36+
37+
def defer(prop, group="default"):
38+
return DeferredProp(prop, group)

0 commit comments

Comments
 (0)