angularjs: Dynamically build a compileable directive with bindings - angularjs

im currently trying to build a component which gets an object with specifications which directive it should render.
So this is my angular component componentRenderer.js
angular
.module('app.core')
.component('componentRenderer', {
templateUrl: '/component-renderer/component-renderer.tpl.html',
controller: ComponentRendererController,
bindings: {
data: '='
}
});
function ComponentRendererController($scope, $element, $timeout, $compile) {
var vm = this;
// Called when component is ready
vm.$onInit = function () {
if (vm.data.type !== 'plain') {
var html = '<' + vm.data.type + ' ';
if (vm.data.hasOwnProperty('options')) {
angular.forEach(vm.data.options, function (value, key) {
if(typeof value === 'object') {
html += (key+'="value" ');
} else {
html += (key+'="'+value+'" ');
}
});
}
html += '></' + vm.data.type + '>';
var el = $compile(html)($scope);
$element.find('.widget__content').append(el);
}
};
}
/component-renderer/component-renderer.tpl.html:
<div class="widget">
<div class="widget__header">
<h2>{{ $ctrl.data.title }}</h2>
</div>
<div class="widget__content">
<h3>{{ $ctrl.data.subtitle }}</h3>
</div>
A data object can for example look like that:
{
"type": "activity-widget",
"options": {
"type": "simple",
}
"title": "Activity"
}
My componentRenderer should now build up the corresponding html, so that the following result will be possible:
<activity-widget type="simple"></activity-widget>
So the options of that previously shown object should be rendered as attributes of the component. Finally the activity-widget component should render the final html construct.
deviceActivity.js
angular
.module('app.core')
.component('deviceActivity', {
templateUrl: '/device-activity/device-activity.tpl.html',
controller: DeviceActivityController,
bindings: {
data: "="
}
});
Until here everything works as expected! But no i want to be able to use options as object too.
{
"type": "activity-widget",
"options": {
"jsonObject": {
"surveillance": [
{
"name": "location",
"value": 25,
"max": 100
},
{
"name": "reporting",
"value": 58,
"max": 80
},
{
"name": "reporting",
"value": 9,
"max": 120
}
]
}
},
"title": "Activity"
}
My componentRenderer should now build up the corresponding html, so that the following result will be possible:
<activity-widget object="jsonObject"></activity-widget>
Sadly its not working, it is not binding the jsonObject to my component. I don't know what im doing wrong...Any help is greatly appreciated!
Thanks in advance!

Why it doesn't work
So you would like to get this
<activity-widget object="jsonObject"></activity-widget>
Instead, here is what is being generated:
<activity-widget jsonObject="value"></activity-widget>
(which exactly corresponds to what componentRenderer.js should output given the code that it contains)
And the widget that is being generated doesn't work apparently for 2 reasons:
jsonObject="value": the attribute name is camel-cased, though it should be specified like json-object
jsonObject="value": I don't see where you are trying to inject the value of jsonObject into the scope. So it is not surprising that the value is undefined
How to fix it
If you insist that this is indeed what you'd like to get
<activity-widget object="jsonObject"></activity-widget>
given the object that you specified, here is how you should change componentRenderer.js:
angular.forEach(vm.data.options, function (value, key) {
if(typeof value === 'object') {
// providing attribute=value pair correctly
html += ('object="' + key + '" ');
// adding object's value to the scope
// so that it is passed to the widget during compile below
$scope[key] = value;
} else {
html += (key+'="'+value+'" ');
}
});
Notice that this way you can have only one attribute of object type per widget (all other will be overwritten).
Try changing your code like shown above, for me it seem to work fine. If it still doesn't work for you, please create a fiddle (actually, it is what you should have done in the first place for this kind of question)

Related

Passing Array of Objects into Angular Directive

