Pro jQuery

Pro jQuery

Адам Фриман

Обработка ответа сервера

Все что нам осталось – это сделать что-нибудь полезное с данными, которые мы получили от сервера. Для этой главы я собираюсь использовать простую таблицу. Вы научитесь создавать богатый пользовательский интерфейс при помощи 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: Удаление выделения элемента, связанного с ошибкой
или RSS канал: Что новенького на smarly.net