diff --git a/cypress/integration/websocket_spec.js b/cypress/integration/websocket_spec.js index 341651b..e9c8052 100644 --- a/cypress/integration/websocket_spec.js +++ b/cypress/integration/websocket_spec.js @@ -79,4 +79,12 @@ describe("Integration tests", () => { cy.get('#user-reflex').should('have.text', 'test_user') }) + + 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 0799549..9d94b09 100644 --- a/sockpuppet/consumer.py +++ b/sockpuppet/consumer.py @@ -173,6 +173,7 @@ def reflex_message(self, data, **kwargs): url = data["url"] selectors = data["selectors"] if data["selectors"] else ["body"] target = data["target"] + identifier = data["identifier"] reflex_class_name, method_name = target.split("#") arguments = data["args"] if data.get("args") else [] params = dict(parse_qsl(data["formData"])) @@ -180,10 +181,22 @@ def reflex_message(self, data, **kwargs): if not self.reflexes: self.load_reflexes() + # 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_class_name) reflex = ReflexClass( - self, url=url, element=element, selectors=selectors, params=params + 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 as exc: @@ -218,6 +231,10 @@ def reflex_message(self, data, **kwargs): logger.debug("Reflex took %6.2fms", (time.perf_counter() - start) * 1000) 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) diff --git a/sockpuppet/reflex.py b/sockpuppet/reflex.py index a6f1da0..f5d0e4b 100644 --- a/sockpuppet/reflex.py +++ b/sockpuppet/reflex.py @@ -1,11 +1,16 @@ from django.urls import resolve from urllib.parse import urlparse +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", @@ -13,7 +18,10 @@ 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 @@ -21,6 +29,10 @@ def __init__(self, consumer, url, element, selectors, params): self.session = consumer.scope["session"] self.params = params self.context = {} + self.identifier = identifier + self.is_morph = False + self.reflex_id = reflex_id + self.permanent_attribute_name = permanent_attribute_name def __repr__(self): return f"" @@ -68,3 +80,35 @@ def request(self): def reload(self): """A default reflex to force a refresh""" pass + + 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 c7034f4..578212e 100644 --- a/tests/example/reflexes/example_reflex.py +++ b/tests/example/reflexes/example_reflex.py @@ -31,3 +31,8 @@ class UserReflex(Reflex): def get_user(self): context = self.get_context_data() self.user_reveal = context['object'] + + +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 4665841..0a7a36c 100644 --- a/tests/example/templates/example.html +++ b/tests/example/templates/example.html @@ -37,4 +37,8 @@ {{ text_output }} {{ stimulus_reflex }} + + + +