AngularJS - How can I create a new, isolated scope programmatically? - angularjs

I want to create an AlertFactory with Angular.factory.
I defined an html template like follow
var template = "<h1>{{title}}</h1>";
Title is provided by calling controller and applied as follow
var compiled = $compile(template)(scope);
body.append(compiled);
So, how I can pass isolated scope from controller to factory?
I'm using in controller follow code
AlertFactory.open($scope);
But $scope is global controller scope variable. I just want pass a small scope for factory with just title property.
Thank you.

You can create a new scope manually.
You can create a new scope from $rootScope if you inject it, or just from your controller scope - this shouldn't matter as you'll be making it isolated.
var alertScope = $scope.$new(true);
alertScope.title = 'Hello';
AlertFactory.open(alertScope);
The key here is passing true to $new, which accepts one parameter for isolate, which avoids inheriting scope from the parent.
More information can be found at:
http://docs.angularjs.org/api/ng.$rootScope.Scope#$new

If you only need to interpolate things, use the $interpolate service instead of $compile, and then you won't need a scope:
myApp.factory('myService', function($interpolate) {
var template = "<h1>{{title}}</h1>";
var interpolateFn = $interpolate(template);
return {
open: function(title) {
var html = interpolateFn({ title: title });
console.log(html);
// append the html somewhere
}
}
});
Test controller:
function MyCtrl($scope, myService) {
myService.open('The Title');
}
Fiddle

Followings are the steps:
Add your HTML to the DOM by using var comiledHTML =
angular.element(yourHTML);
Create a new Scope if you want var newScope = $rootScope.$new();
Call $comile(); function which returns link function var linkFun =
$compile(comiledHTML);
Bind the new scope by calling linkFun var finalTemplate =
linkFun(newScope);
Append finalTemplate to your DOM
YourHTMLElemet.append(finalTemplate);

check out my plunkr. I'm programmatically generating a widget directive with a render directive.
https://plnkr.co/edit/5T642U9AiPr6fJthbVpD?p=preview
angular
.module('app', [])
.controller('mainCtrl', $scope => $scope.x = 'test')
.directive('widget', widget)
.directive('render', render)
function widget() {
return {
template: '<div><input ng-model="stuff"/>I say {{stuff}}</div>'
}
}
function render($compile) {
return {
template: '<button ng-click="add()">{{name}}</button><hr/>',
link: linkFn
}
function linkFn(scope, elem, attr) {
scope.name = 'Add Widget';
scope.add = () => {
const newScope = scope.$new(true);
newScope.export = (data) => alert(data);
const templ = '<div>' +
'<widget></widget>' +
'<button ng-click="export(this.stuff)">Export</button>' +
'</div>';
const compiledTempl = $compile(templ)(newScope);
elem.append(compiledTempl);
}
}
}

I assume when you are talking about an isolate scope you are talking about a directive.
Here is an example of how to do it.
http://jsfiddle.net/rgaskill/PYhGb/
var app = angular.module('test',[]);
app.controller('TestCtrl', function ($scope) {
$scope.val = 'World';
});
app.factory('AlertFactory', function () {
return {
doWork: function(scope) {
scope.title = 'Fun';
//scope.title = scope.val; //notice val doesn't exist in this scope
}
};
});
app.controller('DirCtrl', function ($scope, AlertFactory) {
AlertFactory.doWork($scope);
});
app.directive('titleVal',function () {
return {
template: '<h1>Hello {{title}}</h1>',
restrict: 'E',
controller: 'DirCtrl',
scope: {
title: '='
},
link: function() {
}
};
});
Basically, attach a controller to a directive that has defined an isolate scope. The scope injected into the directive controller will be an isolate scope. In the directive controller you can inject your AlertFactory with wich you can pass the isolate scope to.

Related

Get data back from angular directive

