How to create a new isolated scope with an existing object? - angularjs

I want to create a new scope with this object:
$scope.model = {
itemA: "First item",
itemB: "Second item"
};
// I know, this is wrong, but I want to show you, what I would like to do.
var newScope = $scope.$new($scope.model);
The new scope I want to access in the ngTransclude-Part of my directive:
link: function (scope, element, attrs, ctrl, transclude) {
transclude(scope.model, function (clone, scope) {
element.find('section').html("").append(clone);
});
And in the template:
<p>{{itemA}} - {{itemB}}
But this doesn´t work
I have the idea from: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/
but I don´t want to work in the scope of the directive, but in a new scope.

AFAIK when you are creating directive usually it inherits the scope. The idea is to create isolated scope and this is done by doing.
.directive('directiveName', function ($compile) {
return {
restrict: "AE",
scope:{
yourModelName:"=";
}
link: function (scope, element) {
var el = angular.element('<div>Here you put your template scope.yourModelName</div>');
$compile(el)(scope);
element.append(el);
}
};
})
Will be copied from the upper scope but you can change it in the directive without changing it in the upper scope

his is my solution I found to a similar problem. Slightly long-winded but it works!
Create a new, 'empty' class directive with its own scope.
Add this directive as a class attribute to your DOM element. It automatically takes the scope of the new directive.
In your case, you would use it on your p tag. You would then select this element in your link function and call scope() on it:
1. <p id="ID" class="my-empty-directive">{{itemA}} - {{itemB}}
2. create your new directive:
angular.module('sgComponents').directive('panelData', [function () {
return {
restrict: 'C', // a class Directive
scope: true // with its own scope
};
}]);
2. link: function (scope, element, attrs, ctrl, transclude) {
var pTag = jQuery('#ID');
var angularElement = angular.element(pTag);
var newScope = angularElement.scope(); // THIS WILL BE THE NEW EMPTY DIRECTIVE'S SCOPE

Related

Add ngModel directive to dynamic element in Javascript

Is there a way to use javascript to apply the ng-model directive to a created element? In the code below, I want the new select element to be bound using ng-model to a scoped variable inside the controller:
angular.module('myApp').directive('dynamicHtml', function ($q, $http, $compile) {
return {
restrict: 'EAC',
scope: '=',
compile: function(element, attr) {
return function(scope, element, attr) {
var select = document.createElement('select');
// Here I want to use javascript to apply ng-model='controllerVar'
// to the new select element
element.append(select);
}
}
};
});
Whoops - my apologies...I forgot to compile the thing...carry on.

Get element scope from attribute directive

I'm trying to extend functionality of any directive by simply attaching an attribute directive, but I'm having trouble getting the scope of the element on which the attribute is defined.
For example, I have this template:
<div class="flex-item-grow flex-item flex-column report-area">
<sv-report sv-reloadable id="reportId"></sv-report>
</div>
Here, sv-reloadable has some implicit understanding of sv-report, but sv-report has no idea about sv-reloadable.
I've defined sv-reloadable as:
angular
.module( 'sv-reloadable', [
'sv.services',
])
.directive('svReloadable', function(reportServices, $timeout) {
return {
restrict: 'A',
controller: function($scope, $timeout) {
$scope.$on('parameter-changed', function(evt, payload) {
evt.stopPropagation();
$scope.viewModel = getNewViewModel(payload);/* hit the server to retrieve new data */
});
}
};
});
Now, $scope in sv-reloadable is the parent scope of sv-report. I'm wanting sv-reloadable to be able to attach a listener to sv-report's scope, and swap out properties of that scope. I understand that it's possible to grab the sibling scopes, but that causes problems when trying to figure out exactly which element it's attached to.
I attempted the following:
link: function(scope, element, attrs) {
ele = element;
var actualScopyThingy = element.scope();
},
Which I had assumed would give me the scope of the element the attribute was defined on, but alas, it still returns the parent scope of the element.
If it's important, sv-report is defined as the following, but I'd like to be able to keep it the same (since sv-reloadable is going to be attached to many different elements, all of which must have viewModel defined on their scope)
return {
restrict: 'E',
replace: true,
templateUrl: 'sv-report/sv-report.tpl.html',
scope: {
id: '=',
reportParameters: '='
},
controller: function ($scope, svAnalytics) {
/* unrelated code here */
},
link: function(scope, element, attrs) {
initialLoadReport(scope);
}
};
After a bit of digging around, isolateScope() is what I was after (rather than scope()). sv-reloadable's directive becomes:
return {
restrict: 'A',
link: function(scope, element, attrs) {
var elementScope = element.isolateScope();
elementScope.$on('parameter-changed', function(evt, payload) {
...
});
}
};

Directive within another directive - scope var undefined

I'm trying to generate a smart-table directive from within a custom directive I've defined:
<div ng-controller="myContrtoller">
<containing-directive></containing-directive>
</div>
The directive definition:
angular.module('app')
.directive('containingDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<table st-table="collection" st-pipe="scopeFun"></table>',
link: function(scope, elem, attrs) {
scope.scopeFun = function () {
// solve the misteries of life
}
}
}
});
As you can see my directive tries to replace the element by the template generated by the st-table directive, using the st-pipe directive depending on the first, briefly:
ng.module('smart-table')
.controller('stTableController' function () {
// body...
})
.directive('stTable', function () {
return {
restrict: 'A',
controller: 'stTableController',
link: function (scope, element, attr, ctrl) {
// body
}
};
})
.directive('stPipe', function (config, $timeout) {
return {
require: 'stTable',
scope: {
stPipe: '='
},
link: {
pre: function (scope, element, attrs, ctrl) {
var pipePromise = null;
if (ng.isFunction(scope.stPipe)) { // THIS IS ALWAYS UNDEFINED
// DO THINGS
}
},
post: function (scope, element, attrs, ctrl) {
ctrl.pipe();
}
}
};
});
Problem:
The st-pipe directive checks the scope var stPipe if it is defined or not by: if (ng.isFunction(scope.stPipe)). This turns out to be ALWAYS undefined. By inspecting I found two things:
From the stPipe directive, the value supposed to be scope.stPipe that is my scopeFun defined within my containingDirective is undefined on the scope object BUT defined within the scope.$parent object.
If I define my $scope.scopeFun within the myContrtoller I don't have any problem, everything works.
Solution:
I did find a solutions but I don't know what really is going on:
Set replace: false in the containingDirective
Define the scope.scopeFun in the pre-link function of containingDirective
Questions:
Why is the scopeFun available in the stPipe directive scope object if defined in the controller and why it is available in the scope.$parent if defined in the containingDirective?
What is really going on with my solution, and is it possible to find a cleaner solution?
From the docs: "The replacement process migrates all of the attributes / classes from the old element to the new one" so what was happening was this:
<containing-directive whatever-attribute=whatever></containing-directive>
was being replaced with
<table st-table="collection" st-pipe="scopeFun" whatever-attribute=whatever></table>
and somehow st-table did not enjoy the extra attributes (even with no attributes at all..).
By wrapping the containingDirective directive template within another div fixed the problem (I can now use replace:true):
<div><table st-table="collection" st-pipe="scopeFun"></table></div>
If someone has a more structured answer would be really appreciated

