Form

General

  • Forms will be created by using the Form Editor on the Typo3 frontend (HTML form).

  • The Form editor itself consist of two predefined QFQ forms: form and formElement - these forms are often updated during the installation of new QFQ versions.

  • Every form consist of a) a Form record and b) multiple FormElement records.

  • A form is assigned to a table. Such a table is called the primary table for this form.

  • Forms can roughly categorized into:

    • Simple form: the form acts on one record, stored in one table.
      • The form will create necessary SQL commands for insert, update and delete (only primary record) automatically.
    • Advanced form: the form acts on multiple records, stored in more than one table.
      • Fields of the primary table acts like a simple form, all other fields have to be specified with action/afterSave records.
    • Multi form: (multi-form) the form acts simultaneously on more than one record. All records use the same FormElements.
      • The FormElements are defined as a regular simple / or advanced form, plus an SQL Query, which selects and iterates over all records. Those records will be loaded at the same time.
    • Delete form: any form can be used as a form to Delete Record.
  • Form mode: The parameter ‘r’ (given via URL or via SIP) decide if the form is in mode:

    • New:

      • Missing parameter ‘r’ or ‘r=0’
      • On form load, no record is displayed.
      • Saving the form creates a new primary record.
      • E.g.: http://example.com/index.php?id=home&form=Person or http://example.com/index.php?id=home&form=Person&r=0
    • Edit:

      • Parameter ‘r>0’
      • On form load, the specified record (<tablename>.id= <r>) will be loaded and displayed.
      • Saving the form will update the existing record.
      • E.g.: http://example.com/index.php?id=home&form=Person&r=123
    • Providing additional parameter:

      Often, it is necessary to store additional, for the user not visible, parameter in a record. See Form Magic.

  • Display a form:

    • Create a QFQ tt_content record on a Typo 3 page.
    • Inside the QFQ record: form = <formname>. E.g.:
      • Static: form = Person
      • Dynamic 1: form = {{form:SE}} (the left form is a keyword for QFQ, the right form is a variable name)
      • Dynamic 2: form = {{SELECT f.name FROM Form AS f WHERE f.id=…}} (the left form is a keyword for QFQ, the right form is a variable name)
    • With the Dynamic option, it’s easily possible to use one Typo3 page and display different forms on that specific page.

Form process order

Depending on form load / save or record delete, different tasks are performed. Processing is divided into:

  • Native FormElements like: input, select list, checkbox, radio, ….
    • upload elements are categorized as native FormElement, but will be processed later.
    • pill, fieldset and subrecord elements are only processed during form load, they do not impact save or delete.
  • Action FormElement like before…, after…, sendmail

Each FormElement has an order.

Native FormElements which ‘name’:

  • corresponds to a column in the form primary table: are grouped together in one insert or update query.
  • do not correspond to a column in the primary table: needs an explicit defined Action FormElement to be handled.

FormElement processing order:

_images/FormProcess.png

Record locking

Forms and ‘record delete’-function support basic record locking. A user opens a form: starting with the first change of content, a record lock will be acquired in the background. If the lock is denied (e.g. another user already owns a lock on the record) an alert is shown. By default the record lock mode is ‘exclusive’ and the default timeout is 15 minutes. Both can be configured per form. The general timeout can also be configured in Configuration (will be copied to the form during creating the form).

The lock timeout counts from the first change, not from the last modification on the form.

If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again.

A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record.

If a Form acts on further records (e.g. via FE action), those further records are not protected by this basic record locking.

If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied.

If there are different locking modes in multiple forms, the most restricting mode applies for the current lock.

Exclusive

An existing lock on a record forbids any write action on that record.

Advisory

An existing lock on a record informs the user that another user is currently working on that record. Nevertheless, writing is allowed.

None

No locking at all.

Comment- and space-character

The following applies to the fields Form.parameter and FormElement.parameter:

  • Lines will be trimmed - leading and trailing spaces will be removed.
  • If a leading and/or trailing space is needed, escape it: \ hello world \ > ‘ hello world ‘.
  • Lines starting with a ‘#’ are treated as a comment and will not be parsed. Such lines are treated as ‘empty lines’.
  • The comment sign can be escaped with \ .

Form Settings

Name Description
Name Unique and speaking name of the Form. Form will be identified by this name. Use only ‘[a-zA-Z0-9._+-]’. form-name
Title Title, shown on/above the form. form-title
Note Personal editor notes. form-note
Table Primary table of the form. form-tablename
Primary Key Primary key of the indicated table. Only needed if != ‘id’. form-primary-key
Required Parameter NEW Name of required SIP parameter to create a new record (r=0), separated by comma. ‘#’ as comment delimiter. See Required Parameter New|Edit
Required Parameter EDIT Name of required SIP parameter to edit an existing record (r>0), separated by comma. ‘#’ as comment delimiter. See Required Parameter New|Edit
Permit New ‘sip, logged_in, logged_out, always, never’ (Default: sip): See permitNew & permitEdit
Permit Edit ‘sip, logged_in, logged_out, always, never’ (Default: sip): See permitNew & permitEdit
Permit REST See REST
Escape Type Default See Escape/Action class.
Show button ‘new, delete, close, save’ (Default: ‘new,delete,close,save’): Shown named buttons in the upper right corner of the form. See showButton
Forward Mode ‘auto | close | no | url | url-skip-history | url-sip | url-sip-skip-history’ (Default: auto): See Forward: Save / Close.
Forward (Mode) Page
  1. URL / Typo3 page id/alias or b) Forward Mode (via ‘{{…}}’) or combination of a) & b). See Forward: Save / Close.
labelAlign Label align (default/left/center/right)/ Default: ‘default’ (defined by Config). form-label-align
Parameter Misc additional parameters. See Parameter.
BS Label Columns The bootstrap grid system is based on 12 columns. The sum of bsLabelColumns, bsInputColumns and bsNoteColumns should be 12. These values here are the base values for all FormElements. Exceptions per FormElement can be specified per FormElement. Default: label=col-md-3, input=col-md-6, note=col-md-3. See Form Layout.
BS Input Columns
BS Note Columns
multiMode NOT IMPLEMENTED - ‘none, horizontal, vertical’ (Default ‘none’)
multiSql NOT IMPLEMENTED - Optional. SQL Query which selects all records to edit.
multiDetailForm NOT IMPLEMENTED - Optional. Form to open, if a record is selected to edit (double click on record line)
multiDetailFormParameter NOT IMPLEMENTED - Optional. Translated Parameter submitted to detailform (like subrecord parameter)

permitNew & permitEdit

Depending on r, the following access permission will be taken:

  • r=0 - permitNew
  • r>0 - permitEdit
Option Description
sip The parameter ‘form’ and ‘r’ must be supplied via SIP or hard coded in the QFQ tt_content record.
logged_in The form will only be shown if the current User is logged in as a FE User
logged_out The form will only be shown if the current User is not logged in as a FE User
always No access restriction, the form will always be shown
never The form will never be shown
  • sip
    • is always the preferred way. With ‘sip’ it’s not necessary to differ between logged in or not, cause the SIP only exist and is only valid, if it’s created via QFQ/report earlier. This means ‘creating’ the SIP implies ‘access granted’. The grant will be revoked when the QFQ session is destroyed - this happens when a user loggs out or the web browser is closed.
  • logged_in / logged_out: for forms which might be displayed without a SIP, but maybe on a protected or even unprotected page. The option is probably not often used.
  • always: such a form is always allowed to be loaded.
    • permitNew=always: Public accessible forms (e.g. for registration) will allow users to fill and save the form.
    • permitEdit=always: Public accessible forms will allow users to update existing data. This is dangerous, cause the URL parameter (recordId) ‘r’ might be changed by the user (URL manipulating) and therefore the user might see and/or change data from other users. The option is probably not often used.
  • never: such a form is not allowed to be loaded.
    • permitNew=never: Public accessible forms. It’s not possible to create new records.
    • permitEdit=none: Public accessible forms. It’s not possible to update records.

Required Parameter New|Edit

Comma separated list of variable names. On form load, an error message will be shown in case of missing parameters. The parameters must be given by SIP.

The list of required parameter has to be defined for New (r=0, create a new record) and for Edit (r>0, edit existing record).

Optional a comment might be attached after the parameter definition.

E.g.:

New: grId, pId # Always specify a person, grId2
Edit: pId

showButton

Display or hide the button new, delete, close, save.

  • new: Creates a new record. If the form needs any special parameter via SIP or Client (=browser), hide this ‘new’ button - the necessary parameter are not provided.
  • delete: This either deletes the current record only, or (if defined via action FormElement ‘beforeDelete’ ) any specified subrecords.
  • close: Close the current form. If there are changes, a popup opens and ask to save / close / cancel. The last page from the history will be shown.
  • save: Save the form.
  • Default: show all buttons.

Forward: Save / Close

Forward (=forwardMode)

After the user presses Save, Close, Delete or New, different actions are possible where the browser redirects to.

  • auto (default) - the QFQ browser Javascript logic, decides to stay on the page or to force a redirection to a previous page. The decision depends on:
    • Close goes back (feels like close) to the previous page. Note: if there is no history, QFQ won’t close the tab, instead a message is shown.
    • Save stays on the current page.
  • close - goes back (feels like close) to the previous page. Note: if there is no history, QFQ won’t close the tab, instead a message is shown.
  • no - no change, the browser remains on the current side. Close does not close the page. It just triggers a save if there are modified data.
  • url - the browser redirects to the URL or T3 page named in Forward URL / Page. Independent if the user presses save or close.
  • url-skip-history - same as url, but the current location won’t saved in the browser history.
  • url-sip - like url, but any given parameter will be SIP encoded. Only useful if url points to current web instance.
  • url-sip-skip-history - like url-sip, but skips the Browser history.

Only with Forward == url | url-skip-history, the definition of Forward URL / Page becomes active.

Forward URL / Page (=forwardPage)

Format: [<url>] or [<mode>|<url>]

  • <url>:
    • http://www.example.com/index.html?a=123#bottom
    • website.html?a=123#bottom
    • ?id=<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
    • {{SELECT …}}
    • <mode>|<url>
  • <mode> - Valid keywords are as above: auto|close|no|url|url-skip-history|url-sip|url-sip-skip-history

Specifying the mode in forwardPage overwrites formMode (but only if formMode is url…).

Also regular QFQ statements like {{var}} or {{SELECT …}} are possible in forwardPage. This is useful to dynamically redirect to different targets, depending on user input or any other dependencies.

If a forwardMode ‘url…’ is specified and there is no forwardPage, QFQ falls down to auto mode.

On a form, the user might click ‘save’ or ‘save,close’ or ‘close’ (with modified data this leads to ‘save,close’). The CLIENT submit_reason shows the user action:

  • {{submit_reason:CE:alnumx}} = save or save,close

