Giter VIP home page Giter VIP logo

govuk-frontend-wtf's Introduction

GOV.UK Frontend WTForms Widgets

PyPI version govuk-frontend 5.1.0 Python package

GOV.UK Frontend WTForms is a community tool of the GOV.UK Design System. The Design System team is not responsible for it and cannot support you with using it. Contact the maintainers directly if you need help or you want to request a feature.

This repository contains a set of WTForms widgets used to render WTForm fields using GOV.UK Frontend component styling. This is done using Jinja macros from the GOV.UK Frontend Jinja port of the original GOV.UK Frontend Nunjucks macros. These are kept up-to-date with GOV.UK Frontend releases, are thoroughly tested and produce 100% equivalent markup.

This approach also renders the associated error messages in the appropriate place, shows the error summary component at the top of the page and sets all related accessibility ARIA attributes. Adding the appropriate widget to your existing form Python class, along with far simpler templates, makes it quick and easy to produce fully GOV.UK compliant forms.

If you are looking to build a fully featured Flask app that integrates with GOV.UK Frontend Jinja and GOV.UK Frontend WTForms please use the GOV.UK Frontend Flask template repository to generate your app.

How to use

For more detailed examples please refer to the demo app source code.

After running pip install govuk-frontend-wtf, ensure that you tell Jinja where to load the templates from using the PackageLoader, register WTFormsHelpers, then set an environment variable for SECRET_KEY.

app/__init__.py:

from flask import Flask
from govuk_frontend_wtf.main import WTFormsHelpers
from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")

app.jinja_loader = ChoiceLoader(
    [
        PackageLoader("app"),
        PrefixLoader(
            {
                "govuk_frontend_jinja": PackageLoader("govuk_frontend_jinja"),
                "govuk_frontend_wtf": PackageLoader("govuk_frontend_wtf"),
            }
        ),
    ]
)

WTFormsHelpers(app)

Import and include the relevant widget on each field in your form class (see table below). Note that in this example widget=GovTextInput() is the only difference relative to a standard Flask-WTF form definition.

app/forms.py:

from flask_wtf import FlaskForm
from govuk_frontend_wtf.wtforms_widgets import GovSubmitInput, GovTextInput
from wtforms import StringField, SubmitField
from wtforms.validators import Email, InputRequired, Length


class ExampleForm(FlaskForm):
    email_address = StringField(
        "Email address",
        widget=GovTextInput(),
        validators=[
            InputRequired(message="Enter an email address"),
            Length(max=256, message="Email address must be 256 characters or fewer"),
            Email(message="Enter an email address in the correct format, like [email protected]"),
        ],
        description="We’ll only use this to send you a receipt",
    )

    submit = SubmitField("Continue", widget=GovSubmitInput())

Create a route to serve your form and template.

app/routes.py:

from flask import redirect, render_template, url_for

from app import app
from app.forms import ExampleForm

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/example-form", methods=["GET", "POST"])
def example():
    form = ExampleForm()
    if form.validate_on_submit():
        return redirect(url_for("index"))
    return render_template("example_form.html", form=form)

Finally, in your template set the page title appropriately if there are any form validation errors, as per GOV.UK Design System guidance. Include the govukErrorSummary() component at the start of the content block. Pass parameters in a dictionary to your form field as per the associated component macro options.

app/templates/example_form.html:

{% extends "base.html" %}

{%- from 'govuk_frontend_jinja/components/error-summary/macro.html' import govukErrorSummary -%}

{% block pageTitle %}{%- if form and form.errors %}Error: {% endif -%}Example form – GOV.UK Frontend WTForms Demo{% endblock %}

{% block content %}
<div class="govuk-grid-row">
    <div class="govuk-grid-column-two-thirds">
        {% if form.errors %}
            {{ govukErrorSummary(wtforms_errors(form)) }}
        {% endif %}

        <h1 class="govuk-heading-xl">Example form</h1>

        <form action="" method="post" novalidate>
            {{ form.csrf_token }}
            
            {{ form.email_address(params={
              'type': 'email',
              'autocomplete': 'email',
              'spellcheck': false
            }) }}
            
            {{ form.submit }}
        </form>
    </div>
</div>
{% endblock %}

Widgets

The available widgets and their corresponding Flask-WTF field types are as follows:

WTForms Field GOV.​UK Widget(s) Notes
BooleanField GovCheckboxInput
DateField GovDateInput
DateTimeField GovDateInput
DecimalField GovTextInput
FileField GovFileInput
MultipleFileField GovFileInput(multiple=True) Note that you need to specify multiple=True when invoking the widget in your form class. Not when you render it in the Jinja template.
FloatField GovTextInput
IntegerField GovTextInput Use params to specify a type if you need to use HTML5 number elements. This will not happen automatically.
PasswordField GovPasswordInput
RadioField GovRadioInput
SelectField GovSelect
SelectMultipleField GovCheckboxesInput Note that this renders checkboxes as <select multiple> elements are frowned upon.
SubmitField GovSubmitInput
StringField GovTextInput
TextAreaField GovTextArea, GovCharacterCount

