I have a directive which is fetching data through ajax on load. But after an event in the controller which is posting some data, the Directive should re-compile with the new ajax data so that the changes can be reflected. Can you please help.
I have a compile function in the directive which takes data and puts that in HTML file and generates markup.
Then I have a save comment function in the controller which saves a new comment and so the directive gets the new data.
compile: function(tElement, tAttrs) {
var templateLoader = $http.get(base_url + 'test?ticket=' + $routeParams.ticketid, {cache: $templateCache})
.success(function(htmlComment) {
if (htmlComment != '')
tElement.html(htmlComment);
else
tElement.html('');
});
return function (scope, element, attrs) {
templateLoader.then(function (templateText) {
if (tElement.html() != '')
element.html($compile(tElement.html())(scope));
else
element.html('<div class="no-comments comment"><p>Be the first to comment</p></div>');
});
};
}
This is the compile part of the directive. I want this to be called through a normal controller event.
I would recommend #Riley Lark' response but as you already mentioned that your API returns an HTML instead of JSON, here is my take.
Your controller as:
<div ng-controller="MyCtrl">
<button ng-click="save()">Save Comment</button>
<comments></comments>
</div>
myApp.controller('MyCtrl', function($scope) {
$scope.commentHTML = '';
$scope.alert = function(salt) {
alert('You clicked, My Comment ' + salt);
}
$scope.save = function() {
// this imitates an AJAX call
var salt = Math.random(1000);
$scope.commentHTML+= '<div ng-click="alert(' + salt + ')">My Comment ' + salt + '</div>';
};
});
And the comments directive as:
myApp.directive('comments', function($compile) {
return {
restrict: 'E',
link: function(scope, element) {
scope.$watch(function() { return scope.commentHTML; }, function(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
element.html(newVal);
$compile(element)(scope);
}
});
}
}
});
Hope this solves your problem..!
Working Demo
After you fetch the data you need, put the data in a $scope property. Define your template in terms of that property and it will automatically change when the data returns.
For example, your template might be
<div ng-repeat="comment in comments">
{{comment}}
</div>
You don't need a compile function or to "reload a directive" to accomplish this. The solution you posted is a sort of reimplementation of angular. It looks like you want to download a template with the data already interpolated into it, but Angular will help you the most if you separate the template from the data and let Angular interpolate it on the client.
Related
I created a simple directive wrapper around the HTML file input to make angular binding work. Here's my directive:
angular.module('myApp').directive('inputFile', InputFileDirective);
function InputFileDirective() {
var bindings = {
selectLabel: '#',
};
return {
restrict: 'E',
require: ['inputFile', 'ngModel'],
scope: true,
controllerAs: 'inputFileCtrl',
bindToController: bindings,
controller: function () {
},
template: `<input class="ng-hide" id="input-file-id" type="file" />
<label for="input-file-id" class="md-button md-raised md-primary">{{ inputFileCtrl.getButtonLabel() }}</label>`,
link: link
};
function link(scope, element, attrs, controllers) {
if (angular.isDefined(attrs.multiple)) {
element.find('input').attr('multiple', 'multiple');
}
var inputFileCtrl = controllers[0];
var ngModelCtrl = controllers[1];
inputFileCtrl.getButtonLabel = function () {
if (ngModelCtrl.$viewValue == undefined || ngModelCtrl.$viewValue.length == 0) {
return inputFileCtrl.selectLabel;
}
else {
return ngModelCtrl.$viewValue.length + (ngModelCtrl.$viewValue.length == 1 ? " file" : " files") + " selected";
}
};
element.on('change', function (evt) {
ngModelCtrl.$setViewValue(element.find('input')[0].files);
ngModelCtrl.$render();
});
}
};
And here's the HTML
<body ng-app="myApp" ng-controller="MyController as ctrl">
<form name="ctrl.myForm">
<input-file select-label="Select Attachment" ng-model="ctrl.attachment1"></input-file>
<input-file select-label="Select Attachment" ng-model="ctrl.attachment2"></input-file>
</form>
</body>
It's pretty simple and it works - if only one is on the page. As soon as I add a second one, I notice that only the first one ever updates. If I select a file with the second one, the label updates on the first one. My suspicions are that the require ['inputFile'] is pulling in the controller for the first directive instance into the link function or something (which shouldn't happen). Even now as I type this, that doesn't really make sense to me. So what's going on here and how do I fix it?
Here's a codepen for you guys to play with and try to figure it out: http://codepen.io/astynax777/pen/PzzBRv
Your problem is not with your angular... is with you html.
You are assigning the same id twice.
Change your template to this:
template: `<label class="md-button md-raised md-primary">{{ inputFileCtrl.getButtonLabel() }}<input class="ng-hide" type="file" /></label>`
I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};
I need to send data to directive when call is successful... Here is my ajax call from my controller:
$scope.items ={
avatar: ""
};
$scope.addComment = function(segment) {
commentFactory.saveComment($scope.form.comment,segment,0,0)
.success(function(data){
$scope.items.avatar = data.avatar;
})
.error(function(data){
console.log(data);
});
// Reset the form once values have been consumed.
$scope.form.comment = "";
};
And here is 2 directive first use to submit form and ajax req, second use to update content on client side. I need in second directive to load content form ajax... Problem now is directive not wait for ajax to finish call...
.directive("addcomment", function(){
return {
restrict: "E",
template: '<input type="submit" addcomments class="btn btn-default pull-right" value="Send" />'
};
})
.directive("addcomments", function($compile){
return {
link: function (scope, element, attrs) {
var html = '<div>'+scope.items.avatar+'</div>';
element.bind("click", function(){
angular.element(document.getElementById('space-for-new-comment'))
.append($compile(html)(scope));
})
}
};
});
Any solution for this?
I just want to show you another way of writing this:
You want to put some comments, ok in html:
<div class="smartdivforcomments">
<div ng-repeat="comment in newComments">
{{comment.avatar}}
</div>
</div>
In controller: $scope.newComments = [];
Function for adding comments:
commentFactory.saveComment($scope.form.comment,segment,0,0)
.success(function(data){
$scope.newComments.push({avatar : data.avatar});
})
.error(function(data){
console.log(data);
});
Answer to your comment to previous question: You bind to click event that is not angular, so you need to use scope.apply to correctly update your view.
Use a watch in the addcomments directive and wait for the controller scope variable items.avatar to be defined.
.directive("addcomments", function($compile){
return {
link: function (scope, element, attrs) {
scope.$watch('items.avatar', function(newVal, oldVal) {
// wait for async to finish
if(scope.items.avatar === undefined) return;
// loaded, do work now
var html = '<div>'+scope.items.avatar+'</div>';
element.bind("click", function() {
angular.element(document.getElementById('space-for-new-comment'))
.append($compile(html)(scope));
});
});
}
};
});
This is possibly easy, but I have browsed the different questions here on SO and in the Angular documentation and can't really find what I'm looking for.
In a directive:
function ssKendoGrid() {
return {
scope: {
dataSource: "="
},
template: "<div kendo-grid k-options='gridOptions'></div>",
controller: "ssKendoGridCtrl",
}
}
That uses the controller:
function ssKendoGridCtrl($scope) {
alert($scope.dataSource);
//other stuff
}
If I want to access the value of dataSource I assumed I'd be able to do something like this:
<div ng-controller="myController">
<div ss-kendo-grid data-source="test"></div>
</div>
MyController is:
function myController($scope) {
$scope.test = "Tested";
}
But it comes as undefined when I try to alert($scope.dataSource); the value..
Now I know I can do this:
<div ss-kendo-grid="test"></div>
And access it in the directive and controller like this:
return {
scope: {
ssKendoGrid: "="
},
template: "<div kendo-grid k-options='gridOptions'></div>",
controller: "ssKendoGridCtrl"
}
//In controller
alert($scope.ssKendoGrid);
But I would like to be able to pass in a JSON object to do various things with and this doesn't seem as clean as in the markup I'd like it to be more intuitive to look at the html and know what the dataSource is.
What I'm really looking for is an understanding of what I'm doing wrong, why doesn't this work?? I've obviously not got the right understanding of how to pass various things to the isolated scope of the directive.
SOLVED
So, turns out I was using the wrong attribute name. HTML5 recognizes data- as a valid attribute, and Angular ignores the fact that data- is prefixed on the variable, which means that I would need to access the variable this way:
HTML:
<div ss-kendo-grid data-source="test"></div>
JS:
return {
scope: {
dataSource: "=source"
},
template: "<div kendo-grid k-options='gridOptions'></div>",
controller: "ssKendoGridCtrl"
}
Cheers
you need to access the directive scope variable as
<div ss-kendo-grid data-source="test"></div>
similarly as you name the directive in the HTML markup
So, turns out I was using the wrong attribute name. HTML5 recognizes data- as a valid attribute, and Angular ignores the fact that data- is prefixed on the variable, which means that I would need to access the variable this way:
HTML:
<div ss-kendo-grid data-source="test"></div>
JS:
return {
scope: {
dataSource: "=source"
},
template: "<div kendo-grid k-options='gridOptions'></div>",
controller: "ssKendoGridCtrl"
}
And a better convention is to simply not use a directive with "data-" at the beginning of it.
invite.directive('googlePlaces', function (){
return {
restrict:'E',
replace:true,
// transclude:true,
scope: {location:'=location'},
template: '<input id="google_places_ac" name="google_places_ac" type="text" class="input-block-level"/>',
link: function(scope, elm, attrs){
var autocomplete = new google.maps.places.Autocomplete($("#google_places_ac")[0], {});
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
scope.location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
console.log(scope.location);
scope.$apply();
// scope.$apply(function() {
// scope.location = location;
// });
});
}
};
});
I'm trying to figure out if it is possible to validate data client side to ensure that no duplicates are sent to the database. I have an angular app which gets data from an api call. This is my current controller for adding a new subject (functioning perfectly, but without data validation):
angular.module('myApp.controllers')
.controller('SubjectNewCtrl', ['$scope', 'SubjectsFactory', '$location', '$route',
function ($scope, SubjectsFactory, $location, $route) {
// callback for ng-click 'createNewSubject':
$scope.createNewSubject = function () {
SubjectsFactory.create($scope.subjects);
$location.path('/subjects');
}
}]);
And here is what I have been attempting for data validation:
angular.module('myApp.controllers')
.controller('SubjectNewCtrl', ['$scope', 'SubjectsFactory', '$location', '$route',
function ($scope, SubjectsFactory, $location, $route) {
// callback for ng-click 'createNewUser':
$scope.createNewSubject = function () {
var newSubject = $scope.subject.name;
var someSubject = $scope.subjects;
var oldSubject;
if(newSubject){
angular.forEach($scope.subjects, function(allSubjects){
if(newSubject.toLowerCase() == allSubjects.name.toLowerCase()){
oldSubject = true;
}
});
if (!oldSubject){
SubjectsFactory.create($scope.subjects);
}
}
}
}]);
This gives me a console error- TypeError: Cannot read property 'name' of undefined. How do I access the 'name' property of my new subject from the html? Can anyone tell me if what I am trying to do is possible/ makes sense?
If I understand your question correctly, you should use a directive for the specific field you are trying to validate. A unique email directive would be a common example. Here is one I have used in the past. Nothing fancy.
MyApp.directive('uniqueEmail', ['$http', function($http) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
//set the initial value as soon as the input comes into focus
element.on('focus', function() {
if (!scope.initialValue) {
scope.initialValue = ctrl.$viewValue;
}
});
element.on('blur', function() {
if (ctrl.$viewValue != scope.initialValue) {
var dataUrl = attrs.url + "?email=" + ctrl.$viewValue;
//you could also inject and use your 'Factory' to make call
$http.get(dataUrl).success(function(data) {
ctrl.$setValidity('isunique', data.result);
}).error(function(data, status) {
//handle server error
});
}
});
}
};
}]);
Then in your markup you could use it like so.
<input type="text" name="email" ng-model="item.email" data-unique-email="" data-url="/api/check-unique-email" />
<span class="validation-error" ng-show="form.email.$error.isunique && form.email.$dirty">Duplicate Email</span>
Hope this is what you were looking for.
I have implemented object creation in Angular js many times.
My createNew button method typically just created a new javascript Object() and set the scope.currentObject to the new Object();
In your case it appears that $scope.subject is not initialized to anything, hence the error.
I guess that there must be a html input on your form that is bound the subject.name field but without a subject Object to hold the name it is effectively unbound.
If I wanted users to enter a name then click create button to validate that the name is not used. I would bind the new Name input to a different $scope variable (perhaps $scope.newName)
Then in the createNewSubject method you can actually create a new subject like this:
$scope.subject = new Object();
$scope.subject.name = $scope.newName;
Then you can run your validation code.