Angular, ng-repeat to build other directives - angularjs

I'm building a complex layout, that takes a JSON document and then formats it into multiple rows, with each row then having more rows and/or combinations of rows/columns inside them.
I'm new to Angular and am just trying to get to grips with Directives. They are easy to use for very simple things, but quickly become very difficult once you need to anything more complicated.
I guess I'm doing this the wrong way around, but is there a way to simply add the name of a directive (in the example below, I've used ) and get that directive to be rendered on an ng-repeat?
Maybe the same way that you can use {{{html}}} instead of {{html}} inside of mustache to get a partial to render as HTML and not text.
As expected, the example below simply writes the name of the directive into the dom. I need Angluar to take the name of the directive, understand it, and then render before before it is written. Due to the complex layout of the page I need to design, I could be rendering many different directives, all inside each other, all from 1 JSON document (which has been structured into different rows and then row / column combinations).
Example code that renders the name of the directive to the page, but gives you an idea of how I'd like to write a solution the problem...
<div app-pages></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script>
var app = angular.module("app", ['main']);
angular.module('main', [])
.controller("appPageController", ['$scope', function( $scope ){
$scope.pages = [];
var page1 = {
title: 'Page 1',
directive: '<app-page-type-1>'
};
var page2 = {
title: 'Page 2',
directive: '<app-page-type-2>'
};
$scope.pages.push(page1);
$scope.pages.push(page2);
}])
.directive("appPageType2", function factory() {
console.log('into page type 2');
return {
replace: true,
template: 'This is the second page type'
};
})
.directive("appPageType1", function factory() {
console.log('into page type 1');
return {
replace: true,
template: 'This is the first page type'
};
})
.directive("appPages", function factory() {
console.log('into pages');
return {
replace: true,
template: '<ul><li ng-repeat="page in pages">{{page.directive}}</li></ul>'
};
});
</script>

This is one possible alternative to your idea. The idea is to append the directive you defined in page object for each html element inside the ng-repeat. Please take a look at the demo. Hope it helps.
<div ng-app="myApp" ng-controller="appPageController">
<ul>
<li ng-repeat="page in pages" app-pages></li>
</ul>
</div>
.directive("appPages", function ($compile) {
console.log('into pages');
return {
replace: true,
link: function (scope, elements, attrs) {
var html = '<div ' + scope.page.directive + '></div>';
var e = angular.element(html);
elements.append(e);
$compile(e)(scope);
}
};
});
Demo

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.

AngularJS templates + Bootstrap modals

