Обработка ответа сервера
Все что нам осталось – это сделать что-нибудь полезное с данными, которые мы получили от сервера. Для этой главы я собираюсь использовать простую таблицу. Вы научитесь создавать богатый пользовательский интерфейс при помощи jQuery UI в следующей части этой книги, и я не хочу делать вручную то, что я гораздо элегантнее могу сделать с UI виджетами. Вы можете увидеть готовый результат на рисунке 16-6.
Рисунок 16-6: Отображение общего заказа

В листинге 16-9 показан целый документ, который поддерживает это улучшение.
Листинг 16-9: Обработка ответа сервера
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
<script src="jquery-1.7.js" type="text/javascript"></script>
<script src="jquery.tmpl.js" type="text/javascript"></script>
<script src="jquery.validate.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css" />
<style type="text/css">
a.arrowButton {
background-image: url(leftarrows.png);
float: left;
margin-top: 15px;
display: block;
width: 50px;
height: 50px;
}
#right {
background-image: url(rightarrows.png);
}
h1 {
min-width: 0px;
width: 95%;
}
#oblock {
float: left;
display: inline;
border: thin black solid;
}
#orderForm {
margin-left: auto;
margin-right: auto;
width: 885px;
}
#bbox {
clear: left;
}
#error {
color: red;
border: medium solid red;
padding: 4px;
margin: auto;
width: 300px;
text-align: center;
margin-bottom: 5px;
}
.invalidElem {
border: medium solid red;
}
#errorSummary {
border: thick solid red;
color: red;
width: 350px;
margin: auto;
padding: 4px;
margin-bottom: 5px;
}
#popup {
text-align: center;
position: absolute;
top: 100px;
left: 0px;
width: 100%;
height: 1px;
overflow: visible;
visibility: visible;
display: block;
}
#popupContent {
color: white;
background-color: black;
font-size: 14px;
font-weight: bold;
margin-left: -75px;
position: absolute;
top: -55px;
left: 50%;
width: 150px;
height: 60px;
padding-top: 10px;
z-index: 2;
}
#summary {
text-align: center;
}
table {
border-collapse: collapse;
border: medium solid black;
font-size: 18px;
margin: auto;
margin-bottom: 5px;
}
th {
text-align: left;
}
th, td {
padding: 2px;
}
tr > td:nth-child(1) {
text-align: left;
}
tr > td:nth-child(2) {
text-align: right;
}
</style>
<script type="text/javascript">
$(document).ready(function () {
$('<div id="popup"><div id="popupContent"><img src="progress.gif"'
+ 'alt="progress"/><div>Placing Order</div></div></div>')
.appendTo('body');
$.ajaxSetup({
timeout: 5000,
converters: {
"text html": function (data) { return $(data); }
}
})
$(document).ajaxError(function (e, jqxhr, settings, errorMsg) {
$('#error').remove();
var msg = "An error occurred. Please try again"
if (errorMsg == "timeout") {
msg = "The request timed out. Please try again"
} else if (jqxhr.status == 404) {
msg = "The file could not be found";
}
$('<div id=error/>').text(msg).insertAfter('h1');
}).ajaxSuccess(function () {
$('#error').remove();
})
$('#row2, #row3, #popup, #summaryForm').hide();
var flowerReq = $.get("flowers.html", function (data) {
var elems = data.filter('div').addClass("dcell");
elems.slice(0, 3).appendTo('#row1');
elems.slice(3).appendTo("#row2");
})
var jsonReq = $.getJSON("additionalflowers.json", function (data) {
$('#flowerTmpl').tmpl(data).appendTo("#row3");
})
var plurals = {
astor: "Astors", daffodil: "Daffodils", rose: "Roses",
peony: "Peonies", primula: "Primulas", snowdrop: "Snowdrops",
carnation: "Carnations", lily: "Lillies", orchid: "Orchids"
}
$('<div id=errorSummary>Please correct the following errors:</div>')
.append('<ul id="errorsList"></ul>').hide().insertAfter('h1');
$('#orderForm').validate({
highlight: function (element, errorClass) {
$(element).addClass("invalidElem");
},
unhighlight: function (element, errorClass) {
$(element).removeClass("invalidElem");
},
errorContainer: '#errorSummary',
errorLabelContainer: '#errorsList',
wrapper: 'li',
errorElement: "div"
});
$.when(flowerReq, jsonReq).then(function () {
$('input').each(function (index, elem) {
$(elem).rules("add", {
required: true,
min: 0,
digits: true,
remote: {
url: "http://node.jacquisflowershop.com/stockcheck",
type: "post",
global: false
},
messages: {
required: "Please enter a number for " + plurals[elem.name],
digits: "Please enter a number for " + plurals[elem.name],
min: "Please enter a positive number for " + plurals[elem.name]
}
})
}).change(function (e) {
if ($('#orderForm').validate().element($(e.target))) {
var total = 0;
$('input').each(function (index, elem) {
total += Number($(elem).val());
});
$('#total').text(total);
}
});
});
$('#orderForm button').click(function (e) {
e.preventDefault();
var formData = $('#orderForm').serialize();
$('body *').not('#popup, #popup *').css("opacity", 0.5);
$('input').attr("disabled", "disabled");
$('#popup').show();
$.ajax({
url: "http://node.jacquisflowershop.com/order",
type: "post",
data: formData,
dataType: "json",
dataFilter: function (data, dataType) {
data = $.parseJSON(data);
var cleanData = {
totalItems: data.totalItems,
totalPrice: data.totalPrice
};
delete data.totalPrice; delete data.totalItems;
cleanData.products = [];
for (prop in data) {
cleanData.products.push({
name: plurals[prop],
quantity: data[prop]
})
}
return cleanData;
},
converters: { "text json": function (data) { return data; } },
success: function (data) {
processServerResponse(data);
},
complete: function () {
$('body *').not('#popup, #popup *').css("opacity", 1);
$('input').removeAttr("disabled");
$('#popup').hide();
}
})
})
function processServerResponse(data) {
if (data.products.length > 0) {
$('body > *:not(h1)').hide();
$('#summaryForm').show();
$('#productRowTmpl').tmpl(data.products).appendTo('tbody');
$('#totalitems').text(data.totalItems);
$('#totalprice').text(data.totalPrice);
} else {
var elem = $('input').get(0);
var err = new Object();
err[elem.name] = "No products selected";
$('#orderForm').validate().showErrors(err);
$(elem).removeClass("invalidElem");
}
}
$('<a id=left></a><a id=right></a>').prependTo('#orderForm')
.addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
$('#right').appendTo('#orderForm');
var total = $('#buttonDiv')
.prepend("<div>Total Items: <span id=total>0</span></div>")
.css({ clear: "both", padding: "5px" });
$('<div id=bbox />').appendTo("body").append(total);
function handleArrowMouse(e) {
var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
$(this).css("background-position", propValue);
}
function handleArrowPress(e) {
var elemSequence = ["row1", "row2", "row3"];
var visibleRow = $('div.drow:visible');
var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"), elemSequence);
var targetRowIndex;
if (e.target.id == "left") {
targetRowIndex = visibleRowIndex - 1;
if (targetRowIndex < 0) { targetRowIndex = elemSequence.length - 1 };
} else {
targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
}
visibleRow.fadeOut("fast", function () {
$('#' + elemSequence[targetRowIndex]).fadeIn("fast")
});
}
});
</script>
<script id="flowerTmpl" type="text/x-jquery-tmpl">
<div class="dcell">
<img src="${product}.png" />
<label for="${product}">${name}:</label>
<input name="${product}" value="0" />
</div>
</script>
<script id="productRowTmpl" type="text/x-jquery-tmpl">
<tr>
<td>${name}</td>
<td>${quantity}</td>
</tr>
</script>
</head>
<body>
<h1>Jacqui's Flower Shop</h1>
<form id="orderForm" method="post" action="http://node.jacquisflowershop.com/order">
<div id="oblock">
<div class="dtable">
<div id="row1" class="drow"></div>
<div id="row2" class="drow"></div>
<div id="row3" class="drow"></div>
</div>
</div>
<div id="buttonDiv">
<button type="submit">Place Order</button>
</div>
</form>
<form id="summaryForm" method="post" action="">
<div id="summary">
<h3>Order Summary</h3>
<table border="1">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th>Number of Items:</th>
<td id="totalitems"></td>
</tr>
<tr>
<th>Total Price:</th>
<td id="totalprice"></td>
</tr>
</tfoot>
</table>
<div id="buttonDiv2">
<button type="submit">Complete Order</button>
</div>
</div>
</form>
</body>
</html>
Я поэтапно разъясню те изменения, которые я сделал.
Добавление новой формы
Первая вещь, которую я сделал, – это добавил новую форму в статическую HTML часть документа:
<form id="summaryForm" method="post" action="">
<div id="summary">
<h3>Order Summary</h3>
<table border="1">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th>Number of Items:</th>
<td id="totalitems"></td>
</tr>
<tr>
<th>Total Price:</th>
<td id="totalprice"></td>
</tr>
</tfoot>
</table>
<div id="buttonDiv2">
<button type="submit">Complete Order</button>
</div>
</div>
</form>
Это сердце нового функционала. Когда пользователь отправляет выбранную им продукцию на сервер, таблица (table
) в этой форме (form
) будет использоваться для отображения данных, которые вы получаете обратно из Ajax запроса.
Совет
В предыдущих примерах я использовал селектор
$('form')
, но поскольку в документе сейчас две формы, я переключился на использование значения атрибутаid
элементаform
.
Я не хочу отображать новую форму немедленно, поэтому я добавил ее в скрипте в список элементов, которые я прячу:
$('#row2, #row3, #popup, #summaryForm').hide();
И как вы можете себе сейчас представить, когда появляются новые элементы, для них используются новые CSS стили:
#summary {
text-align: center;
}
table {
border-collapse: collapse;
border: medium solid black;
font-size: 18px;
margin: auto;
margin-bottom: 5px;
}
th {
text-align: left;
}
th, td {
padding: 2px;
}
tr > td:nth-child(1) {
text-align: left;
}
tr > td:nth-child(2) {
text-align: right;
}
Эти стили обеспечивают отображение таблицы в середине окна браузера, и текст в различных столбцах выровнен по правильному краю.
Завершение Ajax запроса
Следующим шагом является завершение Ajax запроса:
$('#orderForm button').click(function (e) {
e.preventDefault();
var formData = $('#orderForm').serialize();
$('body *').not('#popup, #popup *').css("opacity", 0.5);
$('input').attr("disabled", "disabled");
$('#popup').show();
$.ajax({
url: "http://node.jacquisflowershop.com/order",
type: "post",
data: formData,
dataType: "json",
dataFilter: function (data, dataType) {
data = $.parseJSON(data);
var cleanData = {
totalItems: data.totalItems,
totalPrice: data.totalPrice
};
delete data.totalPrice; delete data.totalItems;
cleanData.products = [];
for (prop in data) {
cleanData.products.push({
name: plurals[prop],
quantity: data[prop]
})
}
return cleanData;
},
converters: { "text json": function (data) { return data; } },
success: function (data) {
processServerResponse(data);
},
complete: function () {
$('body *').not('#popup, #popup *').css("opacity", 1);
$('input').removeAttr("disabled");
$('#popup').hide();
}
})
})
Я удалил явную задержку в функции complete
и добавил в запрос настройки dataFilter
, converters
и success
.
Я использую настройку dataFilter
, чтобы добавить функцию, которая трансформирует данные JSON, которые я получаю от сервера, в что-то более полезное. Сервер отправляет мне строку JSON:
{"astor":"4","daffodil":"1","snowdrop":"2","totalItems":7,"totalPrice":"15.93"}
Я разбиваю данные JSON и реструктурирую их, чтобы получить вот это:
{"totalItems":7,
"totalPrice":"15.93",
"products":[{"name":"Astors","quantity":"4"},
{"name":"Daffodils","quantity":"1"},
{"name":"Snowdrops","quantity":"2"}]
}
В этом формате есть два преимущества. Во-первых, он лучше подходит для использования с шаблонами данных, потому что я могу передать свойство products
методу tmpl
. Во-вторых, я могу проверить, выбрал ли пользователь какой-либо элемент с products.length
. Хотя это и минимальные преимущества, но я хотел интегрировать как можно больше возможностей, описанных в предыдущих главах. Обратите внимание, что я также изменил название продукта (например, orchid
) на множественное число (Orchids
).
После того, как я разбил данные JSON и получил JavaScript объект (при помощи метода parseJSON
, о котором я расскажу в главе 33), я хочу отключить встроенный конвертер, который будет пытаться сделать то же самое. С этой целью я определил пользовательский конвертер для JSON, который просто передает данные без модификаций:
converters: {"text json": function(data) { return data;}}
Обработка данных
Для настройки success
во время вызова метода ajax
я указал функцию processServerResponse
, которая определяется следующим образом:
function processServerResponse(data) {
if (data.products.length > 0) {
$('body > *:not(h1)').hide();
$('#summaryForm').show();
$('#productRowTmpl').tmpl(data.products).appendTo('tbody');
$('#totalitems').text(data.totalItems);
$('#totalprice').text(data.totalPrice);
} else {
var elem = $('input').get(0);
var err = new Object();
err[elem.name] = "No products selected";
$('#orderForm').validate().showErrors(err);
$(elem).removeClass("invalidElem");
}
}
Если данные с сервера содержат информацию о продукции, тогда я прячу все элементы в документе, которые не должны быть показаны (включая оригинальный элемент form
и дополнения, которые я сделал в скрипте), и показываю новый элемент form
. Я заполняю table
, используя следующий шаблон данных:
<script id="productRowTmpl" type="text/x-jquery-tmpl">
<tr><td>${name}</td><td>${quantity}</td></tr>
</script>
Это очень простой шаблон, который создает строку таблицы для каждого выбранного элемента продукции. И наконец, при помощи метода text
я заполняю содержанием ячейки, которые отображают итоговую сумму и общее число единиц продукции:
$('#totalitems').text(data.totalItems);
$('#totalprice').text(data.totalPrice);
Если же данные, полученные от сервера, не содержат никакой информации о продукции (это обозначает, что пользователь оставил значения всех элементов input
равными нулю), я делаю нечто совсем другое. Во-первых, я выбираю первый из элементов input
:
var elem = $('input').get(0);
Затем я создаю объект, который содержит свойство, чье имя является значением имени элемента input
и чье значение является сообщением к пользователю. Затем для элемента form
я вызываю метод validate
и для результата вызываю метод showErrors
:
var err = new Object();
err[elem.name] = "No products selected";
$('#orderForm').validate().showErrors(err);
Это позволяет мне вручную вставить ошибку в систему валидации и получить преимущества над структурой и форматированием, которые я создал раньше. Я могу добавить имя элемента, и таким образом плагин валидации может выделить место, где произошла ошибка, хотя это выглядит и не идеально, как видно по рисунку 16-7.
Рисунок 16-7: Отображение ошибки выборки

Я вывожу на экран общее сообщение, а выделяется только один input
элемент. Чтобы решить эту проблему, я удаляю класс, который плагин валидации использует для выделения элементов:
$(elem).removeClass("invalidElem");
Результат можно увидеть на рисунке 16-8.
Рисунок 16-8: Удаление выделения элемента, связанного с ошибкой
