This JavaScript™ powered multifunctional class allows you to create an end-user-manipulatable HTML form that requires an unknown number of text-input form fields. When you don’t want to limit your visitors to a minimal number of input options, but don’t want your page display to be bloated with a large number of empty input boxes, this is the utility you need. Since the version 2.2 release, an end-user can even cut, copy, and paste entire sections of an HTML form to/from multi-clip clipboards. We use this class in our MasterColorPicker package, and you can see useful working integrated examples there (once for the “MyPalette” sub-application, and again for the “Color Filter” extension).
What do you use this for? When you have to ask someone to list each of something you request, and they may need one (1) form field, or they may need fifty (50), or anywhere in between or even more! Consider asking about someone’s children and grandchildren. In one family there may be two children, each with one grandchild. What about a large family with 7 children, each with an average of 5 grandchildren? Do you create a form that accommodates the large family, forcing the small family to wade through your web page like trying to navigate swamp water? And what about the family with 12 children and 78 grandchildren? Of course there are functions that say “click this button to create a new form field”. Clicking once or twice may be OK. I've seen that approach used to upload files, and it annoyed me when I had to upload two dozen, simply because it broke the “flow” selecting each file; and that was all point and click. When you have to actually type in a phrase, then switch to the mouse to click, then back to the keyboard... This is much easier.
The best way to understand this class is to exemplify it, so we created a working demo. You should also view the highlighted demo source code to understand what is happening, why and how. You can download the JavaScript file from our download page.
Using this class is simple and strait-forward, requiring only a few JavaScript™ event handlers be added to the form field(s) you want duplicated (cloned). But it is no simple class: not only can it clone the form field that the event handlers are attached to, but any associated form fields, their labels, and any other DOM nodes that go along. We will attempt to explain its use in terms that a non-programmer can understand, so you can incorporate this class successfully in your site, but a basic amount of HTML understanding and a minimum amount of knowledge on using JavaScript™ in your HTML pages is required. It has a list of options that allow you to fine-tune the way it works making it incredibly versatile. Plus it allows plug-in callback functions that allow it infinite flexibility. It intelligently updates the name associated with each form field cloned based on each individual name; if your given name is incompatible with this class’ capabilities, your custom plugin can handle it. And it does all this without any additional markup to your HTML.
When adding a new form-field or group of form-fields (using the popNewField()
method),
the FormFieldGenie can create (clone) one based on what already exists in the form (more on that below),
or you can explicitly give it a form-field or group of form-fields to clone.
You may define an explicit DOM node (form-field or group of form-fields) to clone
when creating an instance of the FormFieldGenie; for example:
myGenie=new SoftMoon.WebWare.FormFieldGenie({……my options……}, ……my DOM node to clone……);
myGenie.popNewField(……)
After creating an instance of the FormFieldGenie, you may also
set the instance.clone (myGenie.clone
in these examples) to the explicit DOM node
(form-field or group of form-fields) you want to clone (if any).
An example of passing an explicit node to clone:
myGenie=new SoftMoon.WebWare.FormFieldGenie({……my options……});
myGenie.clone= ……my DOM node to clone……
myGenie.popNewField(………)
The publicly accessible properties of a FormFieldGenie instance are:
- .clone
- .clipboard
- .defaults
- .tabbedOut
- .catchKey ← this is not defined natively, but is recognized by the
catchTab
method of an instance.
The publicly accessible methods of a FormFieldGenie instance are:
popNewField(fieldNodeGroup, opts)
- returns
true
if a newfieldNodeGroup
is ‘popped’ orfalse
if not. deleteField(fieldNodeGroup, opts)
- returns
true
if thefieldNodeGroup
was deleted,false
if not. cutField(fieldNodeGroup, opts)
- returns
true
if thefieldNodeGroup
was deleted,false
if not.fieldNodeGroup
will always be copied to the clipboard. copyField(fieldNodeGroup, opts)
- returns
null
.fieldNodeGroup
will always be copied to the clipboard. pasteField(fieldNodeGroup, opts)
- returns
false
if the clipboard clip is empty,true
if it is pasted.
Note you can paste two different ways using three different methods:
- paste over an existing
fieldNodeGroup
usingpasteField(fieldNodeGroup, {clip: %%your-clip-reference%%})
- insert a new
fieldNodeGroup
usingpasteField(fieldNodeGroup, {doso: 'insert', clip: %%your-clip-reference%%})
- insert a new
fieldNodeGroup
usingpopNewField(fieldNodeGroup, {doso: 'paste', clip: %%your-clip-reference%%})
The difference between popNewField()
and pasteField()
is that pasteField()
will return false
if the clip is empty,
while popNewField()
will simply pop a new “blank” clone if the clip is empty.
After creating an instance of the FormFieldGenie,
the clipboard Object may be accessed through instance.clipboard
;
each clipboard Object property may contain an individual clip (DOM node).
The first parameter passed to this class’ methods is the entire
DOM node you want auto-regenerated, deleted, cut, copied, inserted before, or pasted-over.
A quick look at the source code of the demonstration example will help clarify this.
If you only need the form field itself repeated, pass the value of this
to the function.
Remember, in an event handler for form fields, the keyword this
refers to the
DOM node of the form-field itself.
If the field has a <label>
tag (or any other tag) around it that you want cloned also,
simply pass this.parentNode
.
If you want to clone a whole group of fields and associated text,
simply repeat parentNode
in a chain as many times as needed to move up the DOM tree.
From there, the class makes one requirement: there must be a tag enclosing the DOM nodes you want cloned. It is into this enclosing tag the clones are added. First, let’s be clear that any HTML “element” tags may be used; it does not matter to the class. For the sake of discussion, we will refer to these these as follows:
- the <input> and <textarea> tags themselves are referred to as the
fieldNode
(only<input> type='text'
,type='password'
, ortype='file'
). ThesefieldNode
s need to have the proper event handlers attached. - the entire DOM node you want cloned is referred to as the
fieldNodeGroup
- the tag enclosing the cloned DOM nodes is referred to as the
fieldNodeGroupFieldset
The fieldNodeGroupFieldset
may contain any other HTML besides the clones;
but the clones will always be added to the fieldNodeGroupFieldset
directly following the last fieldNodeGroup
.
See also the groupClass
and groupTag
options described below.
Beyond this, you are free to develop your page the way you see fit.
Any time your keyboard cursor is focused on a fieldNode
and then you leave it
by pressing the tab key or by clicking the mouse button somewhere else,
the class looks at all the fieldNodeGroup
s within fieldNodeGroupFieldset
.
If any are empty (except for the last one), by default it deletes them
(you can change this default action with options; see below).
It then looks within the last fieldNodeGroup
and
by default finds the first text-based <input> tag
(only <input> type='text'
, type='password'
, or type='file'
),
or if there are no <input>s, by default finds the first <textarea>,
then looks to see if anything was typed in (or similar for type='file'
).
If this fieldNode
has been filled by the user,
the class ‘pops’ a new fieldNodeGroup
.
If the end-user pressed the tab key from within a fieldNode
(to exit it)
and a new fieldNodeGroup
was popped,
the cursor is focused within a specified fieldNode
of the new fieldNodeGroup
.
If nothing was popped, the cursor is passed onto the next form field as usual.
If the end-user clicked out of the fieldNode
, the usual action is taken depending on where you click,
whether or not a new fieldNodeGroup
was popped.
That sums up the main framework of the class. Its true power begins to shine when you look at how it updates the names associated with each form field cloned, and how the available options you can pass to the class can modify the default actions of the main framework and the process of updating the field names. This gives it the ability to work with virtually any HTML form of any complexity, and the corresponding supporting server side script.
When this class updates a name, it considers several things. Is the name indexed, or not?
(i.e. name[index]
or name[index][index]
etc.) If it is indexed,
is the last index virtual? (i.e. name[]
or name[index][]
etc.)
Remember in PHP, when multiple form fields return the same virtual indexed name
they are automatically placed into a sequentially numbered array.
If the field’s name is not indexed, this function considers
whether the last characters are numeric digits, or not.
(i.e. name
or name123
etc.)
Remember when a form returns multiple fields of the same name without using virtual indexes,
the last one overwrites all the others.
It should be mentioned here that if these built-in processes for updating the name don’t suit your needs,
you may write your own plug-in that can do anything you want!
We’ll look at that in the section on options, below.
The one other thing to be considered when updating the name is a special case involving the
value of checkboxes and radio-buttons; this involves reasons similar to those involved when
updating virtually indexed names, and we will take a look at this further down...
Virtually Indexed Names
Lets look at these first. There is generally no need to do anything to these names.
The server side script (PHP) handles them as they should be.
The exception is when you use checkboxes and radio buttons.
Take a look at the source code of the demonstration example and also at the test-demo itself.
Notice how the nickname section uses checkboxes within the fieldNodeGroup
to be cloned.
If we simply clone these and leave their names unchanged, the resulting data no longer co-ordinates with
its associate data. You get a sequential array of all the checkboxes checked by the user,
but there is no way to tell which array element corresponds to which nickname.
So checkboxes and radio buttons with virtually indexed names are instead treated as fully indexed names.
Fully Indexed Names
The class looks for the last index in the name that is numeric and increases it by one for each new field popped. Note this also applies to checkboxes and radio buttons with virtually indexed names (see the paragraph above). If no index is found to be numeric, the name is left unchanged.
Non-indexed Names
Non-indexed names without digits at the end are simply left alone. This is generally only useful for radio buttons or <button> tags. Names that end in a sequence of digits have that sequence numerically increased by one for each new field popped. Pay close attention to the favorite cars example and the data it produces when the form is submitted. Note how only the last sequence of digits is increased, and the rest are left alone.
Special case: checkboxes and radio buttons with “indexed” values
In some cases the need arises for the value of these form fields to be updated, not the name.
The favorite pets section in the demonstration exemplifies this.
If the value is a number within square brackets
(i.e. [0]
or [1]
or [1756]
etc.)
it can be updated instead of the name.
Furthermore, you can control what style name (virtual, indexed, non-indexed, or combinations)
allows updating the value; while allowing flexibility, this becomes truly invaluable when
fieldNodeGroup
s are nested within each other.
Before we look at how to control this using options, let’s look at options in general.
Passing Options to the FormFieldGenie
Passing options to this class is simple: use an object with properties named accordingly.
Any properties may be included or not, making it simple to control any option without worrying about the others.
If you are not so familiar with JavaScript™, pay close attention to the
demonstration example and how to define an object right in the event handler text.
You can also define your object in a separate <script></script>
,
then simply pass it to the class by name. If you don’t understand how to do this,
please refer to a good book on JavaScript™ programming.
Options may be defined or passed at three levels: global defaults, instance defaults, and when calling a method.
The class’ methods, popNewField()
, cutField()
, copyField()
,
pasteField()
, and deleteField()
, only take two parameters
(the fieldNodeGroup
and the options object). By setting the option values of the
defaults
property of FormFieldGenie
, you can control the option defaults globally.
When creating an instance (myGenie = new SoftMoon.WebWare.FormFieldGenie( { ...options... } )
)
you can override the global defaults, and when calling instance methods you can override the instance defaults.
The options object may contain these following properties: (and more, but they will be ignored)
- maxTotal: number
- maximum number of clones (
fieldNodeGroups
) in thefieldNodeGroupFieldset
. Note that the default value is 100. There is no minTotal, as this would impose restrictions on how thefieldNodeGroupFieldset
is structured. To retain a minimum total, use a custom function fordumpEmpties
(see below) which can make this distinction. - indxTier: number
- Number of index “tiers” to ignore at the end of a name; used to skip over tier(s) when updating names.
climbTiers
must be true (see directly below). Example: name →myField[4][3][2]
whenindxTier=2
the FormFieldGenie updates/modifies the index that contains “4”. Note the Genie looks for the next numeric index, so note the following example: name →myField[4][subsection][3][2]
whenindxTier=2
the FormFieldGenie updates/modifies the index that contains “4”. - climbTiers:
true | false
- Check all levels of indices for a numeric value (
true
is default), or only the last? - updateValue:
"all" | "non-implicit" | "non-indexed" | "indexed" | "implicit"
- Controls the application of updating values instead of names in
checkbox and radio-button fields that have values formatted similar to
"[0]"
Any other passed condition yields no values updated. No passed condition yields the default action"all"
.
===↓ examples ↓===all name name[string] name[number] name[] non-implicit name name[string] name[number] non-indexed name indexed name[string] name[number] implicit name[]
===↑ examples only show final indices or lack of; indexed names may have additional indices ↑=== - focusField: number
- ========= this applies to pasteField() and popNewField() only =========
Pass the field number (counted from zero)
of the text/filename field you want the cursor focused on,
if the user pressed the tab key or
opts.focus=true
, when popping or when pasting a new fieldNodeGroup. - focus:
true | false
- ========= this applies to pasteField() and popNewField() only =========
If
true
, thefocusField
(see above) will receive focus, whether or not the tab key was pressed. Iffalse
, thefocusField
will not receive focus when the tab key is pressed. If no value is passed, then the tab key will cause thefocusField
to receive focus when popping a newfieldNodeGroup
. - dumpEmpties:
true | false | function(empty_fieldNodeGroupInQuestion, deleteFlag) { …your custom code makes the distinction… }
- remove emptied fields on the fly?
========= this applies to
deleteField()
andpopNewField()
only, and not when inserting or pasting ========= If a function is supplied, it should returntrue | false | null
and ifnull
is returned, the function should remove the field itself. If you usedeleteField()
, thefieldNodeGroup
will be removed even ifdumpEmpties===false
; however, ifdumpEmpties
is a function, it will be called with the value ofdeleteFlag=true
and its return value (true|false
) will be respected. - checkForEmpty:
"all" | "one" | "some"
- ========= this applies to
deleteField()
andpopNewField()
only, and not when inserting or pasting ========= If set, the corresponding text/filename fields in thefieldNodeGroup
will be checked. By default only the first one is checked. If'one'
or'some'
, thecheckField
option should be used also. If'some'
, each of the firstcheckField
number of fields will be checked. - checkField: number
- ========= this applies to
deleteField()
andpopNewField()
only, and not when inserting or pasting ========= Used in conjunction withcheckForEmpty
Pass the field number (counted from zero) of the field or fields you want checked for being “empty” when popping. IfcheckForEmpty='some'
the each of the first number of fields will be checked. - updateName:
function(field, indxOffset, fieldNodeGroupFieldset, params) { …your plugin code… }
- Pass a plugin callback function to handle the process of updating each name.
The function will be passed each individual form DOM object
(<input> or <textarea> or <select> or <button>)
one at a time in the
field
variable. TheindxOffset
variable contains the numerical positional offset of the newfield
compared to thefield
passed. The function should pass back a string of the new name, ornull
. If a string is returned, the name of the DOM object will be set to that value; no need for your function to alter the name directly, unless returningnull
. Ifnull
is returned, the usual process of updating the name continues. The callback function may do anything it needs including partial updating the name directly (to be continued by the usual process), and/or updating the value, and/or updating the parentNode text, and/or whatever you can imagine... - cbParams: variable
- This will be passed through to the
updateName()
plugin callback function as the fourth variable (params), and to theisActiveField()
†,cloneCustomizer()
‡,eventRegistrar()
‡ andgroupCusomizer()
‡ plugin callback functions as the †second or ‡third. It may be any type as required by your plugin callback functions, but if they share you may want to use an object with separate properties. - isActiveField:
function(fieldNode, params) { …your customizing code… }
- This can replace the standard function to check if a form field is currently active or not;
i.e. is it disabled, or is it even displayed at all?
You may add/subtract your own rules, perhaps checking the status of another element.
Inactive elements will not be considered when deciding to pop a new fieldNodeGroup or dump an empty one.
The
params
variable is the user’s call-back parameters (seecbParams
above in this list of user-options). Your function should returntrue|false
. - cloneCustomizer:
function(fieldNodeGroup, pasteOver, params) { …your customizing code… }
- If there is something special you want to do to each
fieldNodeGroup
cloned, you may pass a function to handle that. All field names will have been updated, but the node will not yet have been added to the document. The passed variablepasteOver
will betrue | false | 'paste-over'
—true
if pasting and inserting,'paste-over'
if pasting over an existingfieldNodeGroup
(the old existing one will be discarded). Theparams
variable is the user’s call-back parameters (seecbParams
above in this list of user-options). This function is called only when a newfieldNodeGroup
is being popped or pasted over. - eventRegistrar:
function(fieldNodeGroup, pasteOver, params) { …your customizing code… }
- While HTML attributes including event handlers are cloned when a DOM node is cloned,
DOM level 2 (and similar for MSIE) event handlers are not cloned.
If you need event handlers registered for any elements in your cloned
fieldNodeGroup
, you must do them “by hand” through this function. The function will be passed thefieldNodeGroup
after it has been added to the document. SeecloneCustomizer
(above) for info onpasteOver
andparams
. This function is called only when a newfieldNodeGroup
is being popped or pasted over. - groupCusomizer:
function(fieldNodeGroupFieldset, pasteOver, params) { …your customizing code… }
- This is called when a new
fieldNodeGroup
is being popped, pasted, or when afieldNodeGroup
is deleted or was empty and has been dumped. It is called from asetTimeout
function, so the DOM will be fully updated. Use it to do any final customizing. Note it is passed the wholefieldNodeGroupFieldset
node containing allfieldNodeGroup
s including the new one after it has been added to the document, not simply the newly cloned group. SeecloneCustomizer
(above) for info onpasteOver
andparams
. - doso:
true | 'insert' | 'paste'
- ========= this applies to popNewField() and pasteField() only =========
If you pass (Boolean)
true
when using popNewField(), a newfieldNodeGroup
will be popped at the end of thefieldNodeGroupFieldset
regardless of whether the lastfieldNodeGroup
is empty; but not exceedingmaxTotal
. EmptyfieldNodeGroup
s may be removed as usual. EmptyfieldNodeGroup
s will not be automatically removed if"insert"
when using popNewField(). If you pass"insert"
or"paste"
when using popNewField(), a new field will be popped and inserted before the passedfieldNodeGroup
, regardless of whether the last field is empty; but not exceedingmaxTotal
.
WithpopNewField()
, “insert” inserts an empty fieldNodeGroup.
WithpasteField()
, “insert” inserts the selected clip.
WithpopNewField()
, “paste” inserts the selected clip. - addTo:
true
- ========= this applies to popNewField() only =========
If you pass
opts.addto=true
, then the value that would be passed intopopNewField()
asfieldNodeGroup
will be instead considered thefieldNodeGroupFieldset
. This will allow you to add a newfieldNodeGroup
to an emptyfieldNodeGroupFieldset
but only if •theGenie.clone
is set; •oropts.doso='paste'
while the clipboard has contents. Passingopts.addto=true
acts similar as passingopts.doso=true
in that it will always pop a new field (unless as noted above thefieldNodeGroupFieldset
is empty and there is no clone and no paste). Note thatpasteField()
withopts.doso='insert'
internally calls callspopNewField()
, and this option may then take effect. - clip:
Object-member-identifier === ( instanceof Number || String.match( /^[_a-z][_a-z0-9]*$/i ) )
- ( a.k.a. %%your-clip-reference%% )
This is a reference to the member of the clipboard object associated with an instance of the FormFieldGenie. Each FormFieldGenie instance has its own clipboard, and each clipboard can hold an “unlimited” number of clips (limited by the machine). You may copy, cut and paste into/from any clip. - groupClass: string or RegExp
- If you supply a
groupClass
then the Genie will only consider childnodes of thefieldNodeGroupFieldset
that have a matching CSS class to befieldNodeGroup
s. - groupTag: string
- If you supply a
groupTag
then the Genie will only consider childnodes of thefieldNodeGroupFieldset
that are matching DOM nodes to befieldNodeGroup
s.
Most of these options do not need much explanation, especially if you study the demonstration example. A few do, so we will touch on them here.
By using the groupClass
and/or groupTag
, you may include other tags in your
fieldNodeGroupFieldset
, including form-inputs or tags with form-inputs as children,
that are not to be duplicated, automatically cleared, or otherwise messed-with by the Genie.
When you want to use a plugin function to update names, you may write one that accepts
a set of parameters, and then pass different parameters to your plugin for different form fields.
There is an included plugin with the demonstration example and the downloadable source code that
shows how this works.
The supplied standard demo plugin can accept two different parameters (passed in the one object).
You may create your own “order” and pass it through to this plugin, should you choose.
If you understand Perl compatible regular expressions, and can understand how this simple plugin functions,
then you could also pass a custom RegExp to the plugin and have even greater control over how it updates the name.
We chose using the first index, because this works well with this plugin’s logic.
Pay attention to the fact that the logic requires matching a single character,
then the “incremental word” follows. Another example of a naming style that would work
with this function’s logic is name_first
name_second
using the RegExp /_([a-z]+)$/
or name_first[]
using /_([a-z]+)\[/