I have a scenario where a directive's link function is called with an empty element parameter leading to errors.
A code example describes the scenario best:
index.html:
<html ng-app="app">
<body>
<div outer></div>
</body>
</html>
Scripts:
var app = angular.module('app', []);
app.directive('outer', function() {
return {
replace: true,
controller: function($scope) {
$scope.show = true;
},
templateUrl: 'partial.html'
};
});
app.directive('inner', function($window) {
return {
link: function(scope, element, attrs) {
console.log('element: ' + element);
var top = element[0].offsetTop;
}
};
});
partial.html (referenced by templateUrl on outer, above):
<div ng-switch on="show">
<div ng-switch-when="true">
<div inner>showing it</div>
</div>
</div>
Loading index.html in Chrome, the console reports the error: "TypeError: Cannot read property 'offsetTop' of undefined" - because element is an empty array!
Some notes:
replace must be set to true on directive outer.
templateUrl must be used by directive outer to load its partial.
I'm uncertain whether I've overlooked some configuration requirement or if this is an Angular issue. Is this a valid scenario? If it is then is it a problem with ng-switch or is there a deeper problem within Angular?
Try
app.directive('inner', function($window) {
return {
link: function(scope, element, attrs) {
console.log('element: ', element);
if(!element.length){
console.log('Returning since there is no element');
return;
}
var top = element[0].offsetTop;
console.log('top', top)
}
};
});
Related
I'm consuming dynamically generated HTML from an API which may contain hyperlinks, and I'm wanting to replace the hrefs within them with ngClicks. The following directive appears to modify the HTML as intended when I check it in a DOM inspector, but clicking it does nothing. What am I doing wrong?
app.directive('replaceLinks', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function(scope) {
return scope.$eval(attrs.replaceLinks);
}, function(value) {
element.html(value);
angular.forEach(element.contents().find("a"), function(link) {
link.removeAttribute("href");
link.removeAttribute("target");
link.setAttribute("ng-click", "alert('test')");
});
$compile(element.contents())(scope);
});
}
};
}]);
Instead of removing the href please set it to blank (this will preserve the link css), also the ng-click calling the alert can be done by calling the alert('test') inside of a scope method, why the alert didn't fire, is explained in this SO Answer, please refer the below sample code!
// <body ng-app='myApp' binds to the app being created below.
var app = angular.module('myApp', []);
app.directive('replaceLinks', ['$compile', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.forEach(element.find("a"), function(link) {
link.setAttribute("href", "");
link.removeAttribute("target");
link.setAttribute("ng-click", "scopeAlert('test')");
});
$compile(element.contents())(scope);
}
};
}]);
// Register MyController object to this app
app.controller('MyController', ['$scope', MyController]);
// We define a simple controller constructor.
function MyController($scope) {
// On controller load, assign 'World' to the "name" model
// in <input type="text" ng-model="name"></input>
$scope.name = 'World';
$scope.scopeAlert = function(name) {
window.alert(name);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller='MyController' ng-app="myApp">
<div replace-links>
test 1
test 2
test 3
</div>
</div>
I am quite new to angularjs, I am trying to append html from another html file using my directive. I only want to do this only if I get a success callback from the server from my http request.
Currently I have my directive, I am not sure if this is the way to do it. I have also attached my directive to the div I want to append to.
host.directive('newHost', function(){
return {
restrict: 'E',
link: function(scope, element, attr){
scope.newBox = function(){
templateUrl: 'newHostTemplate.html';
};
}
}
});
I then call $scope.newBox() on my success callback which at that point I want to append to the div.
I have followed the answer below, and tried to adapt it to my scenario, however I am getting the error $scope.newBox is not a function, here is my current implementation.
host.directive('newHost', function(){
return {
restrict: 'E',
template: '<div ng-include="getTemplateUrl()"></div>',
link: function(scope, element, attr){
scope.newBox = function(){
console.log('newBox');
scope.getTemplateUrl = function(){
return 'newHostTemplate.html'
};
};
}
}
});
//controller, this does all the routing on client side
host.controller('hostsController', function($scope, $http, $window, $rootScope){
$rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
console.log("failed to change routes");
});
$scope.newHost = {};
$scope.addNewHost = function() {
$http({
method : 'POST',
url : 'http://192.168.0.99:5000/newHost',
data : JSON.stringify($scope.newHost), // pass in data as strings
})
.success(function(data) {
console.log(data);
$scope.newBox()
//on success we want to close the modal and reset the data
$scope.newHost = {}
$scope.dismiss()
});
};
});
Here are two examples:
Using ng-include and a function that points to the external file
I created a plunker showing you a working example. As soon as you click the 'process data' link, it will call NewBox() that will append the content from an external file. This link simulates your callback.
In the directive, the template is defined as template:
<div ng-include="getTemplateUrl()"></div>
And in the link function, I setup getTemplateUrl() once newBox() is called... the getTemplateUrl() function returns the name of the external file (e.g. template.html):
link: function(scope, element, attr) {
scope.newBox = function() {
console.log('new Box');
scope.getTemplateUrl = function() {
return 'template.html';
}
}
}
The full JS file is:
angular.module('components', [])
.directive('newHost', function() {
return {
restrict: 'E',
template: '<div ng-include="getTemplateUrl()"></div>',
link: function(scope, element, attr) {
scope.newBox = function() {
console.log('new Box');
scope.getTemplateUrl = function() {
return 'template.html';
}
}
}
}
});
angular.module('HelloApp', ['components'])
.controller('MyCtrl', ['$scope', function($scope) {
$scope.name = 'This is the controller';
$scope.go = function() {
console.log('Processing...');
$scope.newBox();
}
}]);
index.html is:
<!doctype html>
<html ng-app="HelloApp" >
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
<script src="app.js"></script>
</head>
<body>
<div ng-controller="MyCtrl">
<new-host></new-host>
<br/>
<a ng-href='#here' ng-click='go()'>process data</a>
</div>
</body>
</html>
And template.html is a simple example:
<div>
This is some content from template.html
</div>
If you look in plunker, once you press 'process data', the template.html content is then added using the newBox() function. Now, you would replace that link with your callback.
Using ng-show and a boolean to hide/show the content
One way which is going in a slightly different direction than you is to use ng-show and hide the template until newBox() is called.
I created a JSFiddle that shows an example of how to do this.
The new-host tag is hidden at the start using ng-show:
<div ng-controller="MyCtrl">
<new-host ng-show='displayNewHost'></new-host>
<br/>
<a ng-href='#here' ng-click='go()' >process data</a>
</div>
The link process data is to simulate your success callback, so when you click on it, it will call $scope.newBox()
Here is the main JS file:
angular.module('components',[])
.directive('newHost', function() {
return {
restrict: 'E',
link: function(scope, element, attr) {
scope.newBox = function() {
console.log('new Box');
scope.displayNewHost = true;
};
}
}
})
angular.module('HelloApp', ['components'])
function MyCtrl($scope) {
$scope.displayNewHost = false;
$scope.name = 'This is the controller';
$scope.go = function() {
console.log('Processing...');
$scope.newBox();
}
}
angular.module('myApp', ['components'])
As you see, in the controller we set displayNewHost to false hiding the directive. Once one clicks on the process data link, the newBox function sets displayNewHost to true and then the content appears.
In your example, you would replace the 'template' by 'templateUrl' pointing to your file.
That is another solution.
Editing my answer to answer the follow-up question / newBox is not a function error
Just reading your code (without checking using plunker so I may be wrong), I am guessing that the problem is that once you are in the success function $scope points to another scope. Try to change your code to this... please note that I put $scope in a variable 'vm' and then use that in both the core function and the success callback.
host.controller('hostsController', function($scope, $http, $window, $rootScope){
$rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
console.log("failed to change routes");
});
var vm = $scope;
vm.newHost = {};
vm.addNewHost = function() {
$http({
method : 'POST',
url : 'http://192.168.0.99:5000/newHost',
data : JSON.stringify(vm.newHost), // pass in data as strings
})
.success(function(data) {
console.log(data);
vm.newBox()
//on success we want to close the modal and reset the data
vm.newHost = {}
vm.dismiss()
});
};
});'
All:
One question about ng-repeat:
var app = angular.module("vp", []);
app.controller("main", function($scope) {
$scope.names = ["name1", "name2","name3","name4","name5"];
});
app.directive("filter", function(){
return {
restrict: "AE",
templateUrl: "asset/chart.html",
controller: function($scope){
this.setLayout = function(EL){
var d3EL = d3.select(EL[0]);
//here below could be style attr or any DOM operation
d3EL.selectAll(".sm").style("font-size","30px");
}
},
link: function(scope, EL, attrs, controller){
controller.setLayout(EL);
}
};
});
My html is:
<html ng-app="vp">
<!-- here is the head part that I did not write-->
<body ng-controller="main" class="container">
<filter></filter>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.25/angular.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>
</body>
</html>
My template chart.html:
<div id="cnt">
<div ng-repeat="m in names">
<div class="sm">{{m}}</div>
</div>
</div>
When I run the setLayout function, I found those elements have not been generated, I wonder how to handle this if I want to set style to those element inside ng-repeat?
Thanks
I think you're doing it right already, whats missing is just to add another directive for each ng-repeated item that will be using the filter directive controller functions to perform its d3 manipulation. The reason why you're code doesn't work is because the filter directive's link function is triggered before the ng-repeat directive of your assets/chart.html template is evaluated, hence your d3 selection does not catch anything. Furthermore, I the solution below promotes re-usability of the filter directive by isolating its scope and accepting the names scope variable.
DEMO
Javascript
.directive('filter', function() {
return {
restrict: 'EA',
scope: { names: '=' },
templateUrl: 'chart.html',
controller: function() {
this.setLayout = function(element) {
var d3el = d3.select(element[0]);
d3el.select(".sm").style("font-size","30px");
};
}
}
})
.directive('filterItem', function() {
return {
require: '^filter',
link: function(scope, elem, attr, filter) {
filter.setLayout(elem);
}
};
});
HTML
index.html
<filter names="names"></filter>
chart.html
<div id="cnt">
<div ng-repeat="m in names" filter-item>
<div class="sm">{{m}}</div>
</div>
</div>
It's not entirely clear what you want to accomplish here, but you'll want to do your DOM manipulations (or d3 visualizations) in your link function instead of trying to call them in the controller. When that gets difficult to maintain you should pull them out into a separate JavaScript file and inject them.
Your filter directive could look like this:
app.directive("filter", function(){
return {
restrict: "AE",
templateUrl: "chart.html",
link: function(scope, EL, attrs){
d3.select(EL[0])
.selectAll(".sm")
.style("font-size","30px");
}
};
});
Here's a plnkr example
I try the same thing as here
I retrieve with Ajax server Data which Json contains the markup with a directive and trying to use $compile to fire up this one but at this point without any luck
here is my controller where I try use $compile
angular.module('blancAppApp')
.controller('SlugCtrl', function ($scope, WpApi, $compile, $filter, ngProgressLite) {
// the Content to be rendered.
$scope.post = [];
//loading animate starts
ngProgressLite.start();
loadRemoteData();
// load remote data from the server.
function loadRemoteData() {
// The WpApiService returns a promise.
WpApi.getContents()
.then(
function( post ) {
applyRemoteData( post );
});
}
// apply the remote data to the local scope.
function applyRemoteData( newContents ) {
var compiled = $compile( newContents )($scope);
console.log(compiled); // this one shows me the object then object.content contains my masonry directive
firebug output: <div class="ng-scope">[object Object]</div>
}
//loading animate ends
ngProgressLite.done();
}).directive('compiled', function($timeout) {
return {
restrict: 'E',
scope: {
compiled: '=compiled'
},
link: function (scope, element, attrs, $timeout) {
element.append(scope.compiled);
}
};
});
the directive which should be called
angular.module('blancAppApp')
.directive('masonry', function(scope, element, attrs, $timeout) {
console.log('asdasd');
var container = element.querySelector('#grid');
var msnry = new Masonry( container, {
// options...
itemSelector: '.masonry-brick',
columnWidth: 200
});
return {
restrict: 'E'
};
});
the generated view
<div ng-repeat ="article in post " id="#main-content">
<div ng-bind-html="article.content | unsafe">
<div ng-controller="ContentCtrl">
{{ article.content }}
</div>
</div>
</div>
Generated Markup with directive call
<div id="grid" masonry="">
<div class="masonry-brick "><img src=""... /></div>
<div class="masonry-brick "><img src=""... /></div>
<div class="masonry-brick "><img src=""... /></div>
</div>
plunker
You use $compile incorrectly. $compile returns "a link function which is used to bind template". So you need to call returned function. Try:
var compiled = $compile($scope.post)($scope);
console.log(compiled);
Resulting element must be then attached somewhere to DOM document for example with directive:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.js"></script>
</head>
<body ng-app="plunker">
<div data-ng-controller="SlugCtrl">
<div id="compiled_content" data-compiled="compiled">
</div>
</div>
<script>
var app = angular.module('plunker', []);
app.controller('SlugCtrl', function ($scope, $compile) {
$scope.some_var = 'var content';
var template = '<div>{{some_var}}</div>';
$scope.compiled = $compile(template)($scope);
$scope.some_var = 'var content canged';
}).directive('compiled', function() {
return {
restrict: 'A',
scope: {
compiled: '=compiled'
},
link: function (scope, element, attrs) {
element.append(scope.compiled);
}
};
});
</script>
</body>
</html>
I used element.append(scope.compiled); and not just {{scope.compiled}} because compilation result is object and not a string.
Your applyRemoteData function must be like this:
function applyRemoteData( newContents ) {
$scope.compiled = $compile('<div>' + newContents + '</div>')($scope);
}
I have a custom directive and I would like to use it to include an html content to the document after clicking on it.
Plunker: http://plnkr.co/edit/u2KUKU3WgVf637PGA9A1?p=preview
JS:
angular.module("app", [])
.controller("MyController", function ($scope) {
})
.directive('addFooter', ['$compile', '$rootScope', function($compile, $rootScope){
return {
restrict: 'E',
template: '<button>add footer</button>',
controller: 'MyController',
link: function( scope, element, attrs, controller) {
element.bind( "click", function() {
scope.footer = "'footer.html'";
})}
};
}])
HTML:
<body ng-app="app">
<script type="text/ng-template" id="footer.html">
FOOTER
</script>
<div ng-controller="MyController">
<add-footer></add-footer>
<div ng-include="footer"></div>
</div>
</body>
Not sure why it is not working, as it worked fine before it was moved into the directive. Outside the directive, I was also referencing to $scope.footer with some link. I tried using $rootScope, but also no effect. Any tips please?
First. Remove unnecessary quote symbols:
element.bind( "click", function() {
scope.footer = "footer.html"; // not "'footer.html'"
});
Second. You should notify angularjs that you have asynchronously updated scope values:
element.bind("click", function() {
scope.$apply(function() {
scope.footer = "footer.html";
});
});
Or like that
element.bind("click", function() {
scope.footer = "footer.html";
scope.$apply();
});