Skip to content

Consider supporting passing simple callbacks to components from templates #99

Description

@davecoates

In HTML you could just do

<script>
window.myCallback = (e) => console.log("change", e)
</script>

<select onchange="mycallback">...</select>

with alliance UI components as tags

{% component "@alliancesoftware/ui" "Select" %}...

there's no way to do that easily currently because it's rendering a component under the hood. Soon some of these components will just render HTML directly and apply the styles, but even then there's still a use case for passing it direct to components because of things like comboboxes etc that will remain in React.

Achieving this isn't too hard, question is exact API. As a proof of concept:

from alliance_platform.codegen.typescript import Identifier
from alliance_platform.frontend.bundler.ssr import SSRSerializerContext
from alliance_platform.frontend.prop_handlers import CodeGeneratorNode
from alliance_platform.frontend.prop_handlers import ComponentProp
from alliance_platform.frontend.templatetags.react import ComponentSourceCodeGenerator
from allianceutils.template import parse_tag_arguments
from allianceutils.template import resolve
from django import template


register = template.Library()


class CallbackProp(ComponentProp, CodeGeneratorNode):
    def __init__(
        self,
        callback_name: str,
        node,
        context,
    ):
        self.callback_name = callback_name
        super().__init__(callback_name, node, context)

    def get_tag(self):
        return "Primitive"

    def generate_code(self, generator: ComponentSourceCodeGenerator):
        return Identifier(self.callback_name)

    def get_representation(self, ssr_cache: SSRSerializerContext):
        return self.callback_name


class CallbackNode(template.Node):
    def __init__(self, callback_name: str, target_var: str):
        self.callback_name = callback_name
        self.target_var = target_var
        super().__init__()

    def render(self, context):
        context[self.target_var] = CallbackProp(resolve(self.callback_name, context), self, context)
        return ""


@register.tag("js_callback")
def js_callback(parser: template.base.Parser, token: template.base.Token):
    tag_name = token.split_contents()[0]
    args, kwargs, target_var = parse_tag_arguments(parser, token, supports_as=True)
    if len(args) != 1:
        raise template.TemplateSyntaxError(
            f"'{tag_name}' must receive a single argument which is the name of the callback"
        )
    if target_var is None:
        raise template.TemplateSyntaxError(
            f"You must use {{% {tag_name} ... as ... %}} syntax to specify a target variable name for the callback prop"
        )

    return CallbackNode(args[0], target_var=target_var)

Then usage can be

<script>
  window.myCustomOnSelectionChange = (selection) => console.log("selection", selection)
</script>

{% js_callback "myCustomOnSelectionChange" as onchange %}

then you can just pass onchange as a prop to any component and it will receive it as a JS identifier.

{% component "@alliancesoftware/ui" "Select" onSelectionChange=onchange %}...{% endcomponent %}

js_callback is probably a bit misleading as it really is just any JS identifier - so maybe just js_identifier

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions