Angularjs create directive dynamically with ng-repeat - angularjs

I'd like to know is there any way to create dynamic directive with ng-repeat. Something, like this:
<div ng-repeat="document in documents">
<div document.name></div>
</div>

You could use attributes to do that. For example:
<div ng-repeat="document in documents">
<document-viewer name="document.name"></document-viewer>
</div>
and
var app = angular.module("documents", []);
app.controller("AppCtrl", function($scope) {
$scope.documents = [
{id:1, name:'document1'},
{id:2, name:'document2'}
];
});
app.directive("documentViewer", function() {
return {
scope: {
//attribute name will match the name passed from your view,
//and you will be able to use it on your template or logic
name: "="
},
//Add your template here:
template: '<div><button ng-click="openDocument(name);"></div>',
//Add your logic here:
link: function(scope, element, attrs){ ... }
}
});
I don't know if that answers your question, but you are not making clear what is it you are trying to achieve.
Have a look at the following videos as they might come handy for you when dealing with directives and isolate scope:
http://egghead.io/lessons/angularjs-directive-to-directive-communication
http://egghead.io/lessons/angularjs-isolate-scope-expression-binding
http://egghead.io/lessons/angularjs-isolate-scope-attribute-binding
http://egghead.io/lessons/angularjs-isolate-scope-two-way-binding

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

How can I use isolated scope with a component and a directive?

