AngularJS digest cycle limit with directive template and trusted resource - javascript

It seems like the directive is replacing the template, then running the trust resource function, which updates the source, then it is updating the template again and getting stuck in a loop. But this is the recommended method?? The html:
<player videos='[{"type":"mp4","src":"http://vjs.zencdn.net/v/oceans.mp4","poster":"http://www.videojs.com/img/poster.jpg","captions":"http://www.videojs.com/vtt/captions.vtt"},{"type":"webm","src":"http://vjs.zencdn.net/v/oceans.webm"}]' />
And this is the javascript:
module.directive('player', ['$sce', function ($sce) {
'use strict';
return {
restrict: 'E',
scope: {
videos: '='
},
link: function (scope, element, attrs) {
scope.trustSrc = function(src) {
return $sce.trustAsResourceUrl(src);
}
},
template: '<video preload="none" poster="{{ trustSrc(videos[0].poster) }}">' +
'<source ng-repeat="item in videos" ng-src="{{ trustSrc(item.src) }}" type="video/{{ item.type }}" />' +
'<track kind="captions" ng-src="{{ trustSrc(videos[0].captions) }}" srclang="en" label="English" />' +
'</video>'
};
}]);
And here is a working fiddle:
http://jsfiddle.net/kmturley/mgosw7kx/4/

You are using bi-directional bind between a string in your videos attribute and a $scope variable via scope: { videos: '=' } which is not how it should be used.
As per documentation:
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value
of the attr attribute. If no attr name is specified then the attribute
name is assumed to be the same as the local name.
You can either:
scope: {},
link: function(scope, element, attr) {
scope.videos = attr['videos'];
}
Or use '&' which will return a function to execute in the context of the parent scope:
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name.
In this case you would have to do:
scope: {videos: '&'},
link: function(scope, element, attr) {
scope.videos = scope.videos();
}
More info on isolated scopes: $compile

Related

Reuse directive multiplie times with dynamic attributes in another directive's template

What i want to do is to be able to use a directive with different attributes in the same ng-app. The main goal is to run different code when the directive's input (ng-model) changes.
This is what i have now:
app.directive('customInput',
function ($compile) {
var customInputDefinitionObject = {
restrict: 'E',
replace: true,
scope: {
ident: '#'
},
template: '<input type="text" >',
controller: 'customInputController',
compile: function (tElement, tAttrs) {
$('input').removeAttr('ident')
.attr('ng-model', tAttrs.ident)
.attr('ng-change', tAttrs.ident + 'Change()');
var elemLinkFn = $compile(tElement);
return function (scope, element) {
elemLinkFn(scope, function (clone) {
element.replaceWith(clone);
})
}
}
}
return customInputDefinitionObject;
});
It works well in html e.g.:
<custom-input ident="var1"></custom-input>
<custom-input ident="var2"></custom-input>
i'm going to get to input with different ng-model and ng-change function, the controller uses dynamic names to get the $scope variables( $scope.var1Change).
The problem start when i want to use this directive inside another template.
app.directive('customInputGroup', function ($compile) {
var customInputGroupDefinitonObject = {
restrict: 'E',
replace: true,
scope: {
rident: '#',
},
template:''+
'<div>'+
'<custom-input id="first"></custom-input>'+
'<custom-input id="second"></custom-input>'+
'</div>',
controller: 'customInputGroupController',
compile: function (elem, attrs) {
$('#first', elem).removeAttr('id').attr('ident', attrs.rident + 'Start');
$('#second', elem).removeAttr('id').attr('ident', attrs.rident + 'End');
var rangeLinkFn = $compile(elem);
return function (scope, element) {
rangeLinkFn(scope, function (clone) {
element.replaceWith(clone);
})
}
}
}
return customInputGroupDefinitonObject;
});
In this case if i'm going to use it inside the HTML e.g.:
<custom-input-group rident='sg'></custom-input-group>
what i get rendered:
<div>
<input ng-model="sgEnd" ng-change="sgEndChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
</div>
For the 3rd rendered input the ng-change does not working.
If set terminal:ture in the inputGroup directive i get only to "input" rendered but both of them has the same ng-model and ng-change.
So how can i make it to render something like this:
<div>
<input ng-model="sgStart" ng-change="sgStartChange()">
<input ng-model="sgEnd" ng-change="sgEndChange()">
</div>
And if u know how would u be so nice to let me know only the "how" but the "why" aswell.
Thank you in advance.

