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

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

Related

How to add dynamic directive names from JSON with the $interpolate function

I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};

Angularjs - ngModel.$setViewValue is not a function

Here is my plunker and the code I can't get to work starts on line 32
http://plnkr.co/edit/pmCjQL39BWWowIAgj9hP?p=preview
I am trying to apply an equivalent to markdown filter onto a directive... I created the filter and tested with manually applying the filter and it works that way,, but I should only use the filter conditionally when the type of content on directive is set to markdown.
I am trying to accomplish this by updating ng-model >>> ngModel.$setViewValue(html) but I am getting an error
ngModel.$setViewValue is not a function.. which makes me thing that the controller is not recognized although it is required by the directive.
Here is a working controller:
var app = angular.module('testOne', ["ngResource", "ngSanitize"]);
app.controller('testOneCtrl', function ($scope) {
$scope.product = {
id:12,
name:'Cotton T-Shirt, 2000',
description:'### markdown\n - list item 1\n - list item 2',
price:29.99
};
});
app.directive("myText", function ($parse) {
return {
restrict: "E",
require: "?ngModel",
scope:{
css: "#class", type: "#type"
},
controller: function ($scope, $element, $attrs) {},
templateUrl: "template.html",
compile: function(elm, attrs, ngModel){
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = { val: val };
if ( attrs.type == 'markdown'){
var converter = new Showdown.converter();
var html = converter.makeHtml(val);
//scope.exp.val = html;
ngModel.$setViewValue(html);
ngModel.$render();
}
})
scope.$watch('exp.val', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
This is a filter for markdown which works when applied.. (I would consider using the filter if I could figure out the way to conditionally apply it to existing directive but I'd rather do it with ng-model)
/*
app.filter('markdown', function ($sce) {
var converter = new Showdown.converter();
return function (value) {
var html = converter.makeHtml(value || '');
return $sce.trustAsHtml(html);
};
});
*/
Here is the directive template
<div ng-class="{{css}}"
ng-click="view = !view"
ng-bind-html="exp.val">
</div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
This is the directive in use:
<mb-text ng-cloak
type="markdown"
content-type="product"
value="description"
class="test-one-text-2">
</mb-text>
Why ngModel is empty?
When using require on a directive the controller is passed as the 4th argument to the linking function. In you code you try to reference it as an argument of the compile function. The controller is only instantiated before the linking phase so it could never be passed into the compile function anyway.
The bigger issue is that require can only get a controller of the same element ({ require: 'ngModel' }), or parent elements ({ require: '^ngmodel' } ). But you need to reference a controller from a child element (within the template).
How to get ngModel?
Do not use require at all as you cannot get child element's controller with it.
From angular.element docs:
jQuery/jqLite Extras
controller(name) - retrieves the controller of the current element or its parent. By default retrieves controller associated with the ngController directive. If name is provided as camelCase directive name, then the controller for this directive will be retrieved (e.g. 'ngModel').
Inside the linking function you can get the hold of the controller like so:
var ngModel = elm.find('textarea').controller('ngModel');
I fixed your directive:
here is a plunker: http://plnkr.co/edit/xFpK7yIYZtdgGNU5K2UR?p=preview
template:
<div ng-class="{{css}}" ng-bind-html="exp.preview"> </div>
<div>
<textarea rows="4" cols="30" ng-model="exp.val"></textarea>
</div>
Directive:
app.directive("myText", function($parse) {
return {
restrict: "E",
templateUrl: "template.html",
scope: {
css: "#class",
type: "#type"
},
compile: function(elm, attrs) {
var expFn = $parse(attrs.contentType + '.' + attrs.value);
return function(scope, elm, attrs) {
scope.exp = {
val: '',
preview: null
};
if (attrs.type == 'markdown') {
var converter = new Showdown.converter();
var updatePreview = function(val) {
scope.exp.preview = converter.makeHtml(val);
return val;
};
var ngModel = elm.find('textarea').controller('ngModel');
ngModel.$formatters.push(updatePreview);
ngModel.$parsers.push(updatePreview);
}
scope.$parent.$watch(expFn, function(val) {
scope.exp.val = val;
});
scope.$watch('exp.val', function(val) {
expFn.assign(scope.$parent, val);
});
};
}
};
});

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 : Passing a function to an isolated scope of a directive to be called within its controller?

I'm trying to call a function passed from a controller's scope into a directive via the "&" operation from the directive's controller. That method, however, is claimed by Angular to be undefined. After reading my code over and over, scouring the internet, and then repeating that process, I've decided to turn to help here.
Here's the relevant part of my controller. It contains the method I pass to my directive.
angular.module('myApp.controllers', []).controller('PostCtrl', ['$scope', 'postalService', function($scope, postalService) {
$scope.posts = [];
$scope.getPosts = function() {
postalService.getPosts(function(err, posts) {
if(err);
else $scope.posts = posts;
});
};
}]);
Here's my directive. I am unable to invoke onPost.
angular.module('myApp.directives', []).directive('compose', ['postalService', function(postalService) {
return {
restrict: 'E',
transclude: false,
replace: true,
scope: {
onPost: "&" //why will it not
},
templateUrl: "partials/components/compose-partial.html",
controller: function($scope, postalService) {
$scope.title = "";
$scope.content = "";
$scope.newPost = function() {
postalService.newPost($scope.title, $scope.content, function(err) {
if(err) console.log(err + ":(");
else {
console.log("Success getting posts.");
//why can I not invoke onPost()??
$scope.onPost();
}
});
};
},
};
}]);
And here's the relevant part of my html
<div ng-controller="PostCtrl">
<section class="side-bar panel hide-for-small">
<compose onPost="getPosts()"></compose>
</section>
<!--more, non-relevant html here-->
</div>
I know the problem is not with my postalService Service. Instead, the directive reports that no function is passed to it. Why??
Replace
<compose onPost="getPosts()"></compose>
with
<compose on-post="getPosts()"></compose>
and it'll work.
The Angular docs say why it's so:
Directives have camel cased names such as ngBind. The directive can be
invoked by translating the camel case name into snake case with these
special characters :, -, or _.

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.

Categories

Resources