diff --git a/docs/templating.md b/docs/templating.md
new file mode 100644
index 0000000..4db9dbc
--- /dev/null
+++ b/docs/templating.md
@@ -0,0 +1,28 @@
+---
+description: How to use custom template tags
+---
+
+# Template Tags
+
+Multiple custom template tags are provided by the library to make it easier to generate HTML attributes that interact with reflex.
+These are `stimulus_controller`, `reflex` and `*_reflex` where `*` describes valid stimulus actions like _click_, _submit_, ...
+
+## The *_reflex tags
+
+The `*_reflex` tags can be used to connect a HTML attibute like the `` Tag to a reflex. Some syntax examples are
+
+```
+click me
+click me
+
+```
+
+where different tags are used for different _actions_.
+
+## The Syntax of the *_reflex tags
+
+There are three ways to pass data to the *_reflex tags:
+1. Pass `controller` and `reflex` as list attributes and all parameters (`data-` attributes) as kvargs
+2. Use the `*_reflex` tag inside a `stimulus_controlle` Block and only pass `reflex` (controller will be taken automatically from the `stimulus_controller`). Parameters work the same as above.
+3. Use a `dict` as sole list argument which has to contain the keys `controller` and `reflex`. All other dict entries as well as all given kvargs are used as `data-` attributes.
+
diff --git a/sockpuppet/templatetags/sockpuppet.py b/sockpuppet/templatetags/sockpuppet.py
index 737596c..10b0877 100644
--- a/sockpuppet/templatetags/sockpuppet.py
+++ b/sockpuppet/templatetags/sockpuppet.py
@@ -1,5 +1,8 @@
from django import template
-from django.template import Template
+from django.template import Template, RequestContext
+from django.template.base import Token, VariableNode, FilterExpression
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
register = template.Library()
@@ -25,7 +28,136 @@ def render(self, context):
elif node.token.token_type.name == 'VAR':
raw = '{{ ' + node.token.contents + ' }}'
else:
- msg ='{} is not yet handled'.format(node.token.token_type.name)
+ msg = '{} is not yet handled'.format(node.token.token_type.name)
raise Exception(msg)
output = output + raw
return output
+
+
+@register.simple_tag
+def reflex(controller, **kwargs):
+ """
+ Adds the necessary data-reflex tag to handle a click element on the respective element
+ :param controller: Name of the Reflex Controller and Method ({controller}#{handler}).
+ :param kwargs: Further data- attributes that should be passed to the handler
+ """
+ # TODO Validate that the reflex is present and can be handled
+ return generate_reflex_attributes('click', controller, kwargs)
+
+
+def generate_reflex_attributes(action, controller, parameters):
+ data = ' '.join([f'data-{key}="{val}"' for key, val in parameters.items()])
+ return mark_safe(f'data-reflex="{action}->{controller}" {data}')
+
+
+@register.tag
+def stimulus_controller(parser, token, **kwargs):
+ _, controller = token.split_contents()
+ nodelist = parser.parse(('endcontroller',))
+ parser.delete_first_token()
+ controller = controller.strip("'").strip('"')
+ return StimulusNode(controller, nodelist)
+
+
+class ReflexNode(template.Node):
+
+ def __init__(self, action, reflex, controller=None, parameters={}):
+ self.action = action
+ self.reflex = reflex
+ self.controller = controller
+ self.parameters = parameters
+
+ def render(self, context: RequestContext):
+ # First, check if the "reflex" given is a VariableNode. We check to extract stuff from it
+ parameters = {}
+ if isinstance(self.reflex, VariableNode):
+ var_name = self.reflex.filter_expression.token
+ param = context.get(var_name)
+
+ if param is None:
+ raise Exception(f"The given Variable '{var_name}' was not found in the context!")
+
+ if 'controller' not in param or 'reflex' not in param:
+ raise Exception(f"The given object with name '{var_name}' needs to have attributes 'controller' and 'reflex'")
+ else:
+ self.controller = param['controller']
+ self.reflex = param['reflex']
+ parameters.update({k: param[k] for k in param.keys() if k not in ('controller', 'reflex')})
+ if self.controller is None:
+ raise Exception(
+ "A ClickReflex tag can only be used inside a stimulus controller or needs an explicit controller set!")
+
+ for k, v in self.parameters.items():
+ if isinstance(v, VariableNode):
+ value = v.render(context)
+ else:
+ value = v
+ parameters.update({k: value})
+ return generate_reflex_attributes(self.action, f'{self.controller}#{self.reflex}', parameters)
+
+
+def extract_string_or_node(text):
+ stripped = text.strip("'").strip('"')
+ is_numeric = False
+ try:
+ int(stripped)
+ is_numeric = True
+ except:
+ pass
+ if text == stripped and not is_numeric:
+ return VariableNode(FilterExpression(text, parser=None))
+ else:
+ return stripped
+
+
+@register.tag("click_reflex")
+def click_reflex(parser, token: Token):
+ controller, kwargs, reflex = parse_reflex_token(token)
+ return ReflexNode('click', reflex, controller=controller, parameters=kwargs)
+
+
+@register.tag("submit_reflex")
+def submit_reflex(parser, token: Token):
+ controller, kwargs, reflex = parse_reflex_token(token)
+ return ReflexNode('submit', reflex, controller=controller, parameters=kwargs)
+
+
+@register.tag("input_reflex")
+def submit_reflex(parser, token: Token):
+ controller, kwargs, reflex = parse_reflex_token(token)
+ return ReflexNode('input', reflex, controller=controller, parameters=kwargs)
+
+
+def parse_reflex_token(token):
+ splitted = token.split_contents()[1:]
+ args = []
+ kwargs = {}
+ for s in splitted:
+ if s.__contains__("="):
+ k, v = s.split("=")
+ kwargs.update({k: extract_string_or_node(v)})
+ else:
+ args.append(extract_string_or_node(s))
+ if len(args) == 1:
+ reflex = args[0]
+ controller = None
+ elif len(args) == 2:
+ controller = args[0]
+ reflex = args[1]
+ else:
+ raise Exception('Only one or two non-kv parameters can be given!')
+ return controller, kwargs, reflex
+
+
+class StimulusNode(template.Node):
+
+ def __init__(self, controller, nodelist):
+ self.controller = controller
+ self.nodelist = nodelist
+
+ def render(self, context):
+ for node in self.nodelist:
+ if isinstance(node, ReflexNode):
+ node.controller = self.controller
+ output = self.nodelist.render(context)
+ return output
diff --git a/tests/example/templates/second_tag_example.html b/tests/example/templates/second_tag_example.html
new file mode 100644
index 0000000..51148a6
--- /dev/null
+++ b/tests/example/templates/second_tag_example.html
@@ -0,0 +1,6 @@
+{% load sockpuppet %}
+{% stimulus_controller 'example_reflex' %}
+ click me
+{% endcontroller %}
+
+I was done by object definition
diff --git a/tests/example/templates/tag_example.html b/tests/example/templates/tag_example.html
new file mode 100644
index 0000000..4771f31
--- /dev/null
+++ b/tests/example/templates/tag_example.html
@@ -0,0 +1,4 @@
+{% load sockpuppet %}
+click me
+click me
+
diff --git a/tests/example/urls.py b/tests/example/urls.py
index 6b91f8d..5bdb0ea 100644
--- a/tests/example/urls.py
+++ b/tests/example/urls.py
@@ -16,9 +16,11 @@
from django.urls import path
-from .views.example import ExampleView, ParamView
+from .views.example import ExampleView, ParamView, TagExampleView, SecondTagExampleView
urlpatterns = [
path('test/', ExampleView.as_view(), name='example'),
- path('param/', ParamView.as_view(), name='param')
+ path('param/', ParamView.as_view(), name='param'),
+ path('tag/', TagExampleView.as_view(), name='tag'),
+ path('second/', SecondTagExampleView.as_view(), name='second_tag')
]
diff --git a/tests/example/views/example.py b/tests/example/views/example.py
index cccda4e..4c2b4cd 100644
--- a/tests/example/views/example.py
+++ b/tests/example/views/example.py
@@ -18,3 +18,24 @@ def get(self, request, *args, **kwargs):
kwargs.update(dict(self.request.GET.items()))
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
+
+
+class TagExampleView(TemplateView):
+ template_name = 'tag_example.html'
+
+ def get(self, request, *args, **kwargs):
+ kwargs.update({"parameter": "I am a parameter"})
+ kwargs.update(dict(self.request.GET.items()))
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
+
+
+class SecondTagExampleView(TemplateView):
+ template_name = 'second_tag_example.html'
+
+ def get(self, request, *args, **kwargs):
+ kwargs.update({"parameter": "I am a parameter"})
+ kwargs.update({"object_definition": {"controller": "abc", "reflex": "increment", "other_param": 123}})
+ kwargs.update(dict(self.request.GET.items()))
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
diff --git a/tests/test_tag.py b/tests/test_tag.py
new file mode 100644
index 0000000..8e73d4f
--- /dev/null
+++ b/tests/test_tag.py
@@ -0,0 +1,67 @@
+from django.template.response import TemplateResponse
+from django.test import TestCase, Client
+
+
+class TestTagSupport(TestCase):
+
+ def test_click_reflex_tag(self):
+ c = Client()
+ response: TemplateResponse = c.get('/tag/')
+
+ content = response.content.decode('utf-8')
+
+ self.assertTrue(content.__contains__(''
+ 'click me'
+ ''))
+
+ def test_submit_reflex_tag(self):
+ c = Client()
+ response: TemplateResponse = c.get('/tag/')
+
+ content = response.content.decode('utf-8')
+
+ self.assertTrue(content.__contains__(''
+ 'click me'
+ ''))
+
+ def test_input_reflex_tag(self):
+ c = Client()
+ response: TemplateResponse = c.get('/tag/')
+
+ content = response.content.decode('utf-8')
+
+ self.assertTrue(content.__contains__(
+ ''))
+
+ def test_reflex_tag_with_unsafe_input(self):
+ c = Client()
+ response: TemplateResponse = c.get('/tag/', data={"parameter": ""})
+
+ content = response.content.decode('utf-8')
+
+ self.assertTrue(content.__contains__(''
+ 'click me'
+ ''))
+
+ def test_controller_tag(self):
+ c = Client()
+ response: TemplateResponse = c.get('/second/', data={"parameter": ""})
+
+ content = response.content.decode('utf-8')
+
+ self.assertTrue(content.__contains__(
+ 'click me'))
+
+ def test_tag_by_dict(self):
+ c = Client()
+ response: TemplateResponse = c.get('/second/', data={"parameter": ""})
+
+ content = response.content.decode('utf-8')
+
+ print(content)
+
+ self.assertTrue(content.__contains__(
+ 'I was done by object definition'))