AngularJS : How to pass an object from the directive to transcluded template

I have a directive that creates a UI that allows the user to perform a search. The directive wraps content which will transclude and become the template for each individual search result. Something like this:
<search>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
I'd like the ng-click to call the selectResult function on the controller's scope, but have the result object come from the directive. How can I accomplish this with an isolated scope in the directive?
Instead of using ng-transclude, you can build your own search transclude directive that can be used to put result onto the transcluded scope. For example, your search directive might look something like this with ng-repeat and the search-transclude directive where you want the transcluded content:
.directive("search", function (SearchResults) {
return {
restrict: "AE",
transclude: true,
scope: {},
template: '<div ng-repeat="result in results">Search Relevance:' +
'{{result.relevance}}' +
//the most important part search-transclude that receives the current
//result of ng-repeat
'<div search-transclude result="result"></div></div>',
link: function (scope, elem, attrs) {
//get search results
scope.results = SearchResults.results;
}
}
})
Build search transclude directive as follows:
.directive("searchTransclude", function () {
return {
restrict: "A",
link: function (scope, elem, attrs, ctrl, $transclude) {
//create a new scope that inherits from the parent of the
//search directive ($parent.$parent) so that result can be used with other
//items within that scope (e.g. selectResult)
var newScope = scope.$parent.$parent.$new();
//put result from isolate to be available to transcluded content
newScope.result = scope.$eval(attrs.result);
$transclude(newScope, function (clone) {
elem.append(clone);
});
}
}
})
The transcluded content will now be able to see selectResult function if it exists in the scope where the search directive was created. Example here.
Transcluded content will always use the scope in which the directive element resides, i.e. your controller scope. That's why if you want the result argument of selectResult function to get it's value from isolated scope, then you need to establish two way binding between isolated scope's and controller scope's result properties. After setting the result property to desired value in isolated scope the controller's scope result property will be updated to the same value. So , transcluded content will use controller's result which is in sync with isolated scope's result.
1) add resultAttr='result' attribute to directive element.
<search resultAttr='result'>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
2) establish two-way binding for result property when you define isolated scope in directive:
scope: {
result: "=resultAttr"
}
3) set result to some value in directive
I'd like the ng-click [in the directive] to call the selectResult
function on the controller's scope...
To pass functions (or properties) into an isolate scope, you use attributes on the directive tag.
...but have the result object come from the directive [scope].
If you want the contents of a directive tag to have access to the directive's scope, you DON'T use transclude. Specifying transclude: true tells angular NOT to allow the contents of a directive tag to have access to the directive's scope--the opposite of what you want.
To accomplish #1, you could make the user specify the template like this:
<div ng-controller="MainCtrl">
<search external-func='selectResult'>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
</div>
Note, the user needs to add an extra attribute to the <search> tag. Yet, that html may comport better with angular's philosophy that the html should give hints to the developer about what javascript will operate on the elements.
Then you specify the isolate scope like this:
scope: {
selectResult: '=externalFunc'
},
To accomplish #2, don't specify transclude: true in the directive:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectResult = function(result) {
console.log("In MainCtrl: " + result.Name);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
selectResult: '=externalFunc'
},
template: function(element, attrs) {
// ^ ^
// | |
// directive tag --+ +-- directive tag's attributes
var inner_div = element.children();
inner_div.attr('ng-repeat', 'result in results')
//console.log("Inside template func: " + element.html());
return element.html(); //Must return a string. The return value replaces the innerHTML of the directive tag.
},
controller: 'DirectiveCtrl'
}
}]);
The html could provide an even better record of what the javascript does if you make the user specify their template in more detail:
<search external-func='selectResult'>
<div class="someStyle"
ng-click="selectResult(result)"
ng-repeat="result in results">{{result.Name}}
</div>
</search>
But if you insist on the minimalist html:
<search>
<div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>
...then you can dynamically add the ng-repeat attribute(as shown above), and it's also possible to dynamically map an external function to the isolate scope:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectDog = function(result) {
console.log("In MainCtrl: you clicked " + result.Name);
};
$scope.greet = function(result) {
console.log('MainCtrl: ' + result.Name);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
externalFunc: '&externalFunc' //Cannot write => externalFunc: '&'
}, //because the attribute name is
//'external-func', which means
//the left hand side would have to be external-func.
template: function(element, attrs) {
//Retrieve function specified by ng-click:
var inner_div = element.children();
var ng_click_val = inner_div.attr('ng-click'); //==>"selectResult(result)"
//Add the outer_scope<==>inner_scope mapping to the directive tag:
//element.attr('external', ng_click_val); //=> No worky! Angular does not create the mapping.
//But this works:
attrs.$set('externalFunc', ng_click_val) //=> external-func="selectResult(result)"
//attrs.$set('external-func', ng_click_val); //=> No worky!
//Change ng-click val to use the correct call format:
var func_args = ng_click_val.substring(ng_click_val.indexOf('(')); //=> (result)
func_args = func_args.replace(/[\(]([^\)]*)[\)]/, "({$1: $1})"); //=> ({result: result})
inner_div.attr('ng-click', 'externalFunc' + func_args); //=> ng-click="externalFunc({result: result})"
//Dynamically add an ng-repeat attribute:
inner_div.attr('ng-repeat', 'result in results')
console.log("Template: " + element[0].outerHTML);
return element.html();
},
controller: 'DirectiveCtrl'
}
})
If you want to call the external function with more than one argument, you can do this:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.selectResult = function(result, index) {
console.log("In MainCtrl: you clicked "
+ result.Name
+ " "
+ index);
};
}]);
app.controller('DirectiveCtrl', ['$scope', function($scope) {
$scope.results = [
{Name: "Mr. Result"},
{Name: "Mrs. Result"}
]
}]);
app.directive('search', function() {
return {
restrict: 'E',
scope: {
external: '='
},
template: function(element, attrs) {
//Extract function name specified by ng-click:
var inner_div = element.children();
var ng_click_val = inner_div.attr('ng-click'); //=>"selectResult(result, $index)"
var external_func_name = ng_click_val.substring(0, ng_click_val.indexOf('(') ); //=> selectResult
external_func_name = external_func_name.trim();
//Add the outer_scope<==>inner_scope mapping to the directive tag:
//element.attr('externalFunc', ng_click_val); => No worky!
attrs.$set('external', external_func_name); //=> external="selectResult"
//Change name of ng-click function to 'external':
ng_click_val = ng_click_val.replace(/[^(]+/, 'external');
inner_div.attr('ng-click', ng_click_val);
//Dynamically add ng-repeat to div:
inner_div.attr('ng-repeat', 'result in results');
console.log("Template: " + element[0].outerHTML);
return element.html();
},
controller: 'DirectiveCtrl'
}
});

Directive Isolate Scope 1.2.2

I'm working with Angular version 1.2.2 for the first time and trying to make a simple directive that uses isolate scope with '=' binding to pass in an object. I've done this a few times before so I'm wondering if maybe there was a change in 1.2.2 that changed this?
Here is my directive:
.directive('vendorSelector', function (VendorFactory) {
return {
restrict: 'E',
replace: true,
scope: { vendorId: '=' },
template: '<select ng-model="vendorId" ng-options="id for id in vendorIds">' +
'<option value="">-- choose vendor --</option>' +
'</select>',
link: function (scope, element, attrs) {
VendorFactory.getVendorIds().then(function(result) {
scope.vendorIds = result;
});
}
}
})
My HTML template using the directive is as follows:
<div class="padding">
<vendor-selector vendorId="someValue"></vendor-selector>
{{ someValue }}
</div>
And the backing controller:
.controller('AddProductController', function($scope, ProductFactory, AlertFactory) {
$scope.vendorId = 0;
$scope.someValue = undefined;
})
I've tried using both $scope.someValue and $scope.vendorId as the supplied object in the html template. In both cases the error I'm getting back is Expression 'undefined' used with directive 'vendorSelector' is non-assignable!. Am I missing something obvious that is preventing these values from being 2-way bound in the isolate scope?
In your html:
<vendor-selector vendorId="someValue"></vendor-selector>
Change vendorId="someValue"
to vendor-id="someValue"
HTML attributes are case insensitive so to avoid confusion Angular converts all camel cased variables (vendorId) to snake case attributes (vendor-id).
So someValue wasn't bound to vendorId. Resulting in vendorId being undefined in the template. And thus your error.

AngularJS The scope for dynamic content through $compile isn't attached to the controller scope

When I generate a new element through a string that has a directive (that's why I need to compile) and that directive generates an association with a variable in the controller scope through "=", the variable in my controller isn't associated to the one in the directive.
I created a jsfiddle to show the example where the "door" ng-model value should be associated to all the directives model values.
See this fiddle: http://jsfiddle.net/aVJqU/2/
Another thing I notice is that the directive that run from elements present in the html show the correct association through the variables (controller and directive).
The html (there is the directive that binds <door>):
<body ng-app="animateApp">
<div ng-controller="tst">
<h2> Controller with its model </h2>
<input ng-model="doorval" type="text"> </input>
{{doorval}}
<h2> Directive render directly from the html </h2>
<door doorvalue="doorval"></door> <key></key>
<h2> Directives that are compiled </h2>
<list-actions actions="actions"></list-actions>
</div>
</body>
This is the directive:
animateAppModule.directive('door', function () {
return {
restrict: "E",
scope: {
doorvalue:"="
},
template: '<span>Open the door <input type="text" ng-model="doorvalue"> </input> {{doorvalue}}</span>',
replace: true
}
})
This is the controller:
var animateAppModule = angular.module('animateApp', [])
animateAppModule.controller('tst', function ($scope, tmplService) {
$scope.doorval = "open"
$scope.actions = tmplService;
})
animateAppModule.service('tmplService', function () {
return [{
form_layout: '<door doorvalue="doorval"></door> <key></key>'
}, {
form_layout: '<door doorvalue="doorval"></door> with this <key></key>'
}]
})
And finally this is the directive that compiles the string that has the directive that doesn't bind:
animateAppModule.directive('listActions', function ($compile) {
return {
restrict: "E",
replace: true,
template: '<ul></ul>',
scope: {
actions: '='
},
link: function (scope, iElement, iAttrs) {
scope.$watch('actions', function (neww, old,scope) {
var _actions = scope.actions;
for (var i = 0; i < _actions.length; i++) {
//iElement.append('<li>'+ _actions[i].form_layout + '</li>');
//$compile(iElement.contents())(scope)
iElement.append($compile('<li>' + _actions[i].form_layout + '</li>')(scope))
}
})
}
}
})
What can I do to bind all the "door" ng-model values together?
Where is the compiled directive binding to?
You just have to pass the doorval reference down through all directives without skip any one. The problem was the listActions directive didn't had access to doorval in its scope.
Check this out: http://jsfiddle.net/aVJqU/5/
#Danypype is basically correct as the problem occurs due to scope isolation, as explained in the documentation.
An alternative solution is to simply eliminate the scope isolation by removing the scope block from within the directive definition.

AngularJS: How to pass arguments/functions to a directive?

Look at this Fiddle, what do I have to change, that the expressions in the template get evaluated using the arguments I defined in the HTML? The SAVE-button should call the blabla()-function of the controller, since I pass it?
var myApp = angular.module('MyApp',[])
myApp.directive('editkeyvalue', function() {
return {
restrict: 'E',
replace: true,
scope: {
accept: "expression"
},
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() {
$scope.accept();
};
}
}
});
I do not really see through that. Thanks for help!
You can set two way data binding with property: '=' as Roy suggests. So if you want both key and value bound to the local scope you would do
scope: {
key: '=',
value: '='
},
Since you are passing these values, you have access to them in your directive's controller. But in case you want to run a function in the context of the parent scope, which seems to be what you want to do with the accept attribute, then you would need to tell angular like this
scope: {
accept: "&"
}
Now, from your save method you could call the function passed via accept
controller: function($scope, $element, $attrs, $location) {
$scope.save= function() {
$scope.accept()
};
}
Here's a jsfiddle
scope: {
accept: "&"
}
Use lowercase letters for function names, otherwise it doesn't work.
Just a quick note that you dont need the wrapping function save. Just call this in the template:
'<button type="submit" x-ng-click="accept()">SAVE</button></div>',
That transposes the function call and passes the parameters as expected.
This simplifies code and makes it a lot easier to read.

Categories

Resources