Статьи

Сократите код пользовательского интерфейса Javascript с помощью форм Django

Как правило, я пытаюсь реализовать функцию динамического пользовательского интерфейса без JavaScript на первом проходе. Затем я добавляю немного javascript, чтобы сделать его более отзывчивым. Всякий раз, когда я отклоняюсь от этой практики, я неизбежно заканчиваю тем, что заново открывал, почему я начал делать это в первую очередь. В какой-то момент я пришел в ужас от количества написанного мной javascript и растерялся из-за того, сколько времени требуется для его модификации. Короче говоря, все усложнилось.


Все должно быть сделано как можно проще, но не проще.

Альберт Эйнштейн

Вот пример только на днях. В требованиях указывалось поведение формы, которого у нас уже не было на сайте. В списке записей вакансий должна быть ссылка на вакансию. При нажатии на ссылку открывается новая форма, которая должна быть успешно заполнена до фактического закрытия задания. Когда форма будет успешно опубликована, скройте форму и выделите ее серым цветом. Вот рабочий процесс:

Нажатие на близкую работу фактически только показывает реальную форму


Попытка опубликовать эту форму может вызвать исключения проверки.

Когда форма успешно опубликована, она исчезает и строка становится серой.

Конечно, это не слишком сложный пример, но есть несколько движущихся частей. Тем не менее, я получил почти 100 строк кода JavaScript, даже с логикой проверки для внутренней формы на стороне сервера.

$(document).ready(function() {

 // hitting the initial close link, GETs the form from the back-end and shows it
 $(".jobs-record-list a.toggle-job-close").click(function (){

  var link = $(this);
  var record = link.parents(".record");
  var form_box = record.find(".job-close-form-box");
  var job_link = record.find(".title a");

  $.ajax({
   url: link.attr("href") + "?ajax",
   type: "GET",
   success: function (data) {
    form_box.html(data);
    bind_job_close_form(form_box);
   },
   error: function (jqXHR, textStatus, errorThrown) {
    // fallback to closing the job from the job overview page
    window.location = form.attr(job_link.attr("href"));
   }
  });

  link.remove();
  return false;
 });

 // process the inner form submit to actually close the job, then put the open link back on the page
 function bind_job_close_form(form_box) {

  var form = form_box.find("form");
  var parentRow = form.parents(".record");

  form.submit(function () {

   $.ajax({
    url: form.attr("action") + "?ajax",
    type: "POST",
    data: form.serialize(),
    success: function (data) {
     form_box.hide();
     parentRow.find(".content").toggleClass("record-disabled");
     parentRow.find(".block").toggleClass("record-disabled");
     var link = form.find("a");
     link.text(link.text() == "close job" ? "open job" : "close job");
    },
    error: function (jqXHR, textStatus, errorThrown) {
     form_box.html(jqXHR.response);
     bind_job_close_form(form_box); // rebind event handler
    }
   });

   return false;
  });
 }

        // if the page is refresh after the inner form is shown, re-show it
 $("form.toggle-job-open-closed").submit(function () {
  var form = $(this);
  var questions = form.find(".questions");
  if (questions.is(":hidden")) {
   questions.show();
   return false;
  }
  return true;
 });

 // re-opening a job, POST to the server then gray out the row
 $("a.toggle-job-open").submit(function () {

  var form = $(this);
  var parentRow = form.parents(".record");

  $.ajax({
   url: form.attr("action") + "?ajax",
   data: form.serialize(),
   type: form.attr("method"),
   success: function (data) {
    parentRow.find(".content").toggleClass("record-disabled");
    parentRow.find(".block").toggleClass("record-disabled");    
   }
  });

  return false;
 });
});

На данном этапе моя форма довольно проста, но у меня есть точка зрения, которая пытается многое сделать; обрабатывая как Ajax, так и не-Javascript версии запросов на этой странице. Я также делаю сумасшествие, чтобы передать текущую форму в области видимости сессии. Это так, что если пользователь обновляется, форма остается в том же состоянии. Вот только форма:

class OpenCloseJobForm(Html5Form):

    candidate = forms.ChoiceField(required=True)

    def __init__(self, *args, **kwargs):

        kwargs = helpers.copy_to_self(self, "job", kwargs)
        super(OpenCloseJobForm, self).__init__(*args, **kwargs)

        self.fields["candidate"].label = "Who did you hire for this job?"
        self.fields["candidate"].required = False
        self.fields["candidate"].choices = [("", "-------------")]
        candidates = Candidate.objects.filter(job=self.job)
        self.fields["candidate"].choices += [("Candidates", [(c.id, c) for c in candidates] + [("other", "Someone else" if candidates else "Candidate not in %s" % settings.SITE_NAME)])]
        self.fields["candidate"].choices += [("none", "No hire made")]

    def clean_candidate(self):
        candidate = self.cleaned_data.get("candidate")
        if not candidate:
            raise forms.ValidationError("This field is required.")
        return candidate

    def save(self):

        choice = self.data.get("candidate")

        if not choice:
            return None

        Hire.objects.filter(job=self.job).delete()

        if choice == "none":
            return None

        candidate_id = choice
        candidate = None if choice == "other" else Candidate.objects.get(id=candidate_id)

        hire = Hire(job=self.job, candidate=candidate)
        hire.save()
        return hire

