emmett-framework / emmett

The web framework for inventors
BSD 3-Clause "New" or "Revised" License
1.08k stars 72 forks source link

Some help needed with Form styles #521

Open barendburger opened 1 week ago

barendburger commented 1 week ago

I'm at best an intermediate on the Python side. I'm busy doing an app using Bootstrap, and want to get my forms styled, but this section in the docs is just not giving me enough to make a start.

Customizing forms Good applications also need good styles. This is why Emmett forms allows you to set a specific style with the formstyle attribute. But how should you edit the style of your form?

Well, in Emmett, the style of a form is decided by the FormStyle class._

Can anyone please provide some code samples on how to get my forms styled with Bootstrap css classes at the highest possible level in the app, ie, I would rather specifiy things once, than having to inject every time I call a form. But at this point I won't be picky, either approach will work for me.

I'll commit to contribute my learnings to that section in the doicuments, if I can just get started. I've spent way too many hours at this point trying to get this done by myself.

gi0baro commented 1 week ago

Hi @barendburger, the idea behind the FormStyle class is to customise form items using Emmett's html builtin helpers.

In emmett form elements are divided by field type, so you have individual widget functions to return the relevant HTML. A very rough example of a bootstrap style for Emmett forms might look like this:

from emmett.forms import FormStyle
from emmett.html import tag

class BSFormStyle(FormStyle):
    @staticmethod
    def widget_bool(attr, field, value, _id=None):
        return FormStyle.widget_bool(attr, field, value, _class="bool checkbox", _id=_id)

    def on_start(self):
        self.parent = tag.fieldset()

    def style_widget(self, widget):
        wtype = widget['_class'].split(' ')[0]
        if wtype not in ["bool", "upload_wrap", "input-group"]:
            widget['_class'] += " form-control"

    def create_label(self, label):
        wid = self.element.widget['_id']
        return tag.label(label, _for=wid, _class='col-sm-2 control-label')

    def create_comment(self, comment):
        return tag.p(comment, _class='help-block')

    def create_error(self, error):
        return tag.p(error, _class='text-danger')

    def add_widget(self, widget):
        _class = 'form-group'
        label = self.element.label
        wrapper = tag.div(widget, _class='col-sm-10')
        if self.element.error:
            wrapper.append(self.element.error)
            _class += ' has-error'
        if self.element.comment:
            wrapper.append(self.element.comment)
        self.parent.append(tag.div(label, wrapper, _class=_class))

    def add_buttons(self):
        submit = tag.input(_type='submit', _value=self.attr['submit'], _class='btn btn-primary')
        buttons = tag.div(submit, _class="col-sm-10 col-sm-offset-2")
        self.parent.append(tag.div(buttons, _class='form-group'))

    def render(self):
        self.attr['_class'] = self.attr.get('_class', 'form-horizontal')
        return super().render(self)

this should give you an idea on how it works. The only widget implement here is for boolean fields, but you can use the same approach for, let's say, add date pickers implementing the relevant widget_date static method.

There's also a very old extension to Emmett pre-2.0 here (https://github.com/gi0baro/weppy-bs3). It won't work with Emmett directly, but should give you more details on how to put everything together!

barendburger commented 1 week ago

Thank you so much for your hepl @gi0baro , it makes sense, but let me wrap my head around it and see how far I get.

barendburger commented 3 days ago

@gi0baro I need to admit defeat. I've tried to implement your code aboveby following the linked example for an extension, also tried passing the FormStyle in when instantiating the form, but both fail with the error below. I tried that in my app, and then also in your blog example to try simplify things.

TypeError: FormStyle.render() takes 1 positional argument but 2 were given

Seems to happen on return super().render(self)

Here's the full trace:

ERROR in handlers [/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/protocols/rsgi/handlers.py:146]: Application exception: Traceback (most recent call last): File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/protocols/rsgi/handlers.py", line 136, in dynamic_handler http = await self.router.dispatch(request, response) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/routing/router.py", line 230, in dispatch return await match.dispatch(reqargs, response) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/routing/dispatchers.py", line 59, in dispatch rv = self.response_builder(await self.f(**reqargs), response) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/routing/response.py", line 63, in call response.status, self.process(output, response), headers=response.headers, cookies=response.cookies File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett/routing/response.py", line 65, in process return self.route.app.templater.render(self.route.template, output) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/apis.py", line 172, in render return self._render(source, file_path, context) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/apis.py", line 166, in _render make_traceback(exc_info) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/debug.py", line 114, in make_traceback reraise(exc_type, exc_value, tb) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/_internal.py", line 15, in reraise raise value.with_traceback(tb) File "/home/barend/Code/test_emmett/templates/new_post.html", line 4, in template {{=form}} File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/writers.py", line 46, in escape self.write(self._escape_data(data)) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/writers.py", line 42, in _escape_data body = self._to_html(self._to_unicode(data)) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/writers.py", line 29, in _to_unicode return to_unicode(data) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/renoir/_shortcuts.py", line 33, in to_unicode return str(obj) File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett_core/html.py", line 89, in str return self.html() File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett/forms.py", line 231, in html return self._render().html() File "/home/barend/Code/test_emmett/pythonenv/lib/python3.10/site-packages/emmett/forms.py", line 192, in _render return styler.render() File "/home/barend/Code/test_emmett/style.py", line 45, in render return super().render(self) TypeError: FormStyle.render() takes 1 positional argument but 2 were given

If you can show me how to connect/inject the new FormStyle in a way that works, then I should be able to take it from there.

gi0baro commented 3 days ago

@barendburger sorry, typo from my side, just replace that line with super().render()

barendburger commented 3 days ago

LOL, I can't believe the one thing I did't try when playing with arguments was to remove it completely. Thanks, that worked. Let me dig a bit deeper with my current project, and will then do a pull request for some extra documentation when I've worked with it a bit more.