I am trying to create some Bootstrap modals dynamically using AngularJS, as described in here: https://angular-ui.github.io/bootstrap/
For that purpose, I have created a basic script template in AngularJS inside a view of a directive called modalView.html:
<script type="text/ng-template" **id="{{modalId}}.html"**>
<div class="modal-header">
Header
</div>
<div class="modal-body">
Body
</div>
<div class="modal-footer">
Footer
</div>
</script>
And this is my directive (modalDirective.js):
myDirectives.directive('modal', function () {
return {
restrict: 'A',
scope: {},
templateUrl: 'shared/controls/modal/modalView.html',
link: function (scope, element, attributes) {
scope.modalId = attributes['modalId'];
}
}
});
Then, I have another directive that uses the aforementioned directive alongside a textbox that should open the modal when the latter is clicked.
modalTextboxView.html:
<div modal modal-id="{{modalId}}"></div>
<div textbox is-read-only="true" ng-click="openModal()"></div>
modalTextboxDirective.js:
myDirectives.directive('modalTextbox', function ($modal, $log) {
return {
restrict: 'A',
scope: {},
templateUrl: 'shared/controls/modalTextbox/modalTextboxView.html',
link: function (scope, element, attributes) {
scope.modalId = attributes['fieldId'];
scope.openModal = function(modalSize) {
var modalInstance = $modal.open({
**templateUrl: scope.modalId + '.html',**
size: modalSize,
resolve: {
items: function () {
return scope.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
scope.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
}
}
}
});
The html output is as expected. I can see that the script template id of the first view is being set correctly. However, when I click on the textbox I get an error 404. It seems that the modal is trying to be opened but can't find the templateUrl defined, which has been set correctly too. Moreover, if I just type the hardcoded value into the id field of the script template it works flawlessly and it opens the modal. Unfortunately, I need that id to be dynamic as I need to be able to create and generate an undetermined number of modals on my page.
Any ideas?
You can't use the <script type="text/ng-template"> approach to define a dynamically bound template id.
All that Angular is doing (see src) when it encounters this type of <script> tag is that it adds the contents into a $templateCache service with the uninterpolated value of the id attribute; and, in fact, it adds it at compile-time before it could be interpolated.
So, the template is added with a key of "{{modalId}}.html" - literally. And when you request it with templateUrl: "ID123.html", it results in a cache miss and an attempt to download from that non-existent URL.
So, what are you actually trying to get out of this "dynamic" modal URL?
I don't understand the use of your modal directive - all it attempts to do is to dynamically assign an id for the template. Why? If you just need to define N templates and dynamically switch between them, then just define N <script> tags inside your modalTextbox directive:
<script type="text/ng-template" id="modal-type-1">
template 1
</script>
<script type="text/ng-template" id="modal-type-2">
template 2
</script>
<div textbox is-read-only="true" ng-click="openModal()"></div>
and in the invocation of $modal, set templateUrl like so:
$modal.open({
templateUrl: "modal-type-" + scope.modalType,
// ...
});

generating a unique id and class for variable number of directives

Here's a jsbin illustrating the issue, but I will explain it also below with code. I am making a custom directive in AngularJs that will be inserted once for every entry in the database, which I iterate over using ng-repeat as you see here.
<div ng-controller="DemoCtrl as demo">
<h1> Hello {{ name }}
<div class="some-list" ng-repeat="customer in customers">
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
I need to make a unique class or id
</div>
</div>
In case you didn't notice, the directive tucked into that code is my-dumb-graph, with the corresponding myDumbGraphic name in the code below. In order to insert the graphic, which I will do in the link function of the directive below, I need to be able to select a unique id or class in the html above, and I will need to be able to select it from within the link function in the directive, so somehow need to reference the id from the html in the js. You can see in the jsbin that inside the link function of the directive, the id is not yet unique (i.e. the dynamic part hasn't been computed yet), even though it's eventually unique by the time it's rendered to the dom.
<div id="not-unique" class="not-unique" my-dumb-graphic datajson=customer></div>
Rest of the code
var app = angular.module('jsbin', ['ngRoute'])
.config(function ($routeProvider) {
'use strict';
var routeConfig = {
controller: 'DemoCtrll'
};
$routeProvider
.when('/', routeConfig)
.otherwise({
redirectTo: '/'
});
});
app.controller('DemoCtrl', function($scope, $routeParams) {
$scope.name = "World";
$scope.customers = [
{
name: 'David',
street: '1234 Anywhere St.'
},
{
name: 'Tina',
street: '1800 Crest St.'
},
{
name: 'Michelle',
street: '890 Main St.'
}
];
});
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '='
},
link: function (scope, elem, attrs) {
...code to insert my dumb graphic...need to select unique id or class or both...need to select unique id from here
}
};
});
Update
As several people have suggested, there are multiple ways to create a dynamic id for a div, however, the dynamic part of the div id won't have computed by the time I need it in the link function. For example, If, following suggestion of other answer, I set id in the html to this <div id="id_{{::id}}" then the dynamic part of it won't have computed by the time I need it to inside the link function, although if I inspect the dom after it's rendered it has computed. In the link function, I can access the div through the elem like this "#"+elem[0].id; and log statements show that at that time it hasn't computed- this is what log statements show for "#"+elem[0].id; ----> #id_{{::id}}
The directive link function has the corresponding element pass in as a parameter, so you already have the selector:
link: function(scope, element, attrs) {
}
Alternatively, you could take leverage your $scope.customers array keys as unique ids and set view elements, pass the id into the directive, and use that as a selector (this assumes you jQuery loaded):
<h1> Hello {{ name }} </h1>
<!-- use customers array index -->
<div class="some-list" ng-repeat="(customerId, customer) in customers">
<!-- append customer id to this element id so it is unique -->
<div id="directive-element-selector-{{customerId}}" class="not-unique" my-dumb-graphic datajson=customer customer-array-id=customerId></div>
<div id="inner-selector-{{customerId}">
I need to make a unique class or id
</div>
</div>
</div>
directive:
app.directive('myDumbGraphic', function () {
return {
restrict: 'A',
scope: {
datajson: '=',
customerArrayId: '='
},
link: function (scope, elem, attrs) {
$('#directive-element-selector-' + customerArrayId) // jQuery selector, this should equal elem pass in as link function param
$('#inner-selector-' + customerArrayId) // inner selector
}
};
});
You can use the $scope $id property which will be unique for each directive and scope in your system, so you can do something like
<div class="myclass_{{::$id}}"></div>
or something like that
Note:
the :: is for one time binding