На этом этапе я решаю провести рефакторинг, чтобы переместить как можно больше логики пользовательского интерфейса в саму форму и уменьшить количество javsacript. Я заканчиваю с виджетом и формой, которая реализует внутреннюю форму отображения / скрытия. Эта часть увеличивается с 40 строк до 90 строк.

class ToggleLinkWidget(widgets.HiddenInput):

    button_value = None

    def __init__(self, attrs=None, check_test=bool, button_value="toggle"):
        super(ToggleLinkWidget, self).__init__(attrs)
        self.button_value = button_value

    def render(self, name, value, attrs=None):
        html = super(ToggleLinkWidget, self).render(name, value, attrs)
        link = ""
        if not value:
            button_value = self.button_value
            return "<input class='button-link' type='submit' name='%(name)s' value='%(button_value)s'>" % locals()
        return html

class OpenCloseJobForm(Html5Form):

    job_id = fields.CharField(required=True, widget=widgets.HiddenInput())
    expanded = fields.BooleanField(required=False, initial=False, widget=ToggleLinkWidget(button_value="close"))
    candidate = fields.ChoiceField(required=True)

    def __init__(self, *args, **kwargs):

        super(OpenCloseJobForm, self).__init__(*args, **kwargs)
        self.job = Job.objects.get(id=self.data.get("job_id"))

        self.fields["candidate"].label = "Who did you hire for this job?"
        self.fields["candidate"].required = False
        self.fields["candidate"].choices = [("", "-------------")]
        candidates = Candidate.objects.filter(job=self.job)
        self.fields["candidate"].choices += [("Candidates", [(c.id, c) for c in candidates] + [("other", "Someone else" if candidates else "Candidate not in %s" % settings.SITE_NAME)])]
        self.fields["candidate"].choices += [("none", "No hire made")]

        # the close button is NOT toggled on; hide thother fields
        if not self.is_expanded():
            del self.fields["candidate"]

        # don't show validation errors when first expanding the form
        if not self.data.get("inner-submit"):
            self._errors = {}

    def is_valid(self):
        if self.data.get("inner-submit"):
            return super(OpenCloseJobForm, self).is_valid()
        return False

    def is_expanded(self):
        return self.data and self.data.get("expanded") == "close"

    def as_ul(self):
        html = super(OpenCloseJobForm, self).as_ul()
        if self.is_expanded():
            button = "<input type='submit' name='inner-submit' value='Close Job'>"
            return mark_safe("<div class='expanded-box'>%(html)s %(button)s</div>" % locals())
        return html

    def clean_candidate(self):
        candidate = self.cleaned_data.get("candidate")
        if not candidate:
            raise forms.ValidationError("This field is required.")
        return candidate

    def save(self, request):

        self.job.is_open = not self.job.is_open

        if self.job.is_open:
            Hire.objects.filter(job=self.job).delete()
            messages.success(request, "%s was set to open and is publicly available." % self.job)
        else:
            messages.success(request, "%s was closed and is no longer available publicly." % self.job)

        self.job.save()

        choice = self.data.get("candidate")

        if not choice:
            return None

        Hire.objects.filter(job=self.job).delete()

        if choice == "none":
            return None

        candidate_id = choice
        candidate = None if choice == "other" else Candidate.objects.get(id=candidate_id)

        hire = Hire(job=self.job, candidate=candidate)
        hire.save()
        return hire

But look at the javascript, which has shrunk from 90 lines to 40.

$(document).ready(function() {

 function submit_handler() {

  var button = $(this);
  var record = button.parents(".record");
  var form = record.find(".job-prompt-hire-form");
  var form_box = form.parents(".form-box");
  var job_link = record.find(".title a");

  var form_data = form.serialize();
  // also record which input button was pressed
  if (button.is(".button-link")) {
   form_data = form_data += "&expanded=close";
  } else {
   form_data = form_data += "&inner-submit=Close Job";
  }

  $.ajax({
   url: form.attr("action") + "?ajax",
   data: form_data,
   type: form.attr("method"),
   success: function (data) {
    form_box.html(data);
    if (!data) {
     record.find(".content").toggleClass("record-disabled");
     record.find(".block").toggleClass("record-disabled");
    }
   },
   error: function (jqXHR, textStatus, errorThrown) {
    // fallback to closing the job from the job overview page
    window.location = form.attr(job_link.attr("href"));
   }
  });

  return false;
 }

 // jQuery does not support live submit handling in IE
 // also, need to record actual pressed element
 $(".jobs-record-list .job-prompt-hire-form .button-link").live("click", submit_handler);
 $(".jobs-record-list .job-prompt-hire-form input[type=submit]").live("click", submit_handler);

});

Interestingly, the total line count has remained around the same. This suggests that for my style, the code density of Python and jQuery is about the same. Personally, I find the python code to be more maintainable. In particular, I very much like that I’m minimizing the number of times jQuery is changing the DOM, and rebinding events. All of the state transitions are happening in the form. As a side effect, the show/hide behavior is totally functional without javascript enabled.


Source: http://bitkickers.blogspot.com/2011/10/reduce-javascript-ui-code-with-django.html