I've created a directive which I called my-tree, and I'm calling this directive from a view exemple-tree-view.html as following:
<my-tree ng-model="sampleTreeView.listNoeuds" ... />
this view's controller called sampleTreeView.
In my directive's link function I have a function that returns some data, which I affect to scope variable declared in the directive's controller, as following :
function linkFn(scope, element, attrs) {
//some code
scope.createNode = function ($event) {
var sel = $(element).jstree(true).create_node($($event.currentTarget)[0].closest('.jstree-node').id);
if (sel) {
$(element).jstree(true).edit(sel, '', function (node, success, cancelled) {
scope.treeActionsResult.createdNode = node;
});
}
};
//some code
}
My question is how can I get the scope.treeActionsResult.createdNode value in the sampleTreeView controller, since it's the controller for the exemple-tree-view.html where I call my directive.
You can use shared scope between the directive and controller by removing the scope property
like in this example:
MyApp.directive('studentDirective', function () {
return {
template: "{{student.name}} is {{student.age}} years old !!",
replace: true,
restrict: 'E',
controller: function ($scope) {
console.log($scope);
}
}
});
Still you have the $scope object, but in this case the scope object is shared with parent controller's scope.
You can read more about it fron the following link
Understanding Scope in AngularJs Custom Directive
If you don't create isolated scope for your directive then you can access directive scope values from your controller. like bellow
your controller and directive:
app.controller('MainCtrl', function($scope) {
$scope.value = 1;
});
app.directive('myTree', function() {
return {
restrict: 'AE',
link: function(scope, element, attrs) {
scope.values = {};
scope.values.price = 1234;
}
};
});
then use in your html like:
<body ng-controller="MainCtrl">
<p>value {{values.price}}</p>
<my-tree att="{{attValue}}"></my-tree>
</body>
here values.price shown from directive in MainCtrl

Passing an object as attribute to compiled directive on the fly

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!

Extending a controller function within directive

I have a cancel function in my controller that I want to pass or bind to a directive. This function essentially clears the form. Like this:
app.controller('MyCtrl', ['$scope', function($scope){
var self = this;
self.cancel = function(){...
$scope.formName.$setPristine();
};
}]);
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
cancel : '&onCancel'
},
templateUrl: 'form.html'
};
});
form.html
<div>
<form name="formName">
</form>
</div>
However, the $setPristine() don't work as the controller don't have access on the form DOM. Is it possible to extend the functionality of controller's cancel within the directive so that I will add $setPristine()?
Some suggested using jQuery to select the form DOM, (if it's the only way) how to do that exactly? Is there a more Angular way of doing this?
Since the <form> is inside the directive, the controller should have nothing to do with it. Knowing it would break encapsulation, i.e. leak implementation details from the directive to the controller.
A possible solution would be to pass an empty "holder" object to the directive and let the directive fill it with callback functions. I.e.:
app.controller('MyCtrl', ['$scope', function($scope) {
var self = this;
$scope.callbacks = {};
self.cancel = function() {
if( angular.isFunction($scope.callbacks.cancel) ) {
$scope.callbacks.cancel();
}
};
});
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
callbacks: '='
},
templateUrl: 'form.html',
link: function(scope) {
scope.callbacks.cancel = function() {
scope.formName.$setPristine();
};
scope.$on('$destroy', function() {
delete scope.callbacks.cancel;
});
}
};
});
Use it as:
<custom-directive callbacks="callbacks"></custom-directive>
I'm not sure I am OK with this either though...

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

Unit testing an AngularJS directive that watches an an attribute with isolate scope

I have a directive that uses an isolate scope to pass in data to a directive that changes over time. It watches for changes on that value and does some computation on each change. When I try to unit test the directive, I can not get the watch to trigger (trimmed for brevity, but the basic concept is shown below):
Directive:
angular.module('directives.file', [])
.directive('file', function() {
return {
restrict: 'E',
scope: {
data: '=',
filename: '#',
},
link: function(scope, element, attrs) {
console.log('in link');
var convertToCSV = function(newItem) { ... };
scope.$watch('data', function(newItem) {
console.log('in watch');
var csv_obj = convertToCSV(newItem);
var blob = new Blob([csv_obj], {type:'text/plain'});
var link = window.webkitURL.createObjectURL(blob);
element.html('<a href=' + link + ' download=' + attrs.filename +'>Export to CSV</a>');
}, true);
}
};
});
Test:
describe('Unit: File export', function() {
var scope;
beforeEach(module('directives.file'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
};
it('should create a CSV', function() {
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
//I've also tried below but that does not help
scope.$apply(function() { scope.input = {}; });
});
What can I do to trigger the watch so my "In watch" debugging statement is triggered? My "In link" gets triggered when I compile.
For a $watch to get triggered, a digest cycle must occur on the scope it is defined or on its parent. Since your directive creates an isolate scope, it doesn't inherit from the parent scope and thus its watchers won't get processed until you call $apply on the proper scope.
You can access the directive scope by calling scope() on the element returned by the $compile service:
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
e.isolateScope().$apply();
This jsFiddler exemplifies that.

Resources