So I'm trying to build an AngularJS app and am having some troubles with two way databinding between a controller and a directive when used with an async callback. I have a page controller that pulls data from a server and then uses multiple custom form directives to edit the data. Here is my setup:
function pageController($scope, $http) {
// this is what the data will look like
$scope.controllerModel = {
obj1: {},
obj2: {}
}
$http.get('get the data').then(function(data) {
$scope.controllerModel = data; // fill in the data
$scope.$broadcast('formDataReady'); // tell the forms the data is ready
});
}
The directive:
module('blah').directive('customForm', function() {
return {
restrict: 'E',
scope: { model: '=' },
transclude: true,
replace: true,
controller: function($scope, $attrs) {
$scope.cleanModel = $scope.model ? _.cloneDeep($scope.model) : {};
$scope.reset = function() {
$scope.model = _.cloneDeep($scope.cleanModel);
};
$scope.isClean = function() {
return _.isEqual($scope.model, $scope.cleanModel);
};
// Let page controllers tell the from when the model has been loaded
$scope.$on('formDataReady', function() {
console.log('custom-form: resetting the clean model');
$scope.reset();
console.log($scope);
console.log($scope.model);
});
$scope.reset();
},
template:
'<div>' +
'<form name="form" novalidate>' +
'<div ng-transclude></div>' +
'<div class="form-actions">' +
'<button class="btn btn-primary" ' +
'ng-click="save()" ' +
'ng-disabled="form.$invalid || isClean()">' +
'Save</button>' +
'<button class="btn" ' +
'ng-click="reset()" ' +
'ng-disabled=isClean()>' +
'Cancel</button>' +
'</div>' +
'</form>' +
'</div>'
};
});
And a bit of html:
<div ng-controller="pageController">
<custom-form model="controllerModel.obj1">
<!-- inputs with ng-model to edit the data -->
</custom-form>
<custom-form model="controllerModel.obj2">
<!-- inputs with ng-model to edit the data -->
</custom-form>
</div>
The problem is that the directive's model is not updated as a result of the async callback. The really strange thing is that in the event listener on the directive, those two console.log calls seem to give contradictory information:
console.log($scope):
...
model: { object with data inside it as expected }
...
console.log($scope.model):
Object {} // empty
So in the first log the scope has the model, but $scope.model is somehow empty.
Thanks so much for any help with this. It really, really appreciated.
If you get your data in a resolve before the controller is instantiated, then the directive should read it from it just fine:
app.config(function($routeProvider) {
$routeProvider.route('myRoute', {
url: '/myroute',
resolve: {
getData: function($http) {
return $http.get('get the data').then(function(data) {
return data;
}
}
}
});
});
function pageController($scope, getData) {
// getData from your $http call is now available before your controller was instantiated
// and can be used by your directive
$scope.controllerModel = getData;
}
I'm not sure why the console log is giving contradictory info though
Related
I have a directive to display a gravatar like follows :
angular.module('ngGravatar').directive('gravatar', function(){
return {
restrict: 'E',
template: '<img ng-src={{gravatarUrl}}>',
scope: {email: '='},
controller: function($scope, md5){
var url = 'http://www.gravatar.com/avatar/';
$scope.gravatarUrl = url + md5.createHash($scope.email || '');
}
};
});
I use it in my view like this
<gravatar email="vm.email"></gravatar>
When the view loads, vm.email gets updated asynchronously, and when its value updates, the gravatar directive won't update itself and stays with the default logo...
How can I make it update itself ? With $scope.$watch ? I thought the two way data binding took care of that.
Is there something I missed out on here ?
Try using $scope.$watch to process changes.
angular.module('ngGravatar').directive('gravatar', function()
{
return {
restrict: 'E',
template: '<img ng-src={{gravatarUrl}}>',
scope: { email: '='},
controller: function($scope, md5){
$scope.$watch('email', function(email) {
if (email)
{
var url = 'http://www.gravatar.com/avatar/';
$scope.gravatarUrl = url + md5.createHash(email || '');
}
});
}
};
});
Your directive doesn't refresh after the asynchronous data arrives because it simply doesn't know about its arrival.
It could only possibly know the changes going on in it's controller and not the updates happening in a parent controller.
You can set up a watch on the variable to make your directive update its contents when the appropriate view model changes.
Check the below code snippet which demonstrates that external changes can be tracked by using watchers and internal changes are tracked automatically and the directive updates its contents through data binding features.
angular
.module('demo', [])
.controller('DefaultController', DefaultController)
.directive('gravatar', gravatar);
DefaultController.$inject = ['$timeout'];
function DefaultController($timeout) {
var vm = this;
$timeout(function() {
vm.gravatarName = 'gravatar.png';
}, 500);
}
function gravatar() {
var directive = {
restrict: 'E',
scope: {
name: '='
},
template: '<img ng-src="{{vm.source}}"/>',
controller: GravatarController,
controllerAs: 'vm',
bindToController: true,
};
return directive;
}
GravatarController.$inject = ['$scope', '$timeout'];
function GravatarController($scope, $timeout) {
var vm = this;
var URL = 'https://d13yacurqjgara.cloudfront.net/users/4085/screenshots/2072398/';
// external changes need to be explicitly watched using watchers
$scope.$watch('vm.name', function(newValue, oldValue) {
if (newValue) {
vm.source = URL + vm.name;
}
});
$timeout(function() {
// internal changes are automatically watched
vm.source = 'https://pbs.twimg.com/profile_images/453956388851445761/8BKnRUXg.png';
}, 2000);
}
img {
height: 250px;
width: 250px;
border: 1px solid #E6E6E6;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.9/angular.min.js"></script>
<div ng-app="demo">
<div ng-controller="DefaultController as vm">
<gravatar name="vm.gravatarName"></gravatar>
</div>
</div>
I am creating a custom directive in AngularJS. This directive should open a popup to display data. The code for the popup is in another html page and the custom directive injects the code into my main page. I am able to open the popup however I cannot display the existing data anywhere in the pop up.
Normally, I am able to display the data in the main page however the data just do not want to go into the html injected by the custom directive.
Like this I do not get any error however it does not pass the data.
Note: I had to trim some of the code here to simplify it.
This is my custom directive:
function updateCandidatePopup() {
var directive = {};
directive.restrict = "E";
directive.scope = {};
directive.templateUrl = "UpdateCandidatePopup.html";
directive.controller = function ($scope) {
$scope.SingleCandidate;
}
return directive;
}
This is where I register it:
myApp.directive("updateCandidatePopup", UpdateCandidatePopup);
This is how I use the directive in the mainpage
<update-candidate-popup value="SingleCandidate" class="modal fade" ng-model="SingleCandidate"
id="myUpdateModal"
role="dialog"
popup-data="SingleCandidate">
zxc</update-candidate-popup>
This is the UpdateCandidatePopup.html:
<div> {{SingleCandidate.FirstName}} </div>
This is the to display the data in the pop up controller: (FYI it is still trimmed)
myApp.controller('CandidatesController', function ($scope, $http, EmployerService, CandidateService) { //we injected localservice
//Select single data for update
$scope.getSingleData = function (C_ID) {
alert(C_ID);
$http.get('http://localhost:49921/api/Candidates/?C_ID=' + C_ID).success(function (data) {
$scope.SingleCandidate = data;
$scope.FName = $scope.SingleCandidate.FirstName;
alert($scope.SingleCandidate.FirstName);
alert($scope.FName);
}).error(function () {
$scope.error = "An Error has occured while loading posts!";
});
};
});
Sorry wrong !, answered your question, here I leave I found a code that will serve for your problem. In the background to the template you want to take, you let a controller and in the statement of your policy, put you going to do with those values, I think in your case is just printing.
myApp.directive('editkeyvalue', function() {
return {
restrict: 'E',
replace: true,
scope: {
key: '=',
value: '=',
accept: "&"
},
template : '<div><label class="control-label">{{key}}</label>' +
'<label class="control-label">{{key}}</label>' +
'<input type="text" ng-model="value" />'+
'<button type="button" x-ng-click="cancel()">CANCEL</button>' +
'<button type="submit" x-ng-click="save()">SAVE</button></div>',
controller: function($scope, $element, $attrs, $location) {
$scope.save= function() {
console.log('from directive', $scope.key, $scope.value);
$scope.accept()
};
}
}
});
jsFiddle
Solved the problem like below. It was only to inject to $scope in the directive controller.
myApp.directive("updateCandidatePopup", function () {
return {
templateUrl : "UpdateCandidatePopup.html",
restrict: 'E',
controller: function ($scope) {
}
}
});
I've a directive that creates an input text element and uses ui bootstrap directive to attach typeahead functionality to the input field.
This input field is dynamically appended to one of the field on the form on dom ready event. I've to do this since, I don't have access to edit/modify html page generated by server. i.e - Dynamically add a typeahead field using angularjs and bootstrap angularjs as well.
I'm using ui boostrap - v0.12.0, angularjs version - v1.2.26 and jquery - v1.8.3
Problem: the directive is not working (or may be not correctly compiled or access scope) in IE 11, whereas works perfectly in chrome browser without any problem. I can see the appended elements on form load with no errors or exceptions on the console, however no typeahead magic.
here's the what i've -
// added required js references
// initialize angular app
var typeAheadApp = angular.module("typeAheadApp", ['smart-table', 'ui.bootstrap']);
controller:
typeAheadApp.controller('TypeaheadCtrl', ['$scope', '$http', '$compile', function ($scope, $http, $compile) {
$scope.getCategoriesSize = 1;
$scope.categorylkp = getCategoryField().val();
$scope.getCategories = function (val) {
return $http({
url: "/some/data/source/url",
method: 'GET',
headers: {
"Accept": "application/json;odata=verbose"
}
}).then(function (response) {
$scope.getCategoriesSize = response.data.d.results.length;
return response.data.d.results.map(function (item) {
return item.categoryName;
});
}, function (ex) {
alert("ERROR!!");
});
};
$scope.selectedCategory = function (item, model, label) {
getCategoryField().val(label);
};
$scope.updateCategory = function (setVal) {
getCategoryField().val(setVal);
};
}]);
directive:
typeAheadApp.directive('categoryLookup', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var typeAheadTemplate = angular.element('<div class="form-inline">' +
'<input id="categorylkpTxt" type="text" ng-model="categorylkp" ng-change="updateCategory(categorylkp)" typeahead="category for category in getCategories($viewValue)" typeahead-on-select="selectedCategory($item, $model, $label)" typeahead-min-length="3" typeahead-loading="loadingCategories" style="width: 345px;" autocomplete="off">' +
'</div>' +
'<div ng-show="loadingCategories">' +
'<i class="icon-refresh"></i> Loading...' +
'</div>' +
'<div ng-show="!getCategoriesSize">' +
'<i class="icon-remove"></i> No Results Found ' +
'</div>');
var compiled = $compile(angular.element('<div>').append(typeAheadTemplate).html())(scope);
element.append(compiled);
}
}
}]);
init function:
function initTypeAhead(){
var typeAheadField = getCategoryField(); // some field on the form
typeAheadField.parent().append('<div id="typeAheadEl"><div ng-controller="TypeaheadCtrl"><div id="category-lookup" class="custom-typeahead" category-lookup></div></div></div>');
// manual bootstrapping the angular
angular.bootstrap($('#typeAheadEl'), ['typeAheadApp']);
}
angular.element(document).ready(function() {
initTypeAhead();
});
Any advise or comments ?
Thanks in advance!
I'd start fixing it from category lookup directive as it looks rather messy, you are compiling in link method what should be in template
typeAheadApp.directive('categoryLookup', function () {
return {
restrict: 'A',
template: '<div class="form-inline">' +
'<input id="categorylkpTxt" type="text" ng-model="categorylkp" ng-change="updateCategory(categorylkp)" typeahead="category for category in getCategories($viewValue)" typeahead-on-select="selectedCategory($item, $model, $label)" typeahead-min-length="3" typeahead-loading="loadingCategories" style="width: 345px;" autocomplete="off">' +
'</div>' +
'<div ng-show="loadingCategories">' +
'<i class="icon-refresh"></i> Loading...' +
'</div>' +
'<div ng-show="!getCategoriesSize">' +
'<i class="icon-remove"></i> No Results Found ' +
'</div>',
controller: 'TypeaheadCtrl'
}
});
and then init function
function initTypeAhead(){
var typeAheadField = getCategoryField(); // some field on the form
typeAheadField.parent().append('<div id="typeAheadEl"><div id="category-lookup" class="custom-typeahead" category-lookup></div></div>');
// manual bootstrapping the angular
angular.bootstrap($('#typeAheadEl'), ['typeAheadApp']);
}
angular.element(document).ready(function() {
initTypeAhead();
});
I am writing an AngularJS directive that makes it easy for me to create swipeable pages (such as the viewpager on Android). I use SwipeJS as library for this.
This is how I use the directive
<ng-swipe ng-if="model.messages">
<div class="page" ng-repeat="message in model.messages">
{{message.title}}
</div>
</ng-swipe>
And the code of the directive looks like this.
var swipe = angular.module('ngSwipe', []);
swipe.directive('ngSwipe', function() {
return {
restrict: 'EA',
replace: false,
transclude: true,
scope: {},
template:
'<div>' +
' <div id="slider" class="swipe">' +
' <div class="swipe-wrap" ng-transclude></div>' +
' </div>' +
' <div class="pagecontrol">' +
' <div class="pagedot" ng-repeat="p in swipe.pages" ng-click="swipe.switchPage($index)"></div>' +
' </div>' +
'</div>',
link: function($scope, $element, $attrs) {
var $model = $scope.swipe = {
pages: [],
switchPage: function(index) {
$model.swipe.slide(index);
}
}
setTimeout(function() {
$model.swipe = new Swipe(document.getElementById('slider'), {
continuous: false,
callback: function(index, elem) {
$model.currentTab = index;
}
});
for(var i=0; i<$model.swipe.getNumSlides(); i++) {
$model.pages.push(i);
}
$scope.$apply();
}, 0);
}
};
});
First of all, the reason I use ng-if in the ng-swipe directive is because the link method should be invoked after my messages are loaded. The messages are being retrieved from the server and it can take up to 2 seconds before they are retrieved. If I don't wait untill the messages are loaded, the new Swipe() object will be created but it won't find pages so it will not work.
But besides that, you can also see that I have a setTimeout() function of 0 milliseconds. If I don't use that one, it just doesn't render the swipe pages.
I made a JSFiddle for this issue. Thanks in advance!
switchPage: function(index) {
$model.swipe.slide(index);
$scope.$apply();
}
I have two element-level directives, a search box and a search results. My markup is something like this (simplified):
<catalogue-search-box query="{{query}}">
<catalogue-search-results></catalogue-search-results>
I'm trying to access the search box controller from the search results directive, but the documentation suggests that in the directive's require property I can only find controllers on the same element or on the parent element. Is there a way to find controllers on adjacent elements?
After you comments here is how I would do it: use an object to hold all your state and pass it to both directives. Demo plunker
HTML
<body ng-controller="MySearchController">
<search-box search="mySearch"></search-box>
<search-results search="mySearch"></search-results>
</body>
JS
var search = angular.module('search', []);
//simulated service
search.service('Search', ['$timeout', '$q', function($timeout, $q) {
return {
findByQuery : function(query) {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve([query + ' result1', query + ' result2']);
console.log('resolved query ' + query);
}, 2000);
return deferred.promise;
}
};
}]);
search.controller('MySearchController', ['$scope', function($scope) {
$scope.mySearch = {
query : ''
}
}]);
search.controller('SearchBoxCtrl', ['$scope', 'Search', function($scope, Search) {
$scope.execute = function(search) {
console.log(search);
if(search.query && search.query.length > 3 && !search.running) {
search.running = true;
search.promise = Search.findByQuery(search.query).then(function(val) {
search.results = val;
});
}
};
}]);
search.directive('searchBox', function(){
return {
restrict: 'E',
scope : {
search : '='
},
controller: 'SearchBoxCtrl',
template : '<div ng-hide="search.results">Query: <input type="text" ng-model="search.query" ng-disabled="search.running"></input> <button ng-click="execute(search)" ng-disabled="search.running">Search</button></div>',
replace: 'true'
};
});
search.controller('SearchResultsCtrl', function(){
});
search.directive('searchResults', function(){
return {
restrict: 'E',
scope : {
search : '='
},
controller: 'SearchResultsCtrl',
template : '<div ng-show="search.results"><div ng-repeat="result in search.results">{{result}}</div></div>',
replace: true,
link : function(scope, element, attrs, ctrl){
}
};
});
PS:
Don't use p tags in directive templates as the root node. The html parser reports 2 nodes if you have p child nodes and angular has a requirement for a single root node.
You can further use the promise in the controller to register other functions to execute when the results come in.
One way I've been experimenting with since the question is having some kind of controller directive i.e.
<catalogue-search>
<catalogue-search-box query="{{query}}">
<catalogue-search-results></catalogue-search-results>
</catalogue-search>
I can then access the "controller directive" this using the parent (^) modifier in my require statement. Each directive can then talk to each other via the controller directive.
Does this seem sensible or is it overcomplicating things?