Динамическое добавление/удаление полей форм в Django
Задача стояла следующая:
есть список сущностей (к примеру, упражнений), каждая из которых в форме выводится в виде нескольких полей ввода (к примеру, сеты и название упражнения), хочется иметь возможность яваскриптом добавлять/удалять упражнения, менять их положение.
Для этого были использованы формсеты (django.forms.formsets) и jquery на клиенте.
В теории всё так: в шаблон передаём формы из формсета (подводный камень тут — для правильной валидации нужно в шаблоне внутри формы нужно не забыть вписать скрытые инпуты, отвечающие за количество форм формсета на странице, для этого пишем в шаблоне {{ exercisesFormset.management_form }}).
В шаблоне у каждого упражнения присутствуют контролы для удаления/добавления и перемещения упражнений, после загрузки страницы ненужные контролы у каждого упражнения скрываются, а после, к примеру, добавления нового упражнения контролы перерисовываются.
Для удаления все поля ввода упражнения очищаются, после чего скрываются.
Это была теория, теперь практика. Ниже гольный код с комментариями.
Во вьюхе нужно добавить следующее:
from django.forms.formsets import formset_factory
...
class NameForm( forms.ModelForm ):
name = forms.CharField( max_length = 250 )
description = forms.CharField( widget = forms.Textarea, required = False )
class ExerciseNameForm( forms.Form ):
name = forms.ModelChoiceField( Exercise.objects.all() )
sets = forms.IntegerField( min_value = 1, max_value = 200 )
...
@login_required
def add( request, id ):
user = request.user
other = Other()
if request.method == "POST":
nameForm = NameForm( request.POST, instance = other )
exercisesFormsetClass = formset_factory( ExerciseNameForm )
exercisesFormset = exercisesFormsetClass( request.POST, prefix = "exercises" )
if nameForm.is_valid() and exercisesFormset.is_valid():
nameData = nameForm.cleaned_data
other.name = nameData["name"]
other.description = nameData["description"]
other.save()
for position, exerciseForm in enumerate( exercisesFormset.forms, start = 1 ):
exerciseData = exerciseForm.cleaned_data
exercise = Exercise()
exercise.exercise = exerciseData["name"]
exercise.other = other
exercise.sets = exerciseData["sets"]
exercise.position = position
exercise.save()
return HttpResponseRedirect( "/other/%s/" % other.id )
hide_exercises = request.POST["hide_exercises"]
else:
exercisesFormsetClass = formset_factory( ExerciseNameForm, extra = 1 )
nameForm = NameForm( instance = other )
exercisesFormset = exercisesFormsetClass( prefix = "exercises" )
hide_exercises = ""
return render_to_response( "add.html", { "nameForm": nameForm, "exercisesFormset": exercisesFormset, "hide_exercises": hide_exercises }, context_instance = RequestContext( request ) )
В шаблоне следующий код:
<script>
$(document).ready(function() {
fixTotalFormsNumber();
initiateControls();
hideDeletedExercises();
});
</script>
{% block content %}
<form action="" method="POST">
<div class="parent">
{% csrf_token %}
<ul>
{{ nameForm.as_ul }}
{{ exercisesFormset.management_form }}
</ul>
{% for exerciseForm in exercisesFormset.forms %}
<ul class="exercise-form-data">
{{ exerciseForm.as_ul }}
<li class="controls">
<span class="up clickable">вверх</span>
<span class="down clickable">вниз</span>
<span class="delete clickable">удалить</span>
<span class="add clickable">добавить</span>
</li>
</ul>
{% endfor %}
</div>
<input type="hidden" name="hide_exercises" value="{{ hide_exercises }}" />
<input type="submit" name="Добавить" />
</form>
{% endblock %}
В шаблоне также грузится яваскрипт:
/**
* При обновлении страницы лишние элементы со страницы пропадут, а количество форм для обработки на стороне сервера останется (фф запомнит)
*/
function fixTotalFormsNumber()
{
var inputNamePrefix = "exercises-",
$parent = $( ".parent" );
var count = $parent.find( "ul.exercise-form-data" ).length;
$parent.find( "input[name=" + inputNamePrefix + "TOTAL_FORMS]" ).val( count );
}
/**
* Инициируем контролы
*/
function initiateControls()
{
var $parent = $( ".parent" );
$parent.find( ".controls .add" ).live( "click", function( Event ) { controlClicked( Event, "add" ) } );
$parent.find( ".controls .delete" ).live( "click", function( Event ) { controlClicked( Event, "delete" ) } );
$parent.find( ".controls .up" ).live( "click", function( Event ) { controlClicked( Event, "up" ) } );
$parent.find( ".controls .down" ).live( "click", function( Event ) { controlClicked( Event, "down" ) } );
updateControlsVisibility();
}
/**
* Скрывает "удалённые" упражнения при перезагрузке страницы
*/
function hideDeletedExercises()
{
var $hideExercisesInput = $( "input[name=hide_exercises]" );
var hideByIndexes = $hideExercisesInput.val().split( "," );
if ( hideByIndexes.length )
{
var $rows = $( ".exercise-form-data" );
for ( var rowNum = 0, count = $rows.length; rowNum < count; rowNum ++ )
{
var $row = $( $rows[rowNum] );
if ( $.inArray( rowNum + "", hideByIndexes ) > -1 ) {
$row.hide();
}
}
}
}
function updateControlsVisibility()
{
var $rows = $( ".exercise-form-data:visible" );
for ( var rowNum = 0, count = $rows.length; rowNum < count; rowNum ++ )
{
var $row = $( $rows[rowNum] );
if ( rowNum == 0 ) {
$row.find( ".up" ).hide();
}
else {
$row.find( ".up" ).show();
}
if ( rowNum == ( count - 1 ) )
{
$row.find( ".down" ).hide();
$row.find( ".add" ).show();
}
else
{
$row.find( ".down" ).show();
$row.find( ".add" ).hide();
}
if ( count == 1 ) {
$row.find( ".delete" ).hide();
}
else {
$row.find( ".delete" ).show();
}
}
}
/**
* При клике на контрол
* @param Event
* @param string ControlType
*/
function controlClicked( Event, ControlType )
{
if ( ControlType == "add" )
{
addNewExerciseInputs();
updateControlsVisibility();
}
else
{
var $el = $( Event.target ),
inputSelectors = [ "select[name$=name]", "input[name$=sets]" ],
rowSelector = ".exercise-form-data";
var $currentRow = $el.parents( rowSelector );
if ( ControlType == "delete" )
{
deleteExercise( $currentRow, inputSelectors );
updateControlsVisibility();
}
else
{
rowSelector += ":visible"
var $changeToRow;
if ( ControlType == "up" ) {
$changeToRow = $currentRow.prevAll( rowSelector );
}
else {
$changeToRow = $currentRow.nextAll( rowSelector )
}
if ( $changeToRow.length ) {
$changeToRow = $changeToRow.eq( 0 );
}
changeExerciseInputValues( $currentRow, $changeToRow, inputSelectors );
}
}
/**
* Добавляем поля ввода для ещё одного упражнения
*/
function addNewExerciseInputs()
{
var inputNamePrefix = "exercises-",
$parent = $( ".parent" ),
// в качестве шаболна используем последний ul для ввода упражнения
$sourceExercise = $parent.find( "ul.exercise-form-data:last" );
// если нашли ul и он правильный
if ( $sourceExercise.length && $sourceExercise.find( "input[name^=" + inputNamePrefix + "]" ).length )
{
// для вычисления номера в шаблоне
var re1 = new RegExp( inputNamePrefix + "(\\d*)-" );
// для замены номеров
var re2 = new RegExp( inputNamePrefix + "\\d*(-[^'\"]*)", "g" );
// копируем html шаблона
var newExerciseHtml = $sourceExercise.html();
// получаем номер
var sourceRowNum = parseInt( re1.exec( newExerciseHtml )[1] );
var newRowNum = sourceRowNum + 1;
// в html нового ul вписываем правильный номер
newExerciseHtml = newExerciseHtml.replace( re2, inputNamePrefix + newRowNum + "$1" );
$parent.append( "<ul class='exercise-form-data'>" + newExerciseHtml + "</ul>" );
// увеличиваем количество форм для обработки на стороне сервера
var $managerNums = $parent.find( "input[name=" + inputNamePrefix + "TOTAL_FORMS]" );
$managerNums.val( parseInt( $managerNums.val() ) + 1 );
}
}
/**
* "Удаляем" упражнение
*/
function deleteExercise( $Row, InputSelectors )
{
if ( $Row.length )
{
// с пустым параметром просто сбросит все значения
exerciseData( $Row, InputSelectors, "set" );
$Row.hide();
// для скрывания этого упражнения при перезагрузке страницы
var $hideExercisesInput = $( "input[name=hide_exercises]" );
$hideExercisesInput.val( $hideExercisesInput.val() + $Row.index( "ul.exercise-form-data" ) + "," );
}
}
/**
* Меняем два упражнения местами
* @param jquery $Row1
* @param jquery $Row2
* @param Array InputSelectors
*/
function changeExerciseInputValues( $Row1, $Row2, InputSelectors )
{
if ( $Row1.length && $Row2.length )
{
var row1Values = exerciseData( $Row1, InputSelectors, "get" ),
row2Values = exerciseData( $Row2, InputSelectors, "get" );
exerciseData( $Row1, InputSelectors, "set", row2Values );
exerciseData( $Row2, InputSelectors, "set", row1Values );
}
}
/**
* Геттер/сеттер всех инпутов упражнения (так сложно - чтобы сеттер всегда понимал формат геттера)
* @param jquery $Row
* @param Array InputSelectors
* @param string Type - get/set
* @param Array Values - для сеттера массив значений
*/
function exerciseData( $Row, InputSelectors, Type, Values )
{
Type = Type == "set" ? "set" : "get";
Values = Values ? Values : [];
// Пробегаемся по всем нужным элементам ввода
// если элемент элемент типа select, то получаем/заполняем выбранный option, для инпутов - то же самое с value
for ( var i = 0, count = InputSelectors.length; i < count; i ++ )
{
var inputSelector = InputSelectors[i];
var $input = $Row.find( inputSelector );
if ( $input.is( "select" ) )
{
if ( Type == "get" ) {
Values[i] = $input[0].selectedIndex;
}
else {
$input[0].selectedIndex = i in Values ? Values[i] : 0;
}
}
else
{
if ( Type == "get" ) {
Values[i] = $input.val();
}
else {
$input.val( i in Values ? Values[i] : "" );
}
}
}
return Values;
}
}
Альтернативы:
http://stackoverflow.com/questions/801354/django-equivalent-of-phps-form-value-array-associative-array.
http://eikke.com/django-generic-ajax-form-validation/ - для валидации формы ajax'ом.
LEAVE A COMMENT
Для отправки комментария вам необходимо авторизоваться.