Example forwardPage

  • {{SELECT IF(‘{{formModeGlobal:S}}’=’requiredOff’, ‘no’, ‘auto’) }}
  • {{SELECT IF(‘{{submit_reason:CE:alnumx}}’=’save’, ‘no’, ‘url’), ‘|http://example.com’ }}

Type: combined dynamic mode & URL/page

Syntax: forwardPage=<mode>|<page>

  • forwardPage={{SELECT IF(a.url=’‘,’no’,’url’), ‘|’, a.url FROM Address AS a }}

Parameter

  • The following parameter are optional and can be configured in the Form.parameter field.
Name Type Description
dbIndex int Database credential index, given via config.qfq.php to let the current Form operate on the database.
bsColumns string Wrap the whole form in ‘<div class=”col-md-.. col-lg-..”>. See Custom field width.
maxVisiblePill int Show pills upto <maxVisiblePill> as button, all further in a drop-down menu. Eg.: maxVisiblePill=3.
class string HTML div with given class, surrounding the whole form. Eg.: class=container-fluid.
classTitle string CSS class inside of the title div. Default ‘qfq-form-title’.
classPill string HTML div with given class, surrounding the pill title line.
classBody string HTML div with given class, surrounding all FormElement.
extraDeleteForm string Name of a form which specifies how to delete the primary record and optional slave records.
data-pattern-error string Pattern violation: Text for error message used for all FormElements of current form.
data-required-error string Required violation: Text for error message used for all FormElements of current form.
data-match-error string Match violation: Text for error message used for all FormElements of current form.
data-error string If none specific is defined: Text for error message used for all FormElements of current form.
buttonOnChangeClass string Color for save button after user modified some content or current form. E.g.: ‘btn-info alert-info’.
ldapServer string FQDN Ldap Server. E.g.: directory.example.com.
ldapBaseDn string E.g.: ou=Addressbook,dc=example,dc=com.
ldapAttributes string List of attributes to fill STORE_LDAP with. E.g.: cn, email.
ldapSearch string E.g.: (mail={{email::alnumx:l}})
ldapTimeLimit int Maximum time to wait for an answer of the LDAP Server.
typeAheadLdap   Enable LDAP as ‘Typeahead’ data source.
typeAheadLdapSearch string Regular LDAP search expression. E.g.: (|(cn=*?*)(mail=*?*))
typeAheadLdapValuePrintf string Value formatting of LDAP result, per entry. E.g.: ‘%s / %s / %s’, mail, roomnumber, telephonenumber
typeAheadLdapIdPrintf string Key formatting of LDAP result, per entry. E.g.: ‘%s’, mail
typeAheadLdapSearchPerToken   Split search value in token and OR-combine every search with the individual tokens.
typeAheadLimit int Maximum number of entries. The limit is applied to the server (LDAP or SQL) and the Client.
typeAheadMinLength int Minimum number of characters which have to typed to start the search.
mode string The value readonly will activate a global readonly mode of the form - the user can’t change any data. See Form mode global
activateFirstRequiredTab digit 0: off, 1: (default) - with formModeGlobal=requiredOffButMark bring pill to front on save. See Form mode global
enterAsSubmit digit 0: off, 1: Pressing enter in a form means save and close. Takes default from Configuration.
submitButtonText string Show a save button at the bottom of the form, with <submitButtonText> . See submitButtonText.
saveButtonActive   0: off, 1: Make the ‘save’-button active on Form load (instead of waiting for the first user change). The save button is still ‘gray’ (record not dirty), but the user can click ‘save’.
saveButtonText string Overwrite default from Configuration
saveButtonTooltip string Overwrite default from Configuration
saveButtonClass string Overwrite default from Configuration
saveButtonGlyphIcon string Overwrite default from Configuration
closeButtonText string Overwrite default from Configuration
closeButtonTooltip string Overwrite default from Configuration
closeButtonClass string Overwrite default from Configuration
closeButtonGlyphIcon string Overwrite default from Configuration
deleteButtonText string Overwrite default from Configuration
deleteButtonTooltip string Overwrite default from Configuration
deleteButtonClass string Overwrite default from Configuration
deleteButtonGlyphIcon string Overwrite default from Configuration
newButtonText string Overwrite default from Configuration
newButtonTooltip string Overwrite default from Configuration
newButtonClass string Overwrite default from Configuration
newButtonGlyphIcon string Overwrite default from Configuration
extraButtonInfoClass string Overwrite default from Configuration
extraButtonInfoMinWidth string See extraButtonInfo
fillStoreVar string Fill the STORE_VAR with custom values. See Store: VARS - V.
showIdInFormTitle string Overwrite default from Configuration
formSubmitLogMode string Overwrite default from Configuration
sessionTimeoutSeconds int Overwrite default from Configuration . See FE-User: Session timeout seconds.
maxFileSize int Overwrite default from Configuration .
requiredPosition int See Required Position .
  • Example:
    • maxVisiblePill = 5
    • class = container-fluid
    • classBody = qfq-form-right

submitButtonText

If specified and non empty, display a regular submit button at the bottom of the page with the given text. This gives the form a ordinary HTML-form look’n’ feel. With this option, the standard buttons on the top right border should be hided to not confuse the user.

  • Optional.
  • Default: Empty

class

  • Optional.
  • Default: container
  • Any CSS class name(s) can be specified.
  • Check typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css for predefined classes.
  • Typical use: adjust the floating rules of the form.

classPill

  • Optional.
  • Default: qfq-color-grey-1
  • Any CSS class name(s) can be specified.
  • Check typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css for predefined classes.
  • Typical use: adjust the background color of the pill title area.
  • Predefined background colors: qfq-color-white, qfq-color-grey-1 (dark), qfq-color-grey-2 (light), qfq-color-blue-1 (dark), qfq-color-blue-2. (light)
  • classPill is only visible on forms with container elements of type ‘Pill’.

classBody

  • Optional.
  • Default: qfq-color-grey-2
  • Any CSS class name(s) can be specified.
  • Check typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css for predefined classes.
  • Typical use:
    • adjust the background color of the FormElement area.
    • make all form labels right align: qfq-form-right.
  • Predefined background colors: qfq-color-white, qfq-color-grey-1 (dark), qfq-color-grey-2 (light), qfq-color-blue-1 (dark), qfq-color-blue-2. (light)

extraDeleteForm

Depending on the database definition, it might be necessary to delete the primary record and corresponding slave records. To not repeat such ‘slave record delete definition’, an ‘extraDeleteForm’ can be specified. If the user opens a record in a form and clicks on the ‘delete’ button, a defined ‘extraDeleteForm’-form will be used to delete primary and slave records instead of using the current form. E.g. if there are multiple different forms to work on the same table, all of theses forms might reference to the same ‘extraDeleteForm’-form. This simplifies the maintenance.

The ‘extraDeleteForm’ parameter might be specified for a ‘form’ and/or for ‘subrecords’

See also: Delete Record.

Form mode global

A form can be operated in modes: standard | readonly | requiredOff | requiredOffButMark.

Mode standard is given if none of the others are defined.

The mode is given via (in this priority):

  • Via STORE_USER: {{formModeGlobal:U}}
  • Via STORE_SIP: {{formModeGlobal:S}}
  • Via Form: form.parameter.formModeGlobal=…
Mode
  • standard:
    • The form will behave like defined in the form editor.
    • Missing required values will a) be indicated and b) block saving the record.
  • readonly: all FormElement of the whole form are temporarily in readonly mode.
    • Fast way to display the form data, without a possibility for the user to change any data of the form.
    • The mode will be be inherited to all subforms.
  • requiredOff:
    • All FormElement with mode=required, will be handled as mode=show.
    • The user can save the form without filling all required inputs!
    • Required inputs are indicated by a red star - missing values won’t be complained.
  • requiredOffButMark:
    • All FormElement with mode=required, will be handled as mode=show.
    • The user can save the form without filling all required inputs!
    • Missing required inputs will be marked:
      • On lost focus.
      • When the user saves the record.
        • After saving the record, by default the first pill with a missing input comes to front.
        • This behaviour can be switch on/off with form.parameter.activateFirstRequiredTab=0
Extra

Via Store: VARS - V the variable {{allRequiredGiven:V}} shows, if the form has been filled completely - independent of the mode. The variable is only accessible during form save.

Usage example

Readonly

Code: SELECT 'p:{{pageAlias}}&form=person&r=1&formModeGlobal=readonly|s|t:View|s|b' AS _link

  • The form is called with SIP parameter formModeGlobal=readonly or form.parameter.mode=readonly.
  • The user can’t change any data.

Readonly systemwide

Code (somewhere): SELECT 'requiredoff' AS '_=formModeGlobal'

Code: SELECT 'p:{{pageAlias}}&form=person&r=1|s|t:View|s|b' AS _link

  • The STORE_USER variable is set formModeGlobal=readonly.
  • All forms will be shown in readonly mode - fast option to give a user access to the website, without the possibility to change any data.

Draft Mode 1

Code: SELECT 'p:{{pageAlias}}&form=person&r=1&formModeGlobal=readquiredOff|s|t:View|s|b' AS _link

  • A form has one or more FormElement with ‘fe.type=required’.
  • Opening the form with formModeGlobal=requiredOff will allow the user to save the form, even if not all FE.type=required elements are given. This can be called ‘draft’ mode.
  • Opening the form without formModeGlobal (that’s the default), forces the user to fill out all FE.type=required fields. This can be called ‘final submit’ mode.

Draft Mode 2

Code: SELECT 'p:{{pageAlias}}&form=person&r=1&formModeGlobal=readquiredOff|s|t:View|s|b' AS _link

  • A form has one or more FormElement with ‘fe.type=required’.

  • Calling the form with formModeGlobal=requiredOff will allow the user to save the form, even if not all FE.type=required elements are given.

  • Define an FE-Action ‘afterSave’, and do some action on {{allRequiredGiven:V0}} like:

    {{UPDATE <table> SET dataValid={{allRequiredGiven:V0}} WHERE id={{id:R}} }}
    
  • In the application logic, open the next process step if all data is given by evaluating dataValid.

FormElements

  • Each form contains one or more FormElement.
  • The FormElements are divided in three categories:
  • Ordering and grouping: Native FormElements and Container-Elements (both with feIdContainer=0) will be ordered by ‘ord’.
  • Inside of a container, all nested elements will be displayed.
  • Technical, it’s not necessary to configure a FormElement for the primary index column id.
  • Additional options to a FormElement will be configured via the FormElement.parameter field (analog to Form.parameter for Forms ).

Class: Container

  • Pills are containers for ‘fieldset’ and / or ‘native’ FormElements.
  • Fieldsets are containers for ‘native’ FormElements.
  • TemplateGroups are containers for ‘fieldset’ and / or ‘native’ FormElements.

Type: fieldset

  • Native FormElements can be assigned to a fieldset.
  • FormElement settings:
    • name: technical name, used as HTML identifier.
    • label: Shown title of the fieldset.

Type: pill (tab)

  • Pill is synonymous for a tab and looks like a tab.
  • If there is at least one pill defined:
    • every native FormElement needs to be assigned to a pill or to a fieldset.
    • every fieldset needs to be assigned to a pill.
  • Mode:
    • show: all child elements will be shown.
    • required: same as ‘show’. This mode has no other meaning than ‘show’.
    • readonly: technical it’s like HTML/CSS disabled.
      • The pill title is shown, but not clickable.
      • The FormElements on the pill still exist, but are not reachable for the user via UI.
    • hidden:
      • The pill is invisible.
      • The FormElements on the pill still exist, but are not reachable for the user via UI.
    • Note: Independent of the mode, all child elements are always rendered and processed by the client/server.
  • Pills are ‘dynamicUpdate’ aware. title and mode are optional recalculated during ‘dynamicUpdate’.
  • FormElement settings:
    • name: technical name, used as HTML identifier.
    • label: Label shown on the corresponding pill button or inside the drop-down menu.
    • mode:
      • show, required: regular mode. The pill will be shown.
      • readonly: the pill and it’s name is visible, but not clickable.
      • hidden: the pill is not shown at all.
    • modeSql:
    • type: pill
    • feIdContainer: 0 - Pill’s can’t be nested.
    • tooltip: Optional tooltip on hover. Especially helpful if the pill is set to readonly.
    • parameter:
      • maxVisiblePill: <nr> - Number of Pill-Buttons shown. Undefined means unlimited. Excess Pill buttons will be displayed as a drop-down menu.

Type: templateGroup

TemplateGroups will be used to create a series of grouped (by the given templateGroup) FormElements.

FormElements can be assigned to a templateGroup. These templateGroup will be rendered upto n-times. On ‘form load’ only a single (=first) copy of the templateGroup will be shown. Below the last copy of the templateGroup an ‘add’-button is shown. If the user click on it, an additional copy of the templateGroup is displayed. This can be repeated up to templateGroup.maxLength times. Also, the user can ‘remove’ previously created copies by clicking on a remove button near beside every templateGroup. The first copy of a templateGroup can’t be removed.

  • FormElement settings:
    • label: Shown in the FormElement-editor container field.
    • maxLength: Maximum number of copies of the current templateGroup. Default: 5.
    • bsLabelColumn, bsInputColumn, bsNoteColumn: column widths for row with the ‘Add’ button.
    • parameter:
      • tgAddClass: Class of the ‘add’ button. Default: btn btn-default.
      • tgAddText: Text shown on the button. Default: Add.
      • tgRemoveClass: Class of the ‘remove’ button. Default: btn btn-default.
      • tgRemoveText: Text shown on the button. Default: Remove.
      • tgClass: Class wrapped around every copy of the templateGroup. E.g. the class qfq-child-margin-top adds a margin between two copies of the templateGroup. Default: empty

Multiple templateGroups per form are allowed.

The name of the native FormElements, inside the templateGroup, which represents the effective table columns, uses the placeholder %d. E.g. the columns grade1, grade2, grade3 needs a FormElement.name = grade%d. The counting will always start with 1. The placeholder %d can also be used in the FormElement.label

Example of styling the Add/ Delete Button: Icons Template Group

Column: primary record

If the columns <name>%d are real columns on the primary table, saving and delete (=empty string) are done automatically. E.g. if there are up to five elements grade1, …, grade5 and the user inputs only the first three, the remaining will be set to an empty string.

Column: non primary record

Additional logic is required to load, update, insert and/or delete slave records.

Load

On each native FormElement of the templateGroup define an SQL query in the FormElement.value field. The query has to select all values of all possible existing copies of the FormElement - therefore the exclamation mark is necessary. Also define the order. E.g. FormElement.value:

{{!SELECT street FROM Address WHERE personId={{id}} ORDER BY id}}
Insert / Update / Delete

Add an action record, type=’afterSave’, and assign the record to the given templateGroup.

In the parameter field define:

slaveId = {{SELECT id FROM Address WHERE personId={{id}} ORDER BY id LIMIT %D,1}}
sqlHonorFormElements = city%d, street%d
sqlUpdate = {{UPDATE Address SET city='{{city%d:FE:alnumx:s}}', street='{{street%d:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1}}
sqlInsert = {{INSERT INTO Address (`personId`, `city`, `street`) VALUES ({{id}}, '{{city%d:FE:alnumx:s}}' , '{{street%d:FE:alnumx:s}}') }}
sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} LIMIT 1}}

The slaveId needs attention: the placeholder %d starts always at 1. The LIMIT directive starts at 0 - therefore use %D instead of %d, cause %D is always one below %d - but can only be used on the action element.

Class: Native

Fields:

