Xzya / django-web-components

A simple way to create reusable template components in Django.
MIT License
159 stars 4 forks source link

Mark attributes as mandatory #8

Open krims0n32 opened 1 year ago

krims0n32 commented 1 year ago

Thank you for this package. It seems like an improvement over django-components.

It would be nice if we could mark certain attributes as mandatory in the component class. This would come in handy for people who are new to a certain component so they can easily see what attributes go in, without having to dig through the template.

Xzya commented 1 year ago

Hello,

Since you can build components in multiple ways (e.g. class based vs function based, logic in template vs logic in python and passed to context etc.), could you please provide a concrete example of a component? Just so that I can get a better understanding of your context.

Indeed I have thought about adding certain validations to attributes / slots, e.g. making them mandatory, validating the type, making sure a value is in a certain list (like Django's choices) etc. However, I am not sure about the best way to add this functionality without adding too much complexity.

Theoretically you can do this right now, although it's probably not in the most user friendly way

@component.register
def alert(context):
    context["dismissible"] = context["attributes"].pop("dismissible", False)

    return CachedTemplate(
        """
        <div class="alert alert-primary">
            {% if dismissible %}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            {% endif %}
        </div>
        """,
        name="alert",
    ).render(context)

This component accepts a dismissible attribute which defaults to False. You can remove the default to make it mandatory, which will result in an exception if you don't pass the attribute

context["dismissible"] = context["attributes"].pop("dismissible")

This approach is very simple, and I think it makes it clear when you read the component that it needs the dismissible attribute, but it's a bit verbose, and for more complex validations it can get even more ugly.

Another approach to this would be to define a way for you to specify the attributes / slots that your component needs in a way similar to Django's Model / Form, or DRF's Serializer, e.g. something like this

dismissible = components.Attribute("dismissible", required=True)
color = components.Attribute("color", choices=["primary", "secondary", "danger"], default="primary")
title = components.Slot("title", required=True)

The problem I see with this is that it might be a bit overkill.

I mostly use the first approach, although I also make heavy use of the merge_attributes helper function, e.g. here is a more complete example of a Bootstrap Alert component

@component.register
def alert(context):
    color = context["attributes"].pop("color", "primary")
    dismissible = context["dismissible"] = context["attributes"].pop("dismissible", False)
    fade = context["attributes"].pop("fade", False)

    context["attributes"] = merge_attributes(
        {
            "class": [
                "alert",
                {
                    f"alert-{color}": color,
                    "alert-dismissible": dismissible,
                    "fade show": fade,
                },
            ],
            "role": "alert",
        },
        context["attributes"],
    )
    return CachedTemplate(
        """
        <div {{ attributes }}>
            {% render_slot slots.inner_block %}

            {% if dismissible %}
                {% close_button data-bs-dismiss="alert" %}{% endclose_button %}
            {% endif %}
        </div>
        """,
        name="alert",
    ).render(context)

Thanks!