Here is the problem I'm trying to solve: generating a form from a variable set of widgets where the exact widgets and their ordering is directed by the data, namely schema. The first approach I've taken looks like (omitting the unnecessary details):
controller.js:
angular.module('app').controller(function($scope) {
$scope.data = {
actions: [{
name: 'Action1',
base: 'nova.create_server',
baseInput: {
flavorId: {
title: 'Flavor Id',
type: 'string'
},
imageId: {
title: 'Image Id',
type: 'string'
}
},
input: [''],
output: [{
type: 'string',
value: ''
}, {
type: 'dictionary',
value: {
key1: '',
key2: ''
}
}, {
type: 'list',
value: ['', '']
}]
}]
};
$scope.schema = {
action: [{
name: 'name',
type: 'string',
}, {
name: 'base',
type: 'string',
}, {
name: 'baseInput',
type: 'frozendict',
}, {
name: 'input',
type: 'list',
}, {
name: 'output',
type: 'varlist',
}
]
};
})
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<div ng-repeat="spec in schema.action" ng-class="{'right-column': $even && isAtomic(spec.type), 'left-column': $odd && isAtomic(spec.type)}">
<typed-field></typed-field>
<div class="clearfix" ng-show="$even"></div>
</div>
</collapsible-panel>
directives.js
.directive('typedField', function($http, $templateCache, $compile) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + scope.spec.type + '.html',
{cache: $templateCache}).success(function(templateContent) {
element.replaceWith($compile(templateContent)(scope));
});
}
}
})
Among the templates located inside '/fields/' the simplest possible template for the string-type field is
<div class="form-group">
<label>{$ spec.title || makeTitle(spec.name) $}</label>
<input type="text" class="form-control" ng-model="item[spec.name]">
</div>
This approach works for - all the widgets are rendered, model bindings work, but on once I type a single letter inside of these widgets, the scope changes and the widgets are redrawn, causing:
* focus losing
* some time delay effective meaning poor performance.
Trying to overcome this drawback, I've rewritten my app in the following way:
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<action></action>
</div>
directives.js
.directive('typedField', function($http, $templateCache, idGenerator, $compile) {
return {
restrict: 'E',
scope: true,
compile: function ($element, $attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + $attrs.type + '.html',
{cache: $templateCache}).success(function (templateContent) {
$element.replaceWith(templateContent);
});
return function (scope, element, attrs) {
scope.title = $attrs.title;
scope.type = $attrs.type;
scope.name = $attrs.name;
}
}
}
})
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
return function(scope, element, attrs) {
}
}
}
})
Instead of getting schema from scope, I'm providing it via dependency injection into compile phase of a directive (which is run only the first time - which seemed quite the thing I needed to avoid repetitive full redraw of widgets). But now instead of nicely looking widgets (as before) I get raw html with data bindings not evaluated at all. I guess that I'm doing something wrong, but fail to graps how should I correctly use the compile function to avoid performance issues. Could you please give an advice on what should be fixed?
Finally I have found out what should be returned from the directive's compile function in that case
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
var linkFns = [];
tElement.children().each(function(tElem) {
linkFns.push($compile(tElem));
});
return function(scope) {
linkFns.forEach(function(linkFn) {
linkFn(scope);
});
}
}
}
})
What actually $compile does is calling the 'compile' function of each directive it encounters - thus calling $compile on current directive's template lead to infinite recursion, but calling it for each child of this directive worked fine.
Related
I need to display some hierarchical data stored in a json object in an html table. I tried with the following code from jsfiddle: http://jsfiddle.net/mrajcok/vGUsu/
//HTML
<div ng-controller="MyCtrl">
<my-table rows='rows'></my-table>
</div>
//javascript
var myApp = angular.module('myApp', []);
myApp.directive('myTable', function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
var html = '<table>';
angular.forEach(scope[attrs.rows], function (row, index) {
html += '<tr><td>' + row.name + '</td></tr>';
if ('subrows' in row) {
angular.forEach(row.subrows, function (subrow, index) {
html += '<tr><td>' + subrow.name + '</td></tr>';
})
}
})
html += '</table>';
element.replaceWith(html)
}
}
});
function MyCtrl($scope) {
$scope.rows = [
{ name: 'a', subrows: [{ name: 'a.1' }, { name: 'a.2' }] },
{ name: 'b', subrows: [{ name: 'b.1',subrows: [{ name: 'b.1.1' }, { name: 'b.1.2' }] }, { name: 'b.2' }] }
];
}
I get the output as:
a
a.1
a.2
b
b.1
b.2
But I need to get:
a
a.1
a.2
b
b.1
b.1.1
b.1.2
b.2
I should be able to traverse as many levels as possible and show them in a table. How can I do that?
It seems that you have a data structure in the shape of a tree and you could solve it with a recursive function to explore your tree.
I wrote the following piece of code that should get you on your way. I'm pretty sure it can be done a bit more elegantly by using additional variables.
JS: (did not alter your controller)
var myApp = angular.module('myApp', []);
myApp.directive('myTable', function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
var text = '';
function tableRec(array) {
if(array.length === 0) {
return text;
} else {
var obj = array.shift();
text += '<tr><td>' + obj.name + '</td></tr>';
//if there are subrows we go deeper into the recursion
if(obj.subrows) {
tableRec(obj.subrows);
}
tableRec(array);
}
}
tableRec(scope[attrs.rows]);
var html = '<table>' + text + '</table>';
element.replaceWith(html)
}
}
});
HTML: (unaltered)
<div ng-controller="MyCtrl">
<my-table rows='rows'></my-table>
</div>
OUTPUT:
a
a.1
a.2
b
b.1
b.1.1
b.1.2
b.2
You can also find my plunker here.
I have an array with a lot of data.
I need to display it in different areas (about 10 times) in the same page (with different filtering).
To prevent long loading time (of multiple 'ng-repeat'), i'm trying to put it in a directive and show the same directive multiple times (instead of 'ng-repeat').
I want the directive to show every time it's same instance and not create new objects (and in that way to speed up the loading).
How can i get the directive to show the same instance and not create itself over and over again?
My example code:
Plunker
var contec = angular.module('app', [])
.controller('MainCtrl', function($scope,$rootScope) {
$scope.change = function(){
var id = Math.floor((Math.random() * 4) + 0);
var val = Math.floor((Math.random() * 100) + 1);
$rootScope.data.items[id].id = val;
}
$rootScope.data = {
items: [{
id: 1,
name: "first"
}, {
id: 2,
name: "second"
}, {
id: 3,
name: "third"
}, {
id: 4,
name: "forth"
}]
}
});
contec.directive('firstDirective', function($rootScope,$compile) {
return {
replace: true,
restrict: 'EA',
scope: {
data: '='
},
link: function(scope, element, attrs) {
var template = '';
angular.forEach($rootScope.data.items, function(item, key) {
var tmp = '<div second-directive data="' + key + '"></div>';
template = template + tmp;
});
element.html(template);
$compile(element.contents())(scope);
}
}
});
contec.directive('secondDirective', function($rootScope,$compile) {
var comp = function(scope,element, attrs,firstDirective){
var index = scope.data;
scope.item = $rootScope.data.items[index];
var template = '<ng-include src="\'itemTemplate.html\'"></ng-include>';
element.html(template);
$compile(element.contents())(scope);
}
return {
restrict: 'EA',
link: comp,
scope: {
data: '='
},
};
});
I think the best way to make your data instance available is to wrap it in a Service (specifically, a factory)
Ex:
contec.factory('DataFactory', function() {
var data = {
items: [{
id: 1,
name: "first"
}, {
id: 2,
name: "second"
}, {
id: 3,
name: "third"
}, {
id: 4,
name: "forth"
}]
}
return {
getData(): function() {
return data;
}
}
}
Then you'll be able to inject the factory in any controller that requires it.
contec.controller('ExampleController', ['DataFactory', ExampleController]);
function ExampleController(DataFactory) {
this.data = DataFactory.getData();
}
This approach has the added benefit of being more testable, since you can easily inject mock data in your unit tests.
I have created custom select2 directive for angular it works perfect for my usecase and works like charm when i use template and it sets and get the values from input/ngModel
but when i use it on view page it do not resolve ngModel via scope.$eval
this is something scope issue please help me on this
please find directive mentioned below:
(function () {
'use strict';
var directiveId = 'snSelect2';
angular.module('app').directive(directiveId, ['datacontext', snSelect2]);
function snSelect2(datacontext) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, controller) {
var urlPrefix =datacontext.urlPrefix;
$(function () {
element.select2({
placeholder: element.attr('placeholder'),
multiple: angular.isDefined(attrs.multiple),
minimumInputLength: 3,
blurOnChange: true,
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: urlPrefix + "/Employees",
dataType: 'json',
data: function (term) {
return {
term: term
};
},
results: function (data) { // parse the results into the format expected by Select2.
// since we are using custom formatting functions we do not need to alter remote JSON data
return { results: data };
}
},
initSelection: function (element, callback) {
var id = scope.$eval(attrs.ngModel);//$(element).val();
if (id != "") {
$.ajax(urlPrefix + "/EmployeeById",
{
data: {
id: id,
format: 'json'
},
datatype: 'json'
}).done(function (data) {
if (angular.isDefined(attrs.multiple)) {
callback(data);
}
else {
callback(data[0]);
}
});
}
//var data = {id: element.val(), text: element.val()};
//callback(data);
},
dropdownCssClass: "bigdrop", // apply css that makes the dropdown taller
escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
}).select2('val', scope.$eval(attrs.ngModel))
.on("change", function (e) {
scope.$apply(function () {
controller.$setViewValue(attrs.ngModel);
});
});
element.bind("change", function (e) {
scope.$apply(function () {
scope[attrs.ngModel] = e.val;
});
});
});
}
}
}})();
http://snag.gy/lcizI.jpg
<!-- html element -->
<input type="hidden" class="form-control" data-ng-model="show" ui-select2="getShow">
$scope.getShow = {
placeholder: "Enter Show Code",
minimumInputLength: 3,
escapeMarkup: function (m) { return m; },
formatSelection: function(obj, container) {
return "ID: " + obj.showid + " - " + obj.name;
},
formatResult: function(obj, container, query) {
var start = obj.startdate ? moment(obj.startdate).format("DD/MM/YYYY") : "Unknown";
var end = obj.enddate ? moment(obj.enddate).format("DD/MM/YYYY") : "Unknown";
return '<div class="list-group-item small">' +
'<i><span class="label label-default pull-right">ID: ' + obj.showid + '</span></i>' +
'<i><span class="label label-info">(' + obj.code + ')</span></i>' +
'<div style="padding-top: 4px"><strong>' + obj.name + '</strong></div>' +
'<i>' + start + " - " + end + '</i>' +
'</div>';
},
query: function(options) {
if (!options.context)
options.context = {};
// status == processing means http request is in process, so don't do it again
if (options.context.status === "processing")
return;
options.context.status = "processing";
// this is just like ajax $http.get("/search").
model.show.search("search", {code: options.term, limit: 10, sort: 'ts desc'} )
.then(function(result) {
// set status = completed to indicate http request finished.
options.context.status = "completed";
// when you get the result from ajax, callback require you to call with
// an object { results: your result } as result
$scope.list.datalist.show = result;
options.callback({
results: result
});
});
}
}
Data from server looks like
[{
code: "MELCCS"
enddate: "2014-03-10T14:00:00.000Z"
id: "5329c28087b375a4d8a2af43"
name: "Melbourne Caravan and Camping Show"
openinghour: ""
showid: 1810
startdate: "2014-03-05T14:00:00.000Z"
state: "VIC"
status: "Research"
ts: 1395245779280
updatedAt: "2014-03-21T09:16:56.859Z"
warehouse: "52ecd53b673a5fba428e21a7"
}]
I'm new to Angular and trying to wrap my brain around how to do stuff.
I'm trying to create a list that's populated on page load by an ajax call to a REST API service, and the elements of that list would fire ajax calls to that same service when clicked on that populated sub-lists below those elements, and so on to depth n.
The initial population of the list is easy: the controller makes the ajax call, gets the JSON object, assigns it to scope and the DOM is handled with a ng-repeat directive. I'm having trouble with the subsequent loading of the sub-lists.
In jQuery, I would have a function tied to each appropriately classed element clicked upon via onClick which would get the required parameters, take the JSON output, parse it into HTML and append that HTML after the element which fired the event. This is direct DOM manipulation and therefore Angular heresy.
I've already looked at this question here, but I still don't quite understand how to implement something like this "the Angular way".
Help?
Edit: Solved this problem by making a recursive directive. Instructions from here: http://jsfiddle.net/alalonde/NZum5/light/.
Code:
var myApp = angular.module('myApp',[]);
myApp.directive('uiTree', function() {
return {
template: '<ul class="uiTree"><ui-tree-node ng-repeat="node in tree"></ui-tree-node></ul>',
replace: true,
transclude: true,
restrict: 'E',
scope: {
tree: '=ngModel',
attrNodeId: "#",
loadFn: '=',
expandTo: '=',
selectedId: '='
},
controller: function($scope, $element, $attrs) {
$scope.loadFnName = $attrs.loadFn;
// this seems like an egregious hack, but it is necessary for recursively-generated
// trees to have access to the loader function
if($scope.$parent.loadFn)
$scope.loadFn = $scope.$parent.loadFn;
// TODO expandTo shouldn't be two-way, currently we're copying it
if($scope.expandTo && $scope.expandTo.length) {
$scope.expansionNodes = angular.copy($scope.expandTo);
var arrExpandTo = $scope.expansionNodes.split(",");
$scope.nextExpandTo = arrExpandTo.shift();
$scope.expansionNodes = arrExpandTo.join(",");
}
}
};
})
.directive('uiTreeNode', ['$compile', '$timeout', function($compile, $timeout) {
return {
restrict: 'E',
replace: true,
template: '<li>' +
'<div class="node" data-node-id="{{ nodeId() }}">' +
'<a class="icon" ng-click="toggleNode(nodeId())""></a>' +
'<a ng-hide="selectedId" ng-href="#/assets/{{ nodeId() }}">{{ node.name }}</a>' +
'<span ng-show="selectedId" ng-class="css()" ng-click="setSelected(node)">' +
'{{ node.name }}</span>' +
'</div>' +
'</li>',
link: function(scope, elm, attrs) {
scope.nodeId = function(node) {
var localNode = node || scope.node;
return localNode[scope.attrNodeId];
};
scope.toggleNode = function(nodeId) {
var isVisible = elm.children(".uiTree:visible").length > 0;
var childrenTree = elm.children(".uiTree");
if(isVisible) {
scope.$emit('nodeCollapsed', nodeId);
} else if(nodeId) {
scope.$emit('nodeExpanded', nodeId);
}
if(!isVisible && scope.loadFn && childrenTree.length === 0) {
// load the children asynchronously
var callback = function(arrChildren) {
scope.node.children = arrChildren;
scope.appendChildren();
elm.find("a.icon i").show();
elm.find("a.icon img").remove();
scope.toggleNode(); // show it
};
var promiseOrNodes = scope.loadFn(nodeId, callback);
if(promiseOrNodes && promiseOrNodes.then) {
promiseOrNodes.then(callback);
} else {
$timeout(function() {
callback(promiseOrNodes);
}, 0);
}
elm.find("a.icon i").hide();
var imgUrl = "http://www.efsa.europa.eu/efsa_rep/repository/images/ajax-loader.gif";
elm.find("a.icon").append('<img src="' + imgUrl + '" width="18" height="18">');
} else {
childrenTree.toggle(!isVisible);
elm.find("a.icon i").toggleClass("icon-chevron-right");
elm.find("a.icon i").toggleClass("icon-chevron-down");
}
};
scope.appendChildren = function() {
// Add children by $compiling and doing a new ui-tree directive
// We need the load-fn attribute in there if it has been provided
var childrenHtml = '<ui-tree ng-model="node.children" attr-node-id="' +
scope.attrNodeId + '"';
if(scope.loadFn) {
childrenHtml += ' load-fn="' + scope.loadFnName + '"';
}
// pass along all the variables
if(scope.expansionNodes) {
childrenHtml += ' expand-to="expansionNodes"';
}
if(scope.selectedId) {
childrenHtml += ' selected-id="selectedId"';
}
childrenHtml += ' style="display: none"></ui-tree>';
return elm.append($compile(childrenHtml)(scope));
};
scope.css = function() {
return {
nodeLabel: true,
selected: scope.selectedId && scope.nodeId() === scope.selectedId
};
};
// emit an event up the scope. Then, from the scope above this tree, a "selectNode"
// event is expected to be broadcasted downwards to each node in the tree.
// TODO this needs to be re-thought such that the controller doesn't need to manually
// broadcast "selectNode" from outside of the directive scope.
scope.setSelected = function(node) {
scope.$emit("nodeSelected", node);
};
scope.$on("selectNode", function(event, node) {
scope.selectedId = scope.nodeId(node);
});
if(scope.node.hasChildren) {
elm.find("a.icon").append('<i class="icon-chevron-right"></i>');
}
if(scope.nextExpandTo && scope.nodeId() == parseInt(scope.nextExpandTo, 10)) {
scope.toggleNode(scope.nodeId());
}
}
};
}]);
function MyCtrl($scope, $timeout) {
$scope.assets = [
{ assetId: 1, name: "parent 1", hasChildren: true},
{ assetId: 2, name: "parent 2", hasChildren: false}
];
$scope.selected = {name: "child 111"};
$scope.hierarchy = "1,11";
$scope.loadChildren = function(nodeId) {
return [
{assetId: parseInt(nodeId + "1"), name: "child " + nodeId + "1", hasChildren: true},
{assetId: parseInt(nodeId + "2"), name: "child " + nodeId + "2"}
];
}
$scope.$on("nodeSelected", function(event, node) {
$scope.selected = node;
$scope.$broadcast("selectNode", node);
});
}
Template:
<div ng-controller="MyCtrl">
<ui-tree ng-model="assets" load-fn="loadChildren" expand-to="hierarchy" selected-id="111" attr-node-id="assetId"></ui-tree>
<div>selected: {{ selected.name }}</div>
</div>
Here's a solution which I prototyped for my own use.
https://embed.plnkr.co/PYVpWYrduDpLlsvto0wR/
Link updated
I'm new at Angularjs... Just got mad making this to work:
angular.module('app', ['ui.select2']).directive("selectCompany", function($timeout) {
return {
restrict: 'A',
replace: true,
template: '<input type="text" name="company_id" ng-model="companySelected" />',
scope: {},
link: function (scope, element, attrs, ctrl) {
$timeout(element.select2({
placeholder : "Buscar empresa", minimumInputLength : 3, allowClear : true,
ajax: {
url : 'http://' + window.location.host + '/ajax/module/company/load-companies',
dataType : 'json',
type : 'post',
quietMillis : '250',
data : function (term, page) { return { name: term }; },
results : function (data, page) { return { results : data }; }
},
formatResult : function(item) { return item.name; },
formatSelection : function(item) { return item.name; },
escapeMarkup : function (m) { return m; },
}));
},
};
});
This is my Angular directive which makes a <div select-company></div> into a select2 control. It works at the moment, it also return the details but I'm getting this error:
Any idea?
Normally, you'd want to work with non-minified versions of scripts during development, because they give more descriptive stack traces...
Hard to say what exactly is going on here, but try:
$timeout(function () {
element.select2({
placeholder: "Buscar empresa",
minimumInputLength: 3,
allowClear: true,
ajax: {
url: 'http://' + window.location.host + '/ajax/module/company/load-companies',
dataType: 'json',
type: 'post',
quietMillis: '250',
data: function (term, page) {
return {
name: term
};
},
results: function (data, page) {
return {
results: data
};
}
},
formatResult: function (item) {
return item.name;
},
formatSelection: function (item) {
return item.name;
},
escapeMarkup: function (m) {
return m;
},
})
});