AngularJS unit testing directive with ngModel

I have made a directive which uses ngModel:
.directive('datetimepicker', function () {
return {
restrict: 'E',
require: ['datetimepicker', '?^ngModel'],
controller: 'DateTimePickerController',
replace: true,
templateUrl: ...,
scope: {
model: '=ngModel'
},
link: function (scope, element, attributes, controllers) {
var pickerController = controllers[0];
var modelController = controllers[1];
if (modelController) {
pickerController.init(modelController);
}
}
}
});
But when testing...
var scope, element;
beforeEach(module('appDateTimePicker'));
beforeEach(module('templates'));
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope;
scope.model = new Date();
element = compile(angular.element('<datetimepicker ng-model="model"></datetimepicker>'))(scope);
scope.$digest();
}));
I can't anyhow set value to ng-model.
Fo example, here scope.model is a date, so scope.year and scope.month should be date and year of that model, but it is undefined.
As seen in the directive's code, I'm using this.init on the controller to initialise all the process.
What am I missing?
EDIT
Example of test:
it('should test', function () {
expect(scope.model).toBe(undefined);
expect(scope.year).toBe(undefined);
});
EDIT
This helped to solve the problem: http://jsfiddle.net/pTv49/3/
The '?^ngModel' mean you are asking for the ng-model on parent elements, but the html in your test has the ng-model on the same element as datetimepicker directive.
If the ng-model really have to be on parent elements, you have to change the html in the test, for example:
element = compile(angular.element('<div ng-model="model"><datetimepicker></datetimepicker></div>'))(scope);
But if it should be on the same element, just remove the ^ symbol in the require:
require: ['datetimepicker', '?ngModel'],
The directive has a scope: {} block so it creates an isolate scope. I assume, in the tests scope refers to the outer scope, while element.isolateScope() should be used to reference the inner scope instead.

How can I dynamically add a directive in AngularJS?

