Passing an object as attribute to compiled directive on the fly - angularjs

I have a angular element on the page which needs to communicate with the rest of the non angular page elements.
I am creating directive elements on the fly, and appending it to its target div. I am trying to pass that created directive an object (ajax object), which contains just attributes.
The issue is that I can't figure out how to pass just this ajax object to the directive, as $compile requires a scope. When the http finishes, and because i have to use = in the directive, the directives are being over-ridden.
Please see my plunk: https://plnkr.co/edit/brTWgUWTotI44tECZXlQ ( sorry about the images ). Click the <button> to trigger the directive.
(function() {
'use strict';
var CHANNEL = 'podOverlay';
angular.module('CavernUI', [])
.controller('CavernCtrl', function($scope,getItemService) {
$scope.model = {};
var _pods = $scope.model.pods = {};
function getData(selector) {
$(selector).each(function(i, pod) {
_pods[+pod.dataset.item] = {
$: $(pod)
};
});
Object.keys($scope.model.pods).map(function(key) {
getItemService.getItem(key).success(function(response) {
_pods[key] = angular.extend(_pods[key], response);
$scope.$broadcast(CHANNEL, _pods[key], $scope);
});
})
}
$scope.runPodCheck = function(selector) {
getData(selector);
}
})
.directive('podchecker', function($compile) {
var createOverlay = function(e,data,scope){
scope.data = data;
// can i just pass data rather than scope.data?
// If I pass the scope, then when another $broadcast happens
// the scope updates, wiping out the last scope change.
// Scope here really needs to be a static object that's
// created purely for the hand off. But I don't know if
// that can be done.
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(scope));
}
return {
restrict: 'E',
scope: {
check: '&',
},
templateUrl: 'tpl.html',
link: function(scope,elm,attr){
scope.$on(CHANNEL,createOverlay);
}
};
})
.directive('overlay', function() {
return {
restrict: 'E',
scope: {
o: '=data' // here is the problem.
},
template: '<div class="overlay"><img ng-src="{{o.images.IT[0]}}"/></div>',
link: function(scope, elm, attr) {
}
}
})
.service('getItemService', ['$http', function($http) {
this.getItem = function(itemId) {
return $http({
method: 'GET',
url: 'https://www.aussiebum.com/ajaxproc/item',
params: {
id: itemId,
ajxop: 1
},
});
};
}]);
}());
Edits:
Expected ouput:

I'm not sure this is the best approach, but one way might be to manually create a new scope for each of the overlays.
So changed this:
var createOverlay = function(e,data,scope){
scope.data = data;
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(scope));
}
to this:
var createOverlay = function(e,data,scope){
var overlayScope = scope.$new(false); // use true here for isolate scope, false to inherit from parent
overlayScope.data = data;
angular.element(data.$[0]).empty().append($compile('<overlay data="data"></overlay>')(overlayScope));
}
Updated Plnkr: https://plnkr.co/edit/wBQ1cqVKfSqwqf04SnPP
More info about $new()
Cheers!

Related

Unable to call Angular Directiive Method on Button Click

