Skip to content

Commit 89a2bc2

Browse files
committed
Ability to update url without page refresh.
1 parent 3d25c41 commit 89a2bc2

File tree

8 files changed

+160
-12
lines changed

8 files changed

+160
-12
lines changed

django_unicorn/components.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def __init__(self, hash):
4444
self.hash = hash
4545

4646

47+
class LocationUpdate:
48+
def __init__(self, redirect, title=None):
49+
self.redirect = redirect
50+
self.title = title
51+
52+
4753
class ComponentLoadError(Exception):
4854
pass
4955

django_unicorn/static/js/messageSender.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,18 @@ export function send(component, callback) {
6161
// TODO: For turbolinks support look at https://github.com/livewire/livewire/blob/f2ba1977d73429911f81b3f6363ee8f8fea5abff/js/component/index.js#L330-L336
6262
if (responseJson.redirect) {
6363
if (responseJson.redirect.url) {
64-
// TODO: Use config to potentially use `component.window.history.pushState()`
65-
component.window.location.href = responseJson.redirect.url;
66-
67-
if (isFunction(callback)) {
68-
callback([], null, null);
64+
if (responseJson.redirect.refresh) {
65+
if (responseJson.redirect.title) {
66+
component.window.document.title = responseJson.redirect.title;
67+
}
68+
69+
component.window.history.pushState(
70+
{},
71+
"",
72+
responseJson.redirect.url
73+
);
74+
} else {
75+
component.window.location.href = responseJson.redirect.url;
6976
}
7077
} else if (responseJson.redirect.hash) {
7178
component.window.location.hash = responseJson.redirect.hash;

django_unicorn/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import orjson
1212

1313
from .call_method_parser import InvalidKwarg, parse_call_method_name, parse_kwarg
14-
from .components import HashUpdate, UnicornField, UnicornView
14+
from .components import HashUpdate, LocationUpdate, UnicornField, UnicornView
1515
from .errors import UnicornViewError
1616
from .serializer import dumps
1717
from .utils import generate_checksum
@@ -398,6 +398,12 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
398398
redirect_data = {
399399
"hash": return_value.hash,
400400
}
401+
elif isinstance(return_value, LocationUpdate):
402+
redirect_data = {
403+
"url": return_value.redirect.url,
404+
"refresh": True,
405+
"title": return_value.title,
406+
}
401407
else:
402408
try:
403409
return_data = orjson.loads(dumps(return_value))

example/unicorn/components/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from coffee.models import Flavor
55

6-
from django_unicorn.components import HashUpdate, UnicornView
6+
from django_unicorn.components import HashUpdate, LocationUpdate, UnicornView
77
from django_unicorn.db import DbModel
88
from django_unicorn.decorators import db_model
99

@@ -33,7 +33,8 @@ def add_instance_flavor(self):
3333
id = self.instance_flavor.id
3434
self.reset()
3535

36-
return HashUpdate(f"#createdId={id}")
36+
# return HashUpdate(f"#createdId={id}")
37+
return LocationUpdate(redirect(f"/models?createdId={id}"), title="new title")
3738

3839
def add_class_flavor(self):
3940
self.class_flavor.save()

tests/js/component/messageSender.test.js

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fetchMock from "fetch-mock";
33
import { getComponent } from "../utils.js";
44
import { send } from "../../../django_unicorn/static/js/messageSender.js";
55

6-
test.cb("click on internal element", (t) => {
6+
test.cb("call_method redirect", (t) => {
77
const html = `
88
<input type="hidden" name="csrfmiddlewaretoken" value="asdf">
99
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
@@ -41,3 +41,88 @@ test.cb("click on internal element", (t) => {
4141
t.end();
4242
});
4343
});
44+
45+
test.cb("call_method refresh redirect", (t) => {
46+
const html = `
47+
<input type="hidden" name="csrfmiddlewaretoken" value="asdf">
48+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
49+
<input unicorn:model='name'></input>
50+
<button unicorn:click='test()'><span id="clicker">Click</span></button>
51+
</div>
52+
`;
53+
54+
const component = getComponent(html);
55+
56+
t.is(component.attachedEventTypes.length, 1);
57+
t.is(component.actionEvents.click.length, 1);
58+
59+
component.actionEvents.click[0].element.el.click();
60+
61+
t.is(component.actionQueue.length, 1);
62+
63+
// mock the fetch
64+
const res = {
65+
id: "aQzrrRoG",
66+
dom: "",
67+
data: {
68+
name: "World",
69+
},
70+
errors: {},
71+
redirect: {
72+
url: "/test/text-inputs?some=query",
73+
refresh: true,
74+
title: "new title",
75+
},
76+
return: {},
77+
};
78+
global.fetch = fetchMock.sandbox().mock().post("/test/text-inputs", res);
79+
80+
send(component, (a, b, err) => {
81+
t.not(err);
82+
t.is(component.window.history.get(), "/test/text-inputs?some=query");
83+
t.is(component.window.document.title, "new title");
84+
fetchMock.reset();
85+
t.end();
86+
});
87+
});
88+
89+
test.cb("call_method hash", (t) => {
90+
const html = `
91+
<input type="hidden" name="csrfmiddlewaretoken" value="asdf">
92+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
93+
<input unicorn:model='name'></input>
94+
<button unicorn:click='test()'><span id="clicker">Click</span></button>
95+
</div>
96+
`;
97+
98+
const component = getComponent(html);
99+
100+
t.is(component.attachedEventTypes.length, 1);
101+
t.is(component.actionEvents.click.length, 1);
102+
103+
component.actionEvents.click[0].element.el.click();
104+
105+
t.is(component.actionQueue.length, 1);
106+
107+
// mock the fetch
108+
const res = {
109+
id: "aQzrrRoG",
110+
dom: "",
111+
data: {
112+
name: "World",
113+
},
114+
errors: {},
115+
redirect: {
116+
hash: "#somehash",
117+
},
118+
return: {},
119+
};
120+
global.fetch = fetchMock.sandbox().mock().post("/test/text-inputs", res);
121+
122+
send(component, (a, b, err) => {
123+
t.not(err);
124+
t.is(component.window.location.hash, "#somehash");
125+
fetchMock.reset();
126+
t.end();
127+
});
128+
});

tests/js/utils.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,26 @@ export function getComponent(html, id, name, data) {
8888

8989
const document = getDocument(html);
9090

91+
const mockHistory = { urls: [] };
92+
mockHistory.pushState = (state, title, url) => {
93+
mockHistory.urls.push(url);
94+
};
95+
mockHistory.get = () => {
96+
return mockHistory.urls[0];
97+
};
98+
9199
const component = new Component({
92100
id,
93101
name,
94102
data,
95103
document,
96104
messageUrl: "test",
97105
walker: walkDOM,
98-
window: { location: { href: "" } },
106+
window: {
107+
document: { title: "" },
108+
history: mockHistory,
109+
location: { href: "" },
110+
},
99111
});
100112

101113
const res = {

tests/views/fake_components.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django import forms
44
from django.shortcuts import redirect
55

6-
from django_unicorn.components import HashUpdate, UnicornView
6+
from django_unicorn.components import HashUpdate, LocationUpdate, UnicornView
77
from example.coffee.models import Flavor
88

99

@@ -28,6 +28,9 @@ def test_method_string_param(self, param):
2828
def test_redirect(self):
2929
return redirect("/something-here")
3030

31+
def test_refresh_redirect(self):
32+
return LocationUpdate(redirect("/something-here"), title="new title")
33+
3134
def test_hash_update(self):
3235
return HashUpdate("#test=1")
3336

tests/views/message/test_call_method.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,35 @@ def test_message_call_method_redirect(client):
4141
body = orjson.loads(response.content)
4242

4343
assert "redirect" in body
44-
assert body["redirect"].get("url") == "/something-here"
44+
redirect = body["redirect"]
45+
assert redirect.get("url") == "/something-here"
46+
assert redirect.get("refresh", False) == False
47+
48+
49+
def test_message_call_method_refresh_redirect(client):
50+
data = {}
51+
message = {
52+
"actionQueue": [
53+
{"payload": {"name": "test_refresh_redirect"}, "type": "callMethod",}
54+
],
55+
"data": data,
56+
"checksum": generate_checksum(orjson.dumps(data)),
57+
"id": "FDHcbzGf",
58+
}
59+
60+
response = client.post(
61+
"/message/tests.views.fake_components.FakeComponent",
62+
message,
63+
content_type="application/json",
64+
)
65+
66+
body = orjson.loads(response.content)
67+
68+
assert "redirect" in body
69+
redirect = body["redirect"]
70+
assert redirect.get("url") == "/something-here"
71+
assert redirect.get("refresh")
72+
assert redirect.get("title") == "new title"
4573

4674

4775
def test_message_call_method_hash_update(client):

0 commit comments

Comments
 (0)