I've only been working with angular for 2 weeks so fairly new to the framework.
I'm making an app that shows data through charts, and i want the data to be viewable in different chart types. The idea is that you can click a button and swap the chart type.
The way i've been doing this is by rendering the chart through a directive using templateURL. Unfortunatly i've been unable to make the templateURL variable. I've tried different things and this is how it looks atm:
main.html:
<chart charttype="{{chartType}}"></chart>
directive:
.directive("chart", function () {
return {
restrict: 'E',
templateUrl: function (element, attrs) {
return 'views/partials/' + attrs.charttype + '.html';
}
}
Controller:
$scope.chartType = "lineChart";
$scope.ChangeChart = function (chartType) {
$scope.chartType = chartType;
}
The code is supposed to render 3 different html files (lineChart.html, barChart.html & pieChart.html). however the {{chartType}} is simply parsed as a string
It works when i use:
<chart charttype="lineChart"></chart>
For whatever reason i can't get the charttype attribute to become variable.
I also realize this might be more of a rails way of fixing an angular problem (i'm used to rendering partials). So maybe this is not the way to do this in angular. I've thought about other ways to do this like hide/show the charts but this just seems ugly to me. I'm up for any suggestions though.
update 1:
so i'm trying to render it via ng-include in all the ways i can think of but it keeps giving me errors or doesn't show anything at all.
i've also tried putting the ng-include directly into my html file
<div ng-include="views/partials/lineChart.html"></div>
However in my browser i see it just comments this line out.
update 2:
I couldn't get ng-include to work with a variable template. So i've decided to solve the problem by removing the templates and using ng-hide in my html file instead.
Once the template url is set, it is not called again. Use ng-include with variable template
Something like
template: "<div ng-include=\"'views/partials/' + charttype + '.html'\"></div>"
Directive templateUrl parameter can't get variable value as argument, just static text. if you want, i can show solution with ng-include directive in directive template parameter.
Related
I have a one-page site that I am building out and this is my first time using Angular on a site. Building it on top of Laravel too for the backend but that is beyond the scope of this question.
I need to be able to open a modal on a main page view which will add a new resource (e.g. a new client) or edit a resource. I want to somehow get the form's html inside the modal body when the $uibModal.open()'s controller is called and set the $scope.modalBody equal to the injected items.modalBody (the only way this works is if I use:
$scope.modalBody = $sce.trustAsHtml(items.modalBody);
The only problem now is that anything inside the HTML body, Angular will not use it's magic and do any data-binding. It is still in the raw form of
{{ object.property }} or since I'm using Laravel and avoiding conflict with the Blade template engine:
<% object.property %>
See screenshot:
screenshot
I have been banging my head against the wall on this one...I have tried putting $scope.$apply() in my directive and my controller, neither of which worked. I have a feeling that is the source of my problem though. I have also tried making the html just a <new-client></new-client> directive and using templateUrl: 'views/clients/add.php' which would be ideal, but the template is not being included inside the <new-client></new-client>.
I'm using ui-bootstrap 0.14.3 and Angular 1.4.8.
Could this be a bug? Or am I doing something wrong? Anyone have a better way of getting a form into my modal? Let me know what code you want to see so I don't clutter this post with unnecessary code blocks.
I have come across a similar issue with using jQuery's AJAX to receive template strings and append it to a server.
So when HTML is added via jQuery, bound html string, etc., angular doesn't know it needs to automagically compile this data.
What you need to do is use the $compile service, to $compile your html and then attach the correct $scope to it:
`$compile('jQuerySelectorReturningHtmlOrAnHTMLStringThatNeedsToBeCompiled')($scope);`
There are multiple examples in Angulars Documentation for $compile that can give you an idea of what is happening. I think by what you have described the same thing is happening here in your situation.
The key is to call this $compile service function after the html has been bound to the page.
EDIT:
There are a few other options based on some comments, that will serve as a viable solution to rendering this content on your view. For example a directive that takes a string attribute representing the HTML string of your desired view.
1. Modify your directive template in the compile step:
You have the ability to modify your template before the directive compiles and binds any attributes to it, to that directives scope:
app.directive('myAwesomeCompileStepDirective', [myAwesomeCompileStepDirectivef]);
function myAwesomeCompileStepDirectiveFn() {
return {
restrict: 'EA',
compile: function compileFn(tAttrs, tElement) {
//Here you can access the attrs that are passed into your directive (aka html string)
tElement.html(tAttrs['stringThatYouWantToReplaceElementWith']);
return function linkFn(scope, element, attrs, controller, transcludeFn) {
//if all you want to do is update the template you really don't have to do anything
//here but I leave it defined anyways.
}
}
}
}
You can view a file I wrote for a npm component which uses this method to modify my directive template before it is compiled on the page & you can also view the codepen for the complete component to see it in action.
2. Use $compile service to call $compile in link function using directive attrs.
In the same way as the aforementioned method, you can instead inject the $compile service, and call the function mentioned above. This provides a bit more work, for you but more flexibility to listen to events and perform scope based functions which is not available in the compile function in option 1.
I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});
I am having troubles implementing a custom directive with transclude: true that can have a "transclusion" content that is using ngRepeat.
[My case]
I want to implement a set of directives, that are fetching the data that they are supposed to show from a $http service. For that I want to use preLink phase interceptor that Angular provides, so I can catch the data and set it to the scope. That way if I have some dynamic (since this term is well overloaded - I mean a data which structure is unknown until the request is done) data coming from the service, I rely on that, that I will be able to retrieve a list with that dynamic data and store it inside the scope, then loop through that data via ngRepeat inside the HTML. Here comes my problem...
[My Problem]
Angular is not using the list that I am assigning to the scope during preLink.
[A plunkr]
I maded a plunker that illustrated just the problem that I am having.
http://plnkr.co/edit/XQOm4KWgKxRhn3pOWqzy?p=preview
[My question]
I really believe that such functionality is covered by angular and I am just missing something in the puzzle.
Can anyone tell me how to implement such behaviour?
Thanks!
EDIT:
Thank you rchawdry for your answer. Here are some details on my intentions. To make it simple I will try to give you an example.
Let's assume that we have these directives:
1. "page" - This directive is a labeled container for all the page content. Visually it is represented as some div - for header, for content and for other fancy stuff if needed. The directive does not know what is its data before the page loads. As the page loads the directive must retrieve the information for itself and its children from a REST resourse! Then the directive is setting the information needed for itself (label and other stuff) and stores its children content in childrenList scope variable. It creates a scope.
2. "section" - This section can be child of "page". Since "page" is retrieving its data from a server, then the information about how many "section"s does our "page" have is dynamic and we don't know how many "section"s we need to show on the screen. This depends on sectionList that is coming from the back-end. The section itself is almost the same as "page" - it is a labeled container, with the differences that - a). "section" is container of elements; b). "section" does retrieve its data from its parrent instead of making $http request. This directive creates a scope.
3. "element" - For this example, in order not to define many different elements and complicate it, let's assume that I have one element, called "element". It can consist of some "input" with "span" and "button" if needed. It is similar to the "section" with that, that it retrieves the data to show from it's parrent (in the general case, this is "section" or "page"). On the other hand it is different than "section" by the fact that it has no transcluded content.
Now after we have some of the concept here is what I am trying to achieve:
<page>
<element id='element1' someOtherStuffHere...></element>
<section id='static_section1' someOtherStuffHere...>
<element id='element2' someOtherStuffHere...></element>
</section>
<div class="row" ng-repeat="section in sections">
<section dynamic_id='dynamic_section'>
<div class="row" ng-repeat="elem in elements">
<element dynamic_id='dynamic_element'></element>
</div>
</section>
</div>
</page>
well, I believe that what your trying to achieve will be able by adding a ng-repeat attribute to the transcluded template.
by letting angular know about the 'repeat', it is supposed to work.
since plunkr is currently unavaliable, I can't prodivde any preview and do not have your original code. Ill try to recall it:
template: "<div id='container'>" +
"<div class='content' ng-repeat='item in [1]' ng-transclude'></div>" +
"</div>"
edit: http://plnkr.co/edit/xba4pU666OGxBtKtcDwl?p=preview
You've got a scope problem. The controller is using a variable that isn't defined in the controller (arrayListItemsPre and arrayListItemsPost). While they are declared in the directives, accessing them in a transcluded scope is a little tricky.
The easy way is to do the following. This will present the scope variables up to the controller where they can be used.
app.directive('container', function($compile) {
return {
transclude: true,
scope: true,
replace: true,
restrict: 'E',
template: "<div class='container'>" +
"<div class='content' ng-transclude></div>" +
"</div>",
compile: function(cElem, cAttrs) {
return {
pre : function preLink(scope, iElement, iAttrs) {
console.log("Hello from Container preLinkPhase");
scope.$parent.arrayListItemsPre = [1, 2];
},
post : function postLink(scope, iElement, iAttrs) {
scope.$parent.arrayListItemsPost = [1, 2];
}
};
}
};
});
There are other ways to do this that are better but it requires understanding why you're trying to iterate on variables that are defined in the directive. If you're going to be using this directive in another page that has different array elements, you'd have to change the directive code.
I'm just getting started on Angular and am trying to wrap my head around proper directive use. I'm writing a custom directive that takes an object array and parses it into a variable number of vertical divs. It's basically a grid system where the elements are arranged into stacked vertical columns rather than in rows. The number of divs dynamically varies with the width of the screen, requiring dynamic changes in the div class as well as reconstructing the ordering of the array elements in the div columns as the page resizes.
When I use the contents of the template as plain, static HTML, everything loads just fine. The filters dynamically change the dataset when you use the input fields, etc.
When I use my directive, the initial page-load looks fine. However, dynamic filtering is broken - it is no longer bound to the input fields. More importantly, on a page resize, the HTML fails to compile at all, leaving a blank screen and uncompiled directive tags in the DOM.
I don't know Angular well enough to troubleshoot this. If I had to guess, it sounds like something is not being bound properly on the page $compile due to a problem with scope.
Note: I know doing string concat for the template is poor practice but I just want to get things working before I start messing around with nesting directives.
Edit: here's a link to the Github repo for my front-end code: https://github.com/danheidel/education-video.net/tree/master/site
HTML
<body ng-controller="channelListController">
Creator: <input ng-model="query.creators">
Tags: <input ng-model="query.tags">
query: {{query}}
<div id="channel-view">
<channel-drawers channels="channels"></channel-drawers>
</div>
</body>
JS
.controller('channelListController', function ($scope, $http){
$http.get('api/v1/channels').success(function(data){
$scope.channels = data;
});
})
.directive('channelDrawers', function($window, $compile){
return{
restrict: 'E',
replace: true,
scope: {
channels: '='
},
controller: 'channelListController',
//templateUrl: 'drawer.html',
link: function(scope, element, attr){
scope.breakpoints = [
{width: 0, columns: 1},
{width: 510, columns: 2},
{width: 850, columns: 3},
{width: 1190, columns: 4},
{width: 1530, columns: 5}
];
angular.element($window).bind('resize', setWindowSize);
setWindowSize(); //call on init
function setWindowSize(){
scope.windowSize = $window.innerWidth;
console.log(scope.windowSize);
_.forEach(scope.breakpoints, function(point){
if(point.width <= scope.windowSize){
scope.columns = point.columns;
}
});
var tempHtml = '';
for(var rep=0;rep<scope.columns;rep++){
tempHtml +=
'<div class="cabinet' + scope.columns + '">' +
'<div class="drawer" ng-class="{' + ((rep%2 === 0)?'even: $even, odd: $odd':'even: $odd, odd:$even') + '}" ng-repeat="channel in channels | looseCreatorComparator : query.creators | looseTagComparator : query.tags | modulusFilter : ' + scope.columns + ' : ' + rep + '">' +
'<ng-include src="\'drawer.html\'"></ng-include>' +
'</div>' +
'</div>';
}
console.log(tempHtml);
element.html(tempHtml);
$compile(element.contents())(scope);
}
}
};
})
The $compile function should be implemented when you want to manipulate your template. The link function should be implemented when you want to bind your template to your scope and/or setup any watchers. If you have dynamic HTML that you're inserting into your DOM, then ask your self these questions:
Are you modifying the template? If so, then create an element template (angular.element(...)) and append it to your element parameter.
Have you modified the template (step 1) and your template contains binding expressions, interpolation expressions, and/or attributes that should bind from other templates? If so, you need to compile and link your element you created from step 1.
Here is an example:
.directive('myDirective',function($compile) {
restrict: 'E',
scope: '=',
compile: function(element, attr) {
// manipulating template?
var e = angular.element('<div ng-model="person">{{person.name}}</div>');
element.append(e);
// the following is your linking function
return function(scope, element, attr) {
// template contains binding expressions? Yes
$compile(e)(scope);
};
}
});
To fix your code, try moving the template manipulation to your compile function, and in your linking function, call $compile(e)(scope).
First, thanks to pixelbits and pfooti for their input. It put me on the right track. However, I wanted the answer to be a clean slate since our discussions got into technical matters that ended up being tangential to the actual answer.
Basically, this question is poorly framed. After doing more reading, it became clear that I was using the Angular elements in ways they aren't really intended for.
In this case, I'm doing a bunch of model manipulation in my directive and it really should occur in the controller instead. I ended up doing that and also moving the window resize handler code to another component.
Now, I don't even need a directive to properly format my data. A couple of nested ng-repeats with a dusting of ng-class and ng-style do the job just fine in 2 lines of HTML.
<div ng-repeat="modColumn in splitChannels"
ng-class="{'col-even' : !$even, 'col-odd' : !$odd}"
ng-style="{ width: 99 / windowAttr.columns + '%' }"
class="cabinet">
<div ng-repeat="channel in modColumn"
ng-class="{'row-even' : !$even, 'row-odd' : !$odd}"
ng-cloak
class="panel roundnborder">
<ng-include src="'panel.html'"></ng-include>
</div>
</div>
If I could give a bit of advice from one Angular beginner to another, it would be this: if your code is getting complex or you're digging into the internals of things, step back and rethink how you're using the Angular components. You're probably doing an action that should be done in another component class. Proper Angular code tends to be very terse, modular and simple.
Is there a reason you're doing it with templates?
Can't you bind the number of columns to a variable on the scope? I've done similar stuff with just fiddling around with either ng-if directives to hide stuff that's not important now, or to have general layout stuff attached to the current scope (I generally stuff it all into properties on $scope.view)
There's also plenty of this kind of stuff that already works in css3's media selectors as well, without needing mess with the DOM at all. Without a clearer picture of what you're trying to accomplish I'm not sure if this is super-necessary. More than one ways to skin a cat, etc etc.
Otherwise, #pixelbits is right - if you are fiddling with the DOM tree directly, that needs to happen in compile - values going into the DOM goes into link.
I'm trying to follow angular best practice recommendation and use directives to
encapsulate reusuable HTML elements.
The error message:
Error: Template must have exactly one root element. was: partials/user/path/to/somedata.html
the directive code:
.directive('stDirectivename', function() {
return {
restrict: 'E',
replace: true,
// transclude: false,
template: 'partials/user/path/to/somedata.html'
};
})
And the template:
<div ng-show="person.condition" class="someclass">
<span class = "personRoi">
<i class="anotherclass " ng-class="{'specialclass1': person.count>=0,'specialclass2':person.count<0}">
</i>{{person.somedata}}%
</span>
</div>
Called in the partial (which is the template of a modal) as:
<st-directivename></st-directivename>
when I replace the template url for a simple html string in the directive. Everything works. Unfortunately I can't do that for the real template that involves both ' and“. besides this solution won't scale to the larger templates I plan to some directives.
Also when I just insert the template html instead of the directive tag, everything works correctly (I'm actually extracting the code from the existing html to make it reusable).
I read in other SO questions that this has to do with having extra space/tags/comments in the template. But I just can't find such elements.
Does anybody know a solution for this? I'll be glad for any help.
your mistake is: you must use templateUrl rather than template so as to indicate the path to the html partial
.directive('stDirectivename', function(){
return {
restrict:'E',
replace:true,
//transclude:false,
templateUrl:'partials/user/path/to/somedata.html'
};
})
For those that may come after, also note that directive templates need to have --as the error says-- only one root element i.e. multiple spans or divs must be enclosed in a root div.
Also note the comments on the OP: Whitespace or trailing comments in the template may result in this error as well.
It appears a fix to make Angular less temperamental about this may be included in the next release/update: github.com/angular/angular.js/issues/1459
For those who are still looking for further clues...I ran into this same error when I had a typo in the templateUrl path of the directive. You get this error if you have replaced: true. Otherwise, you may see more wild error as WARNING: Tried to load angular more than once, which took me quite a while to figure out because the error message is really misleading.