📜 ⬆️ ⬇️

Improving forms or Web-Eight-Zero-Hundred-Three

So, we chose a browser instead of a standalone fat client. The user really wants to enter data. However, the usual forms are bad. Because:



Data and HTML code are mixed.
There is no convenient widget with a drop-down list and a search for the beginning of the name.
There is no convenient built-in tool to load data into forms.
There is no convenient way to send form data via AJAX.
Etc.
')
What do you want?

Fully unlink HTML-code forms from the data.
Have the ability to preload forms.
Have the ability to flexibly customize the form.
Have a convenient drop-down list with a search for the beginning of the name.
Unlink the form controls from its environment.
And so on.

Let's start from the end. What should be the dropdown list? Certainly not as standard SELECT!
First, it must use the directory. That is, a set of pairs ["key", "value"]. At the same time, the string “value” is shown to the user, and a “key” is sent to the server.
Secondly, it must be connected to INPUT along with a reference book, when entering characters, to show a list, run back and forth and get out when leaving focus with INPUT.

To create a drop-down list (we will call it a dropdown for short), we use jQuery. What good is jQuery? At least this:

1. a convenient callback function when the DOM structure of the document is already loaded (and you can spoil it), and any images may not be loaded yet:

$(document).ready(function(){

// ... DOM

});


2. convenient search for the necessary items by ID

$('#some_id')

3. Convenient jQuery binding of any DOM element

$(some_DOM_element)

4. convenient binding of any events

$(li).hover( function() { this.style.fontWeight = 'bold'; }, function() { this.style.fontWeight = 'normal'; } )

This call binds 2 events to the DOM li element: when the cursor moves over an element and when moving away.
In between these events, the DOM element gets a bold font (because this is exactly what it points to in the event handler)

5. convenient jQuery object methods that are universally applied to individual DOM elements or their sets

.show() show
.hide() hide
.val() get control value
.val(new_value) set the value of the control

And so on.

We will take advantage of jQuery to expand. For the basis of our dropdown, I took the autocomplete script, which, when searching in Google, shows similar queries. It's easy to google. The ready solution should work like this: when setting up a form, we should be able to bind the dropdown with a reference to the control like this:

$(form.some_element_name).dropdown( some_directory, options ) ;

some_directory is the reference (some JavaScript object about which later)

How to achieve this? Create your own new jQuery method, which is called above:

jQuery.fn.dropdown = function (data, options)
{
options = options || {};
options.data = (( typeof data == "object" ) && (data.constructor == Array)) ? data : null ;
options.key = options.key || "key" ;
options.value = options.value || "value" ;
options.matchCase = options.matchCase || 0;
options.list_size = options.list_size || 11;
options.containerClass = options.containerClass || "__dropdown_container_class" ;
options.selectedItemClass = options.selectedItemClass || "__dropdown_selected_item_class" ;
options.items = options.data ? clone_quicksort(options.data, options.value, options.matchCase ? less_than_compare : less_than_ignore_case_compare) : null ;
options.items_hash = options.items ? hash_array(options.items, options.key) : null ;
this .each( function () { var input = this ; new jQuery.dropdownConstructor(input, options); } );
return this ;
}


* This source code was highlighted with Source Code Highlighter .


I brought him in full. It is easy to see that this script persistently checks various options (if there are none at all, it creates them). Options are typed with default values. Finally, here is this line:

this.each( function() { var input = this; new jQuery.dropdownConstructor(input, options); } );

walks through the list of elements that the selector has selected, and for each one creates a new jQuery dropdown object using the dropdownConstructor constructor. Which contains all the necessary code and looks like this:

jQuery.dropdownConstructor = function (input, options)
{

var input = input; // DOM
this .control = input;
var $input = $(input); // jQuery , DOM ( jQuery )
var container = document .createElement( "div" ); // DIV list
var $container = $(container);
var list = document .createElement( "ul" );
var $list = $(list);
var active = false ;
var last_key_pressed = null ;
var timeout = null ;
var value_of_key = ""
var prev_truevalue, next_truevalue;
var that = this ;
input.dropdown = that;
input.truevalue = input.truevalue || null ;

container.appendChild(list); //
$container.hide().addClass(options.containerClass).css( "position" , "absolute" ); //
if ( options.width > 0 ) $container.css( "width" , options.width);
$( "body" ).append(container); set_truevalue(input.truevalue);


function postpone(func)
{
if (timeout) clearTimeout(timeout);
timeout = setTimeout( function (){ func(); }, 25);
}

function set_truevalue(new_truevalue)
{
if (!options.items_hash[new_truevalue])
{
input.truevalue = null ;
value_of_key = "" ;
$input.val( "" );
return ;
}
input.truevalue = new_truevalue;
if (input.truevalue) value_of_key = options.items_hash[input.truevalue][options.value]; else value_of_key = "" ;
$input.val( value_of_key );
}

input.update_control = function () { set_truevalue(that.control.truevalue); }

function activate()
{
if (active) return ;
var pos = element_position(input);
var W = (options.width > 0) ? options.width : $input.width();
$container.css( { width: parseInt(W) + "px" , left: pos.x + "px" , top: (pos.y + input.offsetHeight) + "px" } ).show();
var index = binary_search(options.items, value_of_key, options.value, options.matchCase ? less_than_compare : less_than_ignore_case_compare);
build_list(index - (options.list_size >> 1), index);
active = true ;
}

function deactivate()
{
if (!active) return ;
$container.hide();
active = false ;
}

function move_up()
{
...
}

function move_down()
{
...
}

function select_text_part(start, end)
{
...
}


* This source code was highlighted with Source Code Highlighter .


And off we go.

I think the code is quite understandable, but still. truevalue is the key. This is the attribute of the control that stores the key value (not shown to the user). set_truevalue () sets the key and synchronizes the display value. active () shows a drop-down list, deactive () hides it. move_up (), move_down () - move up and down in the list. select_text_part () - selection of part of the text.

This is how we attach to events inside the constructor:

$input.keydown( function (e)
{
last_key_pressed = e.keyCode;
switch (e.keyCode)
{
case 38: e.preventDefault(); move_up(); break ;
case 40: e.preventDefault(); move_down(); break ;
case 13: case 9: select_text_part(); deactivate(); break ;
default : postpone(on_change); break ;
}
}).focus( function (){
activate(); select_text_part();
}).blur( function (){
deactivate();
}).mousedown( function (e) { if (!active && !input.disabled) { e.preventDefault(); $input.focus(); activate(); } } );

* This source code was highlighted with Source Code Highlighter .


Now let's talk about reference books. The reference book is a JavaScript array of similar objects (with the same set of fields). For example, " [ { key:'m', value:'male' }, { key: 'f', value: 'female' } ] ". By default, the key field contains the key, and the value field contains the value. However, this can always be changed.

Our dropdown needs a directory sorted in ascending value (that is, in ascending visible value, not key). And usually with ignoring the case of letters (when the user enters the beginning of the name on the keyboard in most cases he still has CAPS LOCK or not). Therefore, this array must be sorted and for this it was necessary to write a quick sort on JavaScript and binary search. After all, the user can enter and part of the name. I will not give sorting, only binary search for an example:

function less_than_compare(val1, val2)
{
return val1 < val2;
}

function binary_search(items,value,key,compare)
{
key = key || "key" ;
compare = compare || less_than_compare;
var l = -1;
var r = items.length;
while ( true )
{
var m = (r - l) >> 1;
if (m == 0) return r;
m += l;
if ( compare(items[m][key],value) )
l = m;
else if ( compare(value, items[m][key]) )
r = m;
else
return m;
}
}

* This source code was highlighted with Source Code Highlighter .


So, after stubborn coding, our wonderful dropdown has earned:

image www.picamatic.com/show/2009/05/12/06/35/3618111_557x236.png

Now we start to deal with the forms. The form is divided into three parts: HTML-code, JavaScript-code settings and a list of directories that the form requires to work. Our server-side form script will return all these parts on request and, in addition, it must accept data and return processing errors. The server script in the "/foo.js" file looks like this:

function receive_request()
{
if (request._command == "jason" ) return "/joo.js,/boo.js" ;

if (request._command == "html" ) return "\
<tr><th> <td><INPUT name=user_name>\
<tr><th> <td><INPUT name=sex truevalue='u'>\
<tr><th> <td><INPUT name=birth_country>\
<tr><th> <td><INPUT name=country>\
"
;

if (request._command == "code" ) return "\
enterAsTab(form, 1);\
$(form.country).dropdown( jason('/boo.js') );\
$(form.birth_country).dropdown( jason('/boo.js') );\
$(form.sex).dropdown( jason('/joo.js') );\
"
;

if (request._command == "post" )
{
var error = "" ;
if (request.user_name.length == 0) error += "user_name: \r" ;
if (request.user_name.length < 5) error += "user_name: 5 \r" ;
if (request.birth_country.length == 0) error += "country: \r" ;
if (request.country.length == 0) error += "country: \r" ;
if (error) return error;

// .. ..

return "OKAY" ;
}


return "" ;
}

* This source code was highlighted with Source Code Highlighter .


Why JavaScript is used on the server is explained here: habrahabr.ru/blogs/development/48842

HTML is just controls, without the surrounding "<form />". A slightly tricky form configuration code requires two parameters: a DOM element of the form (form) and a function that returns a loaded directory of its url (jason ()).

So, we want all of this on a page to be asynchronously loaded and used. There are two considerations. First, directories belong to the entire page, not just one form. Different forms can work with the same directories. Secondly, the form can be used only after the full and successful loading of all its components.

Actually, then I wrote an asynchronous loader that caches the loaded data and allows hierarchically load the investments into each other (in order, for example, to completely remove all active form loads). Then on top of this is the form uploader, which, upon receiving a list of reference books, asks them to load and waits for them to appear.

So, let's say we have at hand (uploaded to the page) all the necessary form components: HTML, javascript and reference books are ready. The next step is to create a form instance, which is what the user sees. Here is the code that creates an instance of a form called form_name in the DOM element container. The instance is named instance_name:

activate: function (instance_name, form_name, container)
{
if (!form_loader.ready(form_name)) return false ;
var form = document .createElement( "form" ); \\ , DOM- form
var jason = function (url) { return jason_loader.data(url); } \\ , jason()
container.innerHTML = '' ;
container.appendChild(form);
form.innerHTML = "<table class='form' >" + form_loader.html(form_name) + "</table>" ;
try
{
eval(form_loader.code(form_name));
}
catch (e)
{
form_loader.forms[form_name].error = ' <i>' + form_name + '</i><BR>' + e.message;
return false ;
}
this .actor[instance_name] = { type: 'form' , name: form_name, form_elem: form, container_elem: container, disabled: [], post: null };
return true ;
}



* This source code was highlighted with Source Code Highlighter .


I think the idea is clear.

And finally: dessert. So for what all this code was created. Here’s what the bootcode for the HTML page that uses this mechanism looks like:

$( document ).ready( function (){

register_form( "foo" , "/foo.js" ); // foo /foo.js
register_form( "voo" , "/voo.js" ); //
await(load_callback); //
});

function load_callback(form_name)
{
if (form_name) //
$( '#report' ).html( $( '#report' ).html() + '<BR>' + form_loader.error(form_name) );
else
{
// !
}

}

* This source code was highlighted with Source Code Highlighter .


Is not it - very succinctly? This is how the form is used.

Create an instance in the DOM element with id = 'foo_form':

director.activate( 'foo_instance' , "foo" , $( '#foo_form' )[0]);



Load data into a form instance:

director.fill( "foo_instance" , {user_name: 'John Smith' , country: 'RO' } );



And finally, for the sake of what all this is needed - sending data:

director.submit( "foo_instance" , foo_submit);


The foo_submit function might look something like this:

function foo_submit(instance_name, result, error)
{
if (result)
$( '#report' ).html( ' ' + instance_name + ' ' );
else if (error)
$( '#report' ).html( ' ' + instance_name + ' <BR>' + error);
else
$( '#report' ).html( ' ' + instance_name);
}


* This source code was highlighted with Source Code Highlighter .



The submit () method can get a set of data errors from the server script and show them automatically. And the next time you submit, it will remove and make a complete disable form.

image

In general, you can do almost anything with a form: show, clear, load data, make a complete disable and back enable, send to the server and wait for a successful send.
Finally, remove the form instance from the page. Again, we can easily create multiple instances of the same form on the page.
For example, it is possible in such forms to show unsuccessful send to the server.

Here is a solution.


Waiting for comments and suggestions. If I find the time, I’ll syntax highlight JavaScript code (why not at hand on Habré ??).

Source: https://habr.com/ru/post/59413/


All Articles