Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom stimulus tag to make it easier to create stimuli in django… #60

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/templating.md
Original file line number Diff line number Diff line change
@@ -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 `<a>` Tag to a reflex. Some syntax examples are

```
<a href="#" {% click_reflex 'example_reflex' 'increment' parameter=parameter %}>click me</a>
<a href="#" {% submit_reflex 'example_reflex' 'increment' parameter=parameter %}>click me</a>
<input type="text" {% input_reflex 'example_reflex' 'increment' parameter=parameter %}/>
```

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.

136 changes: 134 additions & 2 deletions sockpuppet/templatetags/sockpuppet.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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
6 changes: 6 additions & 0 deletions tests/example/templates/second_tag_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load sockpuppet %}
{% stimulus_controller 'example_reflex' %}
<a href="#" {% click_reflex 'increment' parameter=parameter param2="abc" %}>click me</a>
{% endcontroller %}

<a href="#" {% click_reflex object_definition %}>I was done by object definition</a>
4 changes: 4 additions & 0 deletions tests/example/templates/tag_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load sockpuppet %}
<a href="#" {% click_reflex 'example_reflex' 'increment' parameter=parameter %}>click me</a>
<a href="#" {% submit_reflex 'example_reflex' 'increment' parameter=parameter %}>click me</a>
<input type="text" {% input_reflex 'example_reflex' 'increment' parameter=parameter %}/>
6 changes: 4 additions & 2 deletions tests/example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
]
21 changes: 21 additions & 0 deletions tests/example/views/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
67 changes: 67 additions & 0 deletions tests/test_tag.py
Original file line number Diff line number Diff line change
@@ -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__('<a href="#" data-reflex="click->example_reflex#increment" '
'data-parameter="I am a parameter">'
'click me'
'</a>'))

def test_submit_reflex_tag(self):
c = Client()
response: TemplateResponse = c.get('/tag/')

content = response.content.decode('utf-8')

self.assertTrue(content.__contains__('<a href="#" data-reflex="submit->example_reflex#increment" '
'data-parameter="I am a parameter">'
'click me'
'</a>'))

def test_input_reflex_tag(self):
c = Client()
response: TemplateResponse = c.get('/tag/')

content = response.content.decode('utf-8')

self.assertTrue(content.__contains__(
'<input type="text" data-reflex="input->example_reflex#increment" data-parameter="I am a parameter"/>'))

def test_reflex_tag_with_unsafe_input(self):
c = Client()
response: TemplateResponse = c.get('/tag/', data={"parameter": "</a>"})

content = response.content.decode('utf-8')

self.assertTrue(content.__contains__('<a href="#" data-reflex="click->example_reflex#increment" '
'data-parameter="&lt;/a&gt;">'
'click me'
'</a>'))

def test_controller_tag(self):
c = Client()
response: TemplateResponse = c.get('/second/', data={"parameter": "</a>"})

content = response.content.decode('utf-8')

self.assertTrue(content.__contains__(
'<a href="#" data-reflex="click->example_reflex#increment" data-parameter="&lt;/a&gt;" data-param2="abc">click me</a>'))

def test_tag_by_dict(self):
c = Client()
response: TemplateResponse = c.get('/second/', data={"parameter": "</a>"})

content = response.content.decode('utf-8')

print(content)

self.assertTrue(content.__contains__(
'<a href="#" data-reflex="click->abc#increment" >I was done by object definition</a>'))