So you have this Django app. If you are like me and don’t have quite the skills for a good UI design, you probably stick to bootstrap. You know you will have a bunch of forms and you don’t want to style them yourself. Let’s say, you came to the idea that django-crispy-forms fits your needs perfectly (it definitely fits mine).
How it all began
This is a story about a model Foo.
Model Foo was lonely, so it summoned a view, a template and a url.
The base.html
contents are left out, as they include standard HTML markup,
bootstrap stylesheet include and a mandatory template block content
(in our
specific case).
And this is what we created so far:
Such cool, right?
Oh, wait! Where are the buttons? How do I submit this thing? Foo the Emperor must live!
Ok, I will need a Cancel anchor tag that will look like a button, because I want to redirect to some other page, and a Submit input. Easy!
Meh, not the prettiest way to achieve what I want, but apparently this is the only easy and straight-forward way. Also don’t forget to update the view.
And so this is what we get:
The lack of space between the anchor tag and the input looks awful, right? This
tiny cosmetic issue was discussed in
issue #62 and
solved for groups of buttons and inputs. Unfortunately it persists in case of
an input/button and anchor tag with role="button"
. If it’s bugging you, add
some margin by yourself.
The misfortune
Life goes on and Foo objects seem to be doing just fine. Until a great misfortune takes over their destiny. It doesn’t seem like a big deal, since all that happened was the addition of two more characters: Bar and Baz. What harm can they potentially cause? Well, let’s see.
Bar and Baz follow the same old path Foo did a long time ago:
model/form/view/template/url. The form again will initialize a FormHelper
to
append those 2 elements. It seems like every form will need them (like duhh, how
can I get around the form without them?). And so we copy-pasty, we copy-paste.
And then the pressure of the invisible monster can be felt, crawling under the skin and whispering to you: “You will do this over and over again. For every form. And when you will want to change a tiny detail, you will change it for every form, because you want to stay consistent. That can be the change in the bootstrap anchor tag interface, class of the Submit input, text of the anchor tag.”
This is the punishment for the violation of DRY (Don’t Repeat Yourself) principle.
It’s not too late to fix the situation, until the quicksand doesn’t devour all the codebase into the abyss of “refactoring is too expensive for us”.
Layout objects to the rescue
At first, it seems like a good idea to use Composing layouts. We take that chunk of layout and simply store it into a variable somewhere. But how can we change that Cancel URL? Or maybe we plan to have different input values for submit input for different forms. There might be several variables that we would like to keep flexible for a more or less fixed piece of layout.
One of the solutions is to create our own custom ObjectLayout
. It involves a
little bit of django-crispy-forms
internals, but don’t worry, I’m here to
help.
This is where we start from -
creating your own layout objects.
As it says in the current section, we investigate the objects that live in
layout.py
and bootstrap.py
. We come up with the following cure for the
misfortune:
In the __init__
we extract all the arguments we are interested in, for example
the URL of the Cancel anchor tag. The render
method produces the HTML string
with all the nodes translated. There is no need to create our own template to be
rendered, since we can reuse render
method provided by the django-crispy-forms
.
It already knows how to translate all those objects we have put together, to plain
HTML.
Maybe it doesn’t seem like a big deal, saves us maybe 2 lines of code in the form and adds up lots of code some place else. As the monster warned us, the real danger comes with the growth of objects and forms in number. At some point it will become much more complicated to make any changes to those form actions. It is a small enhancement that might save time for future me.
Alright, that looks much better. Both for Foo, Bar and Baz. Oh, and don’t forget about unittest, otherwise another monster will come to bite your ass.
Let’s see how can we write a unittest for our little layout object. I will
cheat a bit and use some test tools and shortcuts created specifically for
django-crispy-forms
.
In the unittest we render the form with the custom layout object
SubmitCancelFormActions
and check for some keywords if present in the rendered
HTML.
Happy ending
That’s all folks! Keep DRY and write unittests!
P.S. In case you are still in doubt about the provided solution, here is what Daniel Greenfeld says: “As one of the leads of django-crispy-forms, I approve this blog post. :-)”
See The tale of DRY with django-crispy-forms | Part II for a better solution, if you wanna to go an extra mile.