In order to generate things like email fields using GovTextInput you will need to pass additional params through when rendering it as follows:

{{ form.email_address(params={'type': 'email', 'autocomplete': 'email', 'spellcheck': false}) }}

Running the tests

python3 -m venv venv
source venv/bin/activate
pip install -r tests/requirements.txt
pytest --cov=govuk_frontend_wtf --cov-report=term-missing --cov-branch

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

How to contribute

We welcome contribution from the community. If you want to contribute to this project, please review the code of conduct and contribution guidelines.

Contributors

See the full list of contributors on GitHub

Support

This software is provided "as-is" without warranty. Support is provided on a "best endeavours" basis by the maintainers and open source community.

If you are a civil servant you can sign up to the UK Government Digital Slack workspace to contact the maintainers listed above and the community of people using this project in the #govuk-design-system channel.

Otherwise, please see the contribution guidelines for how to raise a bug report or feature request.

govuk-frontend-wtf's People

Contributors

andreyyudin avatar andymantell avatar byzantime avatar dalepotter avatar dependabot[bot] avatar matthew-shaw avatar skablam avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

govuk-frontend-wtf's Issues

Automatically pass maxlength param to character count templates

Is your feature request related to a problem? Please describe.
When using the GovCharacterCount you have to set the length limit in both the WTForm field validator and pass the maxlength param to the template.

Describe the solution you'd like
Take the max length set in the validator and pass it as a default param to the template, so that developers don't have to set it twice, and risk missing or not changing one.

Describe alternatives you've considered
None other than the current approach

Additional context
The maxlength param must always be equal to the length.max validator.

GovDateInput should drop leading zeros

Describe the bug
The GovDateInput widget renders the day and month fields with leading zeros, for example 02 03 2007. This is contrary to what is recommended by the dates pattern which is to omit leading zeros.

To Reproduce

>>> from datetime import date
>>> from wtforms import DateField, Form
>>> from govuk_frontend_wtf.wtforms_widgets import GovDateInput
>>> class MyForm(Form):
...     date = DateField(widget=GovDateInput())
>>> form = MyForm(data={"date": date(2007, 3, 2)})
>>> form.date.widget.map_gov_params(form.date, id="date")
{'id': 'date', 'name': 'date', 'label': {'text': 'Date'}, 'attributes': {}, 'hint': {'text': ''}, 'fieldset': {'legend': {'text': 'Date'}}, 'items': [{'label': 'Day', 'id': 'date-day', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '02'}, {'label': 'Month', 'id': 'date-month', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '03'}, {'label': 'Year', 'id': 'date-year', 'name': 'date', 'classes': 'govuk-input--width-4', 'value': '2007'}]}

Or formatted:

{
    'id': 'date',
    'name': 'date',
    'label': {'text': 'Date'},
    'attributes': {},
    'hint': {'text': ''},
    'fieldset': {'legend': {'text': 'Date'}},
    'items': [
        {'label': 'Day', 'id': 'date-day', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '02'},
        {'label': 'Month', 'id': 'date-month', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '03'},
        {'label': 'Year', 'id': 'date-year', 'name': 'date', 'classes': 'govuk-input--width-4', 'value': '2007'},
    ],
}

Expected behavior
I would expect the day and month field values not to have leading zeros:

{
    ...
    'items': [
        {'label': 'Day', 'id': 'date-day', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '2'},
        {'label': 'Month', 'id': 'date-month', 'name': 'date', 'classes': 'govuk-input--width-2', 'value': '3'},
        {'label': 'Year', 'id': 'date-year', 'name': 'date', 'classes': 'govuk-input--width-4', 'value': '2007'},
    ],
}

This behaviour is due to the format used in GovDateInput: %d %m %Y. To omit leading zeros we can use %-d %-m %Y, although this is platform dependent.

Cannot set fieldset on GovCheckboxInput

Describe the bug
The GovCheckboxInput widget assumes that a fieldset is never used for a single checkbox. This can be useful for confirmation questions, e.g.:

Are these answers correct?
[x] I confirm that all answers are correct

To Reproduce

>>> from wtforms import BooleanField, Form
>>> from govuk_frontend_wtf.wtforms_widgets import GovCheckboxInput
>>> class MyForm(Form):
...     boolean = BooleanField(widget=GovCheckboxInput())
>>> form = MyForm()
>>> form.boolean.widget.map_gov_params(form.boolean, params={"fieldset": {"legend": {"text": "mylegend"}}}, items=[])
{'name': 'boolean', 'items': [], 'hint': {'text': ''}}

Expected behavior
I would expected the supplied fieldset to be preserved:

{'name': 'boolean', 'items': [], 'hint': {'text': ''}, 'fieldset': {'legend': {'text': 'mylegend'}}}

This behaviour is due to GovCheckboxInput trying to remove the default fieldset added by its superclass, GovCheckboxesInput.

