Scope function defined in link not fired - angularjs

I made a directive that display a list with an 'add' button. That table is represented twice in the same page but with different forms and table-data:
<list ng-model="list1" form-name="form1"></list>
<list ng-model="list2" form-name="form2"></list>
In the link function, I define an 'add' function that will use the form and the model specified in the directive declaration:
scope: {
list: '=ngModel',
formName: '#',
},
link: function postLink(scope, element, attrs) {
scope.add = function() {
FormService.show(formName).then(function(item) {
list.push(item);
});
}
var addButton = angular.element('<a ng-click="add()">ADD ITEM</a>');
var container = angular.element('<div></div>');
container.append($compile(addButton)(scope));
element.replaceWith(container);
The problem is that the add function never get called by the 'ADD ITEM' link...
** I cannot use the template configuration object because I need the template to by dynamic

You can dynamically build your template by providing a function that returns a string to the template property of your directive:
scope: {
list: '=ngModel',
formName: '#',
},
template: function(element, attrs) {
var addButton = angular.element('<a ng-click="add()">ADD ITEM</a>');
var container = angular.element('<div></div>');
container.append(addButton);
var str = container.prop('outerHTML');
return str;
},
link: function postLink(scope, element, attrs) {
scope.add = function() {
FormService.show(formName).then(function(item) {
list.push(item);
});
}

try this:
element.append('<a ng-click="add()">ADD ITEM</a>');
$compile(element.contents())(scope)

Related

$scope gets null after calling template in directive

i have a nested directive like this
app.directive('grid', ['$log', '$http', function ($log , $http) {
return {
restrict: 'E',
scope: {},
//template: '<span></span>', //i uncomment this line
controller: function ($scope, $element, $attrs){
var swColumns = [];
this.setColumns = function (columns) {
swColumns = columns;
$log.log('grid controller');
$log.log(columns);
$scope.swColDefs = columns;
$log.log($scope.swColDefs);
};
this.getColumns = function () {
return swColumns;
};
},
link: function (scope, element, attrs, gridController) {
$log.log('grid link');
$log.log(gridController.getColumns());
$log.log(scope.swColDefs);
}
};
}]);
app.directive('gridColumns', ['$log', function ($log) {
return {
restrict: 'E',
require: ['^grid', 'gridColumns'],
controller: function () {
var columns = [];
this.addColumns = function (column) {
columns.push(column);
};
this.getColumns = function () {
return columns;
};
},
link: function (scope, element, attrs, controllers) {
var gridController = controllers[0];
var gridColumnsController = controllers[1];
gridController.setColumns(gridColumnsController.getColumns());
}
};
}]);
every thing is ok until i uncomment the template in grid directive
after that the swColDefs in grid link function become an empty array
what is wrong with my code ?
<grid>
<grid-columns>
</grid-columns>
...
</grid>
and i am using it like this
Although the question doesn't specify, I'm assuming you're using your directive like so:
<grid>
<grid-columns>
</grid-columns>
...
</grid>
But since you're specifying a template of <grid>, Angular removes the contents and sets the template instead. And so, gridColumns directive never compiles and the link function never runs.
This is where transclusion is needed on grid. Transclusion takes the content out of DOM, compiles it (at compile-phase) and then allows you to link it against whatever scope you need and place it in the contents.
It's not clear from your question what scope gridColumns directive needs be linked against, and whether it has any visual components.
For a simple transclusion case, this can be done with <ng-transclude> in the template of grid:
transclude: true,
template: "<span>whatever</span><div ng-transclude></div>" // template of grid
Or, for more precise control over scope and placement you can use the transclude function (passed as a fifth parameter to the link function):
transclude: true,
template: "<span>whatever</span>",
link: function(scope, element, attrs, gridController, transclude){
transclude(function(transcludedContent){
// place the content where needed, if at all
});
}
I have solved the problem by doing this in grid template
first i add a div with ng-transclude to the template
secod transclude : true in grid directive
app.directive('grid', ['$log', '$http', function ($log , $http) {
return {
restrict: 'E',
scope: {},
transclude:true,//my change
template: '<span>
<div ng-transclude>//my change
</div>
</span>',
controller: function ($scope, $element, $attrs){
var swColumns = [];
this.setColumns = function (columns) {
swColumns = columns;
$log.log('grid controller');
$log.log(columns);
$scope.swColDefs = columns;
$log.log($scope.swColDefs);
};
this.getColumns = function () {
return swColumns;
};
},
link: function (scope, element, attrs, gridController) {
$log.log('grid link');
$log.log(gridController.getColumns());
$log.log(scope.swColDefs);
}
};
}]);

Angularjs controller required by directive can not be found in transclude content

Recently I use angular to develop a directive, there is an directive which like ng-repeat to generate some records, I used transclude to implement it. but it raise an error that "Controller 'aArea', required by directive 'bSpan', can't be found!".
1. ModuleA code
var moduleA = angular.module("moduleA", []);
moduleA.directive("aArea", function () {
return {
restrict: 'E',
transclude:'element',
scope: {
amount:"="
},
template: '<div id=\"cc\" ng-transclude></div>',
controller: function ($scope,$element,$attrs) {
this.getData = function (data) {
return data + " is ok";
}
},
compile: function (tElement, attrs, linker) {
var parentElement = tElement.parent();
return {
pre: function () {
},
post: function (scope) {
linker(scope.$parent,function (clone,scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
}
}
}
}
});
moduleA.directive("bSpan", function () {
return {
restrict: 'E',
scope: {
data: "=",
},
template: '<span style=\"background-color:gray;color:orange\">{{data}}</span>',
require: "^aArea",
link: function ($scope, $element, $attrs, controller) {
var data = "abc";
}
}
});
2. ModuleB COde
var moduleB = angular.module("moduleB", []);
moduleB.directive("myItem", function () {
return {
restrict: 'E',
scope: {
item: "=",
itemTemplate: '='
},
priority: 1000,
terminal:false,
template: '<ng-include src=\"itemTemplate\"/>',
controller: function ($scope, $element, $attrs) {
var data = "";
}
}
})
3. ModuleC Code
var moduleC = angular.module("moduleC", ["moduleA", "moduleB"]);
moduleC.controller("Ctr", function ($scope) {
$scope.item = {};
$scope.item.dataAmount = 1000;
$scope.item.templateUrl = "item-template.html";
})
4. Html Code
<body>
<div ng-app="moduleC">
<div ng-controller="Ctr">
<a-area>
<my-item item="item" item-template="item.templateUrl"></my-item>
</a-area>
</div>
</div>
</body>
5. template code
<div>
<span style="display:block">hello every one</span>
<b-span data="item.dataAmount"></b-span>
</div>
You should not use the transclude function (that you called linker) of the compile function - it is deprecated.
From $compile documentation:
Note: The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
Following this guidance (and a few other minor changes for the better), change the aArea directive as follows:
compile: function(tElement, tAttrs) {
// don't use the template element
//var parentElement = tElement.parent();
return function(scope, element, attrs, ctrls, transclude) {
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
};
}
In fact, you don't even need the transclude function at all and you don't need to do transclude: "element". You could just change to transclude: true and use <div ng-transclude> 3 times in the template.

Custom directive with dynamic template and binding parent scope to ng-model

I have a view that contains a template specified in a custom directive. The template that is used in the custom directive depends on 'dynamicTemplate':
View:
<div ng-controller="MyController">
<custom-dir dynamicTemplate="dynamicTemplateType"></custom-dir>
<button ng-click="ok()">Ok</button>
</div>
View's Controller:
myModule
.controller('MyController', ['$scope', function ($scope) {
$scope.dynamicTemplateType= 'input';
$scope.myValue = "";
$scope.ok = function()
{
// Problem! Here I check for 'myValue' but it is never updated!
}
Custom Directive:
myModule.directive("customDir", function ($compile) {
var inputTemplate = '<input ng-model="$parent.myValue"></input>';
var getTemplate = function (templateType) {
// Some logic to return one of several possible templates
if( templateType == 'input' )
{
return inputTemplate;
}
}
var linker = function (scope, element, attrs) {
scope.$watch('dynamicTemplate', function (val) {
element.html(getTemplate(scope.dynamicTemplate)).show();
});
$compile(element.contents())(scope);
}
return {
restrict: 'AEC',
link: linker,
scope: {
dynamicTemplate: '='
}
}
});
In this above example, I want 'myValue' that is in MyController to be bound to the template that is in my custom directive, but this does not happen.
I noticed that if I removed the dynamic templating (i.e. the contents in my directive's linker) and returned a hardcoded template instead, then the binding worked fine:
// If directive is changed to the following then binding works as desired
// but it is no longer a dynamic template:
return {
restrict: 'AEC',
link: linker,
scope: {
dynamicTemplate: '='
},
template: '<input ng-model="$parent.myValue"></input>'
}
I don't understand why this doesn't work for the dynamic template?
I am using AngularJS 1.3.0.
Maybe you should pass that value into your directives scope, instead of only dynamicTemplate, i think it should work.
You have a good answer about directives scope here: How to access parent scope from within a custom directive *with own scope* in AngularJS?
Hope I was of any help.
js directive :
angular.module('directive')
.directive('myDirective', function ($compile) {
var tpl1 ='<div class="template1">{{data.title}}</div>';
var tpl2 ='<div class="template2">Hi</div>';
var getTemplate = function (data) {
if (data.title == 'hello') {
template = tpl1;
}
else {
template = tpl2;
}
return template;
};
var linker = function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$render = function () {
// wait for data from the ng-model, particulary if you are loading the data from an http request
if (scope.data != null) {
element.html(getTemplate(scope.data));
$compile(element.contents())(scope);
}
};
};
return {
restrict: "E",
require: 'ngModel',
link: linker,
scope: {
data: '=ngModel'
}
};
});
html :
<my-directive
ng-model="myData">
</my-directive>
js controller:
$scope.myData = {title:'hello'};

AngularJS proxy directive

I am trying to create a proxy directive like so:
<x-field x-purpose="choice" x-values="data.countries" ng-model="model.country"/>
Where the field directive forwards this to another directive, causing the following replacement:
<x-choiceField x-values="data.countries" ng-model="model.country"/>
[note:] the ng-model could be replaced by a reference to some new isolated scope.
The "field purpose" directive decides which implementation to use (e.g. drop-down/listbox/autocomplete?) based on how many values there are to choose from, client device size, etc - ultimately resulting in something like this:
<select ng-model="model.country" ng-options="data.countries">
This design is largely out of curiosity rather than for any practical reason, I am interested in how to achieve it rather than whether it is actually a good idea from a performance/simplicity point of view...
After reading [https://stackoverflow.com/a/18895441/1156377], I have something like this:
function proxyDirective($injector, $parse, element) {
return function (scope, element, attrs) {
var target = element.camelCase(attrs.name + '-field');
var model = attrs.ngModel;
var value = $parse(model);
var directive = $injector.get(target);
/* Bind ngModel to new isolated scope "value" property */
scope.$watch(model, function () {
???
});
/* Generate new directive element */
var pElement = angular.element.html('');
var pAttrs = {
value: ???
};
/* Forward to new directive */
return directive.compile(element, attrs, null)(scope, element, attrs);
};
}
function alphaFieldDirective() {
return {
replace: 'true',
template: '<input type="text" ng-value="forwarded value?">'
};
}
function betaFieldDirective() {
return {
replace: 'true',
template: '<textarea attributes? >{{ value }}</textarea>'
};
}
But I'm not sure how to achieve the forwarding or binding. This is my first forage into Angular directives, and it doesn't seem to be a particularly popular way of using them!
The purpose of this is to separate the purpose of a form field from its appearance/implementation, and to provide one simple directive for instantiating fields.
I implemented this via a service which proxies directives:
Fiddle: http://jsfiddle.net/HB7LU/7779/
HTML:
<body ng-app="myApp">
<h1>Directive proxying</h1>
<proxy target="bold" text="Bold text"></proxy>
<h1>Attribute forwarding</h1>
<proxy target="italic" style="color: red;" text="Red, italic text"></proxy>
</body>
Javascript:
angular.module('myApp', [])
.factory('directiveProxyService', directiveProxyService)
.directive('proxy', dirProxy)
.directive('bold', boldDirective)
.directive('italic', italicDirective)
;
function directiveProxyService($compile) {
return function (target, scope, element, attrs, ignoreAttrs) {
var forward = angular.element('<' + target + '/>');
/* Move attributes over */
_(attrs).chain()
.omit(ignoreAttrs || [])
.omit('class', 'id')
.omit(function (val, key) { return key.charAt(0) === '$'; })
.each(function (val, key) {
element.removeAttr(attrs.$attr[key]);
forward.attr(attrs.$attr[key], val);
});
$compile(forward)(scope);
element.append(forward);
return forward;
};
}
function dirProxy(directiveProxyService) {
return {
restrict: 'E',
terminal: true,
priority: 1000000,
replace: true,
template: '<span></span>',
link: function (scope, element, attrs) {
directiveProxyService(attrs.target, scope, element, attrs, ['target']);
}
};
}
function boldDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}
function italicDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}

Passing id and as a parameter to directive

http://jsfiddle.net/mato75/t48qn/
I have a directive, that if id is not passed, then it should generate one, but it looks like that the generation is to slow and the id is not present in the directive.
(function (angular) {
'use strict';
angular.module('Widgets.Module')
.directive('myDirective', [
function () {
function postLink(scope, jqElm, attr) { }
function postCompile(tElement, tAttrs) {
return function postLink(scope, jqElm, attr) {
attr.$observe("id", function (id) { // called on on init
scope.id = id !== undefined ? id : 'something 1';
});
}
}
function Ctrl(scope) {
}
return {
template:
'<div id="{{ id }}">' +
'</div>',
controller: [
'$scope', Ctrl
],
replace: true,
scope: {
id: '#'
},
restrict: 'AC',
link: postLink,
compile: postCompile
};
}
])
;
})(window.angular)
I think using id is special since its a valid DOM attribute. In my case id was also getting added as an attribute to the directive html, not the inner child where I was using it.
I created a new attribute called input-id that doesn't suffer from this name collision.
<autosuggest input-id="country"></autosuggest>
The produced markup is:
<div class="autosuggest"><input id="country"></div>
...which is what I think you are after.
The scope block for the directive looks like this:
scope: {
inputId: '#'
}
One possible solution is to disable the automatic data binding (scope: {}) and do it manually in your link function.
Check this fiddle.
module.directive('myDialog', function () {
return {
replace: true,
restrict: 'E',
scope: {},
template: '<div>Test {{a1}}</div>',
link: function (scope, element, attrs) {
if (!attrs.a1) {
scope.a1 = "default";
} else {
scope.a1 = attrs.a1;
}
}
}
});

Resources