Skip to content

Commit 9cfcff7

Browse files
committed
Add the possibility of updating html through morphs
1 parent 4e63613 commit 9cfcff7

5 files changed

Lines changed: 92 additions & 13 deletions

File tree

cypress/integration/websocket_spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,13 @@ describe("Integration tests", () => {
1919

2020
cy.get('#decrementor').click()
2121
cy.get('#counter-2').should('have.text', '-1')
22+
}),
23+
24+
it("can send a morph in a reflex", () => {
25+
cy.visit('/test')
26+
cy.wait(200)
27+
cy.get('#morph-button').click()
28+
29+
cy.get('#morph').should('have.text', 'I got morphed!')
2230
})
2331
})

sockpuppet/consumer.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,28 @@ def reflex_message(self, data, **kwargs):
146146
url = data['url']
147147
selectors = data['selectors'] if data['selectors'] else ['body']
148148
target = data['target']
149+
identifier = data['identifier']
149150
reflex_name, method_name = target.split('#')
150151
reflex_name = classify(reflex_name)
151152
arguments = data['args'] if data.get('args') else []
152153
element = Element(data['attrs'])
154+
155+
# TODO can be removed once stimulus-reflex has increased a couple of versions
156+
permanent_attribute_name = data.get('permanent_attribute_name')
157+
if not permanent_attribute_name:
158+
# Used in stimulus-reflex >= 3.4
159+
permanent_attribute_name = data['permanentAttributeName']
160+
153161
try:
154162
ReflexClass = self.reflexes.get(reflex_name)
155-
reflex = ReflexClass(self, url=url, element=element, selectors=selectors)
163+
reflex = ReflexClass(
164+
self, url=url,
165+
element=element,
166+
selectors=selectors,
167+
identifier=identifier,
168+
reflex_id=data['reflexId'],
169+
permanent_attribute_name=permanent_attribute_name
170+
)
156171
self.delegate_call_to_reflex(reflex, method_name, arguments)
157172
except Exception as e:
158173
error = '{}: {}'.format(e.__class__.__name__, str(e))
@@ -173,7 +188,7 @@ def reflex_message(self, data, **kwargs):
173188
)
174189
self.broadcast_error(message, data, reflex)
175190
_, _, traceback = sys.exc_info()
176-
exc = SockpuppetError(msg)
191+
exc = SockpuppetError(message)
177192
raise exc.with_traceback(traceback)
178193

179194
logger.debug('Reflex took %6.2fms', (time.perf_counter() - start) * 1000)
@@ -214,6 +229,10 @@ def receive_json(self, data, **kwargs):
214229
print('Unsupported')
215230

216231
def render_page_and_broadcast_morph(self, reflex, selectors, data):
232+
if reflex.is_morph:
233+
# The reflex has already sent a message so consumer doesn't need to.
234+
return
235+
217236
html = self.render_page(reflex)
218237
if html:
219238
self.broadcast_morphs(selectors, data, html, reflex)
@@ -245,7 +264,7 @@ def broadcast_morphs(self, selectors, data, html, reflex):
245264
document = BeautifulSoup(html)
246265
selectors = [selector for selector in selectors if document.select(selector)]
247266

248-
channel = Channel(reflex.get_channel_id(), identifier=data['identifier'])
267+
broadcaster = Channel(reflex.get_channel_id(), identifier=data['identifier'])
249268
logger.debug('Broadcasting to %s', reflex.get_channel_id())
250269

251270
# TODO can be removed once stimulus-reflex has increased a couple of versions
@@ -255,14 +274,14 @@ def broadcast_morphs(self, selectors, data, html, reflex):
255274
permanent_attribute_name = data['permanentAttributeName']
256275

257276
for selector in selectors:
258-
channel.morph({
277+
broadcaster.morph({
259278
'selector': selector,
260279
'html': ''.join([e.decode_contents() for e in document.select(selector)]),
261280
'children_only': True,
262281
'permanent_attribute_name': permanent_attribute_name,
263282
'stimulus_reflex': {**data}
264283
})
265-
channel.broadcast()
284+
broadcaster.broadcast()
266285

267286
def delegate_call_to_reflex(self, reflex, method_name, arguments):
268287
method = getattr(reflex, method_name)

sockpuppet/reflex.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
1+
from django.template.loader import render_to_string
2+
from django.template.backends.django import Template
13
from django.test import RequestFactory
24

5+
from .channel import Channel
6+
37
PROTECTED_VARIABLES = [
48
'consumer',
59
'element',
10+
'is_morph',
611
'selectors',
712
'session',
813
'url',
914
]
1015

1116

1217
class Reflex:
13-
def __init__(self, consumer, url, element, selectors):
18+
def __init__(
19+
self, consumer, url, element, selectors, identifier='', permanent_attribute_name=None, reflex_id=None
20+
):
1421
self.consumer = consumer
1522
self.url = url
1623
self.element = element
1724
self.selectors = selectors
1825
self.session = consumer.scope['session']
19-
20-
def get_channel_id(self):
21-
'''
22-
Override this to make the reflex send to a different channel
23-
other than the session_key of the user
24-
'''
25-
return self.session.session_key
26+
self.identifier = identifier
27+
self.is_morph = False
28+
self.reflex_id = reflex_id
29+
self.permanent_attribute_name = permanent_attribute_name
2630

2731
@property
2832
def request(self):
@@ -31,3 +35,42 @@ def request(self):
3135
request.session = self.consumer.scope['session']
3236
request.user = self.consumer.scope['user']
3337
return request
38+
39+
def get_channel_id(self):
40+
'''
41+
Override this to make the reflex send to a different channel
42+
other than the session_key of the user
43+
'''
44+
return self.session.session_key
45+
46+
def morph(self, selector='', html='', template='', context={}):
47+
"""
48+
If a morph is executed without any arguments, nothing is executed
49+
and the reflex won't send over any data to the frontend.
50+
"""
51+
self.is_morph = True
52+
no_arguments = [not selector, not html, (not template and not context)]
53+
if all(no_arguments) and not selector:
54+
# an empty morph, nothing is sent ever.
55+
return
56+
57+
if html:
58+
html = html
59+
elif isinstance(template, Template):
60+
html = template.render(context)
61+
else:
62+
html = render_to_string(template, context)
63+
64+
broadcaster = Channel(self.get_channel_id(), identifier=self.identifier)
65+
broadcaster.morph({
66+
'selector': selector,
67+
'html': html,
68+
'children_only': True,
69+
'permanent_attribute_name': self.permanent_attribute_name,
70+
'stimulus_reflex': {
71+
'morph': 'selector',
72+
'reflexId': self.reflex_id,
73+
'url': self.url
74+
}
75+
})
76+
broadcaster.broadcast()

tests/example/reflexes/example_reflex.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ def increment(self, step=1):
99
class DecrementReflex(Reflex):
1010
def decrement(self, step=1):
1111
self.session['otherCount'] = int(self.element.dataset['count']) - step
12+
13+
14+
class MorphReflex(Reflex):
15+
def morph_me(self):
16+
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
@@ -24,4 +24,8 @@
2424
>Decrement</button>
2525
<div id="counter-2">{{ otherCount }}</div>
2626
</div>
27+
28+
<button id="morph-button" data-reflex="click->MorphReflex#morph_me">Morph me</button>
29+
<span id="morph"></span>
30+
2731
</body>

0 commit comments

Comments
 (0)