I have an html file with a controller and a directive with a template url. I want to load/compile the directive conditionally in the controller:
Controller:
app.controller('TestController', function TestController($http, $scope, $compile) {
$scope.loadData = function (pageId) {
var pUrl = <some url>
$http({
method: 'GET',
url: pUrl
}).success(function (data, status) {
$scope.pData = data;
var htm = '<test-directive></test-directive>';
var elm = angular.element("#id").append(htm);
$compile(elm)($scope);
}).error(function (data, status) {
alert('error');
});
};
$scope.loadData();
});
Directive:
'use strict';
app.directive('testdirective', function ($http) {
var uDirective = {};
uDirective.restrict = 'E';
uDirective.templateUrl = 'js/directives/testdirective.html';
uDirective.controller = function ($scope, $element, $attrs) {
$scope.showDirectiveData();
$scope.showDirectiveData = function () {
$scope.directiveDataCollection = <get data>;
};
};
uDirective.compile = function (element, attributes) {
// do one-time configuration of element.
var linkFunction = function ($scope, element, atttributes) {
};
return linkFunction;
};
return uDirective;
});
Template used in Directive
<div>
<div ng-repeat="directiveData in directiveDataCollection">
<span><h4>{{directiveData.Title}}</h4></span>
</div>
</div>
How do i get to compile the code in the TestController, load the directive dynamically, and finally load the content and append the content in scope?
Here is a general template for you to reference that abstracts and also demonstrates a few Angular concepts:
JS
.directive('parentDirective', function(Resource, $compile){
return {
restrict: 'E',
link: function(scope, elem, attrs){
Resource.loadData().then(function(result){
scope.data = result.data;
var htm = '<child-directive></child-directive>';
var compiled = $compile(htm)(scope);
elem.append(compiled);
});
}
}
})
.directive('childDirective', function(){
return {
restrict: 'E',
template: '<div>Content: {{data.key}}</div>'
}
})
.factory('Resource', function($http){
var Resource = {};
Resource.loadData = function(){
return $http.get('test.json');
}
return Resource;
})
HTML
<body ng-app="myApp">
<parent-directive></parent-directive>
</body>
Notice that there is no controller code. This is because controllers should never manipulate the DOM - one reason is that it will make your code a PITA to test. So, I put everything in directives, where it should probably be in your case as well.
I also moved the $http service into a factory. Anything state/model related should be in a service. Among other reasons, by doing this, you can inject it almost anywhere (including inside of directives) to access your data without worrying about it disappearing when a controller unloads.
EDIT
You should also consider the dissenting view of the dynamic loading approach in general in the accepted answer of Dynamically adding Angular directives
Related
I have a JSON file with this object:
{"software":[
{"name":"Eclipse", "version":"4.5"},
{"name":"Sublime Text", "version":"3.0"},
{"name":"ConEmu", "version":"1.5"}
]}
I want to get the values using the AngularJS built-in directives, I tried with ng-include and ng-repeat, but doesn't work:
<div ng-include="'software.JSON'" ng-repeat="sw in software">{{sw.name}} v{{sw.version}}</div>
Demo app:
var MyApp = angular.module("MyApp",[]); // Inject app dependencies here
Declare service to fetch JSON file:
MyApp.factory("ConstantsService", ["$http", function($http){
var ConstantsService = {};
ConstantsService.getConstants = function(){
return $http({
method: "GET",
url: "constants.json", //JSON file location
headers: {
'Content-Type': 'application/json'
}
})
.then(function(data){
return data;
});
}
return ConstantsService;
}]);
Inject ConstantsService into your controller and access JSON content:
MyApp.controller('FetchJsonController', ['$scope', 'ConstantsService', function($scope, ConstantsService){
ConstantsService.getConstants().then(function(response){
console.log(response.data.software); //Software object declared in JSON file
$scope.software = response.data.software;
});
}]);
Finally define template:
<div ng-controller="FetchJsonController">
<div ng-repeat="sw in software">{{sw.name}} v{{sw.version}}</div>
</div>
ng-include directive is not made for this purpose.
ngInclude
directive in module ng
Fetches, compiles and includes an external HTML fragment.
Try achieving it with an $http.get in your controller. But if you really need a directive for it...
http://jsfiddle.net/U3pVM/22528/
JS
(function() {
var app = angular.module('app', []);
app.controller("IncludeJsonController", IncludeJsonController);
app.directive("includeJson",[function() {
return {
template: "<div ng-repeat=\"sw in vm.software\"><p>{{ sw.name }} v{{ sw.version}}</p></div>",
controller: "IncludeJsonController",
restrict: "E",
link: function(scope, element, attr, ctrl) {
scope.vm = {};
scope.vm.filename = attr.filename;
scope.vm.software = ctrl.getSoftwares();
}
}}]);
IncludeJsonController.$inject = ['$scope', '$http'];
function IncludeJsonController($scope, $http) {
var self = this;
self.getSoftwares = getSoftwares;
function getSoftwares() {
//in your case, use $http.get('path/to' + $scope.vm.filename);
return [
{"name":"Eclipse", "version":"4.5"},
{"name":"Sublime Text", "version":"3.0"},
{"name":"ConEmu", "version":"1.5"}
];
}
}
}());
HTML
<div ng-app="app">
<include-json filename="software.json"></include-json>
</div>
I am trying to create a page that dynamically loads a template based on the option that the user chooses from a select box. I currently have it loading the template on page load but after that it does not change based on user action.
.directive('ngUsersearch', ['$compile', '$http', '$templateCache', function($compile, $http, $templateCache) {
var getTemplate = function(contentType) {
var templateLoader,
baseUrl = 'view2/components/',
templateMap = {
beer: 'beerList.html',
brewery: 'breweryList.html',
event: 'eventList.html',
guild: 'guildList.html'
};
var templateUrl = baseUrl + templateMap[contentType];
templateLoader = $http.get(templateUrl, {cache: $templateCache});
return templateLoader;
}
var linker = function(scope, element, attrs) {
var loader = getTemplate(scope.ngModel);
var promise = loader.success(function(html) {
element.html(html);
}).then(function (response) {
element.replaceWith($compile(element.html())(scope));
});
}
return {
restrict:"E",
scope: {
ngModel: '='
},
link: linker
}
}]);
Here is my HTML:
<select ng-model="userFilter">
<option value="beer">Beer</option>
<option value="brewery">Brewery</option>
<option value="event">Event</option>
<option value="guild">Guild</option>
</select>
<ng-usersearch ng-model="userFilter"></ng-usersearch>
you forgot listen the change event of the model;
var linker = function(scope, element, attrs) {
scope.$watch('ngModel', function(newValue, oldValue) {
var loader = getTemplate(newValue);
var promise = loader.success(function(html) {
element.html(html);
}).then(function (response) {
element.replaceWith($compile(element.html())(scope)); // you compile and you have isolated scope?
});
});
}
on your compile the only scope available would be ngModel
This solution worked for me. I switched the way that the directive was loading the template. This can be done at the link function, but after the directive is set up and a part of the DOM, I was trying to remove the directive itself from the DOM by replacing it, which does not play well with how Angular's selectors work. So, now I am just replacing its contents. Also, in order to get the ng-repeat to work within the custom directive I had to add the search-results='searchResults' and then define that in the directives scope as well.
HTML:
<ng-usersearch ng-model="userFilter" search-results='searchResults'></ng-usersearch>
Controller:
.controller('View2Ctrl', [ '$scope', 'Restangular', function($scope, Restangular) {
$scope.userSearch = "";
$scope.userFilter = "beer";
$scope.search = function(userSearch, userFilter) {
$scope.searchResults = ("No " + userFilter + " Information Available");
Restangular.all('search?q=' + userSearch + '&type=' + userFilter + '&withBreweries=Y').customGET().then(function(data) {
$scope.searchResults = data;
});
};
}])
Directive:
.directive('ngUsersearch', ['$http', '$templateCache', '$compile', function($http, $templateCache, $compile) {
var getTemplate = function(contentType) {
var templateLoader,
baseUrl = 'view2/components/',
templateMap = {
all: 'all.html',
beer: 'beerList.html',
brewery: 'breweryList.html',
event: 'eventList.html',
guild: 'guildList.html'
};
var templateUrl = baseUrl + templateMap[contentType];
templateLoader = $http.get(templateUrl, {cache: $templateCache.get()});
return templateLoader;
}
var link = function(scope, element) {
scope.$watch('ngModel', function(newValue, oldValue) {
var loader = getTemplate(newValue);
var promise = loader.success(function(html) {
var rendered = $compile(html)(scope);
element.empty();
element.append(rendered); });
});
}
return {
restrict:"E",
scope: {
ngModel: '=',
searchResults: '='
},
link: link
}
}]);
I hope this helps other coders because I struggled with this for a day.
I currently have an AngularJS controller that is basically getting some JSON asynchronously through a $http.get() call, then linking the obtained data to some scope variable.
A resumed version of the controller code:
mapsControllers.controller('interactionsController', ['$http', function($http) {
var ctrlModel = this;
$http.get("data/interactionsPages.json").
success(function(data) {
ctrlModel.sidebar = {};
ctrlModel.sidebar.pages = data;
}).
error(function() {...});
}]);
Then, I have a custom directive which receives those same scope variables through a HTML element.
A resumed version of the directive code:
mapsDirectives.directive('sidebar', function() {
return {
restrict : 'E',
scope : {
pages : '#'
},
controller : function($scope) {
$scope.firstPage = 0;
$scope.lastPage = $scope.pages.length - 1;
$scope.activePage = 0;
//...
},
link : function(scope) {
console.log(scope.pages);
},
templateURL : 'sidebar.html'
}
});
A resumed version of the HTML:
<body>
<div ng-controller='interactionsController as interactionsCtrl'>
<mm-sidebar pages='{{interactionsCtrl.ctrlModel.sidebar.pages}}'>
</mm-sidebar>
</div>
</body>
The problem is, since the $http.get() is asynchronous, the directive is being badly initialised (e.g: $scope.pages.length - 1 is undefined).
I couldn't find anything that solved this problem for me, although there are some presented solutions that would seem to solve the case. Namely, I tried to watch the variables, only initialising the variables after detected changes, as suggested in many other posts. For testing, I used something like:
//... inside the directive's return{ }
link: function() {
scope.$watch('pages', function(pages){
if(pages)
console.log(pages);
});
}
I've tested it, and the $watch function wasn't called more than once (the logged value being undefined), which, I assume, means it isn't detecting the change in the variable value. However, I confirmed that the value was being changed.
So, what is the problem here?
Move the declaration for the sidebar object in the controller and change the scope binding to =.
mapsDirectives.controller("interactionsController", ["$http", "$timeout",
function($http, $timeout) {
var ctrlModel = this;
ctrlModel.sidebar = {
pages: []
};
/*
$http.get("data/interactionsPages.json").
success(function(data) {
//ctrlModel.sidebar = {};
ctrlModel.sidebar.pages = data;
}).
error(function() {});
*/
$timeout(function() {
//ctrlModel.sidebar = {};
ctrlModel.sidebar.pages = ["one", "two"];
}, 2000);
}
]);
mapsDirectives.directive('mmSidebar', [function() {
return {
restrict: 'E',
scope: {
pages: '='
},
controller: function() {},
link: function(scope, element, attrs, ctrl) {
scope.$watch("pages", function(val) {
scope.firstPage = 0;
scope.lastPage = scope.pages.length - 1;
scope.activePage = 0;
});
},
templateUrl: 'sidebar.html'
};
}]);
Then match the directive name and drop the braces.
<mm-sidebar pages='interactionsCtrl.sidebar.pages'>
</mm-sidebar>
Here's a working example: http://plnkr.co/edit/VP79w4vL5xiifEWqAUGI
The problem appears to be your html markup.
In your controller you have specified the ctrlModel is equal to this.
In your html markup you have declared the same this to be named interactionsController.
So tacking on ctrlModel to interactionsController is incorrect.
<body>
<div ng-controller='interactionsController as interactionsCtrl'>
<!-- remove this -->
<mm-sidebar pages='{{interactionsCtrl.ctrlModel.sidebar.pages}}'>
<!-- replace with this -->
<mm-sidebar pages='{{interactionsCtrl.sidebar.pages}}'>
</mm-sidebar>
</div>
</body>
I need to pass an Id defined in the directive to the associated controller such that it can be used in a HTTP Get to retrieve some data.
The code works correctly in its current state however when trying to bind the Id dynamically, as shown in other questions, the 'undefined' error occurs.
The Id needs to be defined with the directive in HTML to meet a requirement. Code follows;
Container.html
<div ng-controller="IntroSlideController as intro">
<div intro-slide slide-id="{54BCE6D9-8710-45DD-A6E4-620563364C17}"></div>
</div>
eLearning.js
var app = angular.module('eLearning', ['ngSanitize']);
app.controller('IntroSlideController', ['$http', function ($http, $scope, $attrs) {
var eLearning = this;
this.Slide = [];
var introSlideId = '{54BCE6D9-8710-45DD-A6E4-620563364C17}'; //Id to replace
$http.get('/api/IntroSlide/getslide/', { params: { id: introSlideId } }).success(function (data) {
eLearning.Slide = data;
});
}])
.directive('introSlide', function () {
return {
restrict: 'EA',
templateUrl: '/Modules/IntroSlide.html',
controller: 'IntroSlideController',
link: function (scope, el, attrs, ctrl) {
console.log(attrs.slideId); //Id from html present here
}
};
});
Instead of defining a controller div that wraps around a directive, a more appropriate approach is to define a controller within the directive itself. Also, by defining an isolated scope for your directive, that slide-id will be available for use automatically within directive's controller (since Angular will inject $scope values for you):
.directive('introSlide', function () {
// here we define directive as 'A' (attribute only)
// and 'slideId' in a scope which links to 'slide-id' in HTML
return {
restrict: 'A',
scope: {
slideId: '#'
},
templateUrl: '/Modules/IntroSlide.html',
controller: function ($http, $scope, $attrs) {
var eLearning = this;
this.Slide = [];
// now $scope.slideId is available for use
$http.get('/api/IntroSlide/getslide/', { params: { id: $scope.slideId } }).success(function (data) {
eLearning.Slide = data;
});
}
};
});
Now your HTML is free from wrapping div:
<div intro-slide slide-id="{54BCE6D9-8710-45DD-A6E4-620563364C17}"></div>
In your IntroSlide.html, you probably have references that look like intro.* (since your original code use intro as a reference to controller's $scope). You will probably need to remove the intro. prefix to get this working.
Require your controller inside your directive, like this:
app.directive( 'directiveOne', function () {
return {
controller: 'MyCtrl',
link: function(scope, el, attr, ctrl){
ctrl.myMethodToUpdateSomething();//use this to send/get some msg from your controller
}
};
});
I'm trying to use prettyprint with some ajax data. The problem is that when the directive is invoked, the data arem't ready, so I get undefined variable output.
Plunkr: http://plnkr.co/edit/fdRi2nIvVzT3Rcy2YIlK?p=preview
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope,$http) {
$scope.result = $http.get('data.json').success(function(result){
return result.data.dom
})
});
app.directive('prettyprint', function() {
return {
restrict: 'C',
link: function postLink(scope, element, attrs) {
element.html(prettyPrintOne(scope.result))
}
};
});
Use scope's $watch method:
scope.$watch("result" , function(n,o){
element.html(prettyPrintOne(scope.result));
})
And instead of this:
$scope.result = $http.get('data.json').success(function(result){
return result.data.dom
})
Use this:
$http.get('data.json').success(function(result){
$scope.result = result.dom;
})
Plunk: http://plnkr.co/edit/Autg0V