I have a very boiled down version of what I am doing that gets the problem across.
I have a simple directive. Whenever you click an element, it adds another one. However, it needs to be compiled first in order to render it correctly.
My research led me to $compile. But all the examples use a complicated structure that I don't really know how to apply here.
Fiddles are here: http://jsfiddle.net/paulocoelho/fBjbP/1/
And the JS is here:
var module = angular.module('testApp', [])
.directive('test', function () {
return {
restrict: 'E',
template: '<p>{{text}}</p>',
scope: {
text: '#text'
},
link:function(scope,element){
$( element ).click(function(){
// TODO: This does not do what it's supposed to :(
$(this).parent().append("<test text='n'></test>");
});
}
};
});
Solution by Josh David Miller:
http://jsfiddle.net/paulocoelho/fBjbP/2/
You have a lot of pointless jQuery in there, but the $compile service is actually super simple in this case:
.directive( 'test', function ( $compile ) {
return {
restrict: 'E',
scope: { text: '#' },
template: '<p ng-click="add()">{{text}}</p>',
controller: function ( $scope, $element ) {
$scope.add = function () {
var el = $compile( "<test text='n'></test>" )( $scope );
$element.parent().append( el );
};
}
};
});
You'll notice I refactored your directive too in order to follow some best practices. Let me know if you have questions about any of those.
In addition to perfect Riceball LEE's example of adding a new element-directive
newElement = $compile("<div my-directive='n'></div>")($scope)
$element.parent().append(newElement)
Adding a new attribute-directive to existed element could be done using this way:
Let's say you wish to add on-the-fly my-directive to the span element.
template: '<div>Hello <span>World</span></div>'
link: ($scope, $element, $attrs) ->
span = $element.find('span').clone()
span.attr('my-directive', 'my-directive')
span = $compile(span)($scope)
$element.find('span').replaceWith span
Hope that helps.
Dynamically adding directives on angularjs has two styles:
Add an angularjs directive into another directive
inserting a new element(directive)
inserting a new attribute(directive) to element
inserting a new element(directive)
it's simple. And u can use in "link" or "compile".
var newElement = $compile( "<div my-diretive='n'></div>" )( $scope );
$element.parent().append( newElement );
inserting a new attribute to element
It's hard, and make me headache within two days.
Using "$compile" will raise critical recursive error!! Maybe it should ignore the current directive when re-compiling element.
$element.$set("myDirective", "expression");
var newElement = $compile( $element )( $scope ); // critical recursive error.
var newElement = angular.copy(element); // the same error too.
$element.replaceWith( newElement );
So, I have to find a way to call the directive "link" function. It's very hard to get the useful methods which are hidden deeply inside closures.
compile: (tElement, tAttrs, transclude) ->
links = []
myDirectiveLink = $injector.get('myDirective'+'Directive')[0] #this is the way
links.push myDirectiveLink
myAnotherDirectiveLink = ($scope, $element, attrs) ->
#....
links.push myAnotherDirectiveLink
return (scope, elm, attrs, ctrl) ->
for link in links
link(scope, elm, attrs, ctrl)
Now, It's work well.
function addAttr(scope, el, attrName, attrValue) {
el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
The accepted answer by Josh David Miller works great if you are trying to dynamically add a directive that uses an inline template. However if your directive takes advantage of templateUrl his answer will not work. Here is what worked for me:
.directive('helperModal', [, "$compile", "$timeout", function ($compile, $timeout) {
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: "app/views/modal.html",
link: function (scope, element, attrs) {
scope.modalTitle = attrs.modaltitle;
scope.modalContentDirective = attrs.modalcontentdirective;
},
controller: function ($scope, $element, $attrs) {
if ($attrs.modalcontentdirective != undefined && $attrs.modalcontentdirective != '') {
var el = $compile($attrs.modalcontentdirective)($scope);
$timeout(function () {
$scope.$digest();
$element.find('.modal-body').append(el);
}, 0);
}
}
}
}]);
Josh David Miller is correct.
PCoelho, In case you're wondering what $compile does behind the scenes and how HTML output is generated from the directive, please take a look below
The $compile service compiles the fragment of HTML("< test text='n' >< / test >") that includes the directive("test" as an element) and produces a function. This function can then be executed with a scope to get the "HTML output from a directive".
var compileFunction = $compile("< test text='n' > < / test >");
var HtmlOutputFromDirective = compileFunction($scope);
More details with full code samples here:
http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs
Inspired from many of the previous answers I have came up with the following "stroman" directive that will replace itself with any other directives.
app.directive('stroman', function($compile) {
return {
link: function(scope, el, attrName) {
var newElem = angular.element('<div></div>');
// Copying all of the attributes
for (let prop in attrName.$attr) {
newElem.attr(prop, attrName[prop]);
}
el.replaceWith($compile(newElem)(scope)); // Replacing
}
};
});
Important: Register the directives that you want to use with restrict: 'C'. Like this:
app.directive('my-directive', function() {
return {
restrict: 'C',
template: 'Hi there',
};
});
You can use like this:
<stroman class="my-directive other-class" randomProperty="8"></stroman>
To get this:
<div class="my-directive other-class" randomProperty="8">Hi there</div>
Protip. If you don't want to use directives based on classes then you can change '<div></div>' to something what you like. E.g. have a fixed attribute that contains the name of the desired directive instead of class.

Resources