Name Type Description
Container int 0 or FormElement.id of container element (pill, fieldSet, templateGroup) part the current Form
Enabled enum(‘yes’|’no’) Process the current FormElement
Dynamic Update enum(‘yes’|’no’) In the browser, FormElements with “dynamicUpdate=’yes’” will be updated depending on user input. Dynamic Update
Name string  
Label string Label of FormElement. Depending on layout model, left or on top of the FormElement Additional label description can be added by wrapping in HTML tag ‘<small>’
Mode enum(‘show’, ‘readonly’, ‘required’, ‘hidden’ )
Show: regular user input field. This is the default.
Required: User has to specify a value. Typically, an <empty string> represents ‘no value’.
Readonly: User can’t change. Data is not saved, except for FormElement with ‘processReadOnly’
Hidden: FormElement is not visible.
Mode sql SELECT statement with a value like in mode A value given here overwrites the setting from mode. Most useful with Dynamic Update. E.g.: {{SELECT IF( ‘{{otherFunding:FR:alnumx}}’=’yes’ ,’show’, ‘hidden’ }}
Class enum(‘native’, ‘action’, ‘container’) Details below.
Type enum(‘checkbox’, ‘date’, ‘time’, ‘datetime’, ‘dateJQW’, ‘datetimeJQW’, ‘extra’, ‘gridJQW’, ‘text’, ‘editor’, ‘annotate’, ‘imageCut’, ‘note’, ‘password’, ‘radio’, ‘select’, ‘subrecord’, ‘upload’, ‘fieldset’, ‘pill’, ‘beforeLoad’, ‘beforeSave’, ‘beforeInsert’, ‘beforeUpdate’, ‘beforeDelete’, ‘afterLoad’, ‘afterSave’, ‘afterInsert’, ‘afterUpdate’, ‘afterDelete’, ‘sendMail’)
Encode ‘none’, ‘specialchar’ With ‘specialchar’ (default) the chars <>”’& will be encoded to their htmlentity. field-encode
Check Type enum(‘auto’, ‘alnumx’, ‘digit’, ‘numerical’, ‘email’, ‘pattern’, ‘allbut’, ‘all’) See: Sanitize class
Check Pattern ‘regexp’ field-checktype: If $checkType==’pattern’: pattern to match
Order string Display order of FormElements (‘order’ is a reserved keyword) field-ord
labelAlign left Label align (default/left/center/right)/ Default: ‘default’ (defined by Form).
Size string Depends on the FormElement type. E.g. visible length (and height) of input element Type: text. Might be omitted, depending on the chosen form layout. Format: <width>[,<(min) height>[,<max height]] (in characters).
BS Label Columns string Number of bootstrap grid columns. By default empty, value inherits from the form. field-bsLabelColumns. See Custom field width
BS Input Columns string
BS Note Columns string
Label / Input / Note enum(…) Switch on/off opening|closing of bootstrap form classes field-rowLabelInputNote
Maxlength string Maximum characters for input. field-maxLength
Note string Note of FormElement. Depending on layout model, right or below of the FormElement. Report syntax can also be used, see FE: ‘Report’ notation. field-note
Tooltip text Display this text as tooltip on mouse over. field-tooltip
Placeholder string Text, displayed inside the input element in light grey. field-placeholder
value text Default value: See FE: Value
sql1 text SQL query. See individual FormEelement. sql1
Parameter text Might contain misc parameter. See Attributes defined in the parameter field
feGroup string Comma-separated list of Typo3 FE Group ID. NOT SURE IF THIS WILL BE IMPLEMENTED. Native FormElements, fieldsets and pills can be assigned to feGroups. Group status: show, hidden, hidden. Group Access: FE-Groups. User will be assigned to FE-Groups and the form definition reference such FE-groups. Easy way of granting permission.
Deleted string ‘yes’|’no’.

FE: Value

By default this field is empty: QFQ will fill it with the corresponding existing column value on form load. For a customized default value define:

{{SELECT IF('{{column:RE}}'='','custom default', '{{column:R}}') }}

For non primary records, this is the place to load an existing value. E.g. we’re on a ‘Person’ detail form and would like to edit, on the same form, a corresponding person email address (which is in a separate table):

{{SELECT a.email FROM Address AS a WHERE a.pId={{id:R0}} ORDER BY a.id LIMIT 1}}

Report syntax can also be used, see FE: ‘Report’ notation.

FE: ‘Report’ notation

The FE fields ‘value’ and ‘note’ understand the Report syntax. Nested SQL queries as well as links with SIP encoding are possible. To distinguish between ‘Form’ and ‘Report’ syntax, the first line has to be #!report:

#!report

10.sql = SELECT ...

20 {
  sql = SELECT ...
  5.sql = SELECT ...
}

Attributes defined in the parameter field

See also at specific FormElement definitions.

Name Note
acceptZeroAsRequired 0|1 - Accept a ‘0’ as a valid input. Default ‘0’ (=0 is not a valid input)
autofocus See autofocus
capture, accept, maxFileSize, fileDestination, fileTrash, fileTrashText, fileReplace, autoOrient, autoOrientCmd, autoOrientMimeType, chmodFile, chmodDir, slaveId, sqlBefore, sqlInsert, sqlUpdate, sqlDelete, sqlAfter, importToTable, importToColumns, importRegion, importMode, importType, importNamedSheetsOnly, importSetReadDataOnly, importListSheetNames, See Type: upload
checkBoxMode checked unchecked label2 itemList emptyHide emptyItemAtStart emptyItemAtEnd buttonClass See Type: checkbox, Type: radio, Type: select
dateFormat yyyy-mm-dd / dd.mm.yyyy
data-pattern-error Pattern violation: Text for error message
data-required-error Required violation: Text for error message
data-match-error Match violation: Text for error message
data-error Violation of ‘check-type’: Text for error message
decimalFormat [precision,scale] Limits and formats input to a decimal number with the specified precision and scale. If no precision and scale are specified, the decimal format is pulled from the table definition.
htmlAfter HTML Code wrapped after the complete FormElement
htmlBefore HTML Code wrapped before the complete FormElement
extraButtonLock [0|1] Show a ‘lock’ on the right side of the input element. See extraButtonLock
extraButtonPassword [0|1] Show an ‘eye’ on the right side of the input element. See extraButtonPassword
extraButtonInfo Text. Show an ‘i’ on the right side of the input element. See extraButtonInfo
extraButtonInfoClass By default empty. Specify any class to be assigned to wrap extraButtonInfo
extraButtonInfoMinWidth See extraButtonInfo
editor-plugins, editor-toolbar, editor-statusbar, See Type: editor
fileButtonText Overwrite default ‘Choose File’
fillStoreVar Fill the STORE_VAR with custom values. See Store: VARS - V.
form, page, title, extraDeleteForm, detail, subrecordTableClass, See Type: subrecord
min s/d/n

Minimum and/or maximum allowed values for input field. Can be used for numbers, dates, or strings.

Always use the international format ‘yyyy-mm-dd[ hh:mm[:ss]]

max s/d/n
processReadOnly [0|1] By default FE’s with type=’readonly’ are not processed during ‘save’. This option forces to process them during ‘save’ as well.
retype, retypeLabel, retypeNote, characterCountWrap, hideZero, emptyMeansNull, See Type: text
showSeconds 0|1 - Shows the seconds on form load. Default: 0
showZero 0|1 - Empty timestamp: ‘0’(default) - nothing shown, ‘1’ - the string ‘0000-00-00 00:00:00’ is displayed
timeIsOptional 0|1 - Used for datetime input. 0 (default): Time is required - 1: Entering a time is optional (defaults to 00:00:00 if none entered).
typeAheadLimit, typeAheadInitialSuggestion, typeAheadMinLength, typeAheadSql, typeAheadSqlPrefetch, typeAheadPedantic See Type Ahead
typeAheadTag, typeAheadGlueInsert, typeAheadGlueDelete, typeAheadTagInsert See Type Ahead Tag
wrapRow If specified, skip default wrapping (<div class=’col-md-?’>). Instead the given string is used.
wrapLabel
wrapInput
wrapNote
trim By default, whitespace is trimmed. To disable, use ‘trim=none’. You can also specify custom trim characters: ‘trim=\ ‘ only trims spaces.
sqlValidate See Parameter: sqlValidate
expectRecords
messageFail
dataReference Optional. See Application Test
requiredPosition See Required Position.
minWidth See Checkbox / Radio: minWidth.
  • s/d/n: string or date or number.

slaveId, sqlBefore, sqlAfter, …

See Parameter: slaveId

Native FormElements

  • Like ‘input’, ‘checkbox’, …
autofocus

The first FormElement with this attribute will get the focus after form load. If there is no such attribute given to any FormElement, the attribute will be automatically assigned to the first editable FormElement.

To disable ‘autofocus’ on a form, set ‘autofocus=0’ on the first editable FormElement.

Note: If there are multiple pills defined on a form, only the first pill will be set with ‘autofocus’.

extraButtonLock
  • The user has to click on the lock, before it’s possible to change the value. This will protect data against unwanted modification.
  • After Form load, the value is shown, but not editable.
  • Shows a ‘lock’ on the right side of an input element of type text, date, time or datetime.
  • This option is not available for FormElements with mode=readonly.
  • There is no value needed for this parameter.
extraButtonPassword
  • The user has to click on the eye (unhide) to see the value.
  • After Form load, the data is hided by asteriks.
  • Shows an ‘eye’ on the right side of an input element of type text, date, time or datetime.
  • There is no value needed for this parameter.
extraButtonInfo
  • After Form load, the info button/icon is shown but the information message is hidden.
  • The user has to click on the info button/icon to see an additional message.
  • The value of this parameter is the text shown to the user.
  • Shows an info button/icon, depending of extraButtonInfoPosition in Configuration
    • auto, depending on FormElement type:
      • on the right side of an input element for type text, date, time or datetime,
      • below the FormElement for all other types.
    • below: below the FormElement for all types.
  • extraButtonInfoMinWidth: default is 250 and defines a minimal width.
  • For FormElement with mode below, a span element with the given class in extraButtonInfoClass (FE, F, Configuration) will be applied. E.g. this might be pull-right to align the info button/icon on the right side below the input element.

Checkbox / Radio: minWidth

Checkbox and Radio Elements, shown in plain horizontal mode, receives a minWidth to align them. The default is 80px and might be defined per Form or per FormElement.

Required Position

By default, input elements with Mode=required will be displayed with a ‘red asterix’ right beside the label. The position of the ‘red asterix’ can be choosen via the parameter field:

requiredPosition = label-left|label-right|input-left|input-right|note-left|note-right

The default is ‘label-right’.

The definition can be set per Form (=affects all FormElements) or per FormElement.

Type: checkbox

Checkboxes can be rendered in mode:

  • single:

    • One column in a table corresponds to one checkbox.
    • The value for statuses checked and unchecked are free to choose.
    • This mode is selected, if a) checkBoxMode = single, or b) checkBoxMode is missing and the number of fields of the column definition is <3.
    • FormElement.parameter:
      • checkBoxMode = single (optional)
      • checked = <value> (optional, the value which represents ‘checked’)
        • If checked is empty or missing: If type = ‘enum’ or ‘set’, get first item of the definition. If type = string, get default.
      • unchecked = <value> (optional, the value which represents ‘unchecked’)
        • If unchecked is empty or missing: If type = ‘enum’ or ‘set’, get second item of checked. If type = ‘string’, get ‘’.
      • label2 = <value> (Text right beside checkbox) (optional)
  • multi:

    • One column in a table represents multiple checkboxes. This is typically useful for the column type set.
    • The value for status checked are free to choose, the value for status unchecked is always the empty string.
    • Each field key (or the corresponding value from the key/value pair) will be rendered right beside the checkbox.
    • FormElement.parameter
      • checkBoxMode = multi
      • itemList - E.g.:
        • itemList=red,blue,orange
        • itemList=1:red,2:blue,3:orange
        • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange`
    • FormElement.sql1 = {{!SELECT id, value FROM SomeTable}}
    • FormElement.maxlength - vertical or horizontal alignment:
      • Value: ‘’, 0, 1 - The check boxes will be aligned vertical.
      • Value: >1 - The check boxes will be aligned horizontal, with a linebreak every ‘value’ elements.
  • FormElement.parameter:

    • emptyHide: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty entry, but the empty value should not be selectable.

    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.

    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.

    • buttonClass: Instead of the plain HTML checkbox fields, Bootstrap buttons. are rendered as checkbox elements. Use one of the following classes:

      • btn-default (default, grey),
      • btn-primary (blue),
      • btn-success (green),
      • btn-info (light blue),
      • btn-warning (orange),
      • btn-danger (red).

      With a given buttonClass, all buttons (=radios) are rendered horizontal. A value in FormElement.maxlength has no effect.

  • No preselection:

    • If a form is in ‘new’ mode and if there is a default value configured on a table column, such a value is shown by default. There might be situations, where the user should be forced to select a value (e.g. specifying the gender). An unwanted default value can be suppressed by specifying an explicit definition on the FormElement field value:

      {{<columnName>:RZ}}
      

      For existing records the shown value is as expected the value of the record. For new records, it’s the value 0, which is typically not one of the ENUM / SET values and therefore nothing is selected.

Type: date

Type: datetime

  • Range datetime: ‘1000-01-01 00:00:00’ to ‘9999-12-31 23:59:59’ or ‘0000-00-00 00:00:00’. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html)
  • Optional:
    • FormElement.parameter:
      • dateFormat = yyyy-mm-dd | dd.mm.yyyy
      • showSeconds = 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed ‘1’ or not ‘0’.
      • showZero = 0|1 - For an empty timestamp, With ‘0’ nothing is displayed. With ‘1’ the string ‘0000-00-00 00:00:00’ is displayed.

Type: extra

  • The element behaves like, and can be used as, a HTML hidden input element - with the difference & advantage, that the element never leaves the server and therefore can’t be manipulated by a user.
  • The following names are reserved and can’t be used to name ‘extra’ FormElements: ‘id’, ‘type’, ‘L’.
  • The element is not transferred to the the browser.
  • The element can be used to define / pre calculate values for a column, which do not already exist as a native FormElement.
  • The element is build / computed on form load and saved alongside with the SIP parameter of the current form.
  • The element is not recalculated during save - it’s stored during ‘Form Load’ inside the current form SIP handle.
  • Access the value without specifying any store (default store priority is sufficient).

Type: text

General input for any text.

  • By default, the maximum length of input data is automatically restricted by the underlying database column.
  • HTML decides between one line input (=Input text) and multiline input (=Textarea).
  • FormElement.size = [<width>[,<min height (lines)>[,<max height (pixel)>]]]
    • The parameter is optional and controls the behaviour of the input / textarea element.
    • The <width> is counted in ‘characters’.
      • But: the visible width of an input element is defined by the Bootstrap column width (and not the width given here). Finally: the value here is meaningless. Nevertheless it has to be given for future compatibility.
    • <min-height>:
      • Counted as ‘lines’.
      • If not set the height is treated as 1.
      • A <min-height> of 1 forces an one line input. Exception: <max-height> > 0 enables auto-grow.
    • <max-height>:
      • Controls the auto-grow-Mode.
      • Counted in ‘pixel’.
      • If not set it becomes the default of 350 pixels.
      • If > 0, the auto-grow mode is activated and the height of the textarea will be dynamically updated up to <max-height>.
      • If = 0, the auto-grow mode is disabled.
  • FormElement.parameter:
    • retype = 1 (optional): Current input element will be rendered twice. The form can only submitted if both elements are equal.
      • retypeLabel = <text> (optional): The label of the second element.
      • retypeNote = <text> (optional): The note of the second element.
    • characterCountWrap = <span class=”qfq-cc-style”>Count: | </span> (optional). Displays a character counter below the input/textarea element.
    • Also check the Attributes defined in the parameter field data-…-error to customize error messages shown by the validator.
    • hideZero = 0|1 (optional): with hideZero=1 a ‘0’ in the value will be replaced by an empty string.
    • emptyMeansNull = [0|1] (optional): with emptyMeansNull or emptyMeansNull=1 a NULL value will be written if the value is an empty string
    • inputType = number (optional). Typically the HTML tag ‘type’ will be ‘text’, ‘textarea’ or ‘number’ (detected automatically). If necessary, the HTML tag ‘type’ might be forced to a specific given value.
    • step = Step size of the up/down buttons which increase/decrease the number of in the input field. Optional. Default 1. Only useful with inputType=number (defined explicit via inputType or detected automatically).
    • textareaResize = 0|1 (optional). Be default = 1 (=on). A textarea element is resizable by the user.

Type Ahead

Activating typeahead functionality offers an instant lookup of data and displaying them to the user, while the user is typing, a drop-down box offers the results. As datasource the regular SQL connection or a LDAP query can be used. With every keystroke (starting from the typeAheadMinLength characters), the already typed value will be transmitted to the server, the lookup will be performed and the result, upto typeAheadLimit entries, are displayed as a drop-down box.

  • FormElement.parameter:
    • typeAheadLimit = <number>. Max numbers of result records to be shown. Default is 20.
    • typeAheadMinLength = <number>. Minimum length to type before the first lookup starts. Default is 2.

Depending of the typeahead setup, the given FormElement will contain the displayed value or id (if an id/value dict is configured).

Configuration via Form / FormElement

All of the typeAhead* (except typeAheadLdap, typeAheadInitialSuggestion) and ldap* parameter can be specified either in Form.parameter or in FormElement.parameter.

SQL
  • FormElement.parameter:
    • typeAheadSql = SELECT ... AS 'id', ... AS 'value' FROM ... WHERE name LIKE ? OR firstName LIKE ? LIMIT 100
      • If there is only one column in the SELECT statement, that one will be used and there is no dict (key/value pair).
      • If there is no column id or no column value, then the first column becomes id and the second column becomes value.
      • The query will be fired as a ‘prepared statement’.
      • The value, typed by the user, will be replaced on all places where a ? appears.
      • All ? will be automatically surrounded by ‘%’. Therefore wildcard search is implemented: … LIKE ‘%<?>%’ …
    • typeAheadSqlPrefetch = SELECT firstName, ' ', lastName FROM Person WHERE id = ?
      • If the query returns several results, only the first one is returned and displayed.
      • If the query selects multiple columns, the columns are concatenated.
    • typeAheadInitialSuggestion = {{!SELECT fr.id AS id, fr.name AS value FROM Fruit AS fr}}
      • Shows suggestions when the input element gets the focus, before the user starts to type anything.
      • If given, typeAheadMinLength will be set to 0.
      • Limit the number of rows via SQL ... LIMIT ... clause.

Type Ahead Tag

Extend a TypeAhead input element to take more than one token (=tag) in the same input element.

This mode supports only typeAheadSql (no LDAP).

Usage: A user might choose one or more tags from a typeahead list (to minimize typos and to reuse already given tags).

The user starts typing and for each keypress typeAheadSql is searched for all matches. The user selects an element by clicking on it or by using one of the typeAheadTagDelimiter key presses (by default tab or comma). If a tag is selected, it will be visual separated from the input cursor. Already selected tags can not be edited but removed (clicking on the x). Further tags can be added.

typeAheadTag support two different modes: a) Tag , b) Glue.

Mode: Tag

Tags will be loaded and saved as a comma separated list. Maximum length of saved tags is limit by the size of the column (incl. separator).

Additional arguments needed for typeAheadTag:

  • FormElement.parameter:
    • typeAheadTag = [0|1] - Default 0 (=off), existence or =1 switches the mode typeAheadTag on.
    • typeAheadTagDelimiter = List of ASCII codes to separate tags during input. Default ‘9,44’ (tab and comma).
Mode: Glue

For each selected tag a glue record, pointing to the tag, is created.

The Glue mode will be activated by setting FormElement.parameter.typeAheadGlueInsert with a corresponding SQL statement.

Glue records will be created or deleted, as the user select or deselect tags. Processing of those Glue records will be done after the primary form record has been written and before any after*-action FormElements will be processed.

FormElement.name should not point to a column in the form primary table. Instead a free name should be used for the typeAhead FormElement.

The maximum number of tags is not limited - but take care to size the FormElement big enough (FormElement.maxLength) to show all tags.

On Form load (to show already assigned tags) a comma separated list has to be given in FormElement.value, based on the previously saved Glue records. The string format is identically to the one used in mode Tag.

Extra parameter for mode = Tag :

  • FormElement.parameter:
    • typeAheadTagInsert = {{INSERT INTO Tag (….) VALUES (…)}} - Only needed with typeAheadPedantic=0.
    • typeAheadGlueInsert = {{INSERT INTO glueTag (…) VALUES (…)}}
    • typeAheadGlueDelete = {{DELETE FROM glueTag WHERE …}}

Example:

Table Person with some records. Table Fruit with a list of fruits. Table FruitPerson with glue records.

Usage: assign favourite fruits to a person. The fruits are the tags, the glue records will assign the fruits to a person.

The form will be open with a person record and has only one FormElement.

  • Form.name=personFavouriteFruits
  • Form.title=Person Favourite Fruits
  • Form.primaryTable = Person
  • FormElement[1].name = myFavoriteFruits
  • FormElement[1].type = Text
  • FormElement[1].value = {{SELECT GROUP_CONCAT( CONCAT(f.id, ‘:’, f.name)) FROM FruitPerson AS fp, Fruit AS f WHERE fp.pId={{id:R}} AND fp.fruitId=f.id ORDER BY f.name}}
  • FormElement[1].parameter:
    • typeAheadTag = 1
    • typeAheadSql = SELECT f.id AS ‘id’, f.name AS ‘value’ FROM Fruit AS f WHERE f.name LIKE ?
    • typeAheadMinLength = 1
    • typeAheadGlueInsert = {{INSERT INTO FruitPerson (pId, fruitId) VALUES ({{id:R}}, {{tagId:V}} ) }}
    • typeAheadGlueDelete = {{DELETE FROM FruitPerson WHERE pId={{id:R}} AND fruitId={{tagId:V}} }}

Explanation:

  • On form load, without any assigned tags (=fruits), FormElement.value will be empty.
  • The User will assign three fruits: Apple, Banana, Lemon.
  • On form save, QFQ does:
    • compares the old tag assigment (empty) with the new tag assigment (3 elements).
    • for each new assigned tag:
      • the tagId and tagValue will be stored in STORE_VAR (that’s the one selected by the user and defined via typeAheadSql)
      • typeAheadGlueInsert will be fired (with the replaced variable {{tagId:V}}).
  • The user loads the person favourite fruit form again (same user).
  • FormElement.value will now be: 1:Apple,3:Banana,10:Lemon.
  • The user removes ‘Banana’ and adds ‘Orange’.
  • On form save, QFQ does:
    • compares the old tag assigment (3 elements) with the new tag assigment (also 3 elements, but different).
    • for each new assigned tag:
      • the tagId and tagValue will be stored in STORE_VAR.
      • typeAheadGlueInsert will be fired (with the replaced variable {{tagId:V}}).
    • for each removed assigned tag:
      • the tagId and tagValue will be stored in STORE_VAR.
      • typeAheadGlueDelete will be fired (with the replaced variable {{tagId:V}}).

Type: editor

  • TinyMCE (https://www.tinymce.com, community edition) is used as the QFQ Rich Text Editor.

  • The content will be saved as HTML inside the database.

  • All configuration and plugins will be configured via the ‘parameter’ field. Just prepend the word ‘editor-‘ in front of each TinyMCE keyword. Check possible options under:

  • Bars:

    • Top: menubar - by default hidden.
    • Top: toolbar - by default visible.
    • Bottom: statusbar - by default hidden, exception: min_height and max_height are given via size parameter.
  • The default setting in FormElement.parameter is:

    editor-plugins=code link lists searchreplace table textcolor textpattern visualchars
    editor-toolbar=code searchreplace undo redo | styleselect link table | fontselect fontsizeselect | bullist numlist outdent indent | forecolor backcolor bold italic editor-menubar=false
    editor-statusbar=false
    
  • To deactivate the surrouding <p> tag, configure in FormElement.parameter:

    editor-forced_root_block = false
    

    This might have impacts on the editor. See https://www.tinymce.com/docs/configure/content-filtering/#forced_root_block

  • Set ‘extended_valid_elements’ to enable HTML tags and their attributes. Example:

    editor-extended_valid_elements = span[class|style]
    
  • Set ‘editor-content_css’ to use a custom CSS to style elements inside the editor. Example:

    editor-content_css = fileadmin/custom.css
    
  • FormElement.size = <min_height>,<max_height>: in pixels, including top and bottom bars. E.g.: 300,600

Type: annotate

Annotate image or text. Typically the image or text has been uploaded during a previous step. The annotation will be saved in FormElement.name column of the current record. The uploaded file itself will not be modified. The annotations can be shown in edit (and might be modified) or in readonly mode.

Two modes are available:

grafic
A simple grafic editor to paint on top of the image (best by a tablet with pen or grafic tablet). The uploaded image is shown in the background. All drawings are saved as a JSON fabric.js data string. Supported file types: png, svg. PDF files can be easily divided into per page SVG files during upload - see Split PDF Upload
text
Per code line, annotation(s) can be added. Different users can add individual annotations. A user can only edit the own annotations. The annotations are saved as QFQ internal JSON string.

Note

Drawing with fabric.js might produce a lot data. Take care the column type/size is big enough (>=64kB).

Grafic

An image, specified by FormElement.parameter.imageSource={{pathFileName}}, will be displayed in the background. On form load, both, the image and an optional already given grafical annotations, will be displayed. The image is SIP protected and will be loaded on demand.

Form.parameter

Attribute Value Description
annotateType grafic grafic|text. Default is grafic. Select mode.
imageSource <path filename> Background image. E.g. fileadmin/images/scan.png
defaultPenColor <rgb hex value> | Pen default color, after loading the fabric element. Default is ‘0000FF’ (blue).

Note

By using the the FormElement annotate, the JS code fabric.min.js and qfq.fabric.min.js has to be included. See Setup CSS & JS.

Code

Form.parameter

Attribute Value Description
annotateType text grafic|text. Default is grafic. Select mode.
textSource <path filename> Text file to annotate.
annotateUserName <john doe> Will be shown at annotation line.
annotateUserUid <123> Will be shown at annotation line.
annotateUserAvatar <https://gravatar…> Will be shown at annotation line.
highlight auto off,auto,javascript,qfq,python,matlab

Type: imageCut

Uploaded images can be cut or rotate via QFQ (via fabric.js). The modified image is saved under the given pathFileName.

  • The ‘value’ of the FormElement has to be a valid PathFileName to an image.
  • Valid image file formats are SVG, PNG, JPG, GIF.
  • Invalid or missing filenames results to an empty ‘imageCut’ element.
  • FormElement.parameter:
    • resizeWidth = <empty>|[width in pixel] - the final width of the modified image. If empty (or not given), no change.
    • keepOriginal = <empty>|[string] - By default: ‘.save’. If empty (no string given), don’t keep the original. If an extension is given and if there is not already a <pathFileName><.extension>, than the original file is to copied to it.

Type: note

An FormElement without any ‘input’ functionality -just to show some text. Use the typical fields ‘label’, ‘value’ and ‘note’ to be displayed in the corresponding three standard columns.

Type: password

  • Like a text element, but every character is shown as an asterisk.

Type: radio

  • Radio Buttons will be built from one of three sources:

    1. ‘sql1’: E.g. {{!SELECT type AS label FROM Car }} or {{!SELECT type AS label, typeNr AS id FROM Car}} or {{!SHOW tables}}.
      • Resultset format ‘named’: column ‘label’ and optional a column ‘id’.
      • Resultset format ‘index’:
        • One column in resultset >> first column represents label
        • Two or more columns in resultset >> first column represents id and second column represents label.
    2. FormElement.parameter:
      • itemList = <attribute> E.g.: itemList=red,blue,orange or itemList=1:red,2:blue,3:orange
      • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange
    3. Definition of the enum or set field (only labels, ids are not possible).
  • FormElement.maxlength = <value>

    • Applies only to ‘plain’ radio elements (not the Bootstrap ‘buttonClass’ from below)
    • vertical or horizontal alignment:
      • <value>: ‘’, 0, 1 - The radios will be aligned vertical.
      • <value>: >1 - The readios will be aligned horizontal, with a linebreak every ‘value’ elements.
  • FormElement.parameter:

    • emptyHide: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty entry, but the empty value should not be selectable.

    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.

    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.

    • buttonClass = <class> - Instead of the plain radio fields, Bootstrap buttons. are rendered as radio elements. Use one of the following classes:

      • btn-default (default, grey),
      • btn-primary (blue),
      • btn-success (green),
      • btn-info (light blue),
      • btn-warning (orange),
      • btn-danger (red).

      With a given buttonClass, all buttons (=radios) are rendered horizontal. A value in FormElement.maxlength has no effect.

  • No preselection:

    • If there is a default configured on a table column, such a value is selected by default. If the user should actively choose an option, the ‘preselection’ can be omitted by specifying an explicit definition on the FormElement field value:

      {{<columnName>:RZ}}
      

      For existing records the shown value is as expected the value of the record. For new records, it’s the value 0, which is typically not one of the ENUM values and therefore nothing is selected.

Type: select

  • Select lists will be built from one of three sources:
    • FormElement.sql1 = {{!<SQL Query}}
      • E.g. {{!SELECT type AS label FROM Car }} or {{!SELECT type AS label, typeNr AS id FROM Car}} or {{!SHOW tables}}.
      • Resultset format ‘named’: column ‘label’ and optional a column ‘id’.
      • Resultset format ‘index’:
        • One column in resultset >> first column represents label
        • Two or more columns in resultset >> first column represents id and second column represents label.
    • FormElement.parameter:
      • itemList = <attribute> - E.g.: itemList=red,blue,orange or itemList=1:red,2:blue:3:orange
      • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange
    • Definition of the enum or set field (only labels, ids are not possible).
  • FormElement.size = <value>
    • <value>: <empty>|0|1: drop-down list.
    • <value>: >1: Select field with size rows height. Multiple selection of items is possible.
  • FormElement.parameter:
    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.
    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.
    • emptyHide: Existence of this item hides the empty entry. This is useful for e.g. Enums, which have an empty entry and the empty value should not be an option to be selected.
    • datalist: Similar to ‘typeAhead’. Enables the user to select a predefined option (sql1, itemList) or supply any free text. Attention: Safari (and some other) browsers do not support this fully - https://caniuse.com/#search=datalist.

Type: subrecord

The FormElement type ‘subrecord’ renders a list of records (so called secondary records), typically to show, edit, delete or add new records. The list is defined as an SQL query. The number of records shown is not limited. These FormElement will be rendered inside the form as a HTML table.

  • mode / modeSql = <type/value>

    • show / required: the regular mode to show the subrecords
    • readonly: New / Edit / Delete Buttons are disabled
    • hidden: The FormElement is rendered, but hidden with display=’none’.
  • dynamicUpdate - not supported at the moment.

  • sql1 = {{!SQL Query}}

    • SQL query to select records. E.g.:

      {{!SELECT addr.id AS id, CONCAT(addr.street, addr.streetnumber) AS a, addr.city AS b, addr.zip AS c FROM Address AS addr}}
      
    • Notice the exclamation mark after ‘{{‘ - this is necessary to return an array of elements, instead of a single string.

    • Exactly one column ‘id’ has to exist; it specifies the primary record for the target form. In case the id should not be visible to the user, it has to be named ‘_id’.

    • Column name: [title=]<title>[|[maxLength=]<number>][|nostrip][|icon][|link][|url][|mailto][|_rowClass][|_rowTooltip]

      • If the keyword is used, all parameter are position independent.

      • Parameter are separated by ‘|’.

      • [title=]<text>: Title of the column. The keyword ‘title=’ is optional. Columns with a title starting with ‘_’ won’t be rendered.

      • [maxLength=]<number>: Max. number of characters displayed per cell. The keyword ‘maxLength=’ is optional. Default maxLength ‘20’. A value of ‘0’ means no limit. This setting also affects the title of the column.

      • nostrip: by default, html tags will be stripped off the cell content before rendering. This protects the table layout. ‘nostrip’ deactivates the cleaning to make pure html possible.

      • icon: the cell value contains the name of an icon in typo3conf/ext/qfq/Resources/Public/icons. Empty cell values will omit an html image tag (=nothing rendered in the cell).

      • link: value will be rendered as described under Column: _link

      • url: value will be rendered as a href url.

      • mailto: value will be rendered as a href mailto.

      • _rowClass

      • _rowTooltip

        • Defines the title attribute (=tooltip) of a subrecord table row.
      • Examples:

        {{!SELECT id, note1 AS 'Comment', note2 AS 'Comment|50' , note3 AS 'title=Comment|maxLength=100|nostrip', note4 AS '50|Comment',
        'checked.png' AS 'Status|icon', email AS 'mailto', CONCAT(homepage, '|Homepage') AS 'url',
        CONCAT('d|s|F:', pathFileName) AS 'Download|link',
        ELT(status,'info','warning','danger') AS '_rowClass', help AS '_rowTooltip' ...}}
        
  • FormElement.parameter

    • form = <form name> - Target form, e.g. form=person

    • page = <T3 page alias or id> - Target page with detail form. If none specified, use the current page.

    • extraDeleteForm: Optional. The per row delete Button will reference the form specified here (for deleting) instead of the default (form).

    • detail = <string> - Mapping of values from

        1. the primary form,
        1. the current row,
        1. any constant or ‘{{…}}’ -

      to the target form (defined via form=…).

      • Syntax:

        <source table column name 1|&constant 1>:<target column name 1>[,<source table column name 2|&constant 2>:<target column name 2>][...]
        
      • Example: detail=id:personId,rowId:secId,&12:xId,&{{a}}:personId (rowId is a column of the current selected row defined by sql1)

      • By default, the given value will overwrite values on the target record. In most situations, this is the wished behaviour.

      • Exceptions of the default behaviour have to be defined on the target form in the corresponding FormElement in the field value by changing the default Store priority definition. E.g. {{<columnName>:RS0}} - For existing records, the store R will provide a value. For new records, store R is empty and store S will be searched for a value: the value defined in detail will be choosen. At last the store ‘0’ is defined as a fallback.

      • source table column name: E.g. A person form is opened with person.id=5 (r=5). The definition detail=id:personId and form=address maps person.id to address.personId. On the target record, the column personId becomes ‘5’.

      • Constant ‘&’: Indicate a ‘constant’ value. E.g. &12:xId or {{…}} (all possibilities, incl. further SELECT statements) might be used.

    • subrecordTableClass: Optional. Default: ‘table table-hover qfq-subrecord-table qfq-color-grey-2’. If given, the default will be overwritten. Example:

      subrecordTableClass = table table-hover qfq-subrecord-table qfq-table-50
      
    • Tablesorter in Subrecord:

      subrecordTableClass = table table-hover qfq-subrecord-table tablesorter tablesorter-pager tablesorter-filter
      
    • subrecordColumnTitleEdit: Optional. Will be rendered as the column title for the new/edit column.

    • subrecordColumnTitleDelete: Optional. Will be rendered as the column title for the delete column.

Subrecord DragAndDrop

Subrecords inherently support drag-and-drop, see also Drag and drop. The following parameters can be used in the parameter field to customize/activate drag-and-drop:

  • orderInterval: The order interval to be used, default is 10.
  • dndTable: The table that contains the records to be ordered. If not given, the table name of the form specified via form=… is used.
  • orderColumn: The dedicated order column in the specified dndTable (needs to match a column in the table definition). Default is ord.

If dndTable is a table with a column orderColumn, QFQ automatically applies drag-and-drop logic to the rendered subrecord. It does so by using the subrecord field sql1. The sql1 query should include a column id (or _id) and a column ord (or _ord). E.g.:

FE.sql1 = {{!SELECT p.id AS _id, p.ord AS _ord, p.name FROM Person WHERE p.email!='' ORDER BY p.ord}}

Tips:

  • If you want to deactivate a drag-and-drop that QFQ automatically renders, set the orderColumn to a non-existing column. E.g., orderColumn = nonExistingColumn. This will deactivate drag-and-drop.
  • In order to evaluate the sql1 query dynamically during a drag-and-drop event, the STORE_RECORD (with the current subrecord) is loaded.
  • The stores STORE_RECORD, STORE_SIP and STORE_SYSTEM are supported during a drag-and-drop event and can be used in FE.sql1 query.
    • STORE_SIP: SIP values on form load
    • STORE_RECORD: values of the current record loaded in the form.
  • If the subrecord is rendered with drag-and-drop active, but the order is not affected upon reload, there is most likely a problem with evaluating the sql1 query at runtime.

Type: time

  • Range time: ‘00:00:00’ to ‘23:59:59’ or ‘00:00:00’. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html)
  • Optional:
  • FormElement.parameter
    • showSeconds = 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed ‘1’ or not ‘0’.
    • showZero = 0|1 - For an empty timestamp, With ‘0’ nothing is displayed. With ‘1’ the string ‘00:00[:00]’ is displayed.

Type: upload

An upload element is based on a ‘file browse’-button and a ‘trash’-button (=delete). Only one of them is shown at a time. The ‘file browse’-button is displayed, if there is no file uploaded already. The ‘trash’-button is displayed, if there is a file uploaded already.

After clicking on the browse button, the user select a file from the local filesystem. After choosing the file, the upload starts immediately, shown by a turning wheel. When the server received the whole file and accepts (see below) the file, the ‘file browse’-button disappears and the filename is shown, followed by a ‘trash’-button. Either the user is satisfied now or the user can delete the uploaded file (and maybe upload another one).

Until this point, the file is cached on the server but not copied to the fileDestination. The user have to save the current record, either to finalize the upload and/or to delete a previously uploaded file.

The FormElement behaves like a

  • ‘native FormElement’ (showing controls/text on the form) as well as an
  • ‘action FormElement’ by firing queries and doing some additional actions during form save.

Inside the Form editor it’s shown as a ‘native FormElement’. During saving the current record, it behaves like an action FormElement and will be processed after saving the primary record and before any action FormElements are processed.

  • FormElement.value = <string> - By default, the full path of any already uploaded file is shown. To show something different, e.g. only the filename, define:

    a) {{filenameBase:V}}
    b) {{SELECT SUBSTRING_INDEX( '{{pathFileName:R}}', '/', -1)  }}
    

See also download Button to offer a download of an uploaded file.

FormElement.parameter

  • fileButtonText: Overwrite default ‘Choose File’

  • capture = camera - On a smartphone, after pressing the ‘open file’ button, the camera will be opened and a choosen picture will be uploaded. Automatically set/overwrite accept=image/*.

  • accept = <mime type>,image/*,video/*,audio/*,.doc,.docx,.pdf

    • List of mime types (also known as ‘media types’): http://www.iana.org/assignments/media-types/media-types.xhtml
    • If none mime type is specified, ‘application/pdf’ is set. This forces that always (!) one type is specified.
    • To allow any type, specify * or */* or *.*.
    • One or more media types might be specified, separated by ‘,’.
    • Different browser respect the given definitions in different ways. Typically the ‘file choose’ dialog offer:
      • the specified mime type (some browers only show ‘custom’, if more than one mime type is given),
      • the option ‘All files’ (the user is always free to try to upload other file types) - but the server won’t accept them,
      • the ‘file choose’ dialog only offers files of the selected (in the dialog) type.
    • If for a specific file type is no mime type available, the definition of file extension(s) is possible. This is less secure, cause there is no content check on the server after the upload.
  • maxFileSize = <size> - max filesize in bytes (no unit), kilobytes (k/K) or megabytes (m/M) for an uploaded file. If empty or not given, take value from Form, System or System default.

  • fileTrash = [0|1] - Default: ‘1’. This option en-/disables the trash button right beside the file chooser. By default the trash is visible. The trash is only visible if a) there is already a file uploaded or b) a new file has been chosen.

  • fileTrashText = <string> - Default: ‘’. Will be shown right beside the trash glyph-icon.

  • fileDestination = <pathFileName> - Destination where to copy the file. A good practice is to specify a relative fileDestination - such an installation (filesystem and database) are moveable.

    • If the original filename should be part of fileDestination, the variable {{filename}} (STORE_VARS) can be used. Example

      fileDestination={{SELECT 'fileadmin/user/pictures/', p.name, '-{{filename}}' FROM Person AS p WHERE p.id={{id:R0}} }}
      
      • Several more variants of the filename and also mimetype and filesize are available. See store variables form element upload.
      • The original filename will be sanitized: only ‘<alnum>’, ‘.’ and ‘_’ characters are allowed. German ‘umlaut’ will be replaced by ‘ae’, ‘ue’, ‘oe’. All non valid characters will be replaced by ‘_’.
    • If a file already exist under fileDestination, an error message is shown and ‘save’ is aborted. The user has no possibility to overwrite the already existing file. If the whole workflow is correct, this situation should no arise. Check also fileReplace below.

    • All necessary subdirectories in fileDestination are automatically created.

    • Using the current record id in the fileDestination: Using {{r}} is problematic for a ‘new’ primary record: that one is still ‘0’ at the time of saving. Use {{id:R0}} instead.

    • Uploading of malicious code (e.g. PHP files) is hard to detect. The default mime type check can be easily faked by an attacker. Therefore it’s recommended to use a fileDestination-directory, which is secured against script execution (even if the file has been uploaded, the webserver won’t execute it) - see Secure direct file access.

  • sqlBefore, sqlAfter: available in Upload simple mode and Upload advanced mode.

  • slaveId, sqlInsert, sqlUpdate, sqlDelete, sqlUpdate: available only in Upload advanced mode.

  • fileSize / mimeType

    • In Upload simple mode the information of fileSize and mimeType will be automatically updated on the current record, if table columns fileSize and/or mimeType exist.

      • If there are more than one Upload FormElement in a form, the automatically update for fileSize and/or mimeType are not done automatically.
    • In Upload advanced mode the fileSize and / or mimeType have to be updated with an explicit SQL statement:

      sqlAfter = {{UPDATE Data SET mimeType='{{mimeType:V}}', fileSize={{fileSize:V}} WHERE id={{id:R}} }}
      
  • fileReplace = always - If fileDestination exist - replace it by the new one.

  • chmodFile = <unix file permission mode> - e.g. 660 for owner and group read and writeable. Only the numeric mode is allowed.

  • chmodDir = <unix file permission mode> - e.g. 770 for owner and group read, writeable and executable. Only the numeric mode is allowed. Will be applied to all new created directories.

  • autoOrient: images might contain EXIF data (e.g. captured via mobile phones) incl. an orientation tag like TopLeft, BottomRight and so on. Web-Browser and other grafic programs often understand and respect those information and rotate such images automatically. If not, the image might be displayed in an unwanted oritentation. With active option ‘autoOrient’, QFQ tries to normalize such images via ‘convert’ (part of ImageMagick). Especially if images are processed by the QFQ internal ‘Fabric’-JS it’s recommended to normalize images first. The normalization process does not solve all orientation problems.

    • autoOrient = [0|1]
    • autoOrientCmd = ‘convert -auto-orient {{fileDestination:V}} {{fileDestination:V}}.new; mv {{fileDestination:V}}.new {{fileDestination:V}}’
    • autoOrientMimeType = image/jpeg,image/png,image/tiff

    If the defaults for autoOrientCmd and autoOrientMimeType are sufficient, it’s not necessary to specify them.

  • downloadButton = t:<string> - If given, shows a button to download the previous uploaded file - instead of the string given in fe.value. The button is only shown if fe.value points to a readable file on the server.

    • If downloadButton is empty, just shows the regular download glyph.
    • To just show the filename: downloadButton = t:{{filenameOnly:V}}
    • Additional attributes might be given like downloadButton = t:Download|o:check file. Please check Download.
      • The following attributes are hard coded (can’t be changed): s|M:file|d|F
  • fileUnzip - If the file is a ZIP file (only then) it will be unzipped. If no directory is given via fileUnzip, the basedir of fileDestination is taken, appended by unpack.

    If an unzip will be done, for each file of the archive STORE_VAR will be filled (name, path of the extracted file, mime type, size) and the following will be triggered: sqlValidate, slaveId, sqlBefore, sqlAfter, sqlInsert, sqlUpdate.

    Example:

    fileDestination = fileadmin/file_{{id:R}}.zip
    fileUnzip
    sqlValidate ={{! SELECT '' FROM (SELECT '') AS fake WHERE '{{mimeType:V}}' LIKE 'application/pdf%' }}
    expectRecords=1
    messageFail=Unexpected filetype
    
    # Set new
    sqlAfter={{INSERT INTO Upload (pathFileName) VALUES '{{filename:V}}' }}
    
  • fileSplit, fileDestinationSplit, tableNameSplit: see Split PDF Upload

  • Excel Import: QFQ offers functionality to directly import excel data into the database. This functionality can optionally be combined with saving the file by using the above parameters like fileDestination. The data is imported without formatting. Please note that this means Excel dates will be imported as a number (e.g. 43214), which is the serial value date in Excel. To convert such a number to a MariaDb date, use: DATE_ADD(‘1899-12-30’, INTERVAL serialValue DAY).

    • importToTable = <[db.]tablename> - Required. Providing this parameter activates the import. If the table doesn’t exist, it will be created.
    • importToColumns = <col1>,<col2>,… - If none provided, the Excel column names A, B, … are used. Note: These have to match the table’s column names if the table already exists.
    • importRegion = [tab],[startColumn],[startRow],[endColumn],[endRow]|… - All parts are optional (default: entire 1st sheet). Tab can either be given as an index (1-based) or a name. start/endColumn can be given either numerically (1, 2, …) or by column name (A, B, …). Note that you can specify several regions to import.
    • importMode = append (default) | replace - The data is either appended or replace in the specified table.
    • importType = auto (default) | xls | xlsx | ods | csv - Define what kind of data should be expected by the Spreadsheet Reader.
    • importNamedSheetsOnly = <comma separated list of sheet names>. Use this option if specific sheets cause problems during import and should be skipped, by naming only those sheets, who will be read. This will also reduce the memory usage.
    • importSetReadDataOnly = 0|1. Read only cell data, not the cell formatting. Warning: cell types other than numerical will be misinterpreted.
    • importListSheetNames = 0|1. For debug use only. Will open a dialog and report all found worksheet names.

Immediately after the upload finished (before the user press save), the file will be checked on the server for it’s content or file extension (see ‘accept’).

The maximum size is defined by the minimum of upload_max_filesize, post_max_size and memory_limit (PHP script) in the php.ini.

In case of broken uploads, please also check max_input_time in php.ini.

Deleting a record and the referenced file

If the user deletes a record (e.g. pressing the delete button on a form) which contains reference(s) to files, such files are deleted too. Slave records, which might be also deleted through a ‘delete’-form, are not checked for file references and therefore such files are not deleted on the filesystem.

Only column(name)s which contains pathFileName as part of their name, are checked for file references.

If there are other records, which references the same file, such files are not deleted. It’s a very basic check: just the current column of the current table is compared. In general it’s not a good idea to have multiple references to a single file. Therefore this check is just a fallback.

Upload simple mode

Requires: ‘upload’-FormElement.name = ‘column name’ of an column in the primary table.

After moving the file to fileDestination, the current record/column will be updated to fileDestination. The database definition of the named column has to be a string variant (varchar, text but not numeric or else). On form load, the column value will be displayed as the whole value (pathFileName)

Deleting an uploaded file in the form (by clicking on the trash near beside) will delete the file on the filesystem as well. The column will be updated to an empty string.

This happens automatically without any further definiton in the ‘upload’-FormElement.

Multiple ‘upload’-FormElements per form are possible. Each of it needs an own table column.

Upload advanced mode

Requires: ‘upload’-FormElement.name is unknown as a column in the primary table.

This mode will serve further database structure scenarios.

A typical name for such an ‘upload’-FormElement, to show that the name does not exist in the primary table, might start with ‘my’, e.g. ‘myUpload1’.

  • FormElement.value = <string> - The path/filename, shown during ‘form load’ to indicate a previous uploaded file, has to be queried with this field. E.g.:

    {{SELECT pathFileNamePicture FROM Note WHERE id={{slaveId}} }}
    
  • FormElement.parameter:

    • fileDestination = <pathFileName> - determine the path/filename. E.g.:

      fileDestination=fileadmin/person/{{name:R0}}_{{id:R}}/uploads/picture_{{filename}}
      
    • slaveId = <id> - Defines the target record where to retrieve and store the path/filename of the uploaded file. Check also Parameter: slaveId. E.g.:

      slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}}
      
    • sqlBefore = {{<query>}} - fired during a form save, before the following queries are fired.

    • sqlInsert = {{<query>}} - fired if slaveId=0 and an upload exist (user has choosen a file):

      sqlInsert={{INSERT INTO Note (pId, type, pathFileName) VALUE ({{id:R0}}, 'image', '{{fileDestination}}') }}
      
    • sqlUpdate = {{<query>}} - fired if slaveId>0 and an upload exist (user has choosen a file). E.g.:

      sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
      
    • sqlDelete = {{<query>}} - fired if slaveId>0 and no upload exist (user has not choosen a file). E.g.:

      sqlDelete={{DELETE FROM Note WHERE id={{slaveId:V}}  LIMIT 1}}
      
    • sqlAfter = {{<query>}} - fired after all previous queries have been fired. Might update the new created id to a primary record. E.g.:

      sqlAfter={{UPDATE Person SET noteIdPicture = {{slaveId}} WHERE id={{id:R0}} LIMIT 1 }}
      

Split PDF Upload

Additional to the upload, it’s possible to split the uploaded file (only PDF files) into several SVG or JPEG files, one file per PDF page. The split is done via a) http://www.cityinthesky.co.uk/opensource/pdf2svg/ or b) Image Magick convert.

Currently, QFQ can only split PDF files.

If the source file is not of type PDF, activating fileSplit has no impact: no split and NO complain about invalid file type.

  • FormElement.parameter:

    • fileSplit = <type> - Activate the splitting process. Possible values: svg or jpeg. No default.

    • fileSplitOptions = <command line options>.

      • [svg] - no default
      • [jpeg] - default: -density 150 -quality 90
    • fileDestinationSplit = <pathFileName (pattern)> - Target directory and filename pattern for the created & split’ed files. Default <fileDestination>.split/split.<nr>.<fileSplit>. If explicit given, respect that SVG needs a printf style for <nr>, whereas JPEG is numbered automatically. E.g.

      [svg] fileDestinationSplit = fileadmin/protected/{{id:R}}.{{filenameBase:V}}.%02d.svg
      [jpeg] fileDestinationSplit = fileadmin/protected/{{id:R}}.{{filenameBase:V}}.jpg
      
    • tableNameSplit = <tablename> - Default: name of table of current form. This name will be saved in table Split

The splitting happens immediately after the user pressed save.

To easily access the split files via QFQ, per file one record is created in table ‘Split’.

Table ‘Split’:

Column Description
id Uniq auto increment index
tableName Name of the table, where the reference to the original file (multipage PDF file) is saved.
xId Primary id of the reference record.
pathFileName Path/filename reference to one of the created files

One usecase why to split an upload: annotate individual pages by using the FormElement.type=`annotate`.

Class: Action

Type: before… | after…

These type of ‘action’ FormElements will be used to implement data validation or creating/updating additional records.

Types:

  • beforeLoad (e.g. good to check access permission)
  • afterLoad
  • beforeSave (e.g. to prohibit creating of duplicate records)
  • afterSave (e.g. to to create & update additional records)
  • beforeInsert
  • afterInsert
  • beforeUpdate
  • afterUpdate
  • beforeDelete (e.g. to delete slave records)
  • afterDelete
  • paste (configure copy/paste forms)

Parameter: sqlValidate

Perform checks by firing an SQL query and expecting a predefined number of selected records.
  • OK: the expectRecords number of records has been selected. Continue processing the next FormElement.
  • Fail: the expectRecords number of records has not been selected (less or more): Display the error message messageFail and abort the whole (!) current form load or save.

FormElement.parameter:

  • requiredList = <fe.name[s]> - List of native-FormElement names: only if all of those elements are filled (!=0 and !=’‘), the current action-FormElement will be processed. This will enable or disable the check, based on the user input! If no native-FormElement names are given, the specified check will always be performed.
  • sqlValidate = {{<query>}} - validation query. E.g.: sqlValidate={{SELECT id FROM Person AS p WHERE p.name LIKE {{name:F:all}} AND p.firstname LIKE {{firstname:F:all}} }}
  • expectRecords = <value>- number of expected records.
    • expectRecords = 0 or expectRecords = 0,1 or expectRecords = {{SELECT COUNT(id) FROM Person}}
    • Separate multiple valid record numbers by ‘,’. If at least one of those matches, the check will pass successfully.
  • messageFail = <string> - Message to show. E.g.: messageFail = There is already a person called {{firstname:F:all}} {{name:F:all}}

Parameter: slaveId

FormElement.parameter
  • slaveId = <id>:
    • Auto fill: name the action action-FormElement equal to an existing column (table from the current form definition). slaveId will be automatically filled with the value of the named column.
      • If there is no such named column name, set slaveId = 0.
    • Explicit definition: slaveId = 123 or slaveId = {{SELECT id …}}

Note:

  • {{slaveId:V}} can be used in any query of the current FormElement.
  • If the action-FormElement name exist as a column in the master record: Update that column automatically with the recent slaveId
  • After an INSERT the last_insert_id() becomes the {{slaveId:V}}.
  • fillStoreVar is fired first, than slaveId.
  • If slaveId is known in fillStoreVar, set: slaveId={{someId:V}}.

Parameter: sqlBefore / sqlInsert / sqlUpdate / sqlDelete / sqlAfter

  • Save values of a form to different record(s), optionally on different table(s).
  • Typically useful on ‘afterSave’ - be careful when using it earlier, e.g. beforeLoad.
FormElement.parameter
  • requiredList = <fe.name[s]> - List of native-FormElement: only if all of those elements are filled, the current action-FormElement will be processed.
  • sqlBefore = {{<query>}} - always fired (before any sqlInsert, sqlUpdate, ..)
  • sqlInsert = {{<query>}} - fired if slaveId == 0 or slaveId == ‘’.
  • sqlUpdate = {{<query>}} - fired if slaveId > 0.
  • sqlDelete = {{<query>}} - fired if slaveId > 0, after sqlInsert or sqlUpdate. Be careful not to delete filled records! Always add a check, if values given, not to delete the record! sqlHonorFormElements helps to skip such checks.
  • sqlAfter = {{<query>}} - always fired (after sqlInsert, sqlUpdate or sqlDelete).
  • sqlHonorFormElements = <fe.name[s]> list of FormElement names (this parameter is optional).
    • If one of the named FormElements is not empty:
      • fire sqlInsert if slaveId == 0,
      • fire sqlUpdate if slaveId > 0
    • If all of the named FormElements are empty:
      • fire sqlDelete if slaveId > 0

Example

Situation 1: master.xId=slave.id (1:1)

  • Name the action element ‘xId’: than {{slaveId}} will be automatically set to the value of ‘master.xId’

    • {{slaveId}} == 0 ? ‘sqlInsert’ will be fired.
    • {{slaveId}} != 0 ? ‘sqlUpdate’ will be fired.
  • In case of firing ‘sqlInsert’, the ‘slave.id’ of the new created record are copied to master.xId (the database will be updated automatically).

  • If the automatic update of the master record is not suitable, the action element should have no name or a name which does not exist as a column of the master record. Define slaveId={{SELECT id …}}

  • Two FormElements myStreet and myCity:

    • Without sqlHonorFormElements. Parameter:

      sqlInsert = {{INSERT INTO address (`street`, `city`) VALUES ('{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} AND '{{myStreet:FE:alnumx:s}}'='' AND '{{myCity:FE:alnumx:s}}'='' LIMIT 1 }}
      
    • With sqlHonorFormElements. Parameter:

      sqlHonorFormElements = myStreet, myCity     # Non Templategroup
      sqlInsert = {{INSERT INTO address (`street`, `city`) VALUES ('{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} LIMIT 1 }}
      
      # For Templategroups: sqlHonorFormElements = myStreet%d, myCity%d
      

Situation 2: master.id=slave.xId (1:n)

  • Name the action element different to any column name of the master record (or no name).

  • Determine the slaveId: slaveId={{SELECT id FROM Slave WHERE slave.xxx={{…}} LIMIT 1}}

    • {{slaveId}} == 0 ? ‘sqlInsert’ will be fired.
    • {{slaveId}} != 0 ? ‘sqlUpdate’ will be fired.
  • Two FormElements myStreet and myCity. The person is the master record, address is the slave:

    • Without sqlHonorFormElements. Parameter:

      slaveId = {{SELECT id FROM Address WHERE personId={{id}} ORDER BY id LIMIT 1 }}
      sqlInsert = {{INSERT INTO address (`personId`, `street`, `city`) VALUES ({{id}}, '{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} AND '{{myStreet:FE:alnumx:s}}'='' AND '{{myCity:FE:alnumx:s}}'='' LIMIT 1 }}
      
    • With sqlHonorFormElements. Parameter:

      slaveId = {{SELECT id FROM Address WHERE personId={{id}} ORDER BY id LIMIT 1 }}
      sqlHonorFormElements = myStreet, myCity       # Non Templategroup
      sqlInsert = {{INSERT INTO address (`personId`, `street`, `city`) VALUES ({{id}}, '{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} LIMIT 1 }}
      
      # For Templategroups: sqlHonorFormElements = myStreet%d, myCity%d
      

Type: sendmail

  • Send mail(s) will be processed after:
    • saving the record ,
    • processing all uploads,
    • together with after… action FormElements in the given order.
  • FormElement.value = <string> - Body of the email. See also: html-formatting
  • FormElement.parameter:
    • sendMailTo = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <john@doe.com>. If there is no recipient email address, no mail will be sent.
    • sendMailCc = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <john@doe.com>.
    • sendMailBcc = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <john@doe.com>.
    • sendMailFrom = <string> - Sender of the email. Optional: ‘realname <john@doe.com>’. Mandatory.
    • sendMailSubject = <string> - Subject of the email.
    • sendMailReplyTo = <string> - Reply this email address. Optional: ‘realname <john@doe.com>’.
    • sendMailAttachment = <string> - List of ‘sources’ to attach to the mail as files. Check Attachment for options.
    • sendMailHeader = <string> - Specify custom header.
    • sendMailFlagAutoSubmit = <string> - on|off - If ‘on’ (default), the mail contains the header ‘Auto-Submitted: auto-send’ - this suppress a) OoO replies, b) forwarding of emails.
    • sendMailGrId = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId2 = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId3 = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailMode = <string> - html - if set, the e-mail body will be rendered as html.
    • sendMailSubjectHtmlEntity = <string> - encode|decode|none - the mail subject will be htmlspecialchar() encoded / decoded (default) or none (untouched).
    • sendMailBodyHtmlEntity*= `<string>` - **encode|decode|none* - the mail body will be htmlspecialchar() encoded, decoded (default) or none (untouched).
    • sqlBefore / sqlAfter = <string> - can be used like with other action elements (will be fired before/after sending the e-mail).
  • An empty sendMailTo will cancel any sendmail action, even if sendMailCc|Bcc is set. This can be used to determine during runtime if sending is wished.
  • To use values of the submitted form, use the STORE_FORM. E.g. {{name:F:allbut}}
  • To use the id of a new created or already existing primary record, use the STORE_RECORD. E.g. {{id:R}}.
  • By default, QFQ stores values ‘htmlspecialchars()’ encoded. If such values have to send by email, the html entities are unwanted. Therefore the default setting for ‘subject’ und ‘body’ is to decode the values via ‘htmlspecialchars_decode()’. If this is not wished, it can be turned off by sendMailSubjectHtmlEntity=none and/or sendMailBodyHtmlEntity=none.
  • For debugging, please check Redirect all mail to (catch all).

Example to attach one file1.pdf (with the attachment filename ‘readme.pdf’) and concatenate two PDF, created on the fly from the www.example.com and ?export (with the attachment filename ‘personal.pdf’):

sendMailAttachmemt = F:fileadmin/file1.pdf|d:readme.pdf|C|u:http://www.example.com|p:?id=export&r=123&_sip=1|d:personal.pdf

Type: paste

See also Copy Form.

  • sql1 = {{<query>}} - e.g. {{!SELECT {{id:P}} AS id, ‘{{myNewName:FE:allbut}}’ AS name}} (only one record) or {{!SELECT i.id AS id, {{basketId:P}} AS basketId FROM Item AS i WHERE i.basketId={{id:P}} }} (multiple records)
    • Pay attention to ‘!’.
    • For every row, a new record is created in recordDestinationTable.
    • Column ‘id’ is not copied.
    • The recordSourceTable together with column id will identify the source record.
    • Columns not specified, will be copied 1:1 from source to destination.
    • Columns specified, will overwrite the source value.
  • FormElement.parameter:
    • recordSourceTable = <tableName> - Optional: table from where the records will be copied. Default: <recordDestinationTable>
    • recordDestinationTable = <tableName> - table where the new records will be copied to.
    • translateIdColumn = <column name> - column name to update references of newly created id’s.

Form Magic

Parameter

  • Table column id: QFQ expect that each table, which will be loaded in a form, contains an autoincrement column of name id. It’s not necessary to create a FormElement id in a form - but it won’t disturb.

  • Parameter (one or more) in the SIP url, which exist as a column in the form table (SIP parameter name is equal to a table column name), will be automatically saved in the record. This acts as ‘hidden magic’.

    Example: A slave record (e.g. an address of a person) has to be assigned to a master record (a person). Just give the pId in the link who calls the address form. The following creates a ‘new’ button for an address for all persons, and the pId will be automatically saved in the address table:

    SELECT CONCAT('p:{{pageAlias:T}}&form=address&r=0&pId=', p.id) AS _pagen FROM Person AS p
    

    Such parameter, which the form expects to be in the SIP url, should be specified in Form.permitNew and/or Form.permitEdit. It’s only a check for the webmaster, not to forgot a parameter in a SIP url.

  • FormElement.type = subrecord

    Subrecord’s will automatically create new, edit and delete links. To inject parameter in those automatically created links, use FormElement.parameter.detail . See Type: subrecord.

  • FormElement.type = extra

    If a table column should be saved with a specific value, and the value should not be shown to the user, the FE.type=’extra’ will do the job. The value could be static or calculated on the fly. Often it’s easier to specify such a parameter/value in the SIP url, but if the form is called from multiple places, an extra element is more suitable.

Variables

  • Form.parameter.fillStoreVar / FormElement.parameter.fillStoreVar

    An SQL statement will fill STORE_VARS. Such values can be used during form load and/or save.

Action

  • Action FE

    Via FormElement.parameter.requiredList an element can be enabled / disabled, depending of a user provided input in one of the specified required FEs.

Multi Form

Multi Forms are like a regular form with the difference that the shown FormElements are repeated for each selected record (defined by multiSql).

Name    
multiSql {{!SELECT id, name FROM Person}} Query to select MulitForm records
multiMgsNoRecord Default: No data Message shown if multiSql selects no records
  • Multi Form do not use ‘record-locking’ at all.

The Form is shown as a HTML table.

  • multiSql: Selects the records where the defined FormElements will work on each.
    • A uniq column ‘id’ or ‘_id’ (not shown) is mandatory and has to reflect an existing record id in table primary table.
    • Additional columns, defined in multiSql, will be shown on the form in the same line, before the FormElements.

Simple

General:

  • It’s not possible to create new records in simple mode, only existing records can be modified.

Form:

  • Per row, the STORE_RECORD is filled with the whole record of the primary table, referenced by multiSql.id.

FormElement:

  • The FormElement.name represents a column of the defined primary table.
  • The existing values of such FormElements are automatically loaded.
  • No further definition is required.

Advanced

To handle foreign records (insert/update/delete), use the Parameter: slaveId concept.

Typically the FormElement.name is not a column of the primary table.

Multiple languages

QFQ Forms might be configured for up to 5 different languages. Per language there is one extra field in the Form editor. Which field represents which language is configured in Configuration.

  • The Typo3 installation needs to be configured to handle different languages - this is independent of QFQ and not covered here. QFQ will use the Typo3 internal variable ‘pageLanguage’, which typically correlates to the URL parameter ‘L’ in the URL.
  • In Configuration the Typo3 language index (value of ‘L’) and a language label have to be configured for each language. Only than, the additional language fields in the Form editor will be shown.

Example

Assuming the Typo3 page has the

  • default language, L=0
  • English, L=1
  • Spanish, L=2

Configuration in Configuration:

formLanguageAId = 1
formLanguageALabel = English

formLanguageBId = 2
formLanguageBLabel = Spanish

The default language is not covered in Configuration.

The Form editor now shows on the pill ‘Basic’ (Form and FormEditor) for both languages each an additional parameter input field. Any input field in the Form editor can be redeclared in the corresponding language parameter field. Any missing definition means ‘take the default’. E.g.:

  • Form: ‘person’

    Column Value
    title Eingabe Person
    languageParameterA title=Input Person
    languageParameterB title=Persona de entrada
  • FormElement ‘firstname’ in Form ‘person’:

    Column Value
    title Vorname
    note Bitte alle Vornamen erfassen
    languageParameterA
    title=Firstname
    note=Please give all firstnames
    languageParameterB
    title=Persona de entrada
    note=Por favor, introduzca todos los nombres

The following fields are possible:

  • Form: title, showButton, forwardMode, forwardPage, bsLabelColumns, bsInputColumns, bsNoteColumns, recordLockTimeoutSeconds
  • FormElement: label, mode, modeSql, class, type, subrecordOption, encode, checkType, ord, size, maxLength, bsLabelColumns, bsInputColumns, bsNoteColumns,rowLabelInputNote, note, tooltip, placeholder, value, sql1, feGroup

Dynamic Update

The ‘Dynamic Update’ feature makes a form more interactive. If a user changes a FormElement who is tagged with ‘dynamicUpdate’, all elements who are tagged with ‘dynamicUpdate’, will be recalculated and rerendered.

The following fields will be recalculated during ‘Dynamic Update’

  • ‘modeSql’ - Possible values: ‘show’, ‘required’, ‘readonly’, ‘hidden’
  • ‘label’
  • ‘value’
  • ‘note’
  • ‘parameter.*’ - especially ‘itemList’

To make a form dynamic:

  • Mark all FormElements with dynamic update`=`enabled, which should initiate or receive updates.

See #3426 / Dynamic Update: Inputs loose the new content and shows the old value:

  • On all dynamic update FormElements an explicit definition of value, including a sanitize class, is necessary (except the field is numeric). A missing definition let’s the content overwrite all the time with the old value. A typical definition for value looks like (default store priority is: FSRVD):

    {{<FormElement name>::alnumx}}
    
  • Define the receiving FormElements in a way, that they will interpret the recent user change! The form variable of the specific sender FormElement {{<sender element>:F:<sanitize>}} should be part of one of the above fields to get an impact. E.g.:

    [receiving *FormElement*].parameter: itemList={{ SELECT IF({{carPriceRange:FE:alnumx}}='expensive','Ferrari,Tesla,Jaguar','General Motors,Honda,Seat,Fiat') }}
    

    Remember to specify a ‘sanitize’ class - a missing sanitize class means ‘digit’, every content, which is not numeric, violates the sanitize class and becomes therefore an empty string!

  • If the dynamic update should work on existing and new records, it’s important to guarantee that the query result is not empty! even if the primary record does not exist! E.g. use a LEFT JOIN. The following query is ok for new and edit.

    {{SELECT IF( IFNULL(adr.type,'') LIKE '%token%','show','hidden') FROM (SELECT 1) AS fake LEFT JOIN Address AS adr ON adr.type='{{type:FR0}}' LIMIT 1}}
    

Examples

  • Master FormElement ‘music’ is a radio/enum of ‘classic’, ‘jazz’, ‘pop’.

Content of a select list

  • Slave FormElement ‘interpret’ is ‘select’-list, depending of ‘music’
sql={{!SELECT name FROM Interpret WHERE music={{music:FE:alnumx}} ORDER BY name}}

Show / Hide a FormElement

  • Slave ‘interpret’ is displayed only for ‘pop’. Field ‘modeSql’:
{{SELECT IF( '{{music:FR:alnumx}}'='pop' ,'show', 'hidden' ) }}

Form Layout

The forms will be rendered with Bootstrap CSS classes, based on the 12 column grid model (Bootstrap 3.x). Generally a 3 column layout for label columns on the left side, an input field column in the middle and a note column on the right side will be rendered.

The used default column (=bootstrap grid) width is 3,6,3 (col-md , col-lg) for label, input, note.

  • The system wide defaults can be changed via Configuration.
  • Per Form settings can be done in the Form parameter field. They overwrite the system wide default.
  • Per FormElement settings can be done in the FormElement parameter field. They overwrite the Form setting.

A column will be switched off (no wrapping via <div class=’col-md-?>) by setting a 0 on the respective column.

Custom field width

Per FormElement set BS Label Columns, BS Input Columns or BS Note Columns to customize an individual width. If only a number is specified, it’s used as col-md-<number>. Else the whole text string is used as CSS class, e.g. col-md-3 col-lg-2.

Multiple Elements per row

Every row is by default wrapped in a <div class=’form-group’> and every column is wrapped in a <div class=’col-md-?’>. To display multiple input elements in one row, the wrapping of the FormElement row and of the three columns can be customized via the checkboxes of Label / Input / Note. Every open and every close tag can be individually switched on or off.

E.g. to display 2 FormElements in a row with one label (first FormElement) and one note (last FormElement) we need the following (switch off all non named):

  • First FormElement
    • open row tag: row ,
    • open and close label tag: label, /label,
    • open and close field tag: input, /input,
  • Second FormElement
    • open and close field tag: input, /input,
    • open and close note tag: note, /note,
    • close row tag: /row ,

Copy Form

Records (=master) and child records can be duplicated (=copied) by a regular Form, extended by FormElements of type ‘paste’. A ‘copy form’ works either in:

  • ‘copy and paste now’ mode: the ‘select’ and ‘paste’ Form is merged in one form, only one master record is possible,
  • ‘copy now, paste later’ mode: the ‘select’ Form selects master record(s), the ‘paste’ Form paste’s them later.

Concept

A ‘select action’ (e.g. a Form or a button click) creates record(s) in the table Clipboard. Each clipboard record contains:

  • the ‘id(s)’ of the record(s) to duplicate,
  • the ‘paste’ form id (that Form defines, to which table the master records belongs to, as well as rules of how to duplicate any slave records) and where to copy the new records
  • user identifier (QFQ cookie) to separate clipboard records of different users inside the Clipboard table.

The ‘select action’ is also responsible to delete old clipboard records of the current user, before new clipboard records are created.

The ‘paste form’ iterates over all master record id(s) in the Clipboard table. For each master record id, all FormElements of type paste are fired (incl. the creating of slave records).

E.g. if there is a basket with different items and you want to duplicate the whole basket including new items, create a form with the following parameter

  • Form
    • Name: copyBasket
    • Table: Clipboard
    • Show Button: only close and save
  • FormElement 1: Record id of the source record.
    • Name: idSrc
    • Lable: Source Form
    • Class: native
    • Type: select
    • sql1: {{! SELECT id, title FROM Basket }}
  • FormElement 2: New name of the copied record.
    • Name: myNewName
    • Class: native
    • Type: text
  • FormElement 3: a) Check that there is no name conflict. b)Purge any old clipboard content of the current user.
    • Name: clearClipboard
    • Class: action
    • Type: beforeSave
    • Parameter:
      • sqlValidate={{SELECT f.id FROM Form AS f WHERE f.name LIKE ‘{{myName:FE:alnumx}}’ LIMIT 1}}
      • expectRecords = 0
      • messageFail = There is already a form with this name
      • sqlAfter={{DELETE FROM Clipboard WHERE cookie=’{{cookieQfq:C0:alnumx}}’ }}
  • FormElement 4: Update the clipboard source reference, with current {{cookieQfq:C}} identifier.
    • Name: updateClipboardRecord
    • Class: action
    • Type: afterSave
    • Parameter: sqlAfter={{UPDATE Clipboard SET cookie=’{{cookieQfq:C0:alnumx}}’, formIdPaste={{formId:S0}} /* PasteForm */ WHERE id={{id:R}} LIMIT 1 }}
  • FormElement 5: Copy basket identifier.
    • Name: basketId
    • Class: action
    • Type: paste
    • sql1: {{!SELECT {{id:P}} AS id, ‘{{myNewName:FE:allbut}}’ AS name}}
    • Parameter: recordDestinationTable=Basket
  • FormElement 6: Copy items of basket.
    • Name: itemId
    • Class: action
    • Type: paste
    • sql1: {{!SELECT i.id AS id, {{basketId:P}} AS basketId FROM Item AS i WHERE i.basketId={{id:P}} }}
    • Parameter: recordDestinationTable=Item

Table self referencing records

Records might contain references to other records in the same table. E.g. native FormElements might assigned to a fieldSet, templateGroup or pill, a fieldSet might assigned to other fieldsets or pills and so on. When duplicating a Form and the corresponding FormElements all internal references needs to be updated as well.

On each FormElement.type=`paste` record, the column to be updated is defined via:

  • parameter: translateIdColumn = <column name>

For the ‘copyForm’ this would be ‘feIdContainer’.

The update of the records is started after all records have been copied (of the specific FormElement.type=`paste` record).

Delete Record

Deleting record(s) via QFQ might be solved by either:

  • using the delete button on a form on the top right corner.
  • by letting Report creating a special link (see below). The link contains the record id and:
    • a form name, or
    • a table name.

Deleting a record just by specifying a table name, will only delete the defined record (no slave records).

  • By using a delete button via report or in a subrecord row, a ajax request is send.
  • By using a delete button on the top right corner of the form, the form will be closed after deleting the record.

Example for report:

SELECT p.name, CONCAT('U:form=person&r=', p.id) AS _paged FROM Person AS p
SELECT p.name, CONCAT('U:table=Person&r=', p.id) AS _paged FROM Person AS p

To automatically delete slave records, use a form and create beforeDelete FormElement(s) on the form:

  • class: action
  • type: beforeDelete
  • parameter: sqlAfter={{DELETE FROM <slaveTable> WHERE <slaveTable>.<masteId>={{id:R}} }}

You might also check the form ‘form’ how the slave records ‘FormElement’ will be deleted.

Locking Record / Form

Support for record locking is given with mode:

  • exclusive: user can’t force a write.
    • Including a timeout (default 15 mins recordLockTimeoutSeconds in Configuration) for maximum lock time.
  • advisory: user is only warned, but allowed to overwrite.
  • none: no bookkeeping about locks.

For ‘new’ records (r=0) there is no locking at all.

The record locking protection is based on the tablename and the record id. Different Forms, with the same primary table, will be protected by record locking. On the other side, action-FormElements updating non primary table records are not protected by ‘record locking’: the QFQ record locking is NOT 100%.

The ‘record locking’ mode will be specified per Form. If there are multiple Forms with different modes, and there is already a lock for a tablename / record id pair, the most restrictive will be applied.

Best practice

View: List vs. Detail

As ‘list’ a number of data/rows shown on the page is meant.

As ‘detail’ a form is meant, which shows one single data record and let the user edit it.

To provide an easy understandable navigation structure, it’s nice for the user to stay on the same page, even the user is in ‘detail’ or ‘list’ mode. Create a single QFQ tt-content record on a fresh page:

form = {{form:SE}}

10.sql = SELECT p.name, CONCAT('p:{{pageAlias:T}}&form=Person&r=', p.id) AS _pagee FROM Person AS p
10.rend = <br>
  • If the page is called without any parameter, a list of persons is shown.
  • Behind each name, a button is shown. A click on it opens the form ‘Person’ (with the selected person record) on the same page.

Mode ‘list’ or ‘detail’ is detected automatically: if a form is given via STORE_SIP or STORE_TYPO3, the form (=detail) is shown else the report (=list).

Custom default value only for ‘new records’

Method 1

On Form.parameter define a fillStoreVar query with a column name equal to a form field. That’s all.

Example:

FormElement.name = technicalContact
Form.parameter.fillStoreVar = {{! SELECT CONCAT(p.firstName, ' ', p.name) AS technicalContact FROM Person AS p WHERE p.account='{{feUser:T}}' }}

What we use here is the default STORE prio FSRVD. If the form loads with r=0, ‘F’, ‘S’ and ‘R’ are empty. ‘V’ is filled. If r>0, than ‘F’ and ‘S’ are empty and ‘R’ is filled.

Method 2

In the specific FormElement set value={{columnName:RSE}}. The link to the form should be rendered with ‘”…&columnName=<data>&…” AS _page’. The trick is that the STORE_RECORD is empty for new records, and therefore the corresponding value from STORE_SIP will be returned. Existing records will use the already saved value.

Central configured values

Any variable in Configuration can be used by {{<varname>:Y}} in form or report statements.

E.g.

TECHNICAL_CONTACT = jane.doe@example.net

Could be used in an FormElement.type = sendmail with parameter setting sendMailFrom={{TECHNICAL_CONTACT:Y}}.

Debug Report

Writing “report’s” in the nested notation or long queries broken over several lines, might not interpreted as wished. Best for debugging is to specify in the tt-content record:

debugShowBodyText = 1

Note: Debug information is only display if it’s enabled in Configuration by

  • showDebugInfo: yes or
  • showDebugInfo: auto and logged in in the same Browser as a Typo3 backend user.

More detailed error messages

If showDebugInfo is enabled, a full stacktrace and variable contents are displayed in case of an error.

Form: compute next free ‘ord’ automatically

Requirement: new records should automatically get the highest number plus 10 for their ‘ord’ value. Existing records should not be altered.

Version 1

Compute the next ‘ord’ in advance in the subrecord field of the primary form. Submit that value to the new record via SIP parameter to the secondary form.

On the secondary form: for ‘new’ records choose the computed value, for existing records leave the value unchanged.

  • Master form, subrecord FormElement, field parameter: set

    detail=id:formId,{{SELECT '&', IFNULL(fe.ord,0)+10 FROM Form AS f LEFT JOIN *FormElement* AS fe ON fe.formId=f.id WHERE
    f.id={{r:S0}} ORDER BY fe.ord DESC LIMIT 1}}:ord
    
  • Slave form, ord FormElement, field value: set

    `{{ord:RS0}}`.
    

Version 2

Compute the next ‘ord’ as default value direct inside the secondary form. No change is needed for the primary form.

  • Secondary form, ord FormElement, field value: set {{SELECT IF({{ord:R0}}=0, MAX(IFNULL(fe.ord,0))+10,{{ord:R0}}) FROM (SELECT 1) AS a LEFT JOIN FormElement AS fe ON fe.formId={{formId:S0}} GROUP BY fe.formId}}.

Form: Person Wizard - firstname, city

Requirement: A form that displays the column ‘firstname’ from table ‘Person’ and ‘city’ from table ‘Address’. If the records not exist, the form should create it.

Form primary table: Person

Form slave table: Address

Relation: Person.id = Address.personId

  • Form: wizard
    • Name: wizard
    • Title: Person Wizard
    • Table: Person
    • Render: bootstrap
  • FormElement: firstname
    • Class: native
    • Type: text
    • Name: firstname
    • Label: Firstname
  • FormElement: email, text, 20
    • Class: native
    • Type: text
    • Name: city
    • Label: City
    • Value: {{SELECT city FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}
  • FormElement: insert/update address record
    • Class: action
    • Type: afterSave
    • Label: Manage Address
    • Parameter:
      • slaveId={{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}
      • sqlInsert={{INSERT INTO Address (personId, city) VALUES ({{r}}, ‘{{city:F:allbut:s}}’) }}
      • sqlUpdate={{UPDATE Address SET city=’{{city:F:allbut:s}}’ WHERE id={{slaveId:V}} }}
      • sqlDelete={{DELETE FROM Address WHERE id={{slaveId:V}} AND ‘’=’{{city:F:allbut:s}}’ LIMIT 1}}

Form: Person Wizard - firstname, single note

Requirement: A form that displays the column ‘firstname’ from table ‘Person’ and ‘note’ from table ‘Note’. If the records don’t exist, the form should create it. Column Person.noteId points to Note.id

Form primary table: Person

Form slave table: Address

Relation: Person.id = Address.personId

  • Form: wizard
    • Name: wizard
    • Title: Person Wizard
    • Table: Person
    • Render: bootstrap
  • FormElement: firstname
    • Class: native
    • Type: text
    • Name: firstname
    • Label: Firstname
  • FormElement: email, text, 20
    • Class: native
    • Type: text
    • Name: note
    • Label: Note
    • Value: {{SELECT Note FROM Note AS n, Person AS p WHERE p.id={{r}} AND p.noteId=n.id ORDER BY id }}
  • FormElement: insert/update address record
    • Class: action
    • Type: afterSave
    • Name: noteId
    • Label: Manage Note
    • Parameter:
      • sqlInsert={{INSERT INTO Note (note) VALUES (‘{{note:F:allbut:s}}’) }}
      • sqlUpdate={{UPDATE Note SET note=’{{note:F:allbut:s}}’ WHERE id={{slaveId:V}} }}

Icons Template Group

This example will display graphics instead of text ‘add’ and ‘remove’. Also there is a distance between the templateGroups.

  • FormElement.parameter:

    tgClass = qfq-child-margin-top
    tgAddClass = btn alert-success
    tgAddText = <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
    tgRemoveClass = btn btn-danger alert-danger
    tgRemoveText = <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
    

Chart

  • QFQ delivers a chart JavaScript lib: https://github.com/nnnick/Chart.js.git. Docs: http://www.chartjs.org/docs/

  • The library is not sourced in the HTML page automatically. To do it, either include the lib typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js:

    • in the specific tt_content record (shown below in the example) or
    • system wide via Typo3 Template record.
  • By splitting HTML and JavaScript code over several lines, take care not accidentally to create a ‘nesting’-end token. Check the line after 10.tail =. It’s ‘}’ alone on one line. This is a valid ‘nesting’-end token!. There are two options to circumvent this:

    • Don’t nest the HTML & JavaScript code - bad workaround, this is not human readable.

    • Select different nesting token, e.g. ‘<’ (check the first line on the following example).

      # <
      
      10.sql = SELECT '_'
      10.head =
        <div style="height: 1024px; width: 640px;">
          <h3>Distribution of FormElement types over all forms</h3>
          <canvas id="barchart" width="1240" height="640"></canvas>
        </div>
        <script src="typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js"></script>
        <script>
          $(function () {
            var ctx = document.getElementById("barchart");
            var barChart = new Chart(ctx, {
              type: 'bar',
                data: {
      
      10.tail =
                }
            });
          });
        </script>
      
      # Labels
      10.10 <
        sql = SELECT "'", fe.type, "'" FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type
        head = labels: [
        tail = ],
        rsep = ,
      >
      
      # Data
      10.20 <
        sql = SELECT COUNT(fe.id) FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type
        head = datasets: [ {   data: [
        tail = ],  backgroundColor: "steelblue", label: "FormElements" } ]
        rsep = ,
      >
      

Upload Form Simple

Table Person

Name Type
id int
name varchar(255)
pathFileNamePicture varchar(255)
pathFileNameAvatar varchar(255)
  • Form:

    • Name: UploadSimple
    • Table: Person
  • FormElements:

    • Name: name

      • Type: text
      • Label: Name
    • Name: pathFileNamePicture

      • Type: upload

      • Label: Picture

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}}
        
    • Name: pathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}}
        

Upload Form Advanced 1

Table: Person

Name Type
id int
name varchar(255)

Table: Note

Name Type
id int
pId int
type varchar(255)
pathFileName varchar(255)
  • Form:

    • Name: UploadAdvanced1
    • Table: Person
  • FormElements

    • Name: name

      • Type: text
      • Label: Name
    • Name: mypathFileNamePicture

      • Type: upload

      • Label: Picture

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}}
        slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'picture', {{id:R0}}) }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        
    • Name: mypathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}}
        slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='avatar' LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'avatar', {{id:R0}}) }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        

Upload Form Advanced 2

Table: Person

Name Type
id int
name varchar(255)
noteIdPicture int
noteIdAvatar int

Table: Note

Name Type
id int
pathFileName varchar(255)
  • Form:

    • Name: UploadAdvanced2
    • Table: Person
  • FormElements

    • Name: name

      • Type: text
      • Label: Name
    • Name: mypathFileNamePicture

      • Type: upload

      • Label: Picture

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}}
        slaveId={{SELECT id FROM Note WHERE id={{noteIdPicture}} LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        sqlAfter={{UPDATE Person SET noteIdPicture={{slaveId}} WHERE id={{id:R0}} LIMIT 1
        
    • Name: mypathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}}
        slaveId={{SELECT id FROM Note WHERE id={{noteIdAvatar}} LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        sqlAfter={{UPDATE Person SET noteIdAvatar={{slaveId}} WHERE id={{id:R0}} LIMIT 1
        

Typeahead: SQL

Table: Person

Name Type
id int
name varchar(255)
  • Form:
    • Name: PersonNameTypeahead
    • Table: Person
  • FormElements
    • Name: name
      • Type: text
      • Label: Name
      • Parameter: typeAheadSql = SELECT name FROM Person WHERE name LIKE ? OR firstName LIKE ? LIMIT 100

Typeahead: LDAP with additional values

Table: Person

Name Type
id int
name varchar(255)
firstname varchar(255)
email varchar(255)
  • Form:

    • Name: PersonNameTypeaheadSetNames

    • Table: Person

    • Parameter:

      ldapServer = directory.example.com
      ldapBaseDn = ou=Addressbook,dc=example,dc=com
      
  • FormElements

    • Name: email

      • Class: native

      • Type: text

      • Label: Email

      • Note: Name: {{cn:LE}}<br>Email: {{mail:LE}}

      • dynamicUpdate: checked

      • Parameter:

        # Typeahead
        typeAheadLdapSearch = (|(cn=*?*)(mail=*?*))
        typeAheadLdapValuePrintf ‘%s / %s’, cn, email
        typeAheadLdapIdPrintf  ‘%s’, email
        
        # dynamicUpdate: show note
        fillStoreLdap
        ldapSearch = (mail={{email::alnumx}})
        ldapAttributes = cn, email
        
    • Name: fillLdapValues

      • Class: action

      • Type: afterSave

      • Parameter:

        fillStoreLdap
        ldapSearch = (mail={{email::alnumx}})
        ldapAttributes = cn, email
        
        slaveId={{id:R0}}
        sqlUpdate={{ UPDATE Person AS p SET p.name='{{cn:L:alnumx:s}}' WHERE p.id={{slaveId}} LIMIT 1 }}
        

Import/merge form

The form copyFormFromExt copies a form from table ExtForm / ExtFormElement to Form / FormElement. The import/merge form:

  • offers a drop down list with all forms of ExtForm,
  • an input element for the new form name,
  • create new Form.id
  • copied FormElements get the new Form.id.
  • the copied form will be opened in the FormEditor.

Installation:

  • Play (do all sql statements on your QFQ database, e.g. via mysql <dbname> < copyFormFromExt.sql or phpMyAdmin) the file <ext_dir>/Classes/Sql/copyFormFromExt.sql.

  • Insert a link/button ‘Copy form from ExtForm’ to open the import/merge form. A good place is the list of all forms (see FormEditor). E.g.:

    10.head = {{'b|p:id={{pageAlias:T}}&form=copyFormFromExt|t:Copy form from ExtForm' AS _link }} ...
    

If there are several T3/QFQ instances and if forms should be imported frequently/easily, set up a one shot ‘import Forms from db xyz’ like:

10.sql = CREATE OR REPLACE table ExtForm SELECT * FROM <db xyz>.Form
20.sql = CREATE OR REPLACE table ExtFormElement SELECT * FROM <db xyz>.FormElement