RadioField with no `field.description` outputs an empty `div[class="govuk-hint"]` element

Describe the bug

A WTForm RadioField (using a govuk-frontend-wtf GovRadioInput() widget) with no field.description renders an empty hint div.

This seems to be because map_gov_params adds an empty hint/text string to the output params:

"hint": {"text": field.description},

In turn, this is parsed by the govuk-frontend-jinja macro into an empty govukHint component.

To Reproduce
Create a basic WTForm with a radio field.

from govuk_frontend_wtf.wtforms_widgets import GovRadioInput
from wtforms import Form, RadioField

class ExampleForm(Form):
    example_radio_field = RadioField(
            choices=[
                ("yes", "Yes"),
                ("no", "No"),
            ],
            widget=GovRadioInput(),
        )

Template:

{{ form.example_radio_field }}

Expected behaviour
Given the above example, I would expect no hint element to be outputted.

Actual behaviour

<div class="govuk-form-group">
  <fieldset class="govuk-fieldset" aria-describedby="example_radio_field-hint">
    <legend class="govuk-fieldset__legend">
      Example Radio Field
    </legend>

    <!-- UNEXPECTED HINT ELEMENT -->
    <div id="example_radio_field-hint" class="govuk-hint">
    </div>
    
    <div class="govuk-radios">
      <div class="govuk-radios__item">
        <input class="govuk-radios__input" id="example_radio_field" name="example_radio_field" type="radio" value="yes">
        <label class="govuk-label govuk-radios__label" for="example_radio_field">
          Yes
        </label>
      </div>
      <div class="govuk-radios__item">
        <input class="govuk-radios__input" id="example_radio_field-2" name="example_radio_field" type="radio" value="no">
        <label class="govuk-label govuk-radios__label" for="example_radio_field-2">
          No
        </label>
      </div>
    </div>
  </fieldset>
</div>

This additional hint adds an additional 10px of vertical whitespace.

image

Desktop (please complete the following information):

  • OS: Ubuntu 20.04
  • Browser: Firefox
  • Version: 96.0 (64-bit)
    (I do not believe this to be a OS/browser specific bug.

Additional context

Seems related to 59e186e

Suggested fix is to check if field.description exists, and only assign to the hint param by default if it does.

Add support for FormField widget

There is currently no widget for the FormField (https://wtforms.readthedocs.io/en/2.3.x/fields/#field-enclosures) enclosure to group form fields.

Adding one would allow you to define small, reusable FlaskForm objects with a bunch of fields (kind of like a pattern). This should reduce duplicated code.

The initial code I produced to test this was:

from markupsafe import Markup
from wtforms.widgets.core import html_params


class GovukFieldsetWidget(object):
    def __init__(self):
        pass

    def construct_subfield(self, subfield, field, options):
        subfield_id = subfield.id.replace(field.id + field.separator, "")
        subfield_options = options[subfield_id] if subfield_id in options else {}
        return subfield.widget(subfield, params=subfield_options)

    def __call__(self, field, **kwargs):
        kwargs.setdefault("id", field.id)
        options = kwargs["params"] if "params" in kwargs else {}
        fieldset_options = {}
        html = ["<div class='govuk-form-group'>"]
        html.append("<fieldset class='govuk-fieldset' %s>" % (html_params(**fieldset_options)))
        field_title = (
            options["fieldset"]["legend"]["text"]
            if "fieldset" in options and "legend" in options["fieldset"] and "text" in options["fieldset"]["legend"]
            else field.label.text
            if field.label
            else field.name
            if field.name
            else ""
        )
        if field_title:
            fieldset_classes = (
                options["fieldset"]["legend"]["classes"]
                if "fieldset" in options
                and "legend" in options["fieldset"]
                and "classes" in options["fieldset"]["legend"]
                else ""
            )
            html.append("<legend class='govuk-fieldset__legend %s'>%s</legend>" % (fieldset_classes, field_title))
        if hasattr(field, "task_order"):
            for subfield_name in field.task_order:
                subfield = field[subfield_name]
                html.append(self.construct_subfield(subfield, field, options))
        else:
            for subfield in field:
                html.append(self.construct_subfield(subfield, field, options))
        html.append("</fieldset>")
        html.append("</div>")
        return Markup("".join(html))

You could then use the GovukFieldsetWidget to create a group of fields as a single form where that "pattern" could be validated separately:

from flask_wtf import FlaskForm
from wtforms.fields import IntegerField, FormField, BooleanField
from GovukFieldsetWidget import GovukFieldsetWidget # see class above
from govuk_frontend_wtf.wtforms_widgets import GovCheckboxInput, GovTextInput


class ChargeAmount(FlaskForm):
    class Meta:
        csrf = False

    task_order = ["charge_amount", "undisclosed_amount"]
    charge_amount = IntegerField(
        widget=GovTextInput(),
        validators=[],
    )
    undisclosed_amount = BooleanField(widget=GovCheckboxInput())


pattern = FormField(
    ChargeAmount,
    widget=GovukFieldsetWidget(),
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.