diff --git a/cypress/integration/websocket_spec.js b/cypress/integration/websocket_spec.js
index 95ccfdb..54aaf42 100644
--- a/cypress/integration/websocket_spec.js
+++ b/cypress/integration/websocket_spec.js
@@ -47,4 +47,12 @@ describe("Integration tests", () => {
cy.get('#decrementor').click()
cy.get('#decrementor-counter').should('have.text', '-1')
})
+
+ it("can send a morph in a reflex", () => {
+ cy.visit('/test')
+ cy.wait(200)
+ cy.get('#morph-button').click()
+
+ cy.get('#morph').should('have.text', 'I got morphed!')
+ })
})
diff --git a/sockpuppet/consumer.py b/sockpuppet/consumer.py
index a47b252..e3ad747 100644
--- a/sockpuppet/consumer.py
+++ b/sockpuppet/consumer.py
@@ -148,14 +148,30 @@ def reflex_message(self, data, **kwargs):
url = data['url']
selectors = data['selectors'] if data['selectors'] else ['body']
target = data['target']
+ identifier = data['identifier']
reflex_name, method_name = target.split('#')
reflex_name = classify(reflex_name)
arguments = data['args'] if data.get('args') else []
params = dict(parse_qsl(data['formData']))
element = Element(data['attrs'])
+
+ # TODO can be removed once stimulus-reflex has increased a couple of versions
+ permanent_attribute_name = data.get('permanent_attribute_name')
+ if not permanent_attribute_name:
+ # Used in stimulus-reflex >= 3.4
+ permanent_attribute_name = data['permanentAttributeName']
+
try:
ReflexClass = self.reflexes.get(reflex_name)
- reflex = ReflexClass(self, url=url, element=element, selectors=selectors, params=params)
+ reflex = ReflexClass(
+ self, url=url,
+ element=element,
+ selectors=selectors,
+ identifier=identifier,
+ params=params,
+ reflex_id=data['reflexId'],
+ permanent_attribute_name=permanent_attribute_name
+ )
self.delegate_call_to_reflex(reflex, method_name, arguments)
except TypeError:
if not self.reflexes.get(reflex_name):
@@ -223,6 +239,10 @@ def receive_json(self, data, **kwargs):
print('Unsupported')
def render_page_and_broadcast_morph(self, reflex, selectors, data):
+ if reflex.is_morph:
+ # The reflex has already sent a message so consumer doesn't need to.
+ return
+
html = self.render_page(reflex)
if html:
self.broadcast_morphs(selectors, data, html, reflex)
@@ -252,7 +272,7 @@ def render_page(self, reflex):
def broadcast_morphs(self, selectors, data, html, reflex):
document, selectors = get_document_and_selectors(html, selectors)
- channel = Channel(reflex.get_channel_id(), identifier=data['identifier'])
+ broadcaster = Channel(reflex.get_channel_id(), identifier=data['identifier'])
logger.debug('Broadcasting to %s', reflex.get_channel_id())
# TODO can be removed once stimulus-reflex has increased a couple of versions
@@ -264,14 +284,14 @@ def broadcast_morphs(self, selectors, data, html, reflex):
for selector in selectors:
# cssselect has an attribute css
plain_selector = getattr(selector, 'css', selector)
- channel.morph({
+ broadcaster.morph({
'selector': plain_selector,
'html': parse_out_html(document, selector),
'children_only': True,
'permanent_attribute_name': permanent_attribute_name,
'stimulus_reflex': {**data}
})
- channel.broadcast()
+ broadcaster.broadcast()
def delegate_call_to_reflex(self, reflex, method_name, arguments):
method = getattr(reflex, method_name)
diff --git a/sockpuppet/reflex.py b/sockpuppet/reflex.py
index 339326a..6e631a9 100644
--- a/sockpuppet/reflex.py
+++ b/sockpuppet/reflex.py
@@ -1,8 +1,13 @@
+from django.template.loader import render_to_string
+from django.template.backends.django import Template
from django.test import RequestFactory
+from .channel import Channel
+
PROTECTED_VARIABLES = [
'consumer',
'element',
+ 'is_morph',
'selectors',
'session',
'url',
@@ -10,20 +15,20 @@
class Reflex:
- def __init__(self, consumer, url, element, selectors, params):
+ def __init__(
+ self, consumer, url, element, selectors, params, identifier='',
+ permanent_attribute_name=None, reflex_id=None
+ ):
self.consumer = consumer
self.url = url
self.element = element
self.selectors = selectors
self.session = consumer.scope['session']
self.params = params
-
- def get_channel_id(self):
- '''
- Override this to make the reflex send to a different channel
- other than the session_key of the user
- '''
- return self.session.session_key
+ self.identifier = identifier
+ self.is_morph = False
+ self.reflex_id = reflex_id
+ self.permanent_attribute_name = permanent_attribute_name
@property
def request(self):
@@ -37,3 +42,42 @@ def request(self):
def reload(self):
"""A default reflex to force a refresh"""
pass
+
+ def get_channel_id(self):
+ '''
+ Override this to make the reflex send to a different channel
+ other than the session_key of the user
+ '''
+ return self.session.session_key
+
+ def morph(self, selector='', html='', template='', context={}):
+ """
+ If a morph is executed without any arguments, nothing is executed
+ and the reflex won't send over any data to the frontend.
+ """
+ self.is_morph = True
+ no_arguments = [not selector, not html, (not template and not context)]
+ if all(no_arguments) and not selector:
+ # an empty morph, nothing is sent ever.
+ return
+
+ if html:
+ html = html
+ elif isinstance(template, Template):
+ html = template.render(context)
+ else:
+ html = render_to_string(template, context)
+
+ broadcaster = Channel(self.get_channel_id(), identifier=self.identifier)
+ broadcaster.morph({
+ 'selector': selector,
+ 'html': html,
+ 'children_only': True,
+ 'permanent_attribute_name': self.permanent_attribute_name,
+ 'stimulus_reflex': {
+ 'morph': 'selector',
+ 'reflexId': self.reflex_id,
+ 'url': self.url
+ }
+ })
+ broadcaster.broadcast()
diff --git a/tests/example/reflexes/example_reflex.py b/tests/example/reflexes/example_reflex.py
index 8722416..ddb9f44 100644
--- a/tests/example/reflexes/example_reflex.py
+++ b/tests/example/reflexes/example_reflex.py
@@ -19,3 +19,8 @@ def change_word(self):
class FormReflex(Reflex):
def submit(self):
self.text_output = self.request.POST['text-input']
+
+
+class MorphReflex(Reflex):
+ def morph_me(self):
+ self.morph('#morph', 'I got morphed!')
diff --git a/tests/example/templates/example.html b/tests/example/templates/example.html
index b4193b9..d41f319 100644
--- a/tests/example/templates/example.html
+++ b/tests/example/templates/example.html
@@ -35,4 +35,9 @@
{{ text_output }}
+
+
+
+
+