Skip to content

Commit

Permalink
Support Inertia 2.0 deferred props feature (#56)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 4, 2025
1 parent e01b216 commit 385b700
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 26 deletions.
81 changes: 62 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
![image](https://user-images.githubusercontent.com/6599653/114456558-032e2200-9bab-11eb-88bc-a19897f417ba.png)


# Inertia.js Django Adapter

## Installation

### Backend

Install the following python package via pip

```bash
pip install inertia-django
```

Add the Inertia app to your `INSTALLED_APPS` in `settings.py`

```python
INSTALLED_APPS = [
# django apps,
Expand All @@ -22,6 +23,7 @@ INSTALLED_APPS = [
```

Add the Inertia middleware to your `MIDDLEWARE` in `settings.py`

```python
MIDDLEWARE = [
# django middleware,
Expand All @@ -36,27 +38,29 @@ Now you're all set!

### Frontend

Django specific frontend docs coming soon. For now, we recommend installing [django_vite](https://github.com/MrBin99/django-vite)
Django specific frontend docs coming soon. For now, we recommend installing [django_vite](https://github.com/MrBin99/django-vite)
and following the commits on the Django Vite [example repo](https://github.com/MrBin99/django-vite-example). Once Vite is setup with
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)


You can also check out the official Inertia docs at https://inertiajs.com/.
You can also check out the official Inertia docs at https://inertiajs.com/.

### CSRF

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.
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.

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

In your `entry.js` file

```javascript
axios.defaults.xsrfHeaderName = "X-CSRFToken"
axios.defaults.xsrfCookieName = "csrftoken"
axios.defaults.xsrfHeaderName = "X-CSRFToken";
axios.defaults.xsrfCookieName = "csrftoken";
```

OR

In your Django `settings.py` file

```python
CSRF_HEADER_NAME = 'HTTP_X_XSRF_TOKEN'
CSRF_COOKIE_NAME = 'XSRF-TOKEN'
Expand Down Expand Up @@ -102,7 +106,7 @@ from .models import User

def inertia_share(get_response):
def middleware(request):
share(request,
share(request,
app_name=settings.APP_NAME,
user_count=lambda: User.objects.count(), # evaluated lazily at render time
user=lambda: request.user, # evaluated lazily at render time
Expand All @@ -114,7 +118,7 @@ def inertia_share(get_response):

### External Redirects

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

```python
Expand All @@ -124,10 +128,11 @@ def external():
return location("http://foobar.com/")
```

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

### Lazy Props

On the front end, Inertia supports the concept of "partial reloads" where only the props requested
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.

Expand All @@ -142,10 +147,46 @@ def example(request):
}
```

### Deferred Props

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.

To mark props as deferred on the server side use the `defer` function.

```python
from inertia import defer, inertia

@inertia('ExampleComponent')
def example(request):
return {
'name': lambda: 'Brandon', # this will be rendered on the first load as usual
'data': defer(lambda: some_long_calculation()), # this will only be run after the frontend has initially loaded and inertia requests this prop
}
```

#### Grouping requests

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.

```python
from inertia import defer, inertia

@inertia('ExampleComponent')
def example(request):
return {
'name': lambda: 'Brandon', # this will be rendered on the first load as usual
'data': defer(lambda: some_long_calculation()),
'data1': defer(lambda: some_long_calculation1(), group='group'),
'data2': defer(lambda: some_long_calculation1(), 'group'),
}
```

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.

### Json Encoding

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

### History Encryption
Expand Down Expand Up @@ -191,9 +232,11 @@ class LogoutView(auth_views.LogoutView):
### SSR

#### Backend

Enable SSR via the `INERTIA_SSR_URL` and `INERTIA_SSR_ENABLED` settings

#### Frontend

Coming Soon!

## Settings
Expand Down Expand Up @@ -225,22 +268,22 @@ class ExampleTestCase(InertiaTestCase):

# check the component
self.assertComponentUsed('Event/Index')

# access the component name
self.assertEqual(self.component(), 'Event/Index')

# props (including shared props)
self.assertHasExactProps({name: 'Brandon', sport: 'hockey'})
self.assertIncludesProps({sport: 'hockey'})

# access props
self.assertEquals(self.props()['name'], 'Brandon')

# template data
self.assertHasExactTemplateData({name: 'Brian', sport: 'basketball'})
self.assertIncludesTemplateData({sport: 'basketball'})
# access template data

# access template data
self.assertEquals(self.template_data()['name'], 'Brian')
```

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

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.

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

[![bellaWatt Logo](https://user-images.githubusercontent.com/6599653/114456832-5607d980-9bab-11eb-99c8-ab39867c384e.png)](https://bellawatt.com/)
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
from .utils import lazy, defer
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 LazyProp
from .utils import DeferredProp, LazyProp

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

return deep_transform_callables(_props)

def build_deferred_props():
if is_a_partial_render():
return None

_deferred_props = {}
for key, prop in props.items():
if isinstance(prop, DeferredProp):
_deferred_props.setdefault(prop.group, []).append(key)

return _deferred_props

def render_ssr():
data = json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER)
response = requests.post(
Expand All @@ -64,7 +75,7 @@ def page_data():
if not isinstance(clear_history, bool):
raise TypeError(f"Expected boolean for clear_history, got {type(clear_history).__name__}")

return {
_page = {
'component': component,
'props': build_props(),
'url': request.build_absolute_uri(),
Expand All @@ -73,6 +84,12 @@ def page_data():
'clearHistory': clear_history,
}

_deferred_props = build_deferred_props()
if _deferred_props:
_page['deferredProps'] = _deferred_props

return _page

if 'X-Inertia' in request.headers:
return JsonResponse(
data=page_data(),
Expand Down
9 changes: 7 additions & 2 deletions inertia/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ 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={}):
return {
def inertia_page(url, component='TestComponent', props={}, template_data={}, deferred_props=None):
_page = {
'component': component,
'props': props,
'url': f'http://testserver/{url}/',
Expand All @@ -63,6 +63,11 @@ def inertia_page(url, component='TestComponent', props={}, template_data={}):
'clearHistory': False,
}

if deferred_props:
_page['deferredProps'] = deferred_props

return _page

def inertia_div(*args, **kwargs):
page = inertia_page(*args, **kwargs)
return f'<div id="app" data-page="{escape(dumps(page))}"></div>'
37 changes: 37 additions & 0 deletions inertia/tests/test_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,40 @@ def test_that_csrf_is_included_even_on_initial_page_load(self):
response = self.client.get('/props/')

self.assertIsNotNone(response.cookies.get('csrftoken'))

class DeferredPropsTestCase(InertiaTestCase):
def test_deferred_props_are_set(self):
self.assertJSONResponse(
self.inertia.get('/defer/'),
inertia_page(
'defer',
props={'name': 'Brian'},
deferred_props={'default': ['sport']})
)

def test_deferred_props_are_grouped(self):
self.assertJSONResponse(
self.inertia.get('/defer-group/'),
inertia_page(
'defer-group',
props={'name': 'Brian'},
deferred_props={'group': ['sport', 'team'], 'default': ['grit']})
)

def test_deferred_props_are_included_when_requested(self):
self.assertJSONResponse(
self.inertia.get('/defer/', HTTP_X_INERTIA_PARTIAL_DATA='sport', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
inertia_page('defer', props={'sport': 'Basketball'})
)


def test_only_deferred_props_in_group_are_included_when_requested(self):
self.assertJSONResponse(
self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='sport,team', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
inertia_page('defer-group', props={'sport': 'Basketball', 'team': 'Bulls'})
)

self.assertJSONResponse(
self.inertia.get('/defer-group/', HTTP_X_INERTIA_PARTIAL_DATA='grit', HTTP_X_INERTIA_PARTIAL_COMPONENT='TestComponent'),
inertia_page('defer-group', props={'grit': 'intense'})
)
2 changes: 2 additions & 0 deletions inertia/tests/testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
path('props/', views.props_test),
path('template_data/', views.template_data_test),
path('lazy/', views.lazy_test),
path('defer/', views.defer_test),
path('defer-group/', views.defer_group_test),
path('complex-props/', views.complex_props_test),
path('share/', views.share_test),
path('inertia-redirect/', views.inertia_redirect_test),
Expand Down
19 changes: 18 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, share, location
from inertia import inertia, render, lazy, defer, share, location
from inertia.http import INERTIA_SESSION_CLEAR_HISTORY, clear_history, encrypt_history

class ShareMiddleware:
Expand Down Expand Up @@ -52,6 +52,23 @@ def lazy_test(request):
'grit': lazy(lambda: 'intense'),
}

@inertia('TestComponent')
def defer_test(request):
return {
'name': 'Brian',
'sport': defer(lambda: 'Basketball')
}


@inertia('TestComponent')
def defer_group_test(request):
return {
'name': 'Brian',
'sport': defer(lambda: 'Basketball', 'group'),
'team': defer(lambda: 'Bulls', 'group'),
'grit': defer(lambda: 'intense')
}

@inertia('TestComponent')
def complex_props_test(request):
return {
Expand Down
10 changes: 10 additions & 0 deletions inertia/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ def __init__(self, prop):
def __call__(self):
return self.prop() if callable(self.prop) else self.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)

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

0 comments on commit 385b700

Please sign in to comment.