I'm trying to call directive method on button click from the calling controller.
Here is the directive code:
myApp.directive("helloDirective", function() {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
controller: 'helloDirectiveController',
controllerAs: 'model'
}
})
.controller("helloDirectiveController", function() {
var self = this;
self.actions = {
get: function() {
return self.msg;
},
set: function(msgData) {
self.msg = msgData;
}
});
I have call the get and set method from controller..
myApp.controller("indexController", [function() {
var self = this;
self.helloParam ={};
self.get = function() {
//how to call the Directive get method from here
}
}]);
i tried to create a fiddle here
plnkr
The idea
For me, the cleanest solution (so far) for your problem is a solution used by Angular Material developers (I don't know if they were the authors, but I found it there). I've used it once in my project and it worked like a charm.
The idea is to create a global registry for directives' actions. Directives would be stored there by unique ids. We can also create a service dedicated for each directive, just in case we need some external logic.
The solution
1. Components registry
Firstly, we need a components registry. It can be a really simple service:
angular.module('app');
.service('$componentsRegistry', function() {
var self = this;
var components = {};
self.put = function(id, actions) {
components[id] = actions;
}
self.get = function(id) {
return components[id];
}
})
The components registry has methods for storing and getting components by ids. Of course, there might be much more methods and they might be more complicated, but this is just a simple example.
2. Service for our directive
Let's say we have a simple show-message directive, so we can create a $showMessageService:
angular.module('app')
.service('$showMessageService', ['$componentsRegistry', function($componentsRegistry) {
return function(id) {
return $componentsRegistry.get(id);
}
}])
For now, the only task of the service is to return our directive's actions. But it can be extended in the future, of course.
3. Directive's code
The last thing we need is our show-message directive:
angular.module('app')
.directive('showMessage', function($componentsRegistry) {
return {
restrict: 'E',
scope: {
directiveId: '#' // Unique id is passed from the view
},
template: '<div>{{ text }}</div>',
link: function(scope) {
scope.text = 'TEST TEXT';
// Create actions
scope.actions = {
set: function(value) {
scope.text = value;
}
}
// Store actions in the components registry
$componentsRegistry.put(scope.directiveId, scope.actions);
}
}
})
In a link function, we need to simply register our actions in components registry. We pass the unique id from the view so that developer has control over it inside views/controllers.
Example of usage
And now we can finally use the directive in our application. Here is a simple code which shows how to do that:
View
<div ng-controller="testController as test">
<show-message directive-id="testDirective"></show-message>
<button ng-click="test.changeText()">CHANGE</button>
</div>
Controller
angular.module('app')
.controller('testController', function(['$showMessageService', $showMessageService) {
var self = this;
self.changeText = function() {
$showMessageService('testDirective').set('NEW');
}
}])
I've Change The Directive Code, moved Controller code to link and it's working fine.
testApp.directive("helloDirective", function () {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
link: function (scope, element, attrs) {
var self = scope.model;
var assignMethod = function () {
if (self.param == undefined) {
self.param = {};
}
self.param.actions = {
get: function () {
return self.msg;
},
set: function (msgData) {
self.msg = msgData;
}
};
};
assignMethod();
},
controller: function () { },
controllerAs: 'model'
}
});
now i can call the directive get Method from calling controller like,
self.helloParam = {};
self.click = function () {
alert(self.helloParam.actions.get());
}

Passing asynchronously obtained data to a directive

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>

Directive does not update on service model change

I have a service that contains my application's model:
angular.module('adminClient').factory('myApi', function () {
var model = {};
model.myObject = {
label: 'New York City'
};
return {
model: function () {
return model;
}
};
});
A partial/controller has access to the model and can set a new myObject:
angular.module('adminClient').controller('MySelectNetworkCtrl', function ($scope, myApi) {
$scope.myObject = myApi.model().myObject;
});
In addition to this, I have a directive that should display the label contained in myObject:
angular.module('nav').directive('myTopBar', function(myApi) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: 'nav/directive/my-top-bar/my-top-bar.html',
link: function(scope, element, attrs, fn) {
scope.myObject = myApi.model().myObject;
}
};
});
And here is the HTML:
<div id="my-top-bar">
<span ng-bind="myObject"></span>
</div>
When I run the application, the label is displayed fine (New York City), but as soon as the controller changes the myObject, the label in the directive remains unchanged. I can also see this in the Chrome Angular scope inspector.
Any idea how I can make the directive display the current value even after changed by the controller?
It all depends upon how the controller is updating the model object. Since your code
scope.myObject = myApi.model().myObject;
gets hold of this specific model object it works fine as long as model object myObject properties are changed.
But if the controller does
myApi.model().myObject= {}; //or any new object
Now the model returned by service has different myObject and the directive one is different. Therefore changes do not work.
Instead in directive do:
scope.myObject = myApi.model(); // bind to model
And update the bindings in the directive template accordingly. See if it works
Check this plunkr
If you want to modify your myObject inside your controller, and also want this change to be reflected inside your my-top-bar directive which then updates the directive's scope from myApi service, then you should do it like this.
Update the myObject in your controller and then also update it in your service through a setter method.
// Doing this will update your scope.myObject but
// will not change the moObject in your service
$scope.myObject = {
label: 'updated label'
};
// This will update your service, and anyone making a myApi.myApi.model() after this will get an updated myObject
myApi.setModel($scope.myObject);
// Notify directive of the change
$scope.$broadcast('updateDirective');
And in the factory:
angular.module('plunker').factory('myApi', function() {
var model = {};
model.myObject = {
label: 'New York City'
};
return {
model: function() {
return model;
},
setModel: function(newModel) {
model.myObject = newModel;
}
};
});
Finally, the directive:
angular.module('plunker').directive('myTopBar', function(myApi) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: 'top-bar.html',
link: function(scope, element, attrs, fn) {
console.log('in directive link', myApi.model().myObject);
scope.myObject = myApi.model().myObject;
},
controller: function($scope) {
$scope.$on('updateDirective', function() {
console.log('updating the scope of directive so that ng-bind works');
$scope.$apply(function() {
$scope.myObject = myApi.model().myObject;
console.log($scope.myObject);
});
});
}
};
});

How to dynamically inject service into directive in angularjs

My directive is to build out a scaffolded table with a delete button. I have a service set up named Post, so when I call the directive I'm including a servicename property on the element. How do I dynamically inject my service based on my service name attribute I'm including? Or is there a better way to do this?
var lassoDirectives = angular.module('lassoDirectives', []);
lassoDirectives.directive('autoTable', ['parse', function(parse) {
return {
restrict: 'AE',
scope: {
data: '=',
modelname: '='
},
templateUrl: 'http://local.angular.com/webroot/app/templates/directives/auto-table.html',
controller: function($scope) {
console.log(parse);
$scope.deleteItem = function(id) {
console.log(id);
// rocket = modelname.query();
//console.log(rocket);
}
$scope.check = function() {
parse.get("Posts", 123, function(response) {
console.log(response.toString());
});
}
}
};
}]);
I'm not sure that is the best way to go around it. You should probably have a single service that is charge of all the data related to this lassoDirective, so you could just inject it as normal.
But you may be able to use the $injector provider like...
.directive('autoTable', function($injector) {
return {
restrict: 'EA',
scope: {
servicename: '='
},
controller: function(scope) {
var service = $injector.get(scope.servicename);
scope.deleteItem = function(Id) {
del = service.delete({Id: Id});
items = service.query();
};
}
}
});
If you want your service to perform differently depending on the directive clicked, it may be better to just pass your service additional parameters relating to the directive? Not sure what your situation is so ignore this common if N/A.

AngularJS accessing an attribute in a directive from a controller

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
}
};
});

Resources