ผลต่างระหว่างรุ่นของ "CuneiForm"

จาก คูนิฟ็อกซ์ วิกิ
 
(ไม่แสดง 86 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
บรรทัดที่ 1: บรรทัดที่ 1:
CuneiForm type objects are powered by WTForms engine. However, this object type also introduce a handful of additional variables and attributes to strike a sweet balance between ease of development and flexibility.
CuneiForm type objects are powered by WTForms engine. However, this object type also introduces a handful of additional variables and attributes to strike a sweet balance between ease of development and flexibility.


== Define ==
== Define ==
บรรทัดที่ 28: บรรทัดที่ 28:


==== Original Intentions ====
==== Original Intentions ====
Reading through the CuneiFox code base, one is sure to notice the excessive use of StringFields where one would expect DateFields, TimeFields, FloatFields, and IntegerFields as supported by WTForms. This design choice stems was made to address a challenging browser behaviour. Modern browsers tend to enforce formatting of fields marked as certain types based on the user's locale setting, which can be tied to either the operating system or the browser itself. This automatic formatting can be difficult to override or reverse, leading to inconsistencies and confusion on the user's part especially when switching workstations.
Reading through the CuneiFox code base, one is sure to notice the excessive use of StringFields where one would expect DateFields, TimeFields, FloatFields, and IntegerFields as supported by WTForms. This design choice was made to address a challenging browser behaviour. Modern browsers tend to enforce formatting of fields marked as certain types based on the user's locale setting, which can be tied to either the operating system or the browser itself. This automatic formatting can be difficult to override or reverse, leading to inconsistencies and confusion on the user's part, especially when switching workstations.


To address this issue, CuneiFox developed its own value typing. Our approach applies custom-made JavaScript codes on unformatted and untyped (as far as the browser is aware) fields. This way, the formatting rendered on the client-side perfectly obeys the company's specific preferences within the CuneiFox system.
To address this issue, CuneiFox developed its own value typing. Our approach applies custom-made JavaScript codes to unformatted and untyped (as far as the browser is aware) fields. This way, the formatting rendered on the client-side perfectly obeys the company's specific preferences within the CuneiFox system.


==== Current Incarnation ====
==== Current Incarnation ====
บรรทัดที่ 126: บรรทัดที่ 126:
=== Other Form Definition Variables ===
=== Other Form Definition Variables ===
Other notable MASTER variables that can be set upon form definition include:
Other notable MASTER variables that can be set upon form definition include:
* '''{{code|MASTER_firstonly}}''': A list of field names that are only active (editable) for a new entry, but inactive (not editable) when editing an old entry.
* '''{{code|MASTER_firstonly}}''' ''([str,])'': A list of field names that are only active (editable) for a new entry, but inactive (not editable) when editing an old entry.
* '''{{code|MASTER_skipseqs}}''': A list of field names to be skipped when the user is navigating through the form via {{key press|Tab}} or {{key press|Enter}}.
* '''{{code|MASTER_skipseqs}}''' ''([str,])'': A list of field names to be skipped when the user is navigating through the form via {{key press|Tab}} or {{key press|Enter}}.
* '''{{code|MASTER_skipcols}}''': A list of field names to be skipped when committing data to database. CuneiFox naturally skip fields that are meant for client-side viewing only (fields whose names are not present as database table columns). However, if the developer wants a field to be skipped despite the database having a similarly named column, do put the field name in this list.
* '''{{code|MASTER_skipcols}}''' ''([str,])'': A list of field names to be skipped when committing data to database. CuneiFox naturally skip fields that are meant for client-side viewing only (fields whose names are not present as database table columns). However, if the developer wants a field to be skipped despite the database having a similarly named column, do put the field name in this list.


These 3 variables share the same format:
These 3 variables share the same format:
บรรทัดที่ 224: บรรทัดที่ 224:
* '''in_table''' ''(str: defaults to {{code|lang=python|False}} for free-standing forms)'': The name of the table with this form as the in-line form.
* '''in_table''' ''(str: defaults to {{code|lang=python|False}} for free-standing forms)'': The name of the table with this form as the in-line form.
* '''table_linked''' ''(str)'': Similar to {{code|in_table}} but this one applies to full pop-up form (as opposed to in-line form).
* '''table_linked''' ''(str)'': Similar to {{code|in_table}} but this one applies to full pop-up form (as opposed to in-line form).
* '''in_modal''' ''(str: defaults to {{code|lang=python|False}} for free-standing forms)'': the ID of the modal with this form as the '''MAIN SUBMITTING COMPONENT'''. (See [[CuneiModal]] for details.)
* '''in_modal''' ''(str: defaults to {{code|lang=python|False}} for free-standing forms)'': The ID of the modal with this form as the '''MAIN SUBMITTING COMPONENT'''. (See [[CuneiModal]] for details.)
* '''sequence_bound''' ''(bool: defaults to {{code|lang=python|False}})'': Whether form submission is linked to advance the edit-mode of the page. (See [[Multi-component Page]])
* '''sequence_bound''' ''(bool: defaults to {{code|lang=python|False}})'': Whether form submission is linked to advance the edit-mode of the page. (See [[Multi-component Page]])
* '''slim''' ''(bool: defaults to {{code|lang=python|False}})'': Whether to render the form in slim mode. (Normally, form label and field take 2 'rows' on the screen. In slim mode, form label and field share the same 'row', and the field is drawn with less vertical padding.)
* '''slim''' ''(bool: defaults to {{code|lang=python|False}})'': Whether to render the form in slim mode. (Normally, form label and field take 2 'rows' on the screen. In slim mode, form label and field share the same 'row', and the field is drawn with less vertical padding.)
บรรทัดที่ 270: บรรทัดที่ 270:


'''NOTE THAT''', in this section, unless explicitly specified:
'''NOTE THAT''', in this section, unless explicitly specified:
* '''{{code|form}}''' refers to a CuneiForm instance sent from Flask and accissible by Jinja2
* '''{{code|form}}''' refers to a CuneiForm instance sent from Flask and accessible by Jinja2
* '''{{code|form_dom}}''' refers to the {{code|lang=html|<form>}} DOM element
* '''{{code|form_dom}}''' refers to the {{code|lang=html|<form>}} DOM element
* '''{{code|form_obj}}''' refers to the JavaScript form object.
* '''{{code|form_obj}}''' refers to the JavaScript form object.


=== General Form Head & Form Object ===
=== General Form Head & Form Object ===
'''{{code|lang=javascript|1= general_form_head(form, is_master=false, multipart=false)}}'''
In essence, this Jinja2 script is responsible for the following actions:
In essence, this Jinja2 script is responsible for the following actions:
# Create a {{code|lang=html|<form>}} DOM element corresponding to the form.
# Create a {{code|lang=html|<form>}} DOM element corresponding to the form.
บรรทัดที่ 284: บรรทัดที่ 287:
Below is the list of and notes on form object attributes:
Below is the list of and notes on form object attributes:
* '''Basic Attributes'''
* '''Basic Attributes'''
** '''_id''': Takes on the value of {{code|form._id}}.
** '''id''': Takes on the value of {{code|form._id}}.
** '''self_type''': Takes on value {{code|lang=javascript|'form'}}.
** '''self_type''': Takes on value {{code|lang=javascript|'form'}}.
** '''token''': The CSRF Token of the form, used to verify the form submission request.
** '''token''': The CSRF Token of the form, used to verify the form submission request.


* '''Copied Attributes''' ''(See [[#Notes on Keyword Arguments]] for details)''
* '''Copied Attributes''' ''(See [[#Notes on Keyword Arguments]] for details)''
** '''skipseqs''', '''firstonly''', '''in_table''', '''table_linked''', '''sequence_bound''', '''slim''', '''subslim''', '''seq''', '''populate_route''', '''redirect''', '''wait_long_process''', '''track_route''', and '''wait_redirect''': Takes on the exact value.
** '''firstonly, in_table, populate_route, redirect, seq, sequence_bound, skipseqs, slim, subslim, table_linked, track_route, wait_long_process,''' and '''wait_redirect''': Takes on the exact value.
** '''instacalc''' and '''populate_suppress''': Takes on the exact value, but set to a blank dict {{code|lang=javascript|{} }} and a blank array {{code|lang=javascript|[]}}, respectively, when the value is {{code|lang=python|False}}.
** '''instacalc''' and '''populate_suppress''': Takes on the exact value, but set to a blank dict {{code|lang=javascript|{} }} and a blank array {{code|lang=javascript|[]}}, respectively, when the value is {{code|lang=python|False}}.
** '''submit_id''': Takes on the exact value ''if the corresponding submit button does not require a confirm pop-up''. If a confirm pop-up is needed, the value is suffixed with {{code|_fake}}, e.g. {{code|lang=javascript|'submit_fake'}}. (See [[#Buttons & Submits]] for more details.)
** '''submit_id''': Takes on the exact value ''if the corresponding submit button does not require a confirm pop-up''. If a confirm pop-up is needed, the value is suffixed with {{code|_fake}}, e.g. {{code|lang=javascript|'submit_fake'}}. (See [[#Buttons & Submits]] for more details.)
บรรทัดที่ 307: บรรทัดที่ 310:
** '''proxy_submit_ele''': The mock element meant to be used instead of the form's own submit element. Use cases include the need to call additional functions before form submission and form chaining.
** '''proxy_submit_ele''': The mock element meant to be used instead of the form's own submit element. Use cases include the need to call additional functions before form submission and form chaining.
** '''linked_modals''': An array collection of 'modal objects' linked to the form via search-and-fill relationships.
** '''linked_modals''': An array collection of 'modal objects' linked to the form via search-and-fill relationships.
** '''expand_modals''': An array of the names of expansion modals linked to the form.


* '''Field State & Type Attributes'''
* '''Field State & Type Attributes'''
บรรทัดที่ 343: บรรทัดที่ 347:
=== CuneiForm Jinja2 Macro Pack ===
=== CuneiForm Jinja2 Macro Pack ===
When putting form fields onto the client-side pages, there is absolutely nothing stopping developers from using the ordinary WTForms+Jinja2 approach. However, with the amount of forms and fields CuneiFox will be handling, the code can get unwieldy fast. So, at the cost of some flexibility, CuneiFox comes with a macro series to automate the most common field formats, along with the accompanying scripts and events. Let us take a little tour on these macros.
When putting form fields onto the client-side pages, there is absolutely nothing stopping developers from using the ordinary WTForms+Jinja2 approach. However, with the amount of forms and fields CuneiFox will be handling, the code can get unwieldy fast. So, at the cost of some flexibility, CuneiFox comes with a macro series to automate the most common field formats, along with the accompanying scripts and events. Let us take a little tour on these macros.
Due to the number of arguments in these macros, for the sake of all things holy, '''DO NOT''' rely on argument order farther than ''form'' and ''blank'', '''ALWAYS PROVIDE''' argument keywords.


==== String & Text Area Fields ====
==== String & Text Area Fields ====
<div><ul>
<li style="display: inline-block;">[[ไฟล์:Form StringField fat.png|400px|thumb|none|alt=String fields created via 'render_blank'.|String fields created via 'render_blank'.]]</li>
<li style="display: inline-block;">[[ไฟล์:Form StringField slim.png|400px|thumb|none|alt=String fields (slim) created via 'render_blank'.|String fields (slim) created via 'render_blank'.]]</li>
</ul></div>
String and text-area fields are the most prevalent type of form fields, and also one most riddled with features and expectations. The CuneiFox macro that handles rendering of these fields is {{code|render_blank}}.
String and text-area fields are the most prevalent type of form fields, and also one most riddled with features and expectations. The CuneiFox macro that handles rendering of these fields is {{code|render_blank}}.
Due to the sheer number of arguments here, for the sake of all things holy, '''DO NOT''' rely on argument order farther than ''form'' and ''blank'', '''ALWAYS PROVIDE''' argument keywords.


'''<syntaxhighlight lang="javascript">
'''<syntaxhighlight lang="javascript">
บรรทัดที่ 358: บรรทัดที่ 368:
             hidden=false, tabindex=false,
             hidden=false, tabindex=false,
             pad_bottom="default",
             pad_bottom="default",
            // Arguments exclusive to 'render_blank':
             force_fat=false, force_slim=false, slim_pb=2)
             force_fat=false, force_slim=false, slim_pb=2,
            // Arguments exclusive to 'render_blank_slim':
            bunch_pb=2)
</syntaxhighlight>'''
</syntaxhighlight>'''


บรรทัดที่ 373: บรรทัดที่ 380:
* '''fill''' ''(str)'': The specification for fill function. This argument takes the format {{code|lang=javascript|'<field0>:<col0>,<field1>:<col1>,...'}}; where ''fieldX'' refers to a field within the same form, and ''colX'' refers to a column within the search result.
* '''fill''' ''(str)'': The specification for fill function. This argument takes the format {{code|lang=javascript|'<field0>:<col0>,<field1>:<col1>,...'}}; where ''fieldX'' refers to a field within the same form, and ''colX'' refers to a column within the search result.
* '''expand''' ''(str)'': The specification for form expansion. This argument takes the format {{code|lang=javascript|'<expand_modal_id>:<first_expand_field>'}}. A field with '''expand''' argument given is drawn with an ''Expand'' button [[ไฟล์:Cuneifox formexpand btn.png|25px|frameless|alt=Expand button|Expand button]].
* '''expand''' ''(str)'': The specification for form expansion. This argument takes the format {{code|lang=javascript|'<expand_modal_id>:<first_expand_field>'}}. A field with '''expand''' argument given is drawn with an ''Expand'' button [[ไฟล์:Cuneifox formexpand btn.png|25px|frameless|alt=Expand button|Expand button]].
* '''field_modal''' ''(str)'': The id of the modal holding this field. This value control the showing/hiding for the modal when the field gets or loses focus.
* '''field_tab''' ''(str)'': The id of the tab holding this field. This value control the navigation to/away from the tab when the field gets or loses focus.
* '''prepend''' ''(str)'': The string to attach to the left of the field.
* '''clear_prepend''' ''(bool)'': Whether the HTML element {{code|lang=html|<span>}} holding the '''prepend''' string has a clear background.
* '''append''' ''(str)'': The string to attach to the right of the field.
* '''clear_append''' ''(bool)'': Whether the HTML element {{code|lang=html|<span>}} holding the '''append''' string has a clear background.
* '''label_width''' ''(str: Effective only for fields drawn in slim mode)'': The width of the label {{code|lang=html|<div>}}. The value can be anything interpretable as CSS width, but {{code|lang=css|'<int>px'}} is recommended.
* '''headless''' ''(bool)'': Whether to draw the label {{code|lang=html|<div>}} at all.
* '''hidden''' ''(bool)'': Whether the field and the label is drawn but remain invisible. Hidden field is achieved by setting {{code|lang=css|1= display=none}} on the field and label's collective parent {{code|lang=html|<div>}}.
* '''tabindex''' ''(int)'': The tab-index of the field. This feature is kept open for flexibility; no examples of uses exist at the time of writing.
* '''pad_bottom''' ''(bool OR {{code|lang=javascript|'default'}})'': Whether the field should have some bottom spacing.
* '''force_fat''' ''(bool)'': Whether to draw the field in ''fat'' mode regardless of {{code|form.slim}} and {{code|form.subslim}} values.
* '''force_slim''' ''(bool)'': Whether to draw the field in ''slim'' mode regardless of {{code|form.slim}} and {{code|form.subslim}} values.
* '''slim_pb''' ''(int: 0 ~ 5)'': Set the bottom padding for ''slim'' field. This argument utilizes Bootstrap 4's {{code|lang=css|pb-0}} through {{code|lang=css|pb-5}} classes.
|}
|}


==== Drop-list Fields ====
==== Drop-list Fields ====
<div><ul>
<li style="display: inline-block;">[[ไฟล์:Form SelectField fat.png|300px|thumb|none|alt=Drop-list fields created via 'render_select'.|Drop-list fields created via 'render_select'.]]</li>
<li style="display: inline-block;">[[ไฟล์:Form SelectField slim.png|300px|thumb|none|alt=Drop-list fields (slim) created via 'render_select'.|Drop-list fields (slim) created via 'render_select'.]]</li>
</ul></div>
'''<syntaxhighlight lang="javascript">
render_select(form, blank,
              field_modal=false, field_tab=false,
              label_width="105px", headless=false,
              hidden=false, tabindex=false,
              pad_bottom="default",
              force_fat=false, force_slim=false, slim_pb=2)
</syntaxhighlight>'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' || ''(See descriptions under [[#String & Text Area Fields]] sub-section)''
|}


==== Checkboxes & Toggles ====
==== Checkboxes & Toggles ====
[[ไฟล์:Form_BoolField.png|300px|thumb|center|alt=Checkboxes and toggles created via 'render_check' and 'render_toggle'.|Checkboxes and toggles created via 'render_check' and 'render_toggle'.]]
'''<syntaxhighlight lang="javascript">
render_check(form, blank,
            field_modal=false, field_tab=false,
            hidden=false, tabindex=false,
            disabled=false)
render_toggle(form, blank,
              field_modal=false, field_tab=false,
              hidden=false, tabindex=false,
              disabled=false, pad_bottom="default",
              force_fat=false, force_slim=false)
</syntaxhighlight>'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' || ''(See descriptions under [[#String & Text Area Fields]] sub-section)''
* '''disabled''' ''(bool)'': Whether the field should be drawn disabled on page load.
|}


==== Radios ====
==== Radios ====
[[ไฟล์:Form_RadioField.png|400px|thumb|center|alt=Radio fields created via 'render_radio' (with 2 columns).|Radio fields created via 'render_radio' (with 2 columns).]]
'''<syntaxhighlight lang="javascript">
render_radio(form, blank,
            field_modal=false, field_tab=false,
            label_width="105px", headless=false, col=1,
            hidden=false, tabindex=false,
            force_fat=false, force_slim=false,
            disabled=false)
</syntaxhighlight>'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' || ''(See descriptions under [[#String & Text Area Fields]] sub-section)''
* '''col''' ''(int)'': The number of columns radio options should be divided into. The ordering of radio options is horizontal first and vertical second.
* '''disabled''' ''(bool)'': Whether the field should be drawn disabled on page load.
|}


==== File & Image Fields ====
==== File & Image Fields ====


<div><ul>
<li style="display: inline-block;">[[ไฟล์:Form FileField fat.png|300px|thumb|none|alt=File fields created via 'render_upload'.|File fields created via 'render_upload'.]]</li>
<li style="display: inline-block;">[[ไฟล์:Form FileField slim.png|300px|thumb|none|alt=File fields (slim) created via 'render_upload'.|File fields (slim) created via 'render_upload'.]]</li>
</ul></div>
'''<syntaxhighlight lang="javascript">
render_upload(form, blank,
              field_modal=false, field_tab=false,
              label_width="105px", headless=false,
              hidden=false, tabindex=false,
              pad_bottom="default",
              force_fat=false, force_slim=false, slim_pb=2)
</syntaxhighlight>'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' || ''(See descriptions under [[#String & Text Area Fields]] sub-section)''
|}


'''NOTE THAT''' the image box is not drawn as a part of the Jinja2 macro. Developers must create the {{code|lang=html|<img>}} manually and assign to it the id {{code|lang=javascript|'<field_id>-imgbox'}} ''before running {{code|general_form_tail}}''. The tail macro shall automatically link the image box with the form field.
'''NOTES''' on file field behaviour, additional buttons, and image box:
* ''The body of the field'' itself triggers a file selector.
* The ''view'' button [[ไฟล์:Cuneifox view btn.png|25px|frameless|alt=View file button|View file button]] opens the currently selected file in a new tab.
* The ''cam'' button [[ไฟล์:Cuneifox cam btn.png|25px|frameless|alt=Camera button|Camera button]] activates the camera modal where the user can capture an image with an applicable peripheral. This button only appears if the form has a BooleanField named {{code|lang=python|'<file_field_name>_isshot'}} drawn ''(can be hidden)'' on the page.
* The ''revert'' button [[ไฟล์:Cuneifox revert btn.png|25px|frameless|alt=Revert button|Revert button]] undoes the change made to the file field and restore its original file/value.
* The ''unselect'' button [[ไฟล์:Cuneifox file delete btn.png|25px|frameless|alt=Unselect button|Unselect button]] clears the file field. This button only appears if the form has a BooleanField named {{code|lang=python|'<file_field_name>_isdel'}} drawn ''(can be hidden)'' on the page.
* The ''image box'' triggers the ''view'' button. Note that the image box is not drawn automatically via the {{code|render_upload}} macro. Should it be needed, developers must create the {{code|lang=html|<img>}} element manually and give it the id {{code|lang=javascript|'<file_field_name>-imgbox'}} ''before running {{code|general_form_tail}}''. The tail macro shall automatically detect and link the image box with the form field.


==== Buttons & Submits ====
==== Buttons & Submits ====
[[ไฟล์:Form Buttons.png|400px|thumb|center|alt=Buttons created via 'render_submit' and 'render_btn'.|Buttons created via 'render_submit' and 'render_btn'.]]
'''<syntaxhighlight lang="javascript">
render_submit(form, blank,
              class="primary", confirm_first=false,
              hidden=false, tabindex=false,
              force_fat=false, force_slim=false)
render_btn(id=false, size="normal",
          width=false, height=false,
          label=false, icon_name=false,
          class="primary", custom_color=false,
          hidden=false, tabindex=false,
          slim=false, disabled=false)
</syntaxhighlight>'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' || ''(See descriptions under [[#String & Text Area Fields]] sub-section)''
* '''class''' ''(str)'': The BootStrap 4 class that dictates the button's appearance. ''(See picture above.)''
* '''confirm_first''' ''(list: {{code|lang=javascript|['<confirm_modal_header>', '<field_name_to_confirm>']}}'': If given, clicking on the submit button shows a confirm modal instead of submitting immediately. This effect is achieved by drawing a fake submit button in the designated space, moving the real submit button into the confirm modal, and adding suffix '_fake' to the form's '''submit_id''' attribute.
* '''width''' ''(str)'': The width of the button. The value can be anything interpretable as CSS size, but {{code|lang=css|'<int>px'}} is recommended.
* '''height''' ''(str)'': The height of the button. The value can be anything interpretable as CSS size, but {{code|lang=css|'<int>px'}} is recommended.
* '''label''' ''(str)'': The text label on the button.
* '''icon_name''' ''(str)'': The name of the icon to appear on the button. ''(See picture above for reference.)''
* '''custom_color''' ''(list)'': The custom colour scheme for the button. If '''class''' argument is also given, this argument takes precedence. The argument takes the form of 6-membered list, each can be anything interpretable as CSS colour, in this order:
** Normal background colour
** Normal foreground colour
** Normal border colour
** Hover-state background colour
** Hover-state foreground colour
** Hover-state border colour
** Active-state glow colour
* '''slim''' ''(bool)'': Whether the button should match the slim form atyle.
|}


=== General Form Tail ===
=== General Form Tail ===
At this point, the {{code|form_obj}} has been initiated, all fields and relevant elements are created; it is time to finalize the {{code|form_obj}} attributes and link all the form elements together.
'''{{code|lang=javascript|1= general_form_tail(form, populate=true, pack_del=false)}}'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' ||
* '''form''' ''(FlaskForm-CuneiForm)''
* '''populate''' ''(bool)'': Whether to send an immediate data request to the server.
** ''For single-form pages'', the form populated (besides the default values embedded in the CuneiForm instance itself) here as a sub-routine of {{code|general_form_tail}}.
** ''For [[Multi-component Page|multi-component pages]]'', however, the mass-populating routine is further down (after all tables and forms are created). So, this value should be set to {{code|lang=javascript|false}}.
* '''pack_del''' ''(bool)'': Whether this form has a ''submit as delete functionality'' (with ''del_submit'' and ''is_del'' fields) in addition to a ''normal submit mode''. (See [[#Submit-delete Field Set]].)
|}
The workings of this macro can be thus summarized:
# '''Process the form's ''populate_id'' attribute'''
#* Interpret and repack each string value into a more easily usable format.
#* Where applicable, bind a ''change'' event to the referenced fields so that they trigger a form re-populating routine when changes are detected.
# '''Finalize ''References to DOM Elements & Objects'' and ''Field State & Type Attributes'' families of {{code|form_obj}} attributes.'''
# '''Deal with form fields, adjacent buttons, and image boxes'''
#* Assign additional field attributes to facilitate ''track_tb'', ''search'' and ''instacalc'' routines
#* Bind events to take care of ...
#** Functionalities of adjacent buttons
#** Field formatting for all special types
#** Pattern recognition for date/time types
#** Form navigation while considering the fields' ''search'', ''instacalc'', ''field_modal'', and ''field_tab'' attributes.
#* Minor behaviour tweaks via attributes and/or event bindings
# '''''If 'pack_del' is {{code|lang=javascript|true}}'', bind the relevant submission event'''
# '''Bind pre-submission and submission routine'''
# '''''If 'populate' is {{code|lang=javascript|true}}'', send a data fetch request'''


== Notable Features & Sub-routines ==
== Notable Features & Sub-routines ==
This section aims to give a little insight into some CuneiForm's notable sub-routines.
=== Table Tracking ===
The role of the table tracking form in this feature is only to pack the appropriate value into the attribute '''selection_trackers''' of the corresponding {{code|table_obj}}. The inner working of this function is all handled by [[CuneiTable]], refer to the main article for more details.


=== Search and Fill ===
=== Search and Fill ===
In the search-fill function, a CuneiForm take responsibility in two areas:
# Invoking the corresponding [[CuneiModal|search modal]] (which works along side its corresponding [[CuneiTable|search table]]). The control of the client-side behaviours is then relinquished to the modal.
# Once the modal has retrieved the selection result from the server (via the search table), the client-side control is handed back to the CuneiForm to do the form filling.
''Refer to [[CuneiTable#Shortcut Route for Single-table Page & Search Table|a subsection in the main CuneiTable article]] for a template of a Flask route that handles table populating and search result fetching.''
==== Searching ====
Searching and filling sub-routine can be triggered in 3 ways listed below, with slightly different behaviours. These 3 trigger paths can be conflicting, thus the '''populating''' and '''search_filling''' flag attributes.
# '''Click event''' on the adjacent search button [[ไฟล์:Cuneifox search btn.png|25px|frameless|alt=Search button|Search button]]. This trigger mode always launch the search modal with full selections then cedes control.
# '''Pressing {{key press|Tab}} or {{key press|Enter}}''' to navigate away from the search field. This triggers the ''quick-search'' mode.
# '''Navigating away from the field by any other means''' (handled by ''change'' event on the field) also triggers the ''quick-search'' mode.
CuneiForm only triggers ''quick-search'' mode for single-selection search tables; multi-selection tables are always invoked in normal mode.
In ''quick-search'' mode, CuneiForm commands the search table (directly within the search modal) to send a modified data fetch request first, then relinquishes control. The modified CuneiTable fetch request has a special instruction to:
* Fetch only entries whose search column value start with the field value.
* If the fetch returns multiple entries, the search modal is launched by CuneiTable itself.
* If the fetch returns only 1 entries, automatically make the selection and does not show the search modal at all. (The modal might appear if the server takes longer than 500ms to respond, in that case, the modal will automatically close.)
'''NOTE THAT''' trigger paths #2 and #3 only work if the field value is modified (detected via ''input'' events on the field) to prevent unnecessary requests.
==== Filling ====
Form filling after a search result fetch is handled by the JavaScript function '''{{code|process_invoker}}''', which closely mimics the normal form filling routine with some additional flag manipulation.
After filling, users might expect additional navigation behaviour (especially when the search is initiated by trigger paths #2 and #3). This navigational follow-up is handled by the JavaScript function '''{{code|post_fill_invoker}}'''.


=== Table Tracking ===
Although these 2 functions resides in the JavaScript file for [[CuneiModal]], their inner workings are heavily based on CuneiForm logic.


=== Instacalc Routine ===
=== Instacalc Routine ===
Instacalc is a populating-type operations not too different from search-filling. It is even triggered by the same trigger paths #2 and #3 listed [[#Searching|above]].
The rough step-by-step working of an instacalc cycle is as follows:
# Determine whether the invoked instacalc should be '''processed''' immediately, '''queued''', or '''dumped'''.
# '''Send an instacalc ''POST'' request''' to the server.
# Once a successfull instacalc response is received, '''fill the form''' with appropriate values/formatting. Then, run functions in '''post_insta_cmds''' attribute of the {{code|form_obj}} (if any).
==== Process, Queue, or Dump ====
A single CuneiForm only handles one instacalc routine at a time. This behaviour necessitate a queue.  Each invoked instacalc goes through these filters:
# Check whether an instacalc with '''the same route''' is currently running or already in the queue. (Instacalcs re-invoked from the queue get to skip this filter.)
#* If yes, the invoked instacalc is dumped.
# Check whether the CuneiForm is '''currently running another instacalc''' (via flag '''populating''').
#* If yes, the invoked instacalc is queued.
# For an instacalc re-invoked from the queue, check whether '''all the result fields are already handled''' by the most recent populate-type process.
#* If yes, the invoked instacalc is dumped.
# Once this point is reached, the invoked instacalc is processed.
==== Instacalc Route Template ====
Although instacalc is a form-based procedure, a very similar and related procedure is also presented in [[CuneiTable]], specifically when a column in the table is not presented in the database but is calculated on the fly before being sent over to the client-side. Thus, it is advised that an instacalc route be written with such 'internal calls' in mind.
Below is the model of a typical instacalc route:
<syntaxhighlight lang="python" line="1">
@<blueprint>.route('<instacalc_path>', methods=['POST'])
@login_required # Optional
def <function_name>(internal_args=None):
    # 'internal_args' is given as a dict when internally called.
    if internal_args is None:
        identifiers = json.loads(request.form.get('identifier'))
        <input0> = identifiers['<input0_key>']
        <input1> = identifiers['<input1_key>']
        ...
    else:
        <input0> = internal_args.get('<input0_key>')
        <input1> = internal_args.get('<input1_key>')
        ...
    # Calculation block
    ...
    ...
    return {'<output0_key>':<output0>, '<output1_key>':<output1>, ...}
</syntaxhighlight>


=== Navigation & Focusing ===
=== Navigation & Focusing ===
Being able to move around the form with a mouse provides easy access and peace of mind to new users. However, one cannot deny the efficiency of an intuitive form navigation via a keyboard. To accommodate such functionality, a custom navigation and focusing routine has to be developed for CuneiForm.
Keyboard form navigation in CuneiForm uses:
* {{key press|Tab}} or {{key press|Enter}} to go forward in the form and
* {{key press|Shift|Tab}} or {{key press|Shift|Enter}} to go backward
'''NOTE THAT''' because, if the current field is the last active field, the {{key press|Enter}} key also submits the form, the flag attribute '''{{code|proceeding}}''' is flipped during keyboard navigation to prevent potential conflicting submit action.
==== Determining What's Next ====
CuneiForm follows the steps listed below to determine which element shoud take the focus next:
# Look at the next (or previous) logical field:
#* If the current field has a custom next/previous field defined (via attibutes '''{{code|custom_next_ele}}''' or '''{{code|custom_prev_ele}}'''), look there.
#* Otherwise, look for the next field in the form's '''{{code|seq}}''' array.
# Check whether that field should take the focus.
#* The field must '''NOT''' be skipped (not present in the form's '''{{code|skipseqs}}''' attribute).
#* The field must '''NOT''' be in disabled or read-only state.
#* The field and its parent element (the corresponding elements in '''{{code|fill_eles}}''' and '''{{code|fill_ele_divs}}''') must '''NOT''' be hidden ({{code|lang=javascript|1= element.style.display !== 'none'}}).
#* If the navigation is invoked by {{key press|Enter}}, the element must '''NOT''' be a button.
# Proceed if the field in consideration passes all test in Step #2. Otherwise, go back to Step #1.
# Check if the field has a proxy element defined:
#* If yes, define the next focus element as the proxy.
#* Otherwise, define the next focus element as the field in consideration itself.
# Move focus onto the next focus element. (If the element does not share the same modal or tab with the current field, delay this step to allow the modal or tab to open/close.)
==== Setting Up a Field Proxy ====
There is quite a few (but repeated) steps in setting up a proxy element for a field. Hence, CuneiForm comes with a ready-to-use {{code|link_proxy}} function.
'''{{code|lang=javascript|1= link_proxy(form_name, fname, proxy_ele, proxy_func)}}'''
{| style="margin-left:20px;"
|- style="vertical-align:top;"
| style="width:120px;" | '''Parameters''' ||
* '''form_name''' ''(str)'': The '''id''' attribute of the {{code|form_obj}}.
* '''fname''' ''(str)'': The field name to be bound to the proxy.
* '''proxy_ele''' ''(HTML element)'': The proxy element.
* '''proxy_func''' ''(JavaScript function)'': The function to run when the form is triggered (a usual routine on a [[Multi-component Page]]). This function should accept ''1 boolean argument and returns nothing''.
|}
This function takes care of the proxy linking and relaying some key events on it back to the real form field.
=== Form Submission ===
CuneiForm has modified its submission routine from the default behaviour on HTML. The change is minimal for redirecting forms, but is quite detailed and plays an important role for non-redirecting forms.
==== Redirecting Form ====
Redirecting form mimics normal form submissikon behaviour but with some logic run before the submit request is really sent. A rough explanation for the CuneiForm's redirecting form is as follows:
# If there is another submission routine running (checked via attribute '''pass_finalchk'''), immediate stop the redundant routine.
# If any of the flags '''populating''', '''search_filling''', or '''proceeding''' are active, put the submission routine in the queue via flag '''waiting_to_submit'''. The queue is automatically handled when the obstructing flags are flipped back.
# Send a time-check request to verify the session time and the page time match. ''This action is performed by blocking the submit, performing a time check, and only calling the submission routine again afterward if the check passes. (During the process, the flag '''pass_timechk''' is utilized.)''
# Recheck whether any instacalc/search field needs to be re-fetched by checking the field attribute '''valchanged''' of each. Perform the action if necessary.
# Unformat all special-type fields and clear all search modals.
# Send the form submit request.
==== Non-redirecting Form ====
Non-redirecting form is CuneiFox's way to create a smooth user experience especially when users are expected to interact with pages with multiple forms/tables on a regular basis. The pre-submission routine is not much different form that of redirecting form, the bulk of the routine, however, lies in the post-submission routine.
Below is the rough step-by-step explanation of the routine:
* '''''Pre-submission'''''
*# If any of the flags '''populating''', '''search_filling''', or '''proceeding''' are active, put the submission routine in the queue via flag '''waiting_to_submit'''. The queue is automatically handled when the obstructing flags are flipped back.
*# Send a time-check request to verify the session time and the page time match. ''This action is performed by blocking the submit, performing a time check, and only calling the submission routine again afterward if the check passes. (During the process, the flag '''pass_timechk''' is utilized.)''
*# Recheck whether any instacalc/search field needs to be re-fetched by checking the field attribute '''valchanged''' of each. Perform the action if necessary.
*# If there is another submission routine running (checked via attribute '''submitting'''), immediate stop the redundant routine.
*# Unformat all special-type fields and clear all search modals.
*# Send an AJAX POST form-submit request.
* '''''Post-submission''''': Process what to do next based on pre-defined form attributes and the {{code|data}} returned from the server.
*# If the server demands redirection to another route ('''{{code|data.redirect}}''': ''url_for string'') or reload of the same page ('''{{code|data.reload}}''': ''bool''), do so and end the submission routine.
*# If the server demands a new tab to open a new route ('''{{code|data.new_tab}}''': ''url_for string''), do so before continuing.
*# ''(Only applies when the submission fails FlaskForm validation on the server-side)'' Render field error messages packed in '''{{code|data.err}}''' (''dict'': See note).
*# Render flash messages packed in '''{{code|data.flash_messages}}''' (''dict'': See note).
*# This step is split into 3 paths:
*#* The submission '''triggers a thread-based process''' on the server ({{code|lang=javascript|1= form_obj.wait_long_process === true}}). In this case, '''{{code|data.data}}''' is expected to be the job token to be used in task tracking (string).
*#*# Disable the submit button to prevent redundant request.
*#*# Initiate a task tracking schedule.
*#* '''The submission fails''' ('''{{code|lang=javascript|1= data.data === 'failure'}}'''), set the page flag '''will_proceed_editmode''' to {{code|lang=javascript|false}}. ''(This flag prevents a multi-component page from proceeding through a failed step.)''
*#* '''The submission succeeds''' ('''{{code|lang=javascript|1= data.data === 'success'}}'''):
*#*# If the form must be repopulated ('''{{code|data.repop}}''': ''dict'': hares the same format with the form data in '''{{code|data.crossfill}}''', see note), do so.
*#*# If other tables and forms on the page are affected ('''{{code|data.crossfill}}''': ''dict'': See note), re-populate them.
*#*# If the document-level verification pills must be manipulated ('''{{code|data.docverify}}''': ''dict'': See note), do so.
*#*# If the form belongs to a table as the in-line form ({{code|form_obj.in_table}}) or the fully-expanded form ({{code|form_obj.table_linked}}), commit the change (if any) to the table as well by looking for '''{{code|data.entry}}'''. (See notes under '''{{code|data.crossfill}}''' below).
*#*# If the form belongs to a modal ({{code|form_obj.in_modal}}), clear the modal's '''hide_mode''' attribute to {{code|lang=javascript|'plain'}}.
*# Where applicable, pack the transient data ('''{{code|data._transient_data}}''': ''dict'') to {{code|form_obj.transient_data}} and run each functions in {{code|form_obj.post_submit_cmds}}.
*# On a multi-component page, if the form is bound to the page sequence ({{code|form_obj.sequence_bound}}), and the non-deleting submission is successful (page flag '''will_proceed_editmode'''), proceed through the page's step.
*# Reset '''transient_data''' and all form's flag attributes.
*# Re-format all special-type field values.
'''{{code|data.err}}''' takes the following format:
<syntaxhighlight lang='python'>
data.err = {'<field_name0>':['<error_message00>', '<error_message01>', ...],
            '<field_name1>':['<error_message10>', '<error_message11>', ...],
            ...}
# Usually, there is only 1 error message per field.
</syntaxhighlight>
'''{{code|data.flash_messages}}''' takes the following format:
<syntaxhighlight lang='python'>
data.flash_messages = [['<message0>', '<message_class0>'],
                      ['<message1>', '<message_class1>'],
                      ...]
# 'message_class' employs BootStrap 4's classes 'alert-<class>'.
# Class options include 'primary', 'success', 'warning', 'danger',
# 'info', 'secondary', 'light', and 'dark'.
</syntaxhighlight>
'''{{code|data.crossfill}}''' assumes the following format:
<syntaxhighlight lang='python'>
data.crossfill = {'<table_or_form_id0>':<table_or_form_data0>,
                  '<table_or_form_id1>':<table_or_form_data1>,
                  ...}
# For CuneiForms, table_or_form_data is a dict.
# NOTE THAT fields not presented in the dict are to be populated with blank strings.
# Unaffected fields should be marked for skipping using the value '_skip'.
form_data = {'<field_name0>':<value0>,
            '<field_name1>':<value1>,
            ...}
# For CuneiTables, table_or_form_data takes the following format. The order of
# each member list is dictated by the attribute 'cnames' of the CuneiTable instance.
table_data = [ [<row0_value0>, <row0_value1>, <row0_value2>, ...],
              [<row1_value0>, <row1_value1>, <row1_value2>, ...],
              ... ]
</syntaxhighlight>
'''{{code|data.entry}}''' can take 2 forms:
* For successful add/edit operations, it is formatted like a member array of the table data member in '''{{code|data.crossfill}}'''.
* For successful delete operations, it is simply the string '''{{code|lang=javascript|'deleted'}}'''.
'''{{code|data.docverify}}''' takes the following format:
<syntaxhighlight lang='python'>
data.docverify = {'<verify_key0>':<verify_val0>,
                  '<verify_key1>':<verify_val1>,
                  ...}
# verify_val is 0 for normal-state, 1 for danger (red), 2 for warning (yellow).
# - For special verify_key 'editting', the verify_val is the name of the user
#  currently editing the document (string).
</syntaxhighlight>
== Useful Server-side Patterns ==
=== Non-redirecting Submission Route ===
Below are 2 examples for Flask routes that handle non-redirecting form submission requests. For any form that appears on multiple pages, it makes sense to write a single submit route to which all instances of the form point to.
==== General Format ====
<syntaxhighlight lang='python' line=1>
@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    if form.validate_on_submit():
        data_dict = {}
        # Do something
        return jsonify(data_dict)
    else:
        return jsonify(dict(err=form.errors))
</syntaxhighlight>
For {{code|data_dict}}, see notes under [[#Non-redirecting Form]] sub-section.
==== With Database Commit ====
<syntaxhighlight lang='python' line=1>
@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(...)
    if resp_dict == 403:
        abort(403)
    else:
        # Do something.
        # Modify resp_dict as fit.
    return jsonify(resp_dict)
</syntaxhighlight>
The key player of this pattern is the function '''[[Common Functions#gen_dbcommit_resp|gen_dbcommit_resp]]''' that handles the most repeated routines related to form validation and database commit.
=== Data Fetching Route ===
Below is an example for Flask route that handles a data fetch request for CuneiForm. Again, this template is ideal for forms appearing on multiple pages but shares roughly the same fetch logic.
<syntaxhighlight lang='python' line=1>
@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    # Fetch, calculate, modify data to display on the form.
    return {'<field_name0>':<value0>, '<field_name1>':<value1>, ...}
</syntaxhighlight>
Useful fetching-type functions include, but not limited to, '''{{code|grab_some_entries}}''' and '''{{code|prefetch_all_docs}}'''.
=== All-in-one Route ===
Below is an example for Flask route that handles everything for a single-form page. This format is the most frequently written, since most CuneiForms are used on a unique page (or a set of pages that share the same route anyway).
<syntaxhighlight lang='python' line=1>
@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    if <check_if_fetch>:
        # Fetch, calculate, modify data to display on the form.
        return {'<field_name0>':<value0>, '<field_name1>':<value1>, ...}
    elif <check_if_submit>:
        if form.validate_on_submit():
            data_dict = {}
            # Do something
            return jsonify(data_dict)
        else:
            return jsonify(dict(err=form.errors))
    # Below this point is the preparation for Flask's render_template.
    # Initiate search tables to be used in the form.
    search_tb0 = <CuneiTable_class>(...)
    search_tb1 = <CuneiTable_class>(...)
    ...
    return render_template("<template_path>", ...)
</syntaxhighlight>




{{The Tenko Shrine}}
{{The Tenko Shrine}}

รุ่นแก้ไขปัจจุบันเมื่อ 18:49, 30 สิงหาคม 2567

CuneiForm type objects are powered by WTForms engine. However, this object type also introduces a handful of additional variables and attributes to strike a sweet balance between ease of development and flexibility.

Define

Defining a CuneiForm is essentially similar to declaring a WTForm. There are no differences in declaring fields, validations, render keywords, and validating functions. (There exist a few special functions that can dynamically generate some common fields, but we'll leave that to a later section.)

Let's first have a quick look at a very basic CuneiForm:

class LoginForm(FlaskForm, CuneiForm):
    company = StringField("Company", validators=[InputRequired("Required!")])
    username = StringField("Username", validators=[InputRequired("Required!")])
    password = PasswordField("Password", validators=[InputRequired("Required!")])
    clear_session = BoolField("Log-out of other sessions", default=True)
    submit = SubmitField("Log-in")

    def validate_company(self, company):
        # Validation goes here
    def validate_username(self, username):
        # Validation goes here
    def validate_password(self, password):
        # Validation goes here

Developers might find uses of BoolField and IntField in CuneiFox code base unfamiliar. This convention is to avoid name conflict betweem WTForm and Peewee.

Special Value Types

This feature plays a crucial role in ensuring consistent value formatting for client-side output. While it originated as a solution to address browser formatting inconsistencies, it has evolved to serve several other purposes within the CuneiFox framework.

Original Intentions

Reading through the CuneiFox code base, one is sure to notice the excessive use of StringFields where one would expect DateFields, TimeFields, FloatFields, and IntegerFields as supported by WTForms. This design choice was made to address a challenging browser behaviour. Modern browsers tend to enforce formatting of fields marked as certain types based on the user's locale setting, which can be tied to either the operating system or the browser itself. This automatic formatting can be difficult to override or reverse, leading to inconsistencies and confusion on the user's part, especially when switching workstations.

To address this issue, CuneiFox developed its own value typing. Our approach applies custom-made JavaScript codes to unformatted and untyped (as far as the browser is aware) fields. This way, the formatting rendered on the client-side perfectly obeys the company's specific preferences within the CuneiFox system.

Current Incarnation

Form value types are specified in the variable MASTER_sp_type residing within form definition code. The variable is of format:

MASTER_sptypes = [(str field_name0, str type0), (str field_name1, str type1), ...]

# Example from Accounting Journal's withholding tax form
MASTER_sptypes = [("taxdate", "date"), ("taxmonth", "month"),
                  ("rate", "pct"), ("amt", "acc"), ("tax", "acc"),
                  ("rate_2", "pct"), ("amt_2", "acc"), ("tax_2", "acc"),
                  ...]
# Example from Product information form
MASTER_sptypes = [...,
                  ("pict", ["img", "table", "product"])
                  ...]

Available Types

Special types valid within CuneiFox system are shown below:

  • Date types
    • 'date': Value with date, month, and year.
    • 'month': Value with only date and month.
  • Time types
    • 'time': Value with hour, minute, and second.
  • Numeric types
    • 'acc': Numerical value, rounded to acc decimal places. (See Company Settings)
    • 'pct': Numerical value, rounded to pct decimal places, with "%" sign appended. (See Company Settings)
    • 'qty': Numerical value, rounded to qty decimal places. (See Company Settings)
    • 'amt': Numerical value, rounded to amt decimal places. (See Company Settings)
    • 'int': Numerical value, rounded to nearest integer.
  • File types* (See note below)
    • 'file': File field with CuneiFox custom action buttons.
    • 'img': File field with CuneiFox custom action buttons and an associated img HTML element.
  • Other types
    • 'right': Normal string field whose text is aligned right rather than the normal left.
    • 'color': String field with an associated color selector element.

NOTE THAT when using file types, the typeX value is a 3-membered list instead:

  • Member 0: The type ('file' or 'img')
  • Member 1: The source ('data', 'table', 'files', 'print', 'qr', 'report')
  • Member 2: The sub-directory (string or None)

Instant Calculation

Instant calculation function within CuneiForms, or 'instacalc', allows a change in one form field to initiate a small calculation request to the server and update other affected fields based on the calculation result. It is specified in the variable MASTER_instacalc under form definition.

MASTER_instacalc = [(str trigger_fld0, [
                                           route0, 
                                           [str collect_fld00, str collect_fld01, ...],
                                           [str result_fld00, str result_fld01, ...]]),
                                           ...
                                       ]),
                   [(str trigger_fld1, [
                                           route1, 
                                           [str collect_fld10, str collect_fld11, ...],
                                           [str result_fld10, str result_fld11, ...]]),
                                           ...
                                       ]),
                    ...]

# Example from Accounting Journal's withholding tax form
MASTER_instacalc = [("amt", ["cunei_gl.calcwht", ["amt", "rate"], ["tax"]]),
                    ("rate", ["cunei_gl.calcwht", ["amt", "rate"], ["tax"]]),
                    ...]
  • trigger_fldX is the field whose value change will trigger the instacalc routine.
  • routeX can be either:
    • A string similar to the string argument fed to Flask's url_for.
    • A dict of format {'route': str route_name, **kwargs}, where kwargs represents arguments to be fed to the instacalc route.
  • collect_fldXY are the fields whose values are collected and used in calculation routine.
  • result_fldXY are the fields to be updated with the calculation result.

Auto-numbering Function

Forms with auto-numbering specifications work with CuneiFox core system to serve auto running numbers in a particular format (usually for document numbers).

The specifications for this feature are embedded within MASTER_autorun in this format:

MASTER_autorun = [(str run_field0, str sec_field0, str date_field0, str ref_field0),
                  (str run_field1, str sec_field1, str date_field1, str ref_field1),
                  ...]

# Example from Sale Invoice Form
MASTER_ autorun = [("docno", "section_stid", "docdate", None),
                   ("invno", "section_stid", "invdate", "docno")]
  • run_fieldX is the field to trigger the auto-numbering function.
  • sec_fieldX is the field holding the corresponding section static ID.
  • date_fieldX is the field holding the corresponding date.
  • ref_fieldX is the field holding the original value for WAIT function (Set to None to disable WAIT support).

Other Form Definition Variables

Other notable MASTER variables that can be set upon form definition include:

  • MASTER_firstonly ([str,]): A list of field names that are only active (editable) for a new entry, but inactive (not editable) when editing an old entry.
  • MASTER_skipseqs ([str,]): A list of field names to be skipped when the user is navigating through the form via Tab ↹ or ↵ Enter.
  • MASTER_skipcols ([str,]): A list of field names to be skipped when committing data to database. CuneiFox naturally skip fields that are meant for client-side viewing only (fields whose names are not present as database table columns). However, if the developer wants a field to be skipped despite the database having a similarly named column, do put the field name in this list.

These 3 variables share the same format:

MASTER_firstonly = [str field_name0, str field_name1, ...]

# Example from Accounting Journal's header form
MASTER_firstonly = ["docno", "section_stid", "section_code", "section_name"]

Initiate

Form initiation create an object instance for a CuneiForm. It should be noted that CuneiFox does not override the original __init__ of Flask_WTF's FlaskForm, but has written its own init_form class method which calls the original __init__ and performs other facilitating routines.

init_form(cl, prefix="", form_name="", gen_del=None, has_files=False, **kwargs)

Parameters
  • prefix (str): This value is reflected in FlaskForm's _prefix and CuneiForm's _id attributes during initiation. The _prefix attribute is not directly accessed if one follows the usual coding pattern of CuneiFox.
  • form_name (str or LazyString): The human-readable form name. This is very rarely accessed (examples include Permission forms and Setting Forms which require the form names to be explicitly and dynamically rendered on the client-side) and usually omitted.
  • gen_del: The module to store, at runtime, the parallel CuneiForm class with generic submit-delete field set. (See description under Section: Submit-delete Field Set.
  • has_files (bool): Whether the form contains one or more fields for files. (This has most likely been made obsolete and is always omitted.)
  • Other keyword arguments: See notes under sections below.
Returns A new FlaskForm-CuneiFox object.

Submit-delete Field Set

The way CuneiFox handles data input and deletion requires fields named submit (SubmitField), del_submit (SubmitField), and is_del (BooleanField) to be present in most forms. So, CuneiFox allows developers to define a form with these 3 fields omitted, then provide a not-None gen_del argument upon initiation.

The way CuneiFox goes about this is to create a parallel FlaskForm-CuneiForm class with these 3 fields added, then store the new class in the module specified in the argument for repeated use. The gen_del argument can be:

  • str module_code: In this case, runtime class is stored within the module's 'forms' sub-module.
  • [str module_code, str submodule_name]: In this case, the new class is stored in the specified sub-module.

NOTE THAT this feature does not override any similarly named field in the original form definition. For example, if the definition of the form already contains a 'submit' field, only 'del_submit' and 'is_del' fields are injected.

Notes on Keyword Arguments

The following sub-sections introduce notable keyword arguments. Other keywords not detailed here are also welcome during CuneiForm initiation. Unless explicitly specified, all keyword arguments can be accessed post-initiation as attributes of the created CuneiForm instance.

MASTER-adjacents

Back to the #Define section, we are introduced to a number of MASTER_xxx variables. Those variables set at form definition act as the default values for each form type. However, developers can feed an overriding value that applies only to the form being initiated using a corresponding non-master keyword (e.g. use keyword instacalc to override the class's MASTER_instacalc).

The option to modify object-specific values after initiation is also available. However, since value packing and manipulation occurs during initiation, some details should be noted:

  • skipseqs, skipcols, firstonly, and autorun are normal form attributes and can easily be accessed and modified.
  • instacalc are repacked as a dict upon initiation. The format and an example of post-init modification are shown below:
form.instacalc = {str trigger_fld0: [
                                       url_for_string route0, 
                                       [str collect_fld00, str collect_fld01, ...],
                                       [str result_fld00, str result_fld01, ...]]),
                                       ...
                                    ]),
                  str trigger_fld1: [
                                       url_for_string route1, 
                                       [str collect_fld10, str collect_fld11, ...],
                                       [str result_fld10, str result_fld11, ...]]),
                                       ...
                                    ]),
                  ...]

# Example from Product Return / Cancelled Order header form
head_form.instacalc["refdoc"]  = [
                                    url_for("cunei_iv.fetch_vouch_for_return", bookcode=bookcode),
                                    ["refdoc", "refdate", "docdate"],
                                    ["company_stid", "company_code", "company_name", ...]
                                 ]
head_form.instacalc["refdate"] = [
                                    url_for("cunei_iv.fetch_vouch_for_return", bookcode=bookcode),
                                    ["refdoc", "refdate", "docdate"],
                                    ["company_stid", "company_code", "company_name", ...]]
  • sptypes is distributed and packed as field attributes. We will follow-up on the example given under #Special Value Types section. After initiation, special value specifications are thus set:
tax_form.taxdate.special_type = "date"
tax_form.taxmonth.special_type = "month"
tax_form.rate.special_type = "pct"
tax_form.amt.special_type = "acc"
tax_form.tax.special_type = "acc"
tax_form.rate_2.special_type = "pct"
tax_form.amt_2.special_type = "acc"
tax_form.tax_2.special_type = "acc"

# For file types, values are thus packed:
product_form.pict.special_type = "img"
product_form.pict.file_src_type = "table"
product_form.pict.file_src_dir = "product"

Know Thy Self

On the client-side, forms are going to be present, possibly, on a page along with other forms, tables, and modals. In many cases, they might even have to interact with them. So, CuneiForms need a way to identify themselves and other elements they are meant to work closely with. Below is the list of form attributes whose functions fall in this category:

  • _id (str): The 'prefix' repacked. This value will become the HTML id of the form object. (Note that the attribute name is underscored to avoid conflict with python-native 'id'.)
  • in_table (str: defaults to False for free-standing forms): The name of the table with this form as the in-line form.
  • table_linked (str): Similar to in_table but this one applies to full pop-up form (as opposed to in-line form).
  • in_modal (str: defaults to False for free-standing forms): The ID of the modal with this form as the MAIN SUBMITTING COMPONENT. (See CuneiModal for details.)
  • sequence_bound (bool: defaults to False): Whether form submission is linked to advance the edit-mode of the page. (See Multi-component Page)
  • slim (bool: defaults to False): Whether to render the form in slim mode. (Normally, form label and field take 2 'rows' on the screen. In slim mode, form label and field share the same 'row', and the field is drawn with less vertical padding.)
  • subslim (bool: default is determined upon CuneiTable initiation): Similar to slim but applicable ONLY TO in-line fields of an in-line form. (E.g. fields in an expansion pop-up are not governed by this attribute.)

Sequence and Column Names

These few attributes govern the navigation, submission, and database-mapping of the form:

  • seq ([str,]): Field names as defined via FlaskForm. Its order dictates the tab-sequence of the form on the client-side. If no value is given, the initiation routine automatically generates this list from FlaskForm.
  • cnames ([str,]): Field names as matched with corresponding database model. If no value is given, the list copies from seq. (See notes.)
  • submit_id (str: defaults to 'submit'):

The lists seq and cnames work together to create a mapping key. In other words, data entered through the form field seq[i] is written to database column cnames[i] and vice-versa for reading operations. DO NOTE THAT, under most use cases, seq and cnames both hold the same values. Assigning different values to seq and cnames is legitimate in some use cases. However, the rarity of such cases can make the code harder to parse by future developers. A more recommended way to achieve similar result is to explicitly assign values to appropriate columns/keys during data manipulation.

Getting Data

Under the normal work flow, CuneiFox sent only blank components to the client first to render the page. At this stage, a CuneiForm only displays either the default values (as specified during form definition) or the values fed explicitly before the template is sent. Once the page components are loaded, the elements (including CuneiForms) can then fetch data from the server, each using a separate HTML request.

These attributes below dictate the form's behaviours regarding data request:

  • populate_route (url_for_string: defaults to False): Route to which a data fetch request is to be sent.
  • populate_suppress ([str,]): The list of data fetch modes to suppress. (Refer to Note #1 for available fetch mode.) A most common reason to prevent any single CuneiForm to send a fetch request is so that a page-wide fetch request can be used in its stead. A page-wide request fetch the data for several components on the same page and populate them at once (See Multi-component Page).
  • populate_id ([str,]: defaults to False): The list of fields whose data are to be parts of a data fetch request. For example, if a fetch request needs to know the id of the fetch target, then the content of the id field needs to be a part of the fetch request. Members of this list may take the formats listed below, and refer to Note #2 for additional information:
    • <field_name> (like 'id') refers to a field of the same CuneiForm
    • master.<field_name> (like 'master.id') refers to a field of the Master CuneiForm or CuneiTable. (Usually the header form of a Multi-component Page, see the article to learn how to declare a master.)
    • window.<field_id> (like 'window.pagenav') refers to a field with id '<field_id>'.
      • If <field_id> contains a dash sign (-), the prefix window. can be safely omitted. This is to facilitate references to a field of another arbitrary CuneiForm which normally goes by id <form_id>-<field_name>

NOTE #1: CuneiForm only has one fetch mode: 'grab'. However, the attribute populate_suppress takes a list format to mirror a similar attibutes in CuneiTable, which supports more fetch modes.

NOTE #2: On the client-side, CuneiFox automatically binds 'change' event on the listed elements to the re-fetch and re-populate actions. If such an event-binding is not desirable, prefix the member in populate_id with #. For example, '#id' and '#master.docdate'.

Sending Data

Normal submission behaviour of a FlaskForm is to send a POST request to the same currently displaying route, then the page is refreshed. This approach, although neat and clean, creates a choppy experience for users. The following attributes are CuneiFox's way to override this behaviour and submit a form without refreshing the page:

  • post_route (url_for_string: defaults to False): Route to send non-refreshing submit request.
  • redirect (bool: True when post_route is given, False otherwise): Whether the form submission will lead to a page refresh. This value is mainly for use in internal logic and hardly ever demands outside tampering.

Sometimes, a form submission is followed by a long, thread-based operation. More attributes are needed to dictate the thread-tracking behaviours.

  • wait_long_process (bool: defaults to False): Whether the form submission triggers a thread-based process (e.g. report prompt form).
  • track_route (url_for_string: defaults to False if wait_long_process is not given, defaults to '/task_track' otherwise): Route to send task-tracking request.
  • wait_redirect (url_for_string: defaults to False): Route to redirect to when the long-process associated with the form is completed.

Design & Pre-made Scripts

Now that a CuneiForm is defined, initiated, and sent along with a template, it comes to the frontend part. To put a form up on an HTML page, CuneiFox's standard pattern comes in 3 steps:

  1. Initiate a <form> DOM element and a JavaScript form object. Both are done via the Jinja2 macro general_form_head.
  2. Place form fields at will (preferably with Jinja2 macros included in cunei_forms.html pack).
  3. Finalize the Javascript object and bind relevant events with the Jinja2 macro general_form_tail.

NOTE THAT, in this section, unless explicitly specified:

  • form refers to a CuneiForm instance sent from Flask and accessible by Jinja2
  • form_dom refers to the <form> DOM element
  • form_obj refers to the JavaScript form object.

General Form Head & Form Object

general_form_head(form, is_master=false, multipart=false)

In essence, this Jinja2 script is responsible for the following actions:

  1. Create a <form> DOM element corresponding to the form.
  2. Register the form to the all_tbfm_ids list for future reference.
  3. Create a form_obj corresponding to the form, and assign default values to its many attributes.
  4. Point the variable master_tbfm toward the newly created form_obj (if applicable).
  5. Point the main component object attribute of the modal object referenced by form.in_modal (modal_obj.main_comp_obj) toward the newly created form_obj (if applicable).

Below is the list of and notes on form object attributes:

  • Basic Attributes
    • id: Takes on the value of form._id.
    • self_type: Takes on value 'form'.
    • token: The CSRF Token of the form, used to verify the form submission request.
  • Copied Attributes (See #Notes on Keyword Arguments for details)
    • firstonly, in_table, populate_route, redirect, seq, sequence_bound, skipseqs, slim, subslim, table_linked, track_route, wait_long_process, and wait_redirect: Takes on the exact value.
    • instacalc and populate_suppress: Takes on the exact value, but set to a blank dict {} and a blank array [], respectively, when the value is False.
    • submit_id: Takes on the exact value if the corresponding submit button does not require a confirm pop-up. If a confirm pop-up is needed, the value is suffixed with _fake, e.g. 'submit_fake'. (See #Buttons & Submits for more details.)
    • cnames: Takes on the exact value. However, members of which corresponding fields are not drawn are changed to null.
    • populate_id: Takes on the exact value, with all prefix # cut once event-binding is taken care of.
    • in_modal: Points to the corresponding modal object.
    • post_route: The original value is stored in the attribute orig_post_route. This attribute itself is modified with data when a submit request is being prepared.
  • References to DOM Elements & Objects (Arrays marked with * shares the same length as form_obj.seq, with undefined elements padded with null.)
    • form_ele: Points to the corresponding <form> DOM element.
    • fill_eles*: An array collection of the field elements themselves. You might access this array to manipulate the value or status of a field.
    • fill_ele_divs*: An array collection of the fields' ultimate parent <div> elements. You might access this array to manage the visibility of a field (along with its adjacent label, buttons, status messages, etc.)
    • fill_ele_labels*: An array collection of the fields' label elements. Accessing this array is rarely required manually.
    • fill_ele_errmoms*: An array collection of the fields' non-label elements. Accessing this array is rarely required manually.
    • fill_ele_imgboxes*: An array collection of the fields' corresponding <img> elements. (See note under #File & Image Fields.)
    • submit_ele: The submit element (most likely a <btn>) of the form.
    • proxy_submit_ele: The mock element meant to be used instead of the form's own submit element. Use cases include the need to call additional functions before form submission and form chaining.
    • linked_modals: An array collection of 'modal objects' linked to the form via search-and-fill relationships.
    • expand_modals: An array of the names of expansion modals linked to the form.
  • Field State & Type Attributes
    • readonly: An array collection of the names of fields to be rendered with readOnly = true. This array is used during form triggering to preserve the desired field states.
    • disabled: An array collection of the names of fields to be rendered with disabled = true. This array is used during form triggering to preserve the desired field states.
    • spfield_idx: An array collection of indices of fields with special value types.
  • Custom Sequences (If not null, each member of both arrays takes the format [str form_id, int ele_seq_idx]. Use cases for both arrays include form chaining.)
    • custom_next_ele: An array collection of custom forward tab-sequence. If form_obj.custom_next_ele[i] is defined, pressing Tab ↹ on form_obj.seq[i] will take you to the field referenced in form_obj.custom_next_ele[i] instead of form_obj.seq[i+1].
    • custom_prev_ele: An array collection of custom reverse tab-sequence. If form_obj.custom_prev_ele[i] is defined, pressing ⇧ Shift+Tab ↹ on form_obj.seq[i] will take you to the field referenced in form_obj.custom_prev_ele[i] instead of form_obj.seq[i-1].
  • Status Attributes
    • in_table_row (int): The row index of the entry currently working on by the in-line form.
    • triggered: Whether the form is in the triggered (fill-able and submit-able) state.
    • populating: Whether the form is currently in a populating cycle. This state attribute prevents other populating-based routines to run in conflict and prevents the submission of an incompletely populated form. Only reset this flag via setter routine form_obj.populating_tag_reset.
    • search_filling: Whether the form is currently in a search-fill cycle. Search-filling counts as a populating-based routine. This flag prevents conflicting cycles and submission of incompletely populated form. Only reset this flag via setter routine form_obj.search_filling_tag_reset.
    • proceeding: Whether the form is trying to proceed in its tab sequence. This flag might seem trivial, but since the key press Tab ↹ and ↵ Enter trigger both search-filling and proceeding, it is crucial to correctly handle element in focus. Only reset this flag via setter routine form_obj.proceeding_tag_reset.
    • waiting_instacalc: An array (queue) for intracalc cycles being stopped from running by another cycle in process (e.g. by populating flag).
    • skip_instacalc: A temporary lock put on the form's instacalc cycle. When this flag is true, instacalc cycles will be be triggered by change events on fields. This flag is a normal part of in-line form populating cycle.
    • submit_as_del: Whether the form will be submit in delete mode. This flag is flipped to true only when the real delete button (the one in the confirm modal) is clicked, and is reset immediately when a submission cycle ends. Handle this flag exclusively through setter routine form_obj.set_submit_as_del.
    • pass_timechk: Whether the form has passed a time-check test. Prior to each submission request, the form must send a small time-check request. If the server found that the form is working under the wrong month, a 461 error is raised and the submission is halted.
    • waiting_to_submit: Whether the form has a submit action in queue. This flag is flipped to true when a submit routine is interrupted by other flags or by the need to make a time-check request beforehand. It is reset automatically as a part of the submission cycle.
    • pass_finalchk (Only applies to form with 'redirecting' submit): Whether the form has passed final check and a submission request is underway. This flag prevents redundant submit requests.
    • submitting (Only applies to form with 'non-redirecting' submit): Whether a submission request is underway. This flag prevents redundant submit requests.
  • Data Attributes
    • current_data_raw: Raw data (prior to being formatted for human-readability) in JavaScript object format.
    • transient_data: Temporary data (in JavaScript object format) meant to be used by commands in post_submit_cmds and post_insta_cmds. The data is sent from the server under key '_transient_data' upon each data fetch or instacalc request. The variable is reset to null at the end of each submission and instacalc cycle.
  • Additional Functions & Routines
    • post_load_cmds ([function,] or false): An array of functions to run at the end of a form-populating cycle.
    • post_insta_cmds ([function,] or false): An array of functions to run at the end of an instacalc cycle.
    • post_submit_cmds ([function,] or false: Only applies to forms with 'non-redirecting' submit): An array of functions to run at the end of a submission cycle.
    • field_change_routines: An object that organizes functions to run on change events of different fields.

CuneiForm Jinja2 Macro Pack

When putting form fields onto the client-side pages, there is absolutely nothing stopping developers from using the ordinary WTForms+Jinja2 approach. However, with the amount of forms and fields CuneiFox will be handling, the code can get unwieldy fast. So, at the cost of some flexibility, CuneiFox comes with a macro series to automate the most common field formats, along with the accompanying scripts and events. Let us take a little tour on these macros.

Due to the number of arguments in these macros, for the sake of all things holy, DO NOT rely on argument order farther than form and blank, ALWAYS PROVIDE argument keywords.

String & Text Area Fields

  • String fields created via 'render_blank'.
    String fields created via 'render_blank'.
  • String fields (slim) created via 'render_blank'.
    String fields (slim) created via 'render_blank'.

String and text-area fields are the most prevalent type of form fields, and also one most riddled with features and expectations. The CuneiFox macro that handles rendering of these fields is render_blank.

render_blank(form, blank, track_tb=false,
             search=false, fill=false, expand=false,
             field_modal=false, field_tab=false,
             prepend=false, clear_prepend=false,
             append=false, clear_append=false,
             label_width="105px", headless=false,
             hidden=false, tabindex=false,
             pad_bottom="default",
             force_fat=false, force_slim=false, slim_pb=2)
Parameters
  • form (FlaskForm-CuneiForm)
  • blank (Field)
  • track_tb (str): The specification for table-tracking function. This argument takes the format '<table_id>:<column_to_track>'.
  • search (str): The specification for search function. This argument takes the format '<search_modal_id>:<lookup_col>'. If the search function is meant to fill other fields as well as this field itself, provide fill argument too. A field with search argument given is drawn with a Search button Search button.
  • fill (str): The specification for fill function. This argument takes the format '<field0>:<col0>,<field1>:<col1>,...'; where fieldX refers to a field within the same form, and colX refers to a column within the search result.
  • expand (str): The specification for form expansion. This argument takes the format '<expand_modal_id>:<first_expand_field>'. A field with expand argument given is drawn with an Expand button Expand button.
  • field_modal (str): The id of the modal holding this field. This value control the showing/hiding for the modal when the field gets or loses focus.
  • field_tab (str): The id of the tab holding this field. This value control the navigation to/away from the tab when the field gets or loses focus.
  • prepend (str): The string to attach to the left of the field.
  • clear_prepend (bool): Whether the HTML element <span> holding the prepend string has a clear background.
  • append (str): The string to attach to the right of the field.
  • clear_append (bool): Whether the HTML element <span> holding the append string has a clear background.
  • label_width (str: Effective only for fields drawn in slim mode): The width of the label <div>. The value can be anything interpretable as CSS width, but '<int>px' is recommended.
  • headless (bool): Whether to draw the label <div> at all.
  • hidden (bool): Whether the field and the label is drawn but remain invisible. Hidden field is achieved by setting display=none on the field and label's collective parent <div>.
  • tabindex (int): The tab-index of the field. This feature is kept open for flexibility; no examples of uses exist at the time of writing.
  • pad_bottom (bool OR 'default'): Whether the field should have some bottom spacing.
  • force_fat (bool): Whether to draw the field in fat mode regardless of form.slim and form.subslim values.
  • force_slim (bool): Whether to draw the field in slim mode regardless of form.slim and form.subslim values.
  • slim_pb (int: 0 ~ 5): Set the bottom padding for slim field. This argument utilizes Bootstrap 4's pb-0 through pb-5 classes.

Drop-list Fields

  • Drop-list fields created via 'render_select'.
    Drop-list fields created via 'render_select'.
  • Drop-list fields (slim) created via 'render_select'.
    Drop-list fields (slim) created via 'render_select'.
render_select(form, blank,
              field_modal=false, field_tab=false,
              label_width="105px", headless=false,
              hidden=false, tabindex=false,
              pad_bottom="default",
              force_fat=false, force_slim=false, slim_pb=2)
Parameters (See descriptions under #String & Text Area Fields sub-section)

Checkboxes & Toggles

Checkboxes and toggles created via 'render_check' and 'render_toggle'.
Checkboxes and toggles created via 'render_check' and 'render_toggle'.
render_check(form, blank,
             field_modal=false, field_tab=false,
             hidden=false, tabindex=false,
             disabled=false)

render_toggle(form, blank,
              field_modal=false, field_tab=false,
              hidden=false, tabindex=false,
              disabled=false, pad_bottom="default",
              force_fat=false, force_slim=false)
Parameters (See descriptions under #String & Text Area Fields sub-section)
  • disabled (bool): Whether the field should be drawn disabled on page load.

Radios

Radio fields created via 'render_radio' (with 2 columns).
Radio fields created via 'render_radio' (with 2 columns).
render_radio(form, blank,
             field_modal=false, field_tab=false,
             label_width="105px", headless=false, col=1,
             hidden=false, tabindex=false,
             force_fat=false, force_slim=false,
             disabled=false)
Parameters (See descriptions under #String & Text Area Fields sub-section)
  • col (int): The number of columns radio options should be divided into. The ordering of radio options is horizontal first and vertical second.
  • disabled (bool): Whether the field should be drawn disabled on page load.

File & Image Fields

  • File fields created via 'render_upload'.
    File fields created via 'render_upload'.
  • File fields (slim) created via 'render_upload'.
    File fields (slim) created via 'render_upload'.
render_upload(form, blank,
              field_modal=false, field_tab=false,
              label_width="105px", headless=false,
              hidden=false, tabindex=false,
              pad_bottom="default",
              force_fat=false, force_slim=false, slim_pb=2)
Parameters (See descriptions under #String & Text Area Fields sub-section)

NOTES on file field behaviour, additional buttons, and image box:

  • The body of the field itself triggers a file selector.
  • The view button View file button opens the currently selected file in a new tab.
  • The cam button Camera button activates the camera modal where the user can capture an image with an applicable peripheral. This button only appears if the form has a BooleanField named '<file_field_name>_isshot' drawn (can be hidden) on the page.
  • The revert button Revert button undoes the change made to the file field and restore its original file/value.
  • The unselect button Unselect button clears the file field. This button only appears if the form has a BooleanField named '<file_field_name>_isdel' drawn (can be hidden) on the page.
  • The image box triggers the view button. Note that the image box is not drawn automatically via the render_upload macro. Should it be needed, developers must create the <img> element manually and give it the id '<file_field_name>-imgbox' before running general_form_tail. The tail macro shall automatically detect and link the image box with the form field.

Buttons & Submits

Buttons created via 'render_submit' and 'render_btn'.
Buttons created via 'render_submit' and 'render_btn'.
render_submit(form, blank,
              class="primary", confirm_first=false,
              hidden=false, tabindex=false,
              force_fat=false, force_slim=false)

render_btn(id=false, size="normal",
           width=false, height=false,
           label=false, icon_name=false,
           class="primary", custom_color=false,
           hidden=false, tabindex=false,
           slim=false, disabled=false)
Parameters (See descriptions under #String & Text Area Fields sub-section)
  • class (str): The BootStrap 4 class that dictates the button's appearance. (See picture above.)
  • confirm_first (list: ['<confirm_modal_header>', '<field_name_to_confirm>']: If given, clicking on the submit button shows a confirm modal instead of submitting immediately. This effect is achieved by drawing a fake submit button in the designated space, moving the real submit button into the confirm modal, and adding suffix '_fake' to the form's submit_id attribute.
  • width (str): The width of the button. The value can be anything interpretable as CSS size, but '<int>px' is recommended.
  • height (str): The height of the button. The value can be anything interpretable as CSS size, but '<int>px' is recommended.
  • label (str): The text label on the button.
  • icon_name (str): The name of the icon to appear on the button. (See picture above for reference.)
  • custom_color (list): The custom colour scheme for the button. If class argument is also given, this argument takes precedence. The argument takes the form of 6-membered list, each can be anything interpretable as CSS colour, in this order:
    • Normal background colour
    • Normal foreground colour
    • Normal border colour
    • Hover-state background colour
    • Hover-state foreground colour
    • Hover-state border colour
    • Active-state glow colour
  • slim (bool): Whether the button should match the slim form atyle.

General Form Tail

At this point, the form_obj has been initiated, all fields and relevant elements are created; it is time to finalize the form_obj attributes and link all the form elements together.

general_form_tail(form, populate=true, pack_del=false)

Parameters
  • form (FlaskForm-CuneiForm)
  • populate (bool): Whether to send an immediate data request to the server.
    • For single-form pages, the form populated (besides the default values embedded in the CuneiForm instance itself) here as a sub-routine of general_form_tail.
    • For multi-component pages, however, the mass-populating routine is further down (after all tables and forms are created). So, this value should be set to false.
  • pack_del (bool): Whether this form has a submit as delete functionality (with del_submit and is_del fields) in addition to a normal submit mode. (See #Submit-delete Field Set.)

The workings of this macro can be thus summarized:

  1. Process the form's populate_id attribute
    • Interpret and repack each string value into a more easily usable format.
    • Where applicable, bind a change event to the referenced fields so that they trigger a form re-populating routine when changes are detected.
  2. Finalize References to DOM Elements & Objects and Field State & Type Attributes families of form_obj attributes.
  3. Deal with form fields, adjacent buttons, and image boxes
    • Assign additional field attributes to facilitate track_tb, search and instacalc routines
    • Bind events to take care of ...
      • Functionalities of adjacent buttons
      • Field formatting for all special types
      • Pattern recognition for date/time types
      • Form navigation while considering the fields' search, instacalc, field_modal, and field_tab attributes.
    • Minor behaviour tweaks via attributes and/or event bindings
  4. If 'pack_del' is true, bind the relevant submission event
  5. Bind pre-submission and submission routine
  6. If 'populate' is true, send a data fetch request

Notable Features & Sub-routines

This section aims to give a little insight into some CuneiForm's notable sub-routines.

Table Tracking

The role of the table tracking form in this feature is only to pack the appropriate value into the attribute selection_trackers of the corresponding table_obj. The inner working of this function is all handled by CuneiTable, refer to the main article for more details.

Search and Fill

In the search-fill function, a CuneiForm take responsibility in two areas:

  1. Invoking the corresponding search modal (which works along side its corresponding search table). The control of the client-side behaviours is then relinquished to the modal.
  2. Once the modal has retrieved the selection result from the server (via the search table), the client-side control is handed back to the CuneiForm to do the form filling.

Refer to a subsection in the main CuneiTable article for a template of a Flask route that handles table populating and search result fetching.

Searching

Searching and filling sub-routine can be triggered in 3 ways listed below, with slightly different behaviours. These 3 trigger paths can be conflicting, thus the populating and search_filling flag attributes.

  1. Click event on the adjacent search button Search button. This trigger mode always launch the search modal with full selections then cedes control.
  2. Pressing Tab ↹ or ↵ Enter to navigate away from the search field. This triggers the quick-search mode.
  3. Navigating away from the field by any other means (handled by change event on the field) also triggers the quick-search mode.

CuneiForm only triggers quick-search mode for single-selection search tables; multi-selection tables are always invoked in normal mode.

In quick-search mode, CuneiForm commands the search table (directly within the search modal) to send a modified data fetch request first, then relinquishes control. The modified CuneiTable fetch request has a special instruction to:

  • Fetch only entries whose search column value start with the field value.
  • If the fetch returns multiple entries, the search modal is launched by CuneiTable itself.
  • If the fetch returns only 1 entries, automatically make the selection and does not show the search modal at all. (The modal might appear if the server takes longer than 500ms to respond, in that case, the modal will automatically close.)

NOTE THAT trigger paths #2 and #3 only work if the field value is modified (detected via input events on the field) to prevent unnecessary requests.

Filling

Form filling after a search result fetch is handled by the JavaScript function process_invoker, which closely mimics the normal form filling routine with some additional flag manipulation.

After filling, users might expect additional navigation behaviour (especially when the search is initiated by trigger paths #2 and #3). This navigational follow-up is handled by the JavaScript function post_fill_invoker.

Although these 2 functions resides in the JavaScript file for CuneiModal, their inner workings are heavily based on CuneiForm logic.

Instacalc Routine

Instacalc is a populating-type operations not too different from search-filling. It is even triggered by the same trigger paths #2 and #3 listed above.

The rough step-by-step working of an instacalc cycle is as follows:

  1. Determine whether the invoked instacalc should be processed immediately, queued, or dumped.
  2. Send an instacalc POST request to the server.
  3. Once a successfull instacalc response is received, fill the form with appropriate values/formatting. Then, run functions in post_insta_cmds attribute of the form_obj (if any).

Process, Queue, or Dump

A single CuneiForm only handles one instacalc routine at a time. This behaviour necessitate a queue. Each invoked instacalc goes through these filters:

  1. Check whether an instacalc with the same route is currently running or already in the queue. (Instacalcs re-invoked from the queue get to skip this filter.)
    • If yes, the invoked instacalc is dumped.
  2. Check whether the CuneiForm is currently running another instacalc (via flag populating).
    • If yes, the invoked instacalc is queued.
  3. For an instacalc re-invoked from the queue, check whether all the result fields are already handled by the most recent populate-type process.
    • If yes, the invoked instacalc is dumped.
  4. Once this point is reached, the invoked instacalc is processed.

Instacalc Route Template

Although instacalc is a form-based procedure, a very similar and related procedure is also presented in CuneiTable, specifically when a column in the table is not presented in the database but is calculated on the fly before being sent over to the client-side. Thus, it is advised that an instacalc route be written with such 'internal calls' in mind.

Below is the model of a typical instacalc route:

@<blueprint>.route('<instacalc_path>', methods=['POST'])
@login_required # Optional
def <function_name>(internal_args=None):
    # 'internal_args' is given as a dict when internally called.
    if internal_args is None:
        identifiers = json.loads(request.form.get('identifier'))
        <input0> = identifiers['<input0_key>']
        <input1> = identifiers['<input1_key>']
        ...
    else:
        <input0> = internal_args.get('<input0_key>')
        <input1> = internal_args.get('<input1_key>')
        ...
    # Calculation block
    ...
    ...
    return {'<output0_key>':<output0>, '<output1_key>':<output1>, ...}

Navigation & Focusing

Being able to move around the form with a mouse provides easy access and peace of mind to new users. However, one cannot deny the efficiency of an intuitive form navigation via a keyboard. To accommodate such functionality, a custom navigation and focusing routine has to be developed for CuneiForm.

Keyboard form navigation in CuneiForm uses:

  • Tab ↹ or ↵ Enter to go forward in the form and
  • ⇧ Shift+Tab ↹ or ⇧ Shift+↵ Enter to go backward

NOTE THAT because, if the current field is the last active field, the ↵ Enter key also submits the form, the flag attribute proceeding is flipped during keyboard navigation to prevent potential conflicting submit action.

Determining What's Next

CuneiForm follows the steps listed below to determine which element shoud take the focus next:

  1. Look at the next (or previous) logical field:
    • If the current field has a custom next/previous field defined (via attibutes custom_next_ele or custom_prev_ele), look there.
    • Otherwise, look for the next field in the form's seq array.
  2. Check whether that field should take the focus.
    • The field must NOT be skipped (not present in the form's skipseqs attribute).
    • The field must NOT be in disabled or read-only state.
    • The field and its parent element (the corresponding elements in fill_eles and fill_ele_divs) must NOT be hidden (element.style.display !== 'none').
    • If the navigation is invoked by ↵ Enter, the element must NOT be a button.
  3. Proceed if the field in consideration passes all test in Step #2. Otherwise, go back to Step #1.
  4. Check if the field has a proxy element defined:
    • If yes, define the next focus element as the proxy.
    • Otherwise, define the next focus element as the field in consideration itself.
  5. Move focus onto the next focus element. (If the element does not share the same modal or tab with the current field, delay this step to allow the modal or tab to open/close.)

Setting Up a Field Proxy

There is quite a few (but repeated) steps in setting up a proxy element for a field. Hence, CuneiForm comes with a ready-to-use link_proxy function.

link_proxy(form_name, fname, proxy_ele, proxy_func)

Parameters
  • form_name (str): The id attribute of the form_obj.
  • fname (str): The field name to be bound to the proxy.
  • proxy_ele (HTML element): The proxy element.
  • proxy_func (JavaScript function): The function to run when the form is triggered (a usual routine on a Multi-component Page). This function should accept 1 boolean argument and returns nothing.

This function takes care of the proxy linking and relaying some key events on it back to the real form field.

Form Submission

CuneiForm has modified its submission routine from the default behaviour on HTML. The change is minimal for redirecting forms, but is quite detailed and plays an important role for non-redirecting forms.

Redirecting Form

Redirecting form mimics normal form submissikon behaviour but with some logic run before the submit request is really sent. A rough explanation for the CuneiForm's redirecting form is as follows:

  1. If there is another submission routine running (checked via attribute pass_finalchk), immediate stop the redundant routine.
  2. If any of the flags populating, search_filling, or proceeding are active, put the submission routine in the queue via flag waiting_to_submit. The queue is automatically handled when the obstructing flags are flipped back.
  3. Send a time-check request to verify the session time and the page time match. This action is performed by blocking the submit, performing a time check, and only calling the submission routine again afterward if the check passes. (During the process, the flag pass_timechk is utilized.)
  4. Recheck whether any instacalc/search field needs to be re-fetched by checking the field attribute valchanged of each. Perform the action if necessary.
  5. Unformat all special-type fields and clear all search modals.
  6. Send the form submit request.

Non-redirecting Form

Non-redirecting form is CuneiFox's way to create a smooth user experience especially when users are expected to interact with pages with multiple forms/tables on a regular basis. The pre-submission routine is not much different form that of redirecting form, the bulk of the routine, however, lies in the post-submission routine.

Below is the rough step-by-step explanation of the routine:

  • Pre-submission
    1. If any of the flags populating, search_filling, or proceeding are active, put the submission routine in the queue via flag waiting_to_submit. The queue is automatically handled when the obstructing flags are flipped back.
    2. Send a time-check request to verify the session time and the page time match. This action is performed by blocking the submit, performing a time check, and only calling the submission routine again afterward if the check passes. (During the process, the flag pass_timechk is utilized.)
    3. Recheck whether any instacalc/search field needs to be re-fetched by checking the field attribute valchanged of each. Perform the action if necessary.
    4. If there is another submission routine running (checked via attribute submitting), immediate stop the redundant routine.
    5. Unformat all special-type fields and clear all search modals.
    6. Send an AJAX POST form-submit request.
  • Post-submission: Process what to do next based on pre-defined form attributes and the data returned from the server.
    1. If the server demands redirection to another route (data.redirect: url_for string) or reload of the same page (data.reload: bool), do so and end the submission routine.
    2. If the server demands a new tab to open a new route (data.new_tab: url_for string), do so before continuing.
    3. (Only applies when the submission fails FlaskForm validation on the server-side) Render field error messages packed in data.err (dict: See note).
    4. Render flash messages packed in data.flash_messages (dict: See note).
    5. This step is split into 3 paths:
      • The submission triggers a thread-based process on the server (form_obj.wait_long_process === true). In this case, data.data is expected to be the job token to be used in task tracking (string).
        1. Disable the submit button to prevent redundant request.
        2. Initiate a task tracking schedule.
      • The submission fails (data.data === 'failure'), set the page flag will_proceed_editmode to false. (This flag prevents a multi-component page from proceeding through a failed step.)
      • The submission succeeds (data.data === 'success'):
        1. If the form must be repopulated (data.repop: dict: hares the same format with the form data in data.crossfill, see note), do so.
        2. If other tables and forms on the page are affected (data.crossfill: dict: See note), re-populate them.
        3. If the document-level verification pills must be manipulated (data.docverify: dict: See note), do so.
        4. If the form belongs to a table as the in-line form (form_obj.in_table) or the fully-expanded form (form_obj.table_linked), commit the change (if any) to the table as well by looking for data.entry. (See notes under data.crossfill below).
        5. If the form belongs to a modal (form_obj.in_modal), clear the modal's hide_mode attribute to 'plain'.
    6. Where applicable, pack the transient data (data._transient_data: dict) to form_obj.transient_data and run each functions in form_obj.post_submit_cmds.
    7. On a multi-component page, if the form is bound to the page sequence (form_obj.sequence_bound), and the non-deleting submission is successful (page flag will_proceed_editmode), proceed through the page's step.
    8. Reset transient_data and all form's flag attributes.
    9. Re-format all special-type field values.


data.err takes the following format:

data.err = {'<field_name0>':['<error_message00>', '<error_message01>', ...],
            '<field_name1>':['<error_message10>', '<error_message11>', ...],
            ...}
# Usually, there is only 1 error message per field.


data.flash_messages takes the following format:

data.flash_messages = [['<message0>', '<message_class0>'],
                       ['<message1>', '<message_class1>'],
                       ...]
# 'message_class' employs BootStrap 4's classes 'alert-<class>'.
# Class options include 'primary', 'success', 'warning', 'danger',
# 'info', 'secondary', 'light', and 'dark'.


data.crossfill assumes the following format:

data.crossfill = {'<table_or_form_id0>':<table_or_form_data0>,
                  '<table_or_form_id1>':<table_or_form_data1>,
                  ...}


# For CuneiForms, table_or_form_data is a dict.
# NOTE THAT fields not presented in the dict are to be populated with blank strings.
# Unaffected fields should be marked for skipping using the value '_skip'.
form_data = {'<field_name0>':<value0>,
             '<field_name1>':<value1>,
             ...}

# For CuneiTables, table_or_form_data takes the following format. The order of
# each member list is dictated by the attribute 'cnames' of the CuneiTable instance.
table_data = [ [<row0_value0>, <row0_value1>, <row0_value2>, ...],
               [<row1_value0>, <row1_value1>, <row1_value2>, ...],
               ... ]


data.entry can take 2 forms:

  • For successful add/edit operations, it is formatted like a member array of the table data member in data.crossfill.
  • For successful delete operations, it is simply the string 'deleted'.


data.docverify takes the following format:

data.docverify = {'<verify_key0>':<verify_val0>,
                  '<verify_key1>':<verify_val1>,
                  ...}
# verify_val is 0 for normal-state, 1 for danger (red), 2 for warning (yellow).
# - For special verify_key 'editting', the verify_val is the name of the user
#   currently editing the document (string).

Useful Server-side Patterns

Non-redirecting Submission Route

Below are 2 examples for Flask routes that handle non-redirecting form submission requests. For any form that appears on multiple pages, it makes sense to write a single submit route to which all instances of the form point to.

General Format

@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    if form.validate_on_submit():
        data_dict = {}
        # Do something
        return jsonify(data_dict)
    else:
        return jsonify(dict(err=form.errors))

For data_dict, see notes under #Non-redirecting Form sub-section.

With Database Commit

@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(...)
    if resp_dict == 403:
        abort(403)
    else:
        # Do something.
        # Modify resp_dict as fit.
    return jsonify(resp_dict)

The key player of this pattern is the function gen_dbcommit_resp that handles the most repeated routines related to form validation and database commit.

Data Fetching Route

Below is an example for Flask route that handles a data fetch request for CuneiForm. Again, this template is ideal for forms appearing on multiple pages but shares roughly the same fetch logic.

@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    # Fetch, calculate, modify data to display on the form.
    return {'<field_name0>':<value0>, '<field_name1>':<value1>, ...}

Useful fetching-type functions include, but not limited to, grab_some_entries and prefetch_all_docs.

All-in-one Route

Below is an example for Flask route that handles everything for a single-form page. This format is the most frequently written, since most CuneiForms are used on a unique page (or a set of pages that share the same route anyway).

@<blueprint>.route("<submit_path>", methods=["POST"])
def <function_name>(*args):
    form = <CuneiForm_class>.init_form(...)
    if <check_if_fetch>:
        # Fetch, calculate, modify data to display on the form.
        return {'<field_name0>':<value0>, '<field_name1>':<value1>, ...}
    elif <check_if_submit>:
        if form.validate_on_submit():
            data_dict = {}
            # Do something
            return jsonify(data_dict)
        else:
            return jsonify(dict(err=form.errors))
    # Below this point is the preparation for Flask's render_template.
    # Initiate search tables to be used in the form.
    search_tb0 = <CuneiTable_class>(...)
    search_tb1 = <CuneiTable_class>(...)
    ...
    return render_template("<template_path>", ...)