How to force angular to render outside elements from the custom directive?

I need a small help in angularjs,
please have a look on this
code (chrome browser):
http://jsfiddle.net/Aravind00kumar/CrJn3/
<div ng-controller="mainCtrl">
<ul id="names">
<li ng-repeat="item in Items track by $index">{{item.name}} </li>
</ul>
<ak-test items="Items">
</ak-test>
</br>
<div id="result">
</div>
</div>
var app = angular.module("app",[]);
app.controller("mainCtrl",["$scope",function($scope){
$scope.Items = [
{name:"Aravind",company:"foo"},
{name:"Andy",company:"ts"},
{name:"Lori",company:"ts"},
{name:"Royce",company:"ts"},
];
$scope.Title = "Main";
}]);
app.directive("akTest",["$compile",function($compile){
return {
restrict: 'E',
replace: true,
scope: {
items: "="
},
link: function (scope, element, attrs) {
// var e =$compile('<li ng-repeat="item in Items track by $index">{{item.name}} </li>')(scope);
// $("#names").append(e);
var lilength = $("#names li").length;
var html ='<div> from angular ak-test directive: '+lilength+'</div>';
element.replaceWith(html);
}
};
}]);
$(function(){
$("#result").html('from jquery: '+$("#names li").length);
});
I have created a custom directive and trying to access an element from the view which in the ng-repeat above my custom directive
The problem is, in the directive it was saying ng-repeat not rendered yet.
Here is the problem
I have two elements
<svg>
<g>
List of elements
</g>
<g>
Based on the above rendered elements I have to draw a line between elements like a connection. I have to wait till the above elements to get render then only I can read the x,y positions and can draw a line.
</g>
</svg>
Both elements and the connections are scope variables. As per my understanding both are in the same scope and execution flow starts from parent to child and finishes from child to parent. How can I force above ng-repeat rendering part to complete before starting the custom directive?
is there any alternative available in angular to solve this dependency?
It's been a while, so my Angular is getting a bit rusty. But if I understand your problem correctly, it's one that I have run into a few times. It seems that you want to delay processing some elements of your markup until others have fully rendered. You have a few options for doing this:
You can use timeouts to wait for the page to render:
$timeout(function() {
// do some work here after page loads
}, 0);
This generally works ok, but can cause your page to flash unpleasantly.
You can have some of your code render in a later digest cycle using $evalAsync:
There is a good post on that topic here: AngularJS : $evalAsync vs $timeout. Typically, I prefer this option as it does not suffer from the same page flashing issue.
Alternatively, you can look for ways to refactor your directives so that the dependent parts are not so isolated. Whether that option would help depends a lot on the larger context of your application and how reusable you want these parts to be.
Hope that helps!
I would create a directive for the whole list, and maybe a nested directive for each list item. That would give you more control I would think.
Thanks a lot for your quick response #Royce and #Lori
I found this problem causing because of ng-repeat I have solved it in the following way..
Created a custom directive for list elements and rendered all elements in a for loop before the other directive start. This fix solved the problem temporarily but i'll try the $evalAsync and $timeout too :)
var app = angular.module("app",[]);
app.controller("mainCtrl",["$scope",function($scope){
$scope.Items = [
{name:"Aravind",company:"foo"},
{name:"Andy",company:"ts"},
{name:"Lori",company:"ts"},
{name:"Royce",company:"ts"},
];
$scope.Title = "Main";
}]);
app.directive("akList",["$compile",function($compile){
return {
restrict: 'A',
replace : false,
link: function (scope, element, attrs) {
var _renderListItems = function(){
$(element).empty();
for(var i=0;i<scope.Items.length; i++)
{
var li ='<li> '+ scope.Items[i].name +' </li>';
element.append(li);
}
};
_renderListItems(scope);
scope.$watch('Items.length', function (o, n) {
_renderListItems(scope);
}, true);
}};}]);
app.directive("akTest",["$compile",function($compile){
return {
restrict: 'E',
replace: true,
scope: {
items: "="
},
link: function (scope, element, attrs) {
var lilength = $("#names li").length;
var html ='<div> from angular ak-test directive: '+lilength+'</div>';
element.replaceWith(html);
}
};
}]);
$(function(){
$("#result").html('from jquery: '+$("#names li").length);
});