I have an array of objects in my angular controller, e.g.
[{
label: 'label1',
data: [{
type: TimelineChart.TYPE.POINT,
at: new Date([2015, 1, 1])
}, {
type: TimelineChart.TYPE.POINT,
at: new Date([2015, 2, 1])
}]
}, {
label: 'label2',
data: [{
type: TimelineChart.TYPE.POINT,
at: new Date([2015, 1, 11])
}, {
type: TimelineChart.TYPE.POINT,
at: new Date([2015, 1, 15])
}]
}];
Every object has a Date field. How can I pass this array of objects into my directive?
I tried passing by reference:
<div id= "chart" my-directive my-data="data"></div>
$scope: {
myData: '='
},
link: function($scope, $element, $attrs) {
function init() {
var data = $scope.myData;
var timeline = new TimelineChart($element[0], data);
}
$attrs.$observe("myData", init);
}
However, it didn't work. I get undefined.
Passing in as an attribute, i.e. my-data="{{ data }}" and using JSON.parse($attrs.myData) does not work due to the Date fields.
It's very simple to pass the data to directive. I think what you are missing is scope. You are using $scope instead of scope. Directive Definition object takes scope not $scope. So what you need to do it.
scope: { //make it scope
myData: '='
},
link: function($scope, $element, $attrs) {
function init() {
var data = $scope.myData;
var timeline = new TimelineChart($element[0], data);
}
$attrs.$observe("myData", init);
}
Here's the fiddle
I think the problem is your array object is lost in $scope . For confirmation, use console.log($scope.myData);
And see in browser console what it returns, if undefined then simply write console.log($scope); and then run the application . Go to browser console and expand $parent until you find myData object. Count the number of parent scope.
If suppose you found it under 3rd parent ,the use
$scope.$parent.$parent.$parent.myData instead of $scope.myData .
This will work :)

AngularJS ui bootstrap typeahead never shows results

My html code is:
<p>
<input type="text" ng-model="destination" placeholder="Destination"
uib-typeahead="dest as dest.name for dest in getDestinations($viewValue)" typeahead-loading="loadingDestinations"
typeahead-no-results="noResults" typeahead-min-length="3">
<i ng-show="loadingDestinations">...</i>
<div ng-show="noResults">
<i>xxx - </i> No Destinations Found
</div>
</p>
And my getDestinations() function, which returns a HttpPromise:
$scope.getDestinations = function(viewValue) {
console.log(viewValue);
return $http.get('/api/destAutoComplete', {
params : {
prefix: viewValue,
countryId: 94
}
});
}
Which returns a response for the input "ist" from the server:
[
{
"name": "Costa del Sol / Istan",
"cityId": 5452,
"locationId": 30083
},
{
"name": "Istanbul",
"cityId": 1122,
"locationId": null
}
]
As you see server returns correctly filtered result as a json array, but typeahead never shows results and always says "No Destinations Found". What am I doing wrong? I am trying to show "name" as label in typeahead dropdown, and set the whole dest object to destination in scope, when one is selected.
This is because you forgot to return your promise object. According to Angular Bootsrap UI code comment,
Any function returning a promise object can be used to load values asynchronously.
Try this
$scope.getDestinations = function(viewValue) {
console.log(viewValue);
return $http.get('/api/destAutoComplete', {
params: {
prefix: viewValue,
countryId: 94
}
}).then(function(response) {
// or whatever response you are getting
return response.data.results;
});;
}
Plunker: http://plnkr.co/edit/rIsiEjDBajZb0JPaNjZB?p=preview

Angular directive template unknown scope

