Статьи

Взлом: AngularJS

Это сообщение от Ангела Тодорова из блога Infragistics.

В этом сообщении я хотел бы рассказать о двух вещах:

  1. Как я расширил AngularJS для поддержки журналов транзакций (подробных различий) для массивов. Я разработал проект на GitHub: https://github.com/attodorov/angular.js
  2. Как я создал собственную угловую директиву для сетки Ignite UI.

В результате вы увидите, как IgniteUI Grid очень хорошо интегрируется с Angular, а также поддерживает полное двустороннее связывание данных. 

В прикрепленном проекте есть все, что вам нужно, чтобы увидеть его в действии, просто распакуйте архив и запустите angular.html

Давайте начнем с некоторой справочной информации о том, как AngularJS выполняет обновления между моделями и представлениями. Я бы не хотел подробно останавливаться на этом, потому что это уже подробно описано в следующем посте SO:

http://stackoverflow.com/questions/9682092/databinding-in-angularjs

Это очень элегантный подход, гораздо более элегантный, чем смена слушателей. К сожалению, если вы привязываетесь к двумерному массиву объектов и изменяете свойство некоторого объекта, у вас нет возможности узнать, что именно было изменено. Angular дает вам старый массив и новый массив, и у вас есть выбор, чтобы снова их различать, но это не оптимально. Вот почему я изменил функции equals и $ digest в Angular для поддержки передачи подробной информации о том, что на самом деле изменилось. Таким образом, если какое-либо свойство изменяется где-то в моем массиве, и я ограничил его сеткой пользовательского интерфейса Ignite, используя Angular, я могу перерисовать только ячейку, которая привязана к этому винту, без повторного применения равных ко всему массиву , Вот фрагмент того, как это работает, обратите внимание на аргумент «diff»:

scope.$watch(attrs.source, function (value, last, currentValue, diff) {
	if (Array.isArray(diff)) {
		for (var i = 0; i < diff.length; i++) {
			// update cell values
			if (!diff[i].txlog) {
				continue;
			}
			for (var j = 0; j < diff[i].txlog.length; j++) {
				// get the td
				var colIndex = $("#" + element.attr("id") + "_" + diff[i].txlog[j]["key"]).index();
				var key = scope[attrs.source][diff[i].index][attrs.primarykey];
				var td = element.find("tr[data-id='" + key + "']").children().get(colIndex);
				$(td).html(diff[i].txlog[j]["newVal"]);
			}
		}
	}
}, true);

Аргумент «diff» — это массив объектов, имеющих следующую структуру:

{index: , txlog: []}

Где каждая передача имеет следующий формат:

{key: , oldVal: , newVal: }

Давайте пройдемся по нашей странице шаг за шагом. Сначала нам нужно обратиться к модифицированной библиотеке AngularJS (вы можете получить ее из моего разветвленного проекта; я включил папку «build», которая содержит объединенные и минимизированные ресурсы). Вам также необходимо сослаться на скрипт контроллеров , а также на скрипт, содержащий пользовательские директивы Ignite UI. 

<head>
	<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" type="text/css"></link>
	<link rel="stylesheet" href="css/themes/infragistics/infragistics.theme.css" type="text/css"></link>
	<link rel="stylesheet" href="css/structure/infragistics.css" type="text/css"></link>
	<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
	<script type="text/javascript" src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
	<script type="text/javascript" src="js/angular.min.js"></script>
	<script type="text/javascript" src="js/infragistics.core.js"></script>
	<script type="text/javascript" src="js/infragistics.lob.js"></script>
	<script src="js/controllers.js"></script>
	<script src="js/igniteui-directives.js"></script>
	<title>Angular.JS and Ignite UI</title>
</head>

Сценарий controllers.js имеет действительно простой формат: он просто определяет NorthwindCtrl, который мы позже укажем в HTML, следуя соглашениям Angular, и возвращает источник данных в формате JSON — сам источник данных жестко закодирован в реализации контроллера, для простоты.

function NorthwindCtrl($scope) {
    $scope.northwind = [ ];

}

Следующее, что мы сделаем, это установим директиву ng-controller для тела, которое в нашем случае будет NorthwindCtrl:

<body ng-controller="NorthwindCtrl">

Обратите внимание, что нам также необходимо установить директиву ng-app для тега html, чтобы Angular мог правильно анализировать пользовательские и зарезервированные директивы и шаблоны:

Затем мы объявляем нашу пользовательскую директиву ignitegrid следующим образом:

id="grid1" data-source="northwind" data-height="400px" data-updating="true" data-primarykey="ProductID">
</ignitegrid>

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

<table id="simpletable">
	<tbody>
		<tr ng-repeat="product in northwind">
			<td>{{product.ProductID}}</td>
			<td><input type="text" ng-model="product.ProductName"></input></td>
			<td>{{product.QuantityPerUnit}}</td>
			<td>{{product.UnitPrice}}</td>
		 </tr>
    </tbody>
</table>

Вы можете увидеть, что способ определения сетки зажигания — использование полностью настраиваемого тега HTML, а также некоторых атрибутов data- *, которые отражают параметры виджета. Давайте посмотрим на нашу реализацию пользовательских директив, а также на то, как мы распространяем обновления от обновления сетки до модели Angular:

angular.module('igApp', []).directive('ignitegrid', function () {
	return {
		restrict: "E",
		template: "<table></table>",
		replace: true,

Объявление restrict: «E» означает, что наша пользовательская директива является пользовательским элементом, а не атрибутом. Мы также хотим, чтобы тег по умолчанию и его дочернее содержимое были заменены пустым тегом, где будет инициализирован фактический виджет сетки.

Далее, наиболее важной частью нашей пользовательской директивы является функция «ссылка», которая генерирует параметры инициализации и затем создает виджет igGrid для элемента. Он также обрабатывает iggridupdatingeditrowended клиентское событие, где мы устанавливаем новые значения в модели Angular, а затем вызываем $ apply, чтобы сработали все связанные слушатели и обновился DOM, который связывается с этими объектами. «северный ветер», на который мы ссылаемся из области видимости, в основном определяется в нашем контроллере Angular.

angular.module('igApp', []).directive('ignitegrid', function () {
	return {
		restrict: "E",
		template: "<table></table>",
		replace: true,
		link: function (scope, element, attrs) {
			//initialize an ignite UI grid on element, using attrs.igniteuiModel as the data source
			if (!scope.hasOwnProperty(attrs.source)) {
				throw new Error("The data source (dataSource) does not exist in the current context");
			}
			var ds = scope[attrs.source], opts = {};
			if (typeof (attrs.autogeneratecolumns) !== "undefined") {
				opts.autoGenerateColumns = attrs.autogeneratecolumns === "true" ? true : false;
			}
			opts.dataSource = ds;
			//opts.columns = scope.columns;
			if (attrs.height) {
				opts.height = attrs.height;
			}
			if (attrs.updating && attrs.updating === "true") {
				if (!Array.isArray(opts.features)) {
					opts.features = [];
				}
				opts.autoCommit = true;
				opts.features.push({name: "Updating"});
				// we need to listen for updates in order to support two-way databinding in the grid
				// ensure that we don't handle it twice or create any recursion 
				// the same can/should be done for cell editing, adding and deleting rows
				element.on("iggridupdatingeditrowended", function (e, args) {
					// we need the data source from the scope, but without triggering $digest for the grid itself
					// note that there may be other subscribers
					var ds = angular.element(element).scope()[attrs.source];
					for (var i = 0; i < ds.length; i++) {
						if (ds[i][attrs.primarykey] === args.rowID) {
							ds[i] = args.values;
							break;
						}
					}
					// force $apply
					angular.element(element).scope().$apply();
				});
			}
			if (attrs.primarykey) {
				opts.primaryKey = attrs.primarykey;
			}
			element.igGrid(opts);
			// watch for changes from the data source to the view
			scope.$watch(attrs.source, function (value, last, currentValue, diff) {
				if (Array.isArray(diff)) {
					for (var i = 0; i < diff.length; i++) {
						// update cell values
						if (!diff[i].txlog) {
							continue;
						}
						for (var j = 0; j < diff[i].txlog.length; j++) {
							// get the td
							var colIndex = $("#" + element.attr("id") + "_" + diff[i].txlog[j]["key"]).index();
							var key = scope[attrs.source][diff[i].index][attrs.primarykey];
							var td = element.find("tr[data-id='" + key + "']").children().get(colIndex);
							$(td).html(diff[i].txlog[j]["newVal"]);
						}
					}
				}
			}, true);
		}
	}
});

Подводя итог — используя подход, описанный в этом сообщении в блоге, и разветвленную версию AngularJS, вы получаете плавную поддержку двустороннего обновления и удобную интеграцию с сеткой интерфейса Ignite с использованием пользовательских директив. Вы можете применить тот же подход к другим виджетам Ignite UI.