Angular binding inside an inline ckeditor

I'm using inline editing with CKEditor, and I'd like to bind an element to an angular scope value.
<div contentEditable="true">
<p>Here is the value: {{testval}}</p>
</div>
testval should update in the same manner as it would outside the editor.
To protect this text in the editor, I'd like to do something similar to the placeholder plugin. In other words I plan to have a placeholder, dynamically displaying the final text rather than just the placeholder.
I've seen several examples of how to bind the entire contents with angular, but not individual elements. I'm still fairly new to both angular and ckeditor, so any help or pointers would be much appreciated.
It sounds to me like you will need to use a directive for what you want. I might be soewhat off because I'm not completely familiar, but goig by what you've provided, let's assume this example.
html
<body ng-app="myApp">
<div content-editable content="content"></div>
</body>
javascript
angular.module('myApp', [])
.directive('contentEditable', function() {
restrict: 'A',
replace: true,
scope: {
// Assume this will be html content being bound by the controller
// In the controller you would have:
// $scope.content = '<div>Hello World</div>'
content: "="
},
template: '<div contentEditable="true"><p>Here is the value {{ content }}</p></div>'
});
Still not sure if I completely comprehend, but let me know if I'm getting closer.
I assume that you want to bind the HTML text in model to the element. I used ng-bind-html to render what is in the model and I created the directive ck-inline to add the inline feature and bind the model to the changes that happen in the inline editor. This directive requires a ng-bind-html to work and you also need to have ngSanitize added to your module. Add directive ck-inline to your element and
I also use $timeout because I noticed that if I don't the text is rendered and then ckeditor somehow deletes all the values which messes up the model (this does not happen with the non-inline option). Here is the code.
yourModule.directive('ckInline', ['$sce', '$timeout', function($sce, $timeout){
return{
require : '?ngBindHtml',
scope:{value:"=ngBindHtml"},
link : function(scope, elm, attr, ngBindHtml)
{
$timeout(function()
{
var ck_inline;
elm.attr("contenteditable", "true");
CKEDITOR.disableAutoInline = true;
ck_inline = CKEDITOR.inline(elm[0]);
if (!attr.ngBindHtml)
return;
ck_inline.on('instanceReady', function()
{
ck_inline.setData(elm.html());
});
function updateHtml()
{
scope.$apply(function()
{
scope.value = $sce.trustAsHtml(ck_inline.getData());
});
}
ck_inline.on('blur', updateHtml);
ck_inline.on('dataReady', updateHtml);
});
}
};
}]);

Resources