I am new to writing directives. I have this snippet of code that works and I am trying to make it a directive:
<div ng-click="details=!details" class="row toggleHeader" ng-class="{open : details}">
<div class="col-md-12">
<h4>Some title</h4>
</div>
</div>
<div id="the-details" class="row" ng-show="details">
<div class="col-md-2">
...content...
</div>
</div>
So, my initial pass was
app.directive('pbTogglePanel', function() {
return {
restrict: 'A',
scope: {
variableName: '#',
panelTitle: '#'
},
template: '<div ng-click="somevariable=!somevariable" class="row toggleHeader" ng-class="{open : somevariable}">\n<div class="col-md-12">\n<h4>{{panelTitle}}</h4>\n</div>\n</div>\n\n<div id="the-somevariable" class="row" ng-show="somevariable">\n<div class="col-md-2">\n...\n</div>\n</div>',
};
});
with the html
<div pb-toggle-panel panel-title="My Directive Panel Test" variable-name="foobar"></div>
This works, in the sense that the panel toggles as expected. I obviously did not try to use the variable "foobar" passed from the html attribute. When I try to use it, the code throws an error and the toggle does not work. I tried using the template:
'<div ng-click="{{variableName}}=!{{variableName}}" class="row toggleHeader" ng-class="{open : {{variableName}}}">\n<div class="col-md-12">\n<h4>{{panelTitle}}</h4>\n</div>\n</div>\n\n<div id="the-{{variableName}}" class="row" ng-show="{{variableName}}">\n<div class="col-md-2">\n...\n</div>\n</div>'
Although "foobar" shows up in all those places, the toggling fails and the console logs an angular error.
So, what's the correct way to pull in more than one html attribute?
Also, the goal would be to allow the contents of this DIV to replace ...content... placeholder in my template. Not sure how to pass that either.
I feel if I can just get my head around how this data is passed, I'll be good from there out.
TO HELP CLARIFY
This works: http://codepen.io/smlombardi/pen/MYygpy
But I wanted to pass the name of the variable for the toggler to use from an attribute too, and this does NOT work:
http://codepen.io/smlombardi/pen/KwzPaw
The problem of your last, non-working example is not about passing multiple parameters but rather abusing the angular's interpolation mechanism. If all you want is to use the variableName, the minimal change to make your last code example work (that is to collapse/expand) would be to change the template from string to function with concatenation:
{...
template: function(tElem, tAttr){return '<div ng-click="' + tAttr.variableName + '=!' + tAttr.variableName +...;
}
But there is no point to do so as long as you use directive isolating scope, which effectively hides the value of the variableName from you. You will also probably want to change to scope:false and get attributes from the tAttr of the template function instead. Modified example.
Related
I know its not possible to inject angular directive along html code inside inner html, is there any work around it? I'm trying to read the html and the directive from a html file and inject it inside a div, I thought about using component factory and create the component dynamically but the directive in the html file can be anywhere in the code not sure how I can append it to the right place in dom, I don't have much experience working with component factory so please let me know if there is a way.
ngOnInit(): void {
let loading = this.loadingService.addTask();
this.route.paramMap
.take(1)
.subscribe(params => {
let page: string = params.get("page") || "";
if (page) {
this.trainingService.getPageContent(page)
.take(1)
.subscribe(res => {
this.content = res;
this.loadingService.completeTask(loading);
})
}
else {
this.loadingService.completeTask(loading);
}
},
error => {
this.notificationService.error("Error retrieving training page", error);
this.loadingService.clearAllTasks();
})
here is my template
<div [innerHtml]="content"></div>
here is an html page example that should be injected (this is an external html page file)
<div class="row">
<div class="col-md-12">
<div class="panel panel-default b">
<div class="panel-body">
<div class="row">
<!--the directive-->
<sa-azure-media-player [source]="the link"></ss-azure-media-player>
</div>
</div>
</div>
</div>
</div>
There isn't a way to do this. The workaround would be to change the way you inject html. Instead of injecting as html, write a sub-component, give it the right info (as an [json-]object!), and off you go.
In case you do not have control of the incoming info (in your case html), you are forced to mould it into an object, so you can use it in angular.
In a recent project I had to have text content that might contain iframes (iframes don't work well in angular). The solution was to parse and save the content in the backend as a JSON-object containing text- and iframe-parts in the right order (that cost some time, unfortunately). Once I got hold of it in Angular, I could inject the texts and iframes of the JSON-object by using typescript. And, of course, do the right thing for the right element.
So, in case you have control over the incoming info (be it html or whatever kind of identifiable object), that's where you should change the procedure (and make full use of the angular MVC-stuff). In case you do not have control over that, please provide some more details as to what needs to be done and what the cicumstances are.
I want to know the best way to proceed given the following information:
I have a controller which makes 2 http calls using a factory.
DataFactory.getTestDisplaySections(testID).then(function (response) {
$scope.testSections = JSON.parse(response.data);
});
DataFactory.getTestObjectByTestID(testID).then(function (response) {
$scope.testObject = JSON.parse(response.data);
});
The call returns two arrays. I want to generate html with data from the array embedded in it. Right now, my template for the controller contains only an empty shell of a page.
<div class="container">
<div class="row">
<div ng-bind-html="sectionHTML"></div>
</div>
</div>
The sectionHTML above is my variable which contains the html markup for the entire page.
The first call returns data that gives me names of panels that are to be displayed on the page. These could be 4 or more. I want to use the following html to display each panel.
$scope.sectionHTML = '<div class="col-lg-6">'+
'<div class="panel panel-primary">'+
'<div class="panel-heading lead">' + panelDiaplayName + '</div>' +
'<div class="panel-body">'+
'<div id="test_info" class="col-lg-12">' +
'</div>'+
'</div>'+
'</div>'+
'</div>'
Should I just go through the panel data in a for loop and create the html that way for each panel?
When I try to add the panelDisplayName using {{}} It shows up as just {{panelDisplayName}} will this be an issue, every time I have to evaluate an angular expression? How can I resolve this issue?
Second, I have other information that I need to display within each of the panels. This comes from the second http call. Each panel will have details in it and each piece of information will be editable. Can anybody help with the best way to do it?
I can give more explanation if anyone needs it.
Thanks
Paras
I think you are asking about multiple issues here, so I will try to help solve the first two, looping through your array to build your page and helping you get the {{}} data-binding to work.
For your panels, don't use a for loop and build html in your controller. You are using angular, the angular way is using ng-repeat. You add this to your html template and it will build the dom by iterating over your array. In your example:
.html
<div class="container">
<div class="row">
<!-- this is where we are iterating over the array (ng-repeat) -->
<div class="col-lg-6" ng-repeat="section in testSections">
<div class="panel panel-primary">
<!-- section is the current item being iterated in the array, so we are printing the displayName for the section -->
<div class="panel-heading lead">{{section.panelDiaplayName}}</div>
<div class="panel-body">
<div id="test_info" class="col-lg-12">
</div>
</div>
</div>
</div>
</div>
</div>
As for the data-binding. My assumption here is that you aren't adding ng-app and/or ng-controller to elements that encompass your data-bindings. For example:
.html
<body ng-app="yourApp" ng-controller="yourController">
<h1>{{testMessage}}</h1>
</body>
app.js
var app = angular.module('yourApp').controller('yourController', ['$scope', function($scope) {
$scope.testMessage = "Hello World";
}])
As for your second question about dealing with data that will be edited, I recommend giving that a shot yourself. Look in to angular forms and ng-model. Those should help you get started. After giving that a shot, if you are still struggling, ask a new question for the specific issue you are struggling with.
I have a special template problem... I have a array of products, every products have a property "button_code", this property is a result in plain text of HTML laravel template with some angular code inside.
Actually im using a ng-bind-html="product.button_code" inside a and use this template inside a ng-repeat, the html code is correctly inserted in every repeat iteration, but the code is plain text, and I need to "wake up" the ng-controllers ng-clicks etc inside this html
I try with this:
var targets = $('.buy-button-container').toArray();
for (var target in targets) {
console.log($(targets[target]));
$compile($(targets[target]))($scope);
}
$scope.$apply();
But this make the code inside the container (all html code inserted in the ng-bind-html) dissapear of the DOM.
How i can do this?
PD: and yes, im forced to use these template in these product.button_code because special things...)
Thanks
EDIT: This is a piece of code i want to bind:
<button class="buy-link btn btn-default" data-toggle="modal" role="button" ng-controller="BuyController" ng-click="doProduct({'id':'8888','title':'testestest','price':13.99,'currency':'EUR''preorder_enabled':false,'crossedPrice':100,'stock':true,'short_desc':'bla bla bla.','lbonus':false,'bonus_txt':false})">
<span class="left">
<i class="fa fa-cart"></i>
<span itemprop="price">€13.99</span>
</span>
<span class="right">
{{GETIT}}</span>
</button>
Use the transclude function furnished as the second argument of the function created by the $compile service:
app.directive("compileBindExpn", function($compile) {
return function linkFn(scope, elem, attrs) {
scope.$watch("::"+attrs.compileBindExpn, function (html) {
var expnLinker = $compile(html);
expnLinker(scope, function transclude(clone) {
elem.empty();
elem.append(clone);
})
});
};
});
The above directive evaluates the compile-bind-expn attribute as an AngularJS expression. It then uses the $compile service to bind the evaluated HTML to the element. Any existing content will be removed.
Usage:
<div class="buy-button-container" compile-bind-expn="buttonCode">
<p>This Node disappears when expression binds</p>
</div>
Note that the directive uses a one-time binding in the $watch to avoid memory leaks.
The DEMO on JSFiddle
In order to make HTML render you have to use the following function:
$sce.trustAsHtml('<b>Your html</b>');
You will have to inject $sce into your Controller.
If you are doing this in a ng-repeat you will need a function in your controller that does this. Ex:
$scope.transformHTML = function(html) {
return $sce.trustAsHtml(html);
}
in your template...
<div ng-repat="foo in bar">
<div ng-bind-html="transformHTML(foo.html)"></div>
</div>
Anyway, I don't think that the "Angular" magic within your HTML will work.
I have a directive which loads a template with a bunch on input fields. One of which is the jQuery/Bootstrap datepicker.
<my-directive-buttons></my-directive-buttons>
When a user selects/clicks on the datepicker field, the calendar is displayed. I have also attached an ng-click to the input field:
<div class='col-sm-6'>
<div class="form-group">
<div class='input-group datepick'>
<input type='text' class="form-control" ng-click="addCalendarFooter()"/>
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
</div>
On click, the calender is displayed and $scope.addCalendarFooter is called:
app.directive('myDrectiveButtons', function($compile) {
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {
},
templateUrl: 'controls/input-fields.html',
link: function(scope, elem, attrs) {
},
controller: function($scope) {
$scope.addCalendarFooter = function() {
$('#datepicker').append($('<div></div>').load('myDir/calendar/customFooter.html'));
}
}
}
});
I am successful in appending the contents of customFooter.html to the calendar, however, within customFooter.html are further ng-clicks, which when pressed, are not being called. E.g
customFooter.html
<div>
<button ng-click="controlClick()">Click Me</button>
</div>
Yet, if i move this button out of customFooter.html and in to input-field.html, to test the button logic is correct, the click is called.
I have tried $scope.$apply and $scope.digest after the .append, however i get a 'digest already in progress error'
UPDATE:
Based on comments, below, have tried to remove jQuery and use an 'angular way'
$scope.addCalendarFooter = function() {
var html = "<div ng-include src=\"myDir/calendar/customFooter.html\"><\div>";
var myEl = angular.element(document.getElementsByClassName('datepicker');
myEl.html(html);
$compile(myEl)($scope)
}
The above inserts my .html template via the ng-include however is it replacing the contents of the datepicker rather than inserting at the bottom of it. Tried .append but that didn't worth either.
You're basically loading the footer manually and bypassing angular completely. Angular doesn't see that you're loading the html and so doesn't compile the html template and bind directives, including ng-click, at all.
You can use ng-include directive that loads the specified template, instead of making a custom one yourself. Or if your directive needs other functionality, just have the ng-include in the directive template itself.
Quoting from your UPDATE:
The above inserts my .html template via the ng-include however is it replacing the contents of the datepicker rather than inserting at the bottom of it. Tried .append but that didn't worth either.
The aforementioned issue is due to using .html() method which inserts HTML to a DOM node and overwrites any existing content of the selected node:
var html = "<div ng-include src=\"myDir/calendar/customFooter.html\"><\div>";
var myEl = angular.element(document.getElementsByClassName('datepicker');
myEl.html(html); // <== here
$compile(myEl)($scope)
What you are doing with the above logic is that you first select the .datePicker element, then you replace its inner HTML with the .html() method.
As a workaround, you could have used .append() method instead.
NOTE: angular.element() is Angular's wrapper for an element just as in jQuery you had $(). Therefore, using document.getElementByClassName() method is redundant in your case.
Although the above workaround might solve your problem, but it is better to stick to a cleaner and concise approach which AngularJS may offer.
Angularesque Approach
You don't need to load a template partial by programmatically adding/appending the template in a controller function - at least in Angular's way. This way you might end up not binding the angular directive(s) within the dynamically added HTML correctly with a scope.
Instead, just include the partial within the original directive template (with ng-include) and use ngIf or ngShow to display it when the button is clicked.
Therefore, assuming that you've the footer template (customFooter.html) in the original directive template, you can achieve the expected result as in the following:
Directive Template
<div class='col-sm-6'>
<div class="form-group">
<div class='input-group datepick'>
<input type='text' class="form-control" ng-click="addCalendarFooter()"/>
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
</div>
<div ng-include="myDir/calendar/customFooter.html" ng-if="isCalenderFooterAdded"></div>
Directive Controller
controller: function($scope) {
$scope.addCalendarFooter = function() {
$scope.isCalendarFooterAdded = true;
// or use this if you want to toggle it
// $scope.isCalendarFooterAdded = !$scope.isCalendarFooterAdded ? true: false;
}
}
Plunkr mimicking a similar situation.
What if you include your footer in your template initially but hide it with ng-if / ng-show. Then the function would only change a flag and show the previously hidden footer.
You said that your problem is the fact that after calling $scope.$apply or $scope.digest you get the $digest already in progress error.
A clever way to bypass this is using $evalAsync. One of its biggest advantages is that it knows if it should perform an extra digest cycle of not.
If you're using ng-include don't forget the singlequotes at the begining and end of the path.
Angular doc about ng-include: https://docs.angularjs.org/api/ng/directive/ngInclude
make sure you wrap it in single quotes, e.g. src="'myPartialTemplate.html'"
it should be:
var html = "<ng-include src=\"'myDir/calendar/customFooter.html'\"><\ng-include>";
or:
var html = "<div ng-include=\"'myDir/calendar/customFooter.html'\"><\div>";
but not:
var html = "<div ng-include src=\"myDir/calendar/customFooter.html\"><\div>";
Here is The problem i am trying solve. I would like to create a JS script that uses angular to dynamically create div elements while adding an additional expression eg {{levelone}}.
Here is an an example of what i am expecting the output to be if i know i have 5 iterations of Dom elements.
<div ng-switch-when="0">{{levelZero}}</div>
<div ng-switch-when="1">{{levelOneA}}{{levelOneB}}</div>
<div ng-switch-when="2">{{levelTwoA}}{{levelTwoB}}{{levelTwoC}}</div>
etc.....
So as you can see i am adding an expression on each time. I just dont know how i can keep adding them on via a repeat loop and then get them to compile correctly with AngularJS. I am trying to make my DOM as Dynamic as possible.
EDIT
Every time i loop 1 more i am adding an expression ->{{expression}} to the div with JS or angular. I am not hard coding the expression into each div as each div is also dynamically created. But with a twist i am adding that extra expression ie 3 expressions from the previous div and adding one more expression making four. see example below.
<div ng-switch-when="3"{{levelTwoA}}{{levelTwoB}}{{levelTwoC}}</div>
This one below is dynamically generated
<div ng-switch-when="4"{{levelTwoA}}{{levelTwoB}}{{levelTwoC}}{{THIS EXPRESSION IS GENERATED IN js AND ADDED & COMPILED}}</div>
The DOM will be dynamic as long as your data bindings are. I need more info (data model, expectations) to be more accurate but if you set up your data and bind it correctly, it should work. for example if your data looked like this
var data = {
"levelOne" : {},
"levelTwo" : {
"elements" : ["<span id="firstEl"></span>, ...]
},
"levelThree" : {
"elements" : ["<span id="firstEl"></span>, <span id="secondEl"></span>, ...]
}
Then in your template do what #paulgoblin suggested.
}
You could ng-repeat within each switch case. Eg.
<div ng-switch-when="0">
<span>{{levelZero}}</span>
</div>
<div ng-switch-when="1">
<span ng-repeat="expression in levelOne">{{expression}}</span>
</div>
<div ng-switch-when="2">
<span ng-repeat="expression in levelTwo">{{expression}}</span>
</div>
You might want do this with scope function.
<div ng-switch="assignment.id">
<div ng-switch-when="1">{{ getExpressions(assignment.id) }}</div>
<div ng-switch-when="2">{{ getExpressions(assignment.id) }}</div>
</div>