I know there is a lot of questions and posts about AngularJS and how directives are supposed to be used. And I got mine working just fine until I got another problem which I don't know how to resolve.
I use a directive on a custom HTML element. Directive transforms this element into a regular html tree as defined in a template. The HTML element has some attributes which are used when building the template. Data for one of the elements is received with HTTP request and is successfully loaded. This is the part which I got working fine.
Now I want to do something more. I've created a plunker which is an example of what I want to achieve. It's a fake one, but illustrates my problem well.
index.html:
<body ng-controller="MainCtrl">
<div id="phones">
<phone brand="SmartBrand" model="xx" comment="blah"></phone>
<phone brand="SmarterBrand" model="abc" comment="other {{dynamic.c1}}"></phone>
</div>
</body>
Angular directive:
app.directive('phone', function() {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "1"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.dynamic = fakeResponse.data.dynamic;
}
}
});
Template:
<div class="phone">
<header>
<h2>{{brand}} <strong>{{model}}</strong></h2>
</header>
<p>Comment: <strong>{{comment}}</strong></p>
</div>
So I would like to be able to customize one of the tags in the element (phone comment in this example). The trick is that the number of additional info that is going to be in the tag may vary. The only thing I can be sure of is that the names will match the ones received from AJAX request. I can make the entire comment be received with AJAX and that will solve my problem. But I want to separate template from the variables it is built with. Is it possible?
Ok, I got it working. It may not be the state of the art solution (I think #xelilof suggestion to do it with another directive may be more correct), but I'm out of ideas on how to do it (so feel free to help me out).
I've turned the {{comment}} part into a microtemplate which is analysed by a service. I've made a plunk to show you a working sample.
The JS part looks like this now:
app.directive('phone', ['dynamic', function(dynamic) {
return {
restrict: 'E',
replace: true,
scope: {
'comment': '#',
'brand': '#',
'color': '#',
'photo': '#'
},
templateUrl: 'customTpl.html',
controller: function($scope) {
fakeResponse = {
"data": {
"success": true,
"data": "X300",
"dynamic": {
"c1": "12",
"c2": "2"
}
}
}
$scope.model = fakeResponse.data.data;
$scope.comment2 = dynamic($scope.comment, fakeResponse.data.dynamic);
console.log("Comment after 'dynamic' service is: " + $scope.comment);
}
}
}]);
app.factory('dynamic', function() {
return function(template, vars) {
for (var v in vars) {
console.log("Parsing variable " + v + " which value is " + vars[v]);
template = template.replace("::" + v + "::", vars[v]);
}
return template;
}
});

select2, ng-model and angular

Using jquery-select2 (not ui-select) and angular, I'm trying to set the value to the ng-model.
I've tried using $watch and ng-change, but none seem to fire after selecting an item with select2.
Unfortunately, I am using a purchased template and cannot use angular-ui.
HTML:
<input type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
ClientController:
$scope.updatePerson = function (contact, person) {
console.log('ng change');
console.log(contact);
console.log(person);
} // not firing
$scope.$watch("client", function () {
console.log($scope.client);
}, true); // not firing either
JQuery integration:
var handleSelect2RemoteSelection = function () {
if ($().select2) {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function(){
var $this = $(this);
if ($this.data('remote-search-url') && $this.data('remote-load-url')) {
$this.select2({
placeholder: "Select",
allowClear: true,
minimumInputLength: 1,
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: Routing.generate($this.data('remote-search-url'), {'_format': 'json'}),
type: 'post',
dataType: 'json',
delay: 250,
data: function (term, page) {
return {
query: term, // search term
};
},
results: function (data, page) { // parse the results into the format expected by Select2.
return {
results: $.map(data, function (datum) {
var result = {
'id': datum.id,
'text': datum.name
};
for (var prop in datum) {
if (datum.hasOwnProperty(prop)) {
result['data-' + prop] = datum[prop];
}
}
return result;
})
}
}
},
initSelection: function (element, callback) {
// the input tag has a value attribute preloaded that points to a preselected movie's id
// this function resolves that id attribute to an object that select2 can render
// using its formatResult renderer - that way the movie name is shown preselected
var id = $(element).val(),
displayValue = $(element).data('display-value');
if (id && id !== "") {
if (displayValue && displayValue !== "") {
callback({'id': $(element).val(), 'text': $(element).data('display-value')});
} else {
$.ajax(Routing.generate($this.data('remote-load-url'), {'id': id, '_format': 'json'}), {
dataType: "json"
}).done(function (data) {
callback({'id': data.id, 'text': data.name});
});
}
}
},
});
}
});
}
};
Any advice would be greatly appreciated! :)
UPDATE
I've managed to put together a plunk which seems to similarly reproduce the problem - it now appears as if the ng-watch and the $watch events are fired only when first changing the value.
Nevertheless, in my code (and when adding further complexity like dynamically adding and removing from the collection), it doesn't even seem to fire once.
Again, pointers in the right direction (or in any direction really) would be greatly appreciated!
There are a number of issues with your example. I'm not sure I am going to be able to provide an "answer", but hopefully the following suggestions and explanations will help you out.
First, you are "mixing" jQuery and Angular. In general, this really doesn't work. For example:
In script.js, you run
$(document).ready(function() {
var $elements = $('input[type=hidden].select2remote');
$elements.each(function() {
//...
});
});
This code is going to run once, when the DOM is initially ready. It will select hidden input elements with the select2remote class that are currently in the DOM and initialized the select2 plugin on them.
The problem is that any new input[type=hidden].select2remote elements added after this function is run will not be initialized at all. This would happen if you are loading data asynchronously and populating an ng-repeat, for example.
The fix is to move the select2 initialization code to a directive, and place this directive on each input element. Abridged, this directive might look like:
.directive('select2', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
//$this becomes element
element.select2({
//options removed for clarity
});
element.on('change', function() {
console.log('on change event');
var val = $(this).value;
scope.$apply(function(){
//will cause the ng-model to be updated.
ngModel.setViewValue(val);
});
});
ngModel.$render = function() {
//if this is called, the model was changed outside of select, and we need to set the value
//not sure what the select2 api is, but something like:
element.value = ngModel.$viewValue;
}
}
}
});
I apologize that I'm not familiar enough with select2 to know the API for getting and setting the current value of the control. If you provide that to me in a comment I can modify the example.
Your markup would change to:
<input select2 type="hidden" class="form-control select2remote input-medium"
ng-model="contact.person.id"
value="{{ contact.person.id }}"
data-display-value="{{ contact.person.name }}"
data-remote-search-url="api_post_person_search"
data-remote-load-url="api_get_person"
ng-change="updatePerson(contact, contact.person)">
After implementing this directive, you could remove the entirety of script.js.
In your controller you have the following:
$('.select2remote').on('change', function () {
console.log('change');
var value = $(this).value;
$scope.$apply(function () {
$scope.contact.person.id = value;
});
});
There are two problems here:
First, you are using jQuery in a controller, which you really shouldn't do.
Second, this line of code is going to fire a change event on every element with the select2remote class in the entire application that was in the DOM when the controller was instatiated.
It is likely that elements added by Angular (i.e through ng-repeat) will not have the change listener registered on them because they will be added to the DOM after the controller is instantiated (at the next digest cycle).
Also, elements outside the scope of the controller that have change events will modify the state of the controller's $scope. The solution to this, again, is to move this functionality into the directive and rely on ng-model functionality.
Remember that anytime you leave Angular's context (i.e if you are using jQuery's $.ajax functionality), you have to use scope.$apply() to reenter Angular's execution context.
I hope these suggestions help you out.

