Skip to content

Commit 8177333

Browse files
committed
Add the possibility of updating html through morphs
1 parent 455c71a commit 8177333

File tree

5 files changed

+80
-2
lines changed

5 files changed

+80
-2
lines changed

cypress/integration/websocket_spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,12 @@ describe("Integration tests", () => {
7979

8080
cy.get('#user-reflex').should('have.text', 'test_user')
8181
})
82+
83+
it("can send a morph in a reflex", () => {
84+
cy.visit('/test')
85+
cy.wait(200)
86+
cy.get('#morph-button').click()
87+
88+
cy.get('#morph').should('have.text', 'I got morphed!')
89+
})
8290
})

sockpuppet/consumer.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,30 @@ def reflex_message(self, data, **kwargs):
173173
url = data["url"]
174174
selectors = data["selectors"] if data["selectors"] else ["body"]
175175
target = data["target"]
176+
identifier = data["identifier"]
176177
reflex_class_name, method_name = target.split("#")
177178
arguments = data["args"] if data.get("args") else []
178179
params = dict(parse_qsl(data["formData"]))
179180
element = Element(data["attrs"])
180181
if not self.reflexes:
181182
self.load_reflexes()
182183

184+
# TODO can be removed once stimulus-reflex has increased a couple of versions
185+
permanent_attribute_name = data.get('permanent_attribute_name')
186+
if not permanent_attribute_name:
187+
# Used in stimulus-reflex >= 3.4
188+
permanent_attribute_name = data['permanentAttributeName']
189+
183190
try:
184191
ReflexClass = self.reflexes.get(reflex_class_name)
185192
reflex = ReflexClass(
186-
self, url=url, element=element, selectors=selectors, params=params
193+
self, url=url,
194+
element=element,
195+
selectors=selectors,
196+
identifier=identifier,
197+
params=params,
198+
reflex_id=data['reflexId'],
199+
permanent_attribute_name=permanent_attribute_name
187200
)
188201
self.delegate_call_to_reflex(reflex, method_name, arguments)
189202
except TypeError as exc:
@@ -218,6 +231,10 @@ def reflex_message(self, data, **kwargs):
218231
logger.debug("Reflex took %6.2fms", (time.perf_counter() - start) * 1000)
219232

220233
def render_page_and_broadcast_morph(self, reflex, selectors, data):
234+
if reflex.is_morph:
235+
# The reflex has already sent a message so consumer doesn't need to.
236+
return
237+
221238
html = self.render_page(reflex)
222239
if html:
223240
self.broadcast_morphs(selectors, data, html, reflex)

sockpuppet/reflex.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
11
from django.urls import resolve
22
from urllib.parse import urlparse
33

4+
from django.template.loader import render_to_string
5+
from django.template.backends.django import Template
46
from django.test import RequestFactory
57

8+
from .channel import Channel
9+
610
PROTECTED_VARIABLES = [
711
"consumer",
812
"element",
13+
"is_morph",
914
"selectors",
1015
"session",
1116
"url",
1217
]
1318

1419

1520
class Reflex:
16-
def __init__(self, consumer, url, element, selectors, params):
21+
def __init__(
22+
self, consumer, url, element, selectors, params, identifier='',
23+
permanent_attribute_name=None, reflex_id=None
24+
):
1725
self.consumer = consumer
1826
self.url = url
1927
self.element = element
2028
self.selectors = selectors
2129
self.session = consumer.scope["session"]
2230
self.params = params
2331
self.context = {}
32+
self.identifier = identifier
33+
self.is_morph = False
34+
self.reflex_id = reflex_id
35+
self.permanent_attribute_name = permanent_attribute_name
2436

2537
def __repr__(self):
2638
return f"<Reflex url: {self.url}, session: {self.get_channel_id()}>"
@@ -68,3 +80,35 @@ def request(self):
6880
def reload(self):
6981
"""A default reflex to force a refresh"""
7082
pass
83+
84+
def morph(self, selector='', html='', template='', context={}):
85+
"""
86+
If a morph is executed without any arguments, nothing is executed
87+
and the reflex won't send over any data to the frontend.
88+
"""
89+
self.is_morph = True
90+
no_arguments = [not selector, not html, (not template and not context)]
91+
if all(no_arguments) and not selector:
92+
# an empty morph, nothing is sent ever.
93+
return
94+
95+
if html:
96+
html = html
97+
elif isinstance(template, Template):
98+
html = template.render(context)
99+
else:
100+
html = render_to_string(template, context)
101+
102+
broadcaster = Channel(self.get_channel_id(), identifier=self.identifier)
103+
broadcaster.morph({
104+
'selector': selector,
105+
'html': html,
106+
'children_only': True,
107+
'permanent_attribute_name': self.permanent_attribute_name,
108+
'stimulus_reflex': {
109+
'morph': 'selector',
110+
'reflexId': self.reflex_id,
111+
'url': self.url
112+
}
113+
})
114+
broadcaster.broadcast()

tests/example/reflexes/example_reflex.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ class UserReflex(Reflex):
3131
def get_user(self):
3232
context = self.get_context_data()
3333
self.user_reveal = context['object']
34+
35+
36+
class MorphReflex(Reflex):
37+
def morph_me(self):
38+
self.morph('#morph', 'I got morphed!')

tests/example/templates/example.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@
3737
<span id="text-output">{{ text_output }}</span>
3838

3939
<span id="stimulus-reflex">{{ stimulus_reflex }}</span>
40+
41+
<button id="morph-button" data-reflex="click->MorphReflex#morph_me">Morph me</button>
42+
<span id="morph"></span>
43+
4044
</body>

0 commit comments

Comments
 (0)