The goal here is to let MainCtrl know when there is an error(s) found in the directive. The error must be displayed here:
<div ng-if="errors[0]">Error 1: {{errors[0]}}</div>
How can I get isolated scope with a directive inside a component? The following application works if you uncomment the 2 lines mentioned below. As it is, I get error:
Multiple Directive Resource Contention
I can read the causes. I need to know how to fix this while still allowing the directive to have isolated scope. I may have 3-4 of these directives on a page and each one needs it's own unique of errors that is also available to the parent.
(working case example on codepen)
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.errors = [false, false];
$scope.text = "bobby";
});
app.directive('testDirective', function(){
return {
restrict: 'A',
scope: {
errors: '=',
text: '#'
},
link: function($scope, $element, $attr, ngModel) {
console.log('link fired');
console.log('errors: ', $scope.errors);
console.log('scope.text', $scope.text);
$attr.$observe('text', function (val) {
if($scope.text === 'bobby'){
$scope.errors[0] = true;
}else{
$scope.errors[0] = false;
}
});
},
template: '<p>text: {{ text }} </p>'
+ '<p>errors: {{errors}}</p>'
+ '<p><input type="text" ng-model="errors" /></p>'
};
});
app.component('panel', {
bindings: {
},
template: [
'<div>',
'</div>'
].join(''),
controller: function() {
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js"></script>
<section ng-app="app" ng-controller="MainCtrl">
<h3>Parent Scope</h3>
<p>errors: {{errors}}</p>
<input type="text" ng-model="text"></div>
<div ng-if="errors[0]">Error 1: {{errors[0]}}</div>
<div ng-if="errors[1]">Error 2: {{errors[1]}}</div>
<!-- UNCOMMENT THE FOLLOWING 2 LINES AND THIS APP WILL WORK
<h3>Directive by itself</h3>
<div test-directive text="{{text}}" errors="errors"><div>
-->
<h3>Directive in component</h3>
<panel test-directive text="{{text}}" errors="errors"></panel>
</section>
After researching, I noticed Angular only returns bool from $validators (as opposed to object). At this point I decided my approach was wrong. I decided to create a unique $valiators for each unique error message. Then use ng-message for the output.
In order to work with multiple components on the same page, I also have to check the ngModel.$error as part of validation. This blog covers the basic approach.

Dynamically inclusion of directive in the view

Let's say I have the following directives:
myDirectiveA
myDirectiveB
...
and I have a variable (module) which can be a or b, ... I want to dynamically display the directives depending on module.
I know I could do
<div class="my-directives-a" ng-show="module == 'a'"></div>
<div class="my-directives-b" ng-show="module == 'b'"></div>
but this is not exactly what I want. On my project I'd like to have something like this
<div ng-repeat="module in modules">
<div class="my-directive-{{ module }}" ...></div>
</div>
So I created this plunker script to check my idea but that doesn't seem to work :(
My question are: is this possible and if so how? And is this a good idea in the first place or should I try to solve my problem in a different way?
One option to accomplish dynamic directives is through the use of $compile.
Given a directive:
angular.module(...)
.directive("dynamicDirective", ["$compile", function($compile) {
return {
scope: { dirName: "=" },
link: function(scope, elem, attrs) {
var template = '<div class="my-directives-"' + scope.dirName + '></div>';
directiveElem = $compile(template);
elem.append(directiveElem);
}
};
}]);
You can use the directive as follows:
<div dynamic-directive dir-name="module" ng-repeat="module in modules"></div>

angularJS - variable component templates

Is it possible to define a variable template for a component in angularJS 1.6?
Something like this:
<div class="test">
<{{$ctrl.GetElement()}}>
</div>
for cases in which I want to decide in runtime what the template be like.
Is there a way to do it?
Here is a simple example of a "variable template" using $compile. Let's define a "generator" directive which will be able to generate other directives:
app.directive('createDirective', function($compile) {
return {
scope: {
directiveName: '#'
},
link: function(scope, element) {
var newHtml = '<' + scope.directiveName +'></ '+ scope.directiveName +'>';
element.append($compile(newHtml)(scope));
}
};
});
This "generator" directive takes in a string (via the attribute "directive-name"), assembles new HTML, compiles it, and appends the resulting HTML to the generator directive.
I've defined a separate directive named "Hello", which I want to be called dynamically from the generator directive:
app.directive('hello', function() {
return {
restrict: 'E',
link: function(scope, element) {
element.append("Hello!");
}
}
});
Now, we can use the generator directive to compile the "Hello" directive
<div create-directive directive-name="hello"></div>
which results in this generated HTML
<hello class="ng-scope">
<!-- hello-->
Hello!
</hello>
In addition, we can pass a variable from a controller to the generator directive in a similar way:
app.controller('MainCtrl', function($scope) {
$scope.newDirective = "from-controller";
});
And in the HTML:
<div create-directive directive-name="{{newDirective}}"></div>
Be sure to take a look at the $compile documentation.
Demo

Call a function in a angular-controller from outside of the controller?

I have a lightbox-dierective and controller that looks like this:
directive('modalDialog', function() {
return {
restrict: 'E',
scope: {
show: '='
},
replace: true, // Replace with the template below
transclude: true, // we want to insert custom content inside the directive
template: '<div class="ng-modal" ng-show="show"><div class="ng-modal-overlay" ng-click="hideModal()"></div><div class="ng-modal-dialog" ng-style="dialogStyle"><div class="ng-modal-dialog-content" ng-transclude><div class="ng-modal-close" ng-click="hideModal()">X</div></div></div></div>'
};
}).controller('Lightbox', function($scope) {
$scope.modalShown = false;
$scope.toggleModal = function() {
$scope.modalShown = !$scope.modalShown;
};
});
Here is the desierd html, what I need is to open the secon ligthbox from withing the first one:
<div ng-controller="Lightbox">
<span ng-mousedown='toggleModal()'>Open lightbox one</span>
<modal-dialog show='modalShown'>
<h2>One lightbox <span ng-mousedown='toggleModal()'>Open lightbox two</span></h2>
</modal-dialog>
</div>
<div ng-controller="Lightbox">
<span ng-mousedown='toggleModal()'>Open lightbox one</span>
<modal-dialog show='modalShown'>
<h2>another lightbox</h2>
</modal-dialog>
</div>
For most cases it works great! I use it in several occations throughout the site, with diffrent lightboxes and different content.
I have now come across a case, where I need to call one of the lightboxes from outside of the controller. Can this be achieved and in that case how do I reference the right lightbox?
I'd extend that setting to an object
var modalSet = {
shown: false,
toggle: function(){ modalSet.shown = !modalSet.shown }
}
Then put it on your main controller (the one with ngApp attribute) and have your entire scope modaleble.
Also, directives do have a controller option, but since only one modal is gonna show up at any given time, you might not want to re-create a controller for every new instance.
Upon re-reading your question: Where is it exactly -> "outside of the controller"?

Resources