diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index 8dfc348..cc9f8e8 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -42,7 +42,7 @@ from settings. The first of these defined is used: Form selects are rendered with select2 in templates extending ``keg-elements/form-view.html``. ``keg-elements/select2-scripts.html`` and ``keg-elements/select2-styles.html`` can be included in templates to render select2s without extending form-view. Apps can opt out of select2 -rendering with ``KEG_USE_SELECT2`` config. +rendering with ``KEG_USE_ENHANCED_SELECTS`` config. .. _gs-model: diff --git a/keg_elements/templates/keg-elements/form-view.html b/keg_elements/templates/keg-elements/form-view.html index 794b8ce..bd7a16e 100644 --- a/keg_elements/templates/keg-elements/form-view.html +++ b/keg_elements/templates/keg-elements/form-view.html @@ -1,7 +1,7 @@ {% if is_read_only %} {% import 'keg-elements/forms/horizontal-static.html' as horizontal %} {% else %} - {% import 'keg-elements/forms/horizontal-b4.html' as horizontal %} + {% import 'keg-elements/forms/horizontal.html' as horizontal %} {% endif %} {% set base_template = config.get('BASE_TEMPLATE') or config.get('KEG_BASE_TEMPLATE') %} @@ -9,18 +9,18 @@ {% extends base_template %} {% endif %} -{% block scripts %} +{% block keg_scripts %} {{ super() }} -{% if config.get('KEG_USE_SELECT2', True) %} - {% include 'keg-elements/forms/select2-scripts.html' %} +{% if config.get('KEG_USE_ENHANCED_SELECTS', True) %} + {% include 'keg-elements/forms/enhanced-select-scripts.html' %} {% endif %} {{ horizontal.custom_js() }} {% endblock %} -{% block styles %} +{% block keg_styles %} {{ super() }} -{% if config.get('KEG_USE_SELECT2', True) %} - {% include 'keg-elements/forms/select2-styles.html' %} +{% if config.get('KEG_USE_ENHANCED_SELECTS', True) %} + {% include 'keg-elements/forms/enhanced-select-styles.html' %} {% endif %} {{ horizontal.custom_css() }} {% endblock %} diff --git a/keg_elements/templates/keg-elements/forms/enhanced-select-scripts.html b/keg_elements/templates/keg-elements/forms/enhanced-select-scripts.html new file mode 100644 index 0000000..7e55b37 --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/enhanced-select-scripts.html @@ -0,0 +1,7 @@ +{% if config.get('KEG_TEMPLATE_FLAVOR', 'bootstrap') == 'bootstrap' %} + {% if config.get('KEG_BOOTSTRAP_MAJOR_VERSION', 5) >= 5 %} + {% include 'keg-elements/forms/tomselect-scripts.html' %} + {% else %} + {% include 'keg-elements/forms/select2-scripts.html' %} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/forms/enhanced-select-styles.html b/keg_elements/templates/keg-elements/forms/enhanced-select-styles.html new file mode 100644 index 0000000..2755478 --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/enhanced-select-styles.html @@ -0,0 +1,7 @@ +{% if config.get('KEG_TEMPLATE_FLAVOR', 'bootstrap') == 'bootstrap' %} + {% if config.get('KEG_BOOTSTRAP_MAJOR_VERSION', 5) >= 5 %} + {% include 'keg-elements/forms/tomselect-styles.html' %} + {% else %} + {% include 'keg-elements/forms/select2-styles.html' %} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/forms/horizontal-b5.html b/keg_elements/templates/keg-elements/forms/horizontal-b5.html new file mode 100644 index 0000000..76a31ca --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/horizontal-b5.html @@ -0,0 +1,328 @@ +{%- if _ is not defined -%} + {% macro _(message) -%} + {{ message }} + {%- endmacro %} +{%- endif -%} + +{# Renders field for bootstrap 4 standards. + + Params: + field - WTForm field + kwargs - pass any arguments you want in order to put them into the html attributes. + There are few exceptions: for - for_, class - class_, class__ - class_ + + Example usage: + {{ horz_form.field(form.email, placeholder='Input email', type='email') }} +#} + +{% macro div_form_group(field) -%} +
+ {{ caller(**kwargs) }} +
+{%- endmacro %} + +{% macro description(field) -%} + + {% if field.description is callable %} + {{ field.description()|safe }} + {% else %} + {{ field.description|safe }} + {% endif %} + +{%- endmacro %} + +{% macro field_errors(field) -%} +{# You will need javascript similar to below for validations to work properly. + Alternatively, remove .was-validated from the form element to disable native client side validations by default. + + var forms = document.getElementsByClassName('needs-validation'); + var validation = Array.prototype.filter.call(forms, function(form) { + form.addEventListener('submit', function(event) { + if (form.checkValidity() === false) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); + + var fields = document.getElementsByClassName('is-invalid'); + Array.prototype.filter.call(fields, function(field) { + field.setCustomValidity(false); + }); +#} +
+ {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% elif field.flags.required %} +

This field is required.

+ {% endif %} +
+{% endmacro %} + +{% macro field(field, label_visible=true) -%} + {% call div_form_group(field, **kwargs) %} + {% if (field.type != 'HiddenField' and field.type != 'CSRFTokenField') and label_visible %} + {{ field.label(class_='col-form-label col-3') }} + {% endif %} +
+ {{ field_widget(field, **kwargs) }} + {% if field.description %} + {{ description(field) }} + {% endif %} +
+ {% endcall %} +{%- endmacro %} + + +{% macro custom_css() %} + + +{% endmacro %} + +{% macro custom_js() %} + + +{{ datetime_helper() }} +{% endmacro %} + +{% macro multi_checkbox(field) %} +
+ + +
+
+ {% for choice_id, choice in field.choices %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro field_widget(field) %} + {% if field.type == "MultiCheckboxField" %} + {{ multi_checkbox(field) }} + {% else %} + {% if field.flags.disabled %}{% set _dummy = kwargs.update({'disabled': field.flags.disabled}) %}{% endif %} + {% if field.flags.readonly %}{% set _dummy = kwargs.update({'readonly': field.flags.readonly}) %}{% endif %} + {{ field(class_='form-control is-invalid' if field.errors else 'form-control', **kwargs) }} + {% endif %} + {{ field_errors(field) }} +{% endmacro %} + +{# Renders checkbox fields since they are represented differently in bootstrap + Params: + field - WTForm field (there are no check, but you should put here only BooleanField. + kwargs - pass any arguments you want in order to put them into the html attributes. + There are few exceptions: for - for_, class - class_, class__ - class_ + + Example usage: + {{ horiz_form.checkbox_field(form.remember_me) }} + #} +{% macro checkbox_field(field) -%} + {% call div_form_group(field, **kwargs) %} +
+
+ {% if field.flags.disabled %}{% set _dummy = kwargs.update({'disabled': field.flags.disabled}) %}{% endif %} + {{ field(type='checkbox', class_='form-check-input custom-control-input' + (' is-invalid' if field.errors else ''), **kwargs) }} + {# pt-0 is to align the label with the checkbox by removing the padding #} + + {{ field_errors(field) }} +
+ {% if field.description %} + {{ description(field) }} + {% endif %} +
+ {% endcall %} +{%- endmacro %} + +{# Renders radio field + Params: + field - WTForm field (must have an `iter_choices` method) + kwargs - pass any arguments you want in order to put them into the html attributes. + There are few exceptions: for - for_, class - class_, class__ - class_ + + Example usage: + {{ horiz_form.radio_field(form.answers) }} + #} +{% macro radio_field(field, label_visible=true) -%} +
+ {% if label_visible %} + {{ field.label(class_='col-form-label col-3') }} + {% endif %} +
+ {% for value, label, checked in field.iter_choices() %} +
+ + + {% if loop.last %}{{ field_errors(field) }}{% endif %} +
+ {% endfor %} +
+ {% if field.description %} + {{ description(field) }} + {% endif %} +
+{%- endmacro %} + +{% macro submit_group(action_text='Submit', btn_class='btn btn-primary', cancel_url='') -%} +
+
+ + {% if cancel_url %} + Cancel + {% endif %} +
+
+{%- endmacro %} + +{% macro render_field(f) -%} + {% if f is none %} + {# Do nothing b/c the field is None #} + {% elif f.type == 'BooleanField' or f.widget.input_type == 'checkbox' %} + {{ checkbox_field(f, **kwargs) }} + {% elif f.type == 'RadioField' %} + {{ radio_field(f, **kwargs) }} + {% elif f.type == 'FormField' %} + {{ render_form_fields(f, render_hidden=true) }} + {% else %} + {{ field(f, **kwargs) }} + {% endif %} +{%- endmacro %} + + +{% macro form_errors(form) -%} + {% if form.form_errors %} +
+ {% for e in form.form_errors %} +

{{ e }}

+ {% endfor %} +
+ {% endif %} +{% endmacro %} + + +{# Renders WTForm in bootstrap way. There are two ways to call function: + - as macros: it will render all field forms using cycle to iterate over them + - as call: it will insert form fields as you specify: + e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login', + class_='login-form') %} + {{ macros.render_field(form.email, placeholder='Input email', type='email') }} + {{ macros.render_field(form.password, placeholder='Input password', type='password') }} + {{ macros.render_checkbox_field(form.remember_me, type='checkbox') }} + {% endcall %} + + Params: + form - WTForm class + action_url - url where to submit this form + action_text - text of submit button + class_ - sets a class for form +#} +{% macro form( + form, + field_names=None, + action_url='', + action_text='Submit', + class_='', + btn_class='btn btn-primary', + cancel_url='', + form_upload=false, + dirty_check=false, + form_id=None, + form_name=None, + form_attrs=None +) -%} + +
+ {{ form.hidden_tag() if form.hidden_tag }} + {{ form_errors(form) }} + + {% if caller %} + {{ caller() }} + {% elif field_names %} + {{ fields(form, field_names) }} + {% else %} + {{ render_form_fields(form) }} + {% endif %} + {{ submit_group(action_text=action_text, btn_class=btn_class, cancel_url=cancel_url) }} +
+{%- endmacro %} + +{% macro render_form_fields(form, render_hidden=false) -%} + {# Render hidden tags if flag is passed (for subforms only) #} + {{ form.hidden_tag() if render_hidden and form.hidden_tag }} + {% for f in form %} + {% if not f.widget.input_type == 'hidden' %} + {{ render_field(f) }} + {% endif %} + {% endfor %} +{%- endmacro %} + +{% macro fields(form, field_names) -%} + {% for field_name in field_names %} + {{ render_field(form[field_name]) }} + {% endfor %} +{%- endmacro %} + +{% macro section(heading, form, field_names) -%} +

{{heading}}

+ {% if caller %} + {{ caller() }} + {% else %} + {{ fields(form, field_names) }} + {% endif %} +{%- endmacro %} + +{% macro datetime_helper() -%} + +{%- endmacro %} \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/forms/horizontal.html b/keg_elements/templates/keg-elements/forms/horizontal.html new file mode 100644 index 0000000..7145419 --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/horizontal.html @@ -0,0 +1,3 @@ +{% if config.get('KEG_TEMPLATE_FLAVOR', 'bootstrap') == 'bootstrap' %} + {% extends 'keg-elements/forms/horizontal-b' + config.get('KEG_BOOTSTRAP_MAJOR_VERSION', 5)|string + '.html' %} +{% endif %} \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/forms/tomselect-scripts.html b/keg_elements/templates/keg-elements/forms/tomselect-scripts.html new file mode 100644 index 0000000..afa29e8 --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/tomselect-scripts.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/forms/tomselect-styles.html b/keg_elements/templates/keg-elements/forms/tomselect-styles.html new file mode 100644 index 0000000..2f0ae44 --- /dev/null +++ b/keg_elements/templates/keg-elements/forms/tomselect-styles.html @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/keg_elements/templates/keg-elements/grid-view.html b/keg_elements/templates/keg-elements/grid-view.html index f400504..7bfb87c 100644 --- a/keg_elements/templates/keg-elements/grid-view.html +++ b/keg_elements/templates/keg-elements/grid-view.html @@ -30,7 +30,7 @@

{{ title }}

{% endif %} {% endblock %} -{% block scripts %} +{% block keg_scripts %} {% if base_template %} {{ super() }} {{ grid_scripts() }} diff --git a/kegel_app/config.py b/kegel_app/config.py index 50b04cb..8ab246d 100644 --- a/kegel_app/config.py +++ b/kegel_app/config.py @@ -7,6 +7,8 @@ class DefaultProfile(object): SQLALCHEMY_TRACK_MODIFICATIONS = True SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'sqlite://') + KEG_BOOTSTRAP_MAJOR_VERSION = 4 + class TestProfile(object): KEG_KEYRING_ENABLE = False diff --git a/kegel_app/templates/keg_element/demo-form.html b/kegel_app/templates/keg_element/demo-form.html index 45c3a4b..a72b4f9 100644 --- a/kegel_app/templates/keg_element/demo-form.html +++ b/kegel_app/templates/keg_element/demo-form.html @@ -1,4 +1,4 @@ -{% import 'keg-elements/forms/horizontal-b4.html' as horizontal %} +{% import 'keg-elements/forms/horizontal.html' as horizontal %} {% block main %}

{{ title }}