Pass array object from controller to custom directive in AngularJS

Im trying to pass a array of objects from a angular controller to a custom directive element and iterate the object with ng-repeat, but appears the following error: [ngRepeat:dupes]
home.js:
home.controller("homeController", function ($scope) {
$scope.points =[
{
"url": '../assets/images/concert.jpg',
"id":1
},
{
"url": '../assets/images/dance.jpg',
"id":2
},
{
"url": '../assets/images/music.jpg',
"id":3
},
{
"url": '../assets/images/jazz.jpg',
"id":4
},
{
"url": '../assets/images/violin.jpg',
"id":5
},
{
"url": '../assets/images/music.jpg',
"id":6
}
];
});
Shareddirectives.js:
var sharedDirectives = angular.module("sharedDirectives", []);
sharedDirectives.directive("interestPoints", function () {
function link($scope, element, attributes, controller ) {
$(element).find('#interest-points').owlCarousel({
items : 4, //4 items above 1200px browser width
itemsDesktop : [1200,3], //3 items between 1200px and 992px
itemsDesktopSmall : [992,3], // betweem 992px and 768px
itemsTablet: [850,2], //1 items between 768 and 0
itemsMobile : [600,1] // itemsMobile disabled - inherit from itemsTablet option
});
}
return {
restrict: "E",
templateUrl : "../html/views/interest-points.html",
link: link,
scope: {
interestPoints: '#'
}
};
});
interest-points.html:
<div id="interest-points" class="owl-carousel">
<div ng-repeat="point in interestPoints" class="item">
<img ng-src="{{point.url}}" alt="Owl Image"><h4>27<br>JUL</h4>
</div>
</div>
home.html:
<div ng-controller='homeController'>
<interest-points interest-points="{{points}}""></interest-points>
</div>
I tried with track by $index but the error don't appear and it don't iterate
You are using interestPoints: '#' as the method of binding interestPoints to the scope. That actually binds only the string {{points}} to interestPoints instead of actually evaluating that expression in the parent's scope.
Use the interestPoints: '=' as the binding method and then interest-points="points" to get the desired behaviour.
Related docs under the heading Directive definition object.

Resources