Commit 77dad952 authored by John Kirkwood's avatar John Kirkwood

Create factory for Django fields and update components.

parent eec32b36
Pipeline #1008 passed with stage
in 38 seconds
from .pyreact import *
from .pyreact import ce, cp, get_prop
def Div(props, children):
......@@ -51,6 +51,13 @@ def Checkbox(props=[], children=[]):
return Input(props, children)
def Text(props=[], children=[]):
props.append(
cp('type', "text")
)
return Input(props, children)
def Label(props=[], children=[]):
if children != []:
return ce('label', props, children)
......@@ -68,13 +75,17 @@ def Date(props=[], children=[]):
def Select(props=[], children=[]):
required = get_prop(props, "required")
if required is not None:
# find first option and set it "disabled"
# find first option and disable it if it no value
try:
props_option = children[0]["props"]
props_option.append(
cp("disabled", "disabled"),
)
except (Exception, e):
option_value = get_prop(props_option, "value")
if (option_value is None
or option_value["value"] == ""):
props_option.extend([
cp("disabled", True),
cp("selected", True),
])
except (Exception, ) as e:
pass
return ce('select', props, children)
......
......@@ -6,3 +6,6 @@ from .models import Post
class PostAdmin(admin.ModelAdmin):
form = PostForm
admin.site.register(Post)
from django.forms.boundfield import BoundField
from django.forms.widgets import Widget, TextInput, CheckboxInput
from django.utils.functional import Promise
from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
import chp.mdc.components as mdc
# from chp.components import *
from chp.store import (create_store, render_app)
from chp.mdc.components import *
"""
Boundfield has the following methods to render an HTML widget
using the template engine:
def __str__(self):
""" """Render this field as an HTML widget.""" """
if self.field.show_hidden_initial:
return self.as_widget() + self.as_hidden(only_initial=True)
return self.as_widget()
def as_widget(self, widget=None, attrs=None, only_initial=False):
""" """
Render the field by rendering the passed widget, adding any HTML
attributes passed as attrs. If a widget isn't specified, use the
field's default widget.
""" """
widget = widget or self.field.widget
if self.field.localize:
widget.is_localized = True
attrs = attrs or {}
attrs = self.build_widget_attrs(attrs, widget)
if self.auto_id and 'id' not in widget.attrs:
attrs.setdefault('id', self.html_initial_id if only_initial else self.auto_id)
return widget.render(
name=self.html_initial_name if only_initial else self.html_name,
value=self.value(),
attrs=attrs,
renderer=self.form.renderer,
)
def build_widget_attrs(self, attrs, widget=None):
widget = widget or self.field.widget
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
if widget.use_required_attribute(self.initial) and self.field.required and self.form.use_required_attribute:
attrs['required'] = True
if self.field.disabled:
attrs['disabled'] = True
return attrs
We need to use similar to get the attrs for 'required', etc.
Alternatively, create custom MdcWidgets and override the widget.render()?
Refactor functions here to classes to support code reuse.
"""
class ChpWidgetMixin:
chp_widget = None
def __init__(self, attrs=None, **kwargs):
self.label = kwargs.pop("label", None)
if attrs is not None:
attrs = attrs.copy()
super().__init__(attrs)
def _add_label(self, context):
context['widget'].update(
{'label': self.label,
'id_for_label':
self.id_for_label(context['widget']['attrs']['id'])
})
return context
def _render(self, template_name, context, renderer=None):
'''Ignore template_name and renderer (see django.forms.py)
'''
context = self._add_label(context)
raise NotImplementedError
class MdcCheckboxInput(ChpWidgetMixin, CheckboxInput):
def _render(self, template_name, context, renderer=None):
context = self._add_label(context)
props, children = [], []
props.append(
cp("type", "checkbox"),
)
checked = context['widget']['attrs'].get('checked', False)
if checked:
props.append(cp("checked", checked))
el_id = context['widget']['attrs'].get('id', None)
if el_id:
props.append(cp("id", el_id))
label = context['widget']['label']
if isinstance(label, Promise):
label = conditional_escape(label)
context.update({
"label": label,
"for": context['widget']['id_for_label'],
"type": context['widget']['type'],
})
return CheckboxField(props, children, context)
def MdcCheckbox(field):
# code taken from Boundfield.label_tag()
label_suffix = (field.field.label_suffix
if field.field.label_suffix is not None
else (field.form.label_suffix
if hasattr(field, "form") else ""))
contents = field.label
if label_suffix and contents and contents[-1] not in _(':?.!'):
label = format_html('{}{}', contents, label_suffix)
return field.as_widget(MdcCheckboxInput(label=label))
def MdcInput(field):
# typ = field.field.widget.input_type
typ = field.mdc_type
# widget formatted value
value = field.field.widget.format_value(field.value())
props = [
cp("type", typ),
]
if value:
props.append(cp("value", value))
if field.auto_id:
props.append(cp("id", field.auto_id))
# widget-level attrs
for (key, value) in field.field.widget.attrs.items():
props.append(
cp(key, value)
)
return Input(props, [])
def DjLabel(field):
label = field.label
# cast gettext_lazy strings so they are recognised by AST renderer
if isinstance(label, Promise):
label = conditional_escape(label)
props = [
cp("for", field.id_for_label),
]
context = {
"type": field.mdc_type,
"for": field.id_for_label,
"label": label,
}
return Label(props, label, context)
def MdcTextField(field):
# to differentiate from Django date field
if not hasattr(field, "mdc_type"):
field.mdc_type = "text"
props = [
cp("type", field.mdc_type),
]
children = [
MdcInput(field),
DjLabel(field),
LineRipple(),
]
return InputField(props, children)
def MdcDateField(field):
field.mdc_type = "date"
return MdcTextField(field)
......@@ -11,9 +11,8 @@ from chp.pyreact import (
from chp.store import (create_store, Inject_ast_into_DOM, render_app)
from chp.mdc.components import *
from chp.mdc.django.factory import Factory as MdcField
from .components import (
MdcCheckbox, MdcDateField, MdcTextField)
from .models import Post
......@@ -39,17 +38,17 @@ class PostForm(forms.ModelForm):
[
Flex([],
[
MdcCheckbox(self["checkbox"]),
MdcTextField(self["text"]),
MdcDateField(self["date"]),
# MdcSelect(self["foreignkey"]),
MdcField.render(self["checkbox"]),
MdcField.render(self["text"]),
MdcField.render(self["date"]),
SelectField([
# cp("required", "required"),
cp("name", "foreignkey"),
cp("id", "id_foreignkey"),
cp("required", True),
], [
Option([
cp("value", ""),
cp("selected", "selected"),
]
),
Option([
......
......@@ -4,35 +4,21 @@ from chp.django.example.blog.forms import PostForm
def test_render():
regex = '''
"""Regression test."""
regex = """
<form action="/blog/post/create" method="POST" class="mdc-layout-grid__cell" chp-id="[0-9]+">
<div style="display: flex;" chp-id="[0-9]+">
<div class="mdc-form-field mdc-form-field--align-end" data-mdc-auto-init="MDCFormField" chp-id="[0-9]+">
<div class="mdc-checkbox" data-mdc-auto-init="MDCCheckbox" chp-id="[0-9]+">
<input type="checkbox" id="id_checkbox" class="mdc-checkbox__native-control" type="checkbox" chp-id="[0-9]+" />
<div class="mdc-checkbox__background" chp-id="[0-9]+"></div>
</div>
<label for="id_checkbox" chp-id="[0-9]+">This is my checkbox:</label>
</div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="[0-9]+">
<input type="text" id="id_text" maxlength="200" class="mdc-text-field__input" chp-id="[0-9]+" />
<label for="id_text" class="mdc-floating-label" chp-id="[0-9]+">Input Label</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div>
</div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="[0-9]+">
<input type="date" id="id_date" class="mdc-text-field__input" chp-id="[0-9]+" />
<label for="id_date" class="mdc-floating-label" chp-id="[0-9]+">Type = date</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div>
</div>
<div class="mdc-select" data-mdc-auto-init="MDCSelect" chp-id="[0-9]+">
<select id="id_foreignkey" class="mdc-select__native-control" chp-id="[0-9]+">
<option value selected="selected" chp-id="[0-9]+"></option><option value="grains" chp-id="[0-9]+">Bread, Cereal, Rice, and Pasta</option><option value="vegetables" chp-id="[0-9]+">Vegetables</option><option value="fruit" chp-id="[0-9]+">Fruit</option>
</select>
<label for="id_foreignkey" class="mdc-floating-label" chp-id="[0-9]+">Pick a Food Group</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div>
</div></div>
</form>
'''
<div class="mdc-checkbox" data-mdc-auto-init="MDCCheckbox" chp-id="[0-9]+"><input name="checkbox" id="id_checkbox" class="mdc-checkbox__native-control" type="checkbox" chp-id="[0-9]+" />
<div class="mdc-checkbox__background" chp-id="[0-9]+"></div></div><label for="id_checkbox" chp-id="[0-9]+">This is my checkbox:</label></div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="[0-9]+"><input name="text" required id="id_text" class="mdc-text-field__input" type="text" chp-id="[0-9]+" /><label for="id_text" class="mdc-floating-label" chp-id="[0-9]+">Input Label:</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div></div>
<div class="mdc-text-field" data-mdc-auto-init="MDCTextField" chp-id="[0-9]+"><input name="date" required id="id_date" class="mdc-text-field__input" type="date" chp-id="[0-9]+" /><label for="id_date" class="mdc-floating-label" chp-id="[0-9]+">Type = date:</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div></div>
<div class="mdc-select" data-mdc-auto-init="MDCSelect" chp-id="[0-9]+"><select name="foreignkey" id="id_foreignkey" required class="mdc-select__native-control" chp-id="[0-9]+"><option value="" disabled selected chp-id="[0-9]+"></option><option value="grains" chp-id="[0-9]+">Bread, Cereal, Rice, and Pasta</option><option value="vegetables" chp-id="[0-9]+">Vegetables</option><option value="fruit" chp-id="[0-9]+">Fruit</option></select><label for="id_foreignkey" class="mdc-floating-label" chp-id="[0-9]+">Pick a Food Group</label>
<div class="mdc-line-ripple" chp-id="[0-9]+"></div></div></div></form>
"""
regex = regex.replace("\n", "")
regexc = re.compile(regex)
assert re.match(regexc, PostForm().render()) is not None
result = PostForm().render()
assert re.match(regexc, result) is not None
......@@ -4,6 +4,10 @@ from .. import components as chp
from ..pyreact import (ce, cp, get_prop)
MDC_TYPE_MAP = {
"checkbox": {
"class": "mdc-checkbox",
"init": "MDCCheckbox",
},
"text": {
"class": "mdc-text-field",
"init": "MDCTextField",
......@@ -24,6 +28,7 @@ def Div(props=[], children=[]):
def Grid(children=[]):
"""Set the DMC display to 'grid'."""
props = [
cp('class', 'mdc-layout-grid')
]
......@@ -45,6 +50,7 @@ def Cell(children=[]):
def Flex(props=[], children=[], context={}):
"""Set the MDC display to 'flex'."""
display = context.get("display", "flex")
props.append(
cp("style", f"display: {display};")
......@@ -61,10 +67,10 @@ def Form(props=[], children=[]):
def FormField(children=[]):
'''Wrap an element in an mdc-form-field.
"""Wrap an element in an MDC FormField.
Only required for checkbox and radio button fields
'''
"""
props = [
cp("class", "mdc-form-field mdc-form-field--align-end"),
cp("data-mdc-auto-init", "MDCFormField"),
......@@ -73,10 +79,12 @@ def FormField(children=[]):
def LineRipple():
"""Add an MDC Ripple effect."""
return Div([cp("class", "mdc-line-ripple")], [])
def Label(props=[], children=[], context={}):
"""Add MDC class to a label element."""
if children != []:
if context.get("type", "text") not in ["checkbox"]:
props.append(
......@@ -87,59 +95,121 @@ def Label(props=[], children=[], context={}):
return []
def Input(props=[], children=[]):
def Text(props=[], children=[]):
"""Add MDC class to a text input element."""
props.append(
cp("class", "mdc-text-field__input"),
)
return chp.Text(props, children)
def TextField(props=[], children=[], context={}):
"""Wrap a text input element with a label (from context) in an MDC Field.
If a label is provided, find a "for" field id from context or else the
input element id."""
children_field = [
Text(props, children)
]
label = context.get("label", "")
if label != "":
props_label = []
el_for = context.get("for", "")
if el_for == "":
el_for = get_prop(props, "id")
if el_for is None:
el_for = ""
else:
el_for = el_for["value"]
if el_for != "":
props_label = [cp("for", el_for)]
ast_label = Label(props_label, label, context)
children_field.append(ast_label)
children_field.append(LineRipple())
return InputField([], children_field, context)
def Date(props=[], children=[]):
props.append(
cp("class", "mdc-text-field__input"),
)
return chp.Input(props, children)
return chp.Date(props, children)
def DateField(props=[], children=[], context={}):
"""Wrap a date input element with a label (from context) in an MDC Field.
If a label is provided, find a "for" field id from context or else the
input element id."""
children_field = [
Date(props, children)
]
label = context.get("label", "")
if label != "":
props_label = []
el_for = context.get("for", "")
if el_for == "":
el_for = get_prop(props, "id")
if el_for is None:
el_for = ""
else:
el_for = el_for["value"]
if el_for != "":
props_label = [cp("for", el_for)]
ast_label = Label(props_label, label, context)
children_field.append(ast_label)
children_field.append(LineRipple())
return InputField([], children_field, context)
def Checkbox(props=[], children=[], context={}):
"""Wrap a checkbox input element with a default background
in an MDC Field."""
context.update({"type": "checkbox"})
props.append(
cp('class', 'mdc-checkbox__native-control')
)
ast_input = chp.Checkbox(props, children)
props_field = [
cp("class", "mdc-checkbox"),
cp("data-mdc-auto-init", "MDCCheckbox"),
]
children_field = [
ast_input,
Div([cp("class", "mdc-checkbox__background")], []),
]
ast_field = Div(props_field, children_field)
return ast_field
return InputField([], children_field, context)
def CheckboxField(props=[], children=[], context={}):
"""Wrap a checkbox MDC field with a label (from context)
in an MDC FormField.
If a label is provided, find a "for" field id from context or else the
input element id."""
ast_checkbox = Checkbox(props, children, context)
children_formfield = [ast_checkbox]
label = context.get("label", "")
if label != "":
props_label = []
el_id = get_prop(props, "id")
if el_id is not None:
props_label = [
cp("for", el_id["value"]),
]
el_for = context.get("for", "")
if el_for == "":
el_for = get_prop(props, "id")
if el_for is None:
el_for = ""
else:
el_for = el_for["value"]
if el_for != "":
props_label = [cp("for", el_for)]
ast_label = Label(props_label, label, context)
children_formfield.append(ast_label)
return FormField(children_formfield)
def InputField(props=[], children=[]):
'''Receive props ("type") of the inner Input element
and wrap the children in a new MDC Div.
'''
typ = get_prop(props, "type")
if typ is None:
typ = {"name": "type",
"value": "text",
}
mdc_type = MDC_TYPE_MAP[typ["value"]]
def InputField(props=[], children=[], context={}):
"""Wrap the children in an MDC input field.
Requires context["type"] to lookup MDC classes."""
el_type = context.get("type", "text")
mdc_type = MDC_TYPE_MAP[el_type]
props_field = [
cp("class", mdc_type["class"]),
cp("data-mdc-auto-init", mdc_type["init"]),
......@@ -148,6 +218,7 @@ def InputField(props=[], children=[]):
def SubmitButton(props=[], children=[]):
"""Wrap a submit button in an MDC field."""
ast = chp.SubmitButton(props, children)
props_div = [
cp("class", "mdc-button"),
......@@ -157,6 +228,7 @@ def SubmitButton(props=[], children=[]):
def Select(props=[], children=[]):
"""Add MDC class to a Select element."""
props.append(
cp("class", "mdc-select__native-control")
)
......@@ -164,18 +236,27 @@ def Select(props=[], children=[]):
def SelectField(props=[], children=[], context={}):
"""Wrap a select element with a label (from context) in an MDC Field.
If a label is provided, find a "for" field id from context or else the
select element id."""
context.update({"type": "select"})
ast = Select(props, children)
children_field = [ast]
# add a label add label (optional) and find a "for" field id
label = context.get("label", "")
if label != "":
props_label = []
el_id = get_prop(props, "id")
if el_id is not None:
props_label.append(
cp("for", el_id["value"]))
el_for = context.get("for", "")
if el_for == "":
el_for = get_prop(props, "id")
if el_for is None:
el_for = ""
else:
el_for = el_for["value"]
if el_for != "":
props_label = [cp("for", el_for)]
ast_label = Label(props_label, label, context)
children_field.append(ast_label)
children_field.append(LineRipple())
props_field = [
cp("type", "select")]
return InputField(props_field, children_field)
return InputField([], children_field, context)
from django.forms.boundfield import BoundField
from django.forms.fields import Field
from django.utils.functional import Promise
from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
import django.forms.widgets as Widgets
from ... import components as chp
from .. import components as mdc
# alias for chp.pyreact.create_prop()
cp = chp.cp
class Factory:
@staticmethod
def get_label(field):
# code taken from Boundfield.label_tag()
contents = field.label
label_suffix = (field.field.label_suffix
if field.field.label_suffix is not None
else (field.form.label_suffix
if hasattr(field, "form") else ""))
if label_suffix and contents and contents[-1] not in _(':?.!'):
contents = format_html('{}{}', contents, label_suffix)
return contents
@staticmethod
def render(field):
"""Introspect the field and call an MdcWidget render method."""
if isinstance(field, BoundField):
widget_type = field.field.widget.__class__.__name__
mdc_widget = getattr(
MdcWidgets, f"Mdc{widget_type}")
label = Factory.get_label(field)
return field.as_widget(mdc_widget(label=label))
raise NotImplementedError
class ChpWidgetMixin:
chp_widget = None
def __init__(self, attrs=None, **kwargs):
"""Capture the field label in the widget."""
self.label = kwargs.pop("label", None)
if attrs is not None:
attrs = attrs.copy()
super().__init__(attrs)
def _add_label(self, context):
"""Add the field label to the widget context."""
label = self.label
# cast any lazy translation strings
if isinstance(label, Promise):
label = conditional_escape(label)
context['widget'].update(
{'label': label,
'id_for_label':
self.id_for_label(context['widget']['attrs']['id'])
})
return context
def get_mdc_context(self, context):
"""Update the context for MDC components."""
context = self._add_label(context)
context.update({
"type": context['widget']['type'],
"label": context['widget']['label'],
"for": context['widget']['id_for_label'],
})
return context
def _render(self, template_name, context, renderer=None):
"""Render the widget as a CHP MDC AST."""
# prepare the field label
context = self.get_mdc_context(context)
return self._mdc_render(context)
def _mdc_render(self, context):
"""Override this method for MDC widget rendering."""
raise NotImplementedError
class MdcWidgets:
class MdcCheckboxInput(ChpWidgetMixin, Widgets.CheckboxInput):
mdc_type = "checkbox"
def _mdc_render(self, context):
props, children = [], []
props.append(
cp("name", context['widget']['name']))
for attr, value in context['widget']['attrs'].items():
props.append(cp(attr, value))
return mdc.CheckboxField(props, children, context)
class MdcTextInput(ChpWidgetMixin, Widgets.TextInput):
mdc_type = "text"
def _mdc_render(self, context):
props, children = [], []
props.append(
cp("name", context['widget']['name']))
for attr, value in context['widget']['attrs'].items():
props.append(cp(attr, value))
return mdc.TextField(props, children, context)
class MdcDateInput(ChpWidgetMixin, Widgets.DateInput):
mdc_type = "date"
def _mdc_render(self, context):
props, children = [], []
props.append(
cp("name", context['widget']['name']))
for attr, value in context['widget']['attrs'].items():
props.append(cp(attr, value))
return mdc.DateField(props, children, context)
class MdcSelect(ChpWidgetMixin, Widgets.Select):
mdc_type = "select"
def _mdc_render(self, context):
props, children = [], []
props.append(
cp("name", context['widget']['name']))
for attr, value in context['widget']['attrs'].items():
props.append(cp(attr, value))
# TODO: build children list of options from queryset.
# TODO: create optgroups if required.
return mdc.SelectField(props, children, context)
......@@ -144,9 +144,11 @@ def render_html(el, props, child):
QUOT = "\"" # like &quot;
def render_prop(prop):
if prop['value'] == "":
if prop['value'] is True:
return prop['name']
return f"{prop['name']}={QUOT}{prop['value']}{QUOT}"
if prop["value"] is not False:
return f"{prop['name']}={QUOT}{prop['value']}{QUOT}"
return ""
props_str = ' '.join([render_prop(p)
for p in props if p["name"] != "children"])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment