I am learning angularjs through the process of taking an existing site that was built primarily with JQuery and trying to "angularize" it. I am having trouble reproducing the same functionality in angular.
Please see the following plunker.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
I have a ul element with an angularjs directive called "scroller" as seen below.
<ul class="dropdown-menu-list scroller" scroller style="height: 250px">
<li data-ng-repeat="n in notifications">
<a href="#">
<span class="label label-success"><i class="icon-plus"></i></span>
{{n.summary}}
<span class="time">{{n.time}}</span>
</a>
</li>
</ul>
The scroller directive looks like this:
.directive('scroller', function () {
return {
priority: 0,
restrict: 'A',
scope: {
done: '&',
progress: '&'
},
link: function (scope, element, attrs) {
$('.scroller').each(function () {
var height;
if ($(this).attr("data-height")) {
height = $(this).attr("data-height");
} else {
height = $(this).css('height');
}
$(this).slimScroll({
size: '7px',
color: '#a1b2bd',
height: height,
disableFadeOut: true
});
});
}
};
What i want to happen is that the ng-repeat executes on the notifications array in the controller, producing a collection of li elements that exceed 250px therefore a slimscrollbar would be added. What actually happens is the result of the ng-repeat is not included in the final DOM. I believe the call in the parent scroller directive of $(this).slimScroll() is called after the ng-repeat executes and replaces the DOM. If i remove the scroller attribute, the li elements show up.
I am sure there is a strategy for this and am hoping the community can educate me on a better approach or alternate approach. thoughts? again the plunker is here.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
Thanks,
Dan
The issue is actually your directive scope. You are using an explicit object as the scope, which means you are isolating the scope, which means the directive scope isn't inheriting from its parent anymore. So notifications from the parent controller is no longer reachable from the directive scope (and therefore any elements inside of its element).
If you remove this from your directive it should work:
scope: {
done: '&',
progress: '&'
}
I notice that you aren't using those attributes anyway so it shouldn't break any other functionality.
Look at the API docs http://docs.angularjs.org/guide/directive and look for isolate scope for more details.
An alternative to what you're trying to do would just be something like this
scope.$watch(attr.done, function(val) { //do something when the value changes })
Since I don't know your use case I can't say what the best solution would be.
Related
I have a main component that handles the toolbar and sidnav of my angular application. I would like to make a div inside the toolbar available to child components (and controllers) to customize so that they can do things like change the toolbar title text and add contextual buttons. This feels sort of like the opposite of transposition where a parent component can customize part of a child component (e.g. a menu component customizing the content of a button). One option would be to have the toolbar managed by a service, but even then I can't think of a great way to customize the content of the toolbar without doing a decent amount of javascript that builds up dom elements (one of the things I always try to avoid in angular).
In Angular 1.6.x components use isolate scope only:
Components only control their own View and Data: Components should
never modify any data or DOM that is out of their own scope. Normally,
in AngularJS it is possible to modify data anywhere in the application
through scope inheritance and watches. This is practical, but can also
lead to problems when it is not clear which part of the application is
responsible for modifying the data. That is why component directives
use an isolate scope, so a whole class of scope manipulation is not
possible.
So, to make this work you would need to use a directive instead of a component. The div you want to include would need to be a directive itself to be able to alter the parent scope of the toolbar directive. What you would be doing is transcluding one directive inside another, and using shared scope to alter the parent scope.
This article is a really good resource for what you are trying to accomplish. I would start here: https://www.airpair.com/angularjs/posts/transclusion-template-scope-in-angular-directives
I have altered the example Codepen from that article to show you how it might work: http://codepen.io/jdoyle/pen/aJQpYo
If you select items in the list you can see that the header name changes to whatever is selected.
angular.module("ot-components", [])
.controller("AppController",($scope)=> {
//Normally, this data would be wrapped in a service. For example only.
$scope.header = "Marketing";
$scope.areas = {
list: [
"Floorplan",
"Combinations",
"Schedule",
"Publish"
],
current: "Floorplan"
};
})
.directive("otList", ()=> {
return {
scope: false, // this is one of the major changes
template:
`<ul class="ot-list">
<li class="ot-list--item"
ng-repeat="item in items"
ng-bind="item"
ng-class="{'ot-selected': item === selected}"
ng-click="selectItem(item)">
</li>
</ul>`,
link: (scope, elem, attrs) => {
scope.items = JSON.parse(attrs.items);
scope.selected = attrs.selected;
scope.selectItem = (item) => {
scope.selected = item;
scope.$parent.header = item; // this is the other major change
};
}
};
})
.directive("otSite", ()=> {
return {
scope: true, // another major change
transclude: true,
template:
`<div class="ot-site">
<div class="ot-site--head">
<img class="ot-site--logo" src="//guestcenter.opentable.com/Content/img/icons/icon/2x/ot-logo-2x.png">
<h1>{{header}}</h1>
</div>
<div class="ot-site--menu">
</div>
<div class="ot-site--body" ng-transclude>
</div>
<div class="ot-site--foot">
© 2015 OpenTable, Inc.
</div>
</div>`
};
});
Thanks in advance for taking the time to look into this question
I have serverside generated code that renders a directive wrapped around pre-rendered content.
<serverside-demo color="blue">
<p><strong>Content from Server, wrapped in a directive.</strong></p>
<p>I want this color to show: <span ng-style="{color: color}">{{color}}</span></p>
<button ng-click="onClickButtonInDirective()">Click Me</button>
</serverside-demo>
This means that 1.) the directive tag, 2.) the content inside the directive tag, 3.)the ng-click and 4.) The curly braces syntax are all generated by the server.
I want AngularJs to pick up the generated code, recompile the template and deal with the scope.
The problem is that I am having trouble getting it working. I understand that because the ng-click is inside the controller block, it is picked up not by the directive isolated scope, but the parent controllers. Instead I want the opposite... to pick up the onClickButtonInDirective scope function inside the serversideDemo link
I have created a jsfiddle best explaining my problem, which aims to clearly demonstrate the working "traditional" way of loading the template separately (which works) comparing it to the server-side way.
https://jsfiddle.net/stevewbrown/beLccjd2/3/
What is the best way to do this?
Thank you!
There are two major problem in your code
1- directive name and dom element not matched, - missing in dom element
app.directive('serverSideDemo', function() {
use <server-side-demo color="blue"> instead of <serverside-demo color="blue">
2- you need to compile the html code of server-side-demo dom with directive scope in link function
$compile(element.contents())(scope);
Working jsfiddle
Use templateUrl instead of template to fetch the content of directive from server:
app.directive('serverSideDemo', function() {
return {
restrict: 'AE',
scope: {
color: '='
},
templateUrl: 'link/that/returns/html/content',
link: function(scope, element, attrs) {
scope.onClickButtonInDirective = function() {
console.log('You clicked the button in the serverside demo')
scope.color = scope.color === 'blue' ? 'red' : 'blue';
}
}
};
});
Have a look at angular docs for more details
I'm having a hard time enabling a Bootstrap's popover component to my dom elements.
I'm working with AngularJS and on my template, I am using the ng-repeat directive to create a gallery.
<div ng-repeat="phone in phones" >
<a class="thumb" href="#/phones/{{phone.id}}">
<img class="img-responsive phone_image" ng-src="{{phone.image_path}}" data-content="{{phone.text}}" rel="popover" data-placement="right" data-trigger="hover">
</a>
</div>
On my template controller, I'm fetching the phones data from a third party API and than injecting it to the scopes variable "phones", like so:
phoneControllers.controller('PhoneListCtrl', ['$scope', 'Phones',
function ($scope, Cards) {
// Phones is the service that queries the phone data to the API
Phones.query(function(data){
// Got a response, add received to the phones variable
$scope.phones = data;
// for each .card_image element,give it the popover property
$('.phone_image').popover({
'trigger': 'hover'
});
});
}]
);
My problem lies with the $('.phone_image').popover segment. My thought was that by doing it inside the query's callback function it would work, since that's when the ".phone_image" elements are created. However it doesn't.
I seem to be failing to understand exactly in what scope should I assign the .popover property. I know it works because if I do it on the developer tools console, after all page content has been loaded, it works properly. I just don't know where to call it in my code to begin with.
Thanks in advance
It's happening because you are manipulating the DOM inside a controller. You should not do this, as the documentation says:
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic. Putting any presentation logic into Controllers significantly affects its testability. Angular has databinding for most cases and directives to encapsulate manual DOM manipulation.
In other words, when you use an Angular controller, you're just delegating the DOM manipulation to Angular through $scope databinding.
If you would like to manipulate the DOM, you should rely on directives. In your case, you can create a phonePopover directive like this:
angular
.module('phone', [])
.directive('phonePopover', function() {
return {
restrict: 'A',
replace: false,
scope: {
phoneText: '=phonePopover'
},
link: function(scope, element, attr) {
element.popover({
'trigger': 'hover',
'placement': 'right',
'content': scope.phoneText
});
}
});
And apply it to your element as following:
<img data-phone-popover="{{phone.text}}" class="img-responsive phone_image" ng-src="{{phone.image_path}}">
In my AngularJS application I'm using fontawesome for my loading spinners:
<i class="fa fa-spin fa-spinner" ng-show="loading"></i>
I'm also using AngularToaster for notification messages which has a dependency on ngAnimate. When I include ngAnimate in my AngularJS application, it messes up my loading spinners by animating them in a weird way. I want to stop this from happening but cant find a way to disable the animation on just these loaders (it would also stink to have to update every loader I have in my app).
Heres a plunkr showing my exact problem.
http://plnkr.co/edit/wVY5iSpUST52noIA2h5a
I think the best way to do this is to use the $animateProvider.classNameFilter which will allow you to filter items to animate or in this case not to animate. We'll do something like:
$animateProvider.classNameFilter(/^((?!(fa-spinner)).)*$/);
//$animateProvider.classNameFilter(/^((?!(fa-spinner|class2|class3)).)*$/);
Demo:
http://plnkr.co/edit/lbqviQ87MQoZWeXplax1?p=preview
Angular docs state:
Sets and/or returns the CSS class regular expression that is checked
when performing an animation. Upon bootstrap the classNameFilter value
is not set at all and will therefore enable $animate to attempt to
perform an animation on any element. When setting the classNameFilter
value, animations will only be performed on elements that successfully
match the filter expression. This in turn can boost performance for
low-powered devices as well as applications containing a lot of
structural operations.
As another answer per the comment with the no-animate directive, you could write an ng-show directive that will run at a higher priority and disable the animation. We will only do this if the element has the fa-spinner class.
problemApp.directive('ngShow', function($compile, $animate) {
return {
priority: 1000,
link: function(scope, element, attrs) {
if (element.hasClass('fa-spinner')) {
// we could add no-animate and $compile per
// http://stackoverflow.com/questions/23879654/angularjs-exclude-certain-elements-from-animations?rq=1
// or we can just include that no-animate directive's code here
$animate.enabled(false, element)
scope.$watch(function() {
$animate.enabled(false, element)
})
}
}
}
});
Demo: http://plnkr.co/edit/BYrhEompZAF5RKxU7ifJ?p=preview
Lastly, and similar to the above, we can use the no-animate directive if we want to make it a little more modular. In this case I'm naming the directive faSpin which you could do in the answer above. This is essentially the same only we are preserving the directive from the SO answer mentioned in the comment of the above codeset. If you only care about the fa-spin animation issues naming it this way works well, but if you have other ng-show animation problems you'd want to name it ngShow and add to the if clause.
problemApp.directive('noAnimate', ['$animate',
function($animate) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$animate.enabled(false, element)
scope.$watch(function() {
$animate.enabled(false, element)
})
}
};
}
])
problemApp.directive('faSpin', function($compile, $animate) {
return {
priority: 1000,
link: function(scope, element, attrs) {
if (element.hasClass('fa-spinner')) {
element.attr('no-animate', true);
$compile(element)(scope);
}
}
}
});
Demo: http://plnkr.co/edit/P3R74mUR27QUyxMFcyf4?p=preview
I had a similar problem where my icon would keep spinning until the animation finished, even after the $scope variable turned off. What worked for me was to wrap the <i> fa-icon element in a span.
<span ng-if="loading"><i class="fa fa-refresh fa-spin"></i></span>
Try it!
I've found an easier way.
<i class="fa fa-spinner" ng-show="loading" ng-class="{'fa-spin' : loading}"></i>
Forked plunker: http://plnkr.co/edit/mCsw5wBL1bsLYB7dCtQF
I did run into another small issue as a result of doing this where the icon would appear out of position if it spun for more than 2 seconds, but that was caused by the 'ng-hide-add-active' class, so I just added in my css:
.fa-spinner.ng-hide-add-active {
display: none !important;
}
and that took care of it.
EDIT: Nico's solution is a slightly cleaner version of this, so I'd consider using his.
angular.module('myCoolAppThatIsAwesomeAndGreat')
.config(function($animateProvider) {
// ignore animations for any element with class `ng-animate-disabled`
$animateProvider.classNameFilter(/^((?!(ng-animate-disabled)).)*$/);
});
Then you can just add the class ng-animate-disabled to any element you want.
<button><i class="fa fa-spinner ng-animate-disabled" ng-show="somethingTrue"></i></button>
Updating James Fiala Answer.
<i class="fa fa-spinner fa-spin" ng-show="loading"></i>
You don't need the ng-class as mentioned in #James Fiala Answer. But you should have fa-spin as one of the class.
Add style
.fa-spinner.ng-hide-add-active {
display: none !important;
}
This seems like such a simple thing, but I am just not able to wrap my head around how to do it.
Here is what I want:
<my-buttons>
<my-button ng-click="doOneThing()">abc</my-button>
<my-button ng-click="doAnotherThing()">def</my-button>
</my-buttons>
That turns into something like this:
<ul class="u">
<li class="l"><button ng-click="doOneThing()">abc</button></li>
<li class="l"><button ng-click="doAnotherThing()">def</button></li>
</ul>
Notice how the ng-click is on the button, inside a wrapping li. However, the normal transclusion will place the ng-click on the li.
My best try is on this fiddle: http://jsfiddle.net/WTv7k/1/ There I have replaced the ng-click with a class, so it is easy to see when it works and not.
Any ideas of how to get this done? If it is really easy, maybe the tabs/pane example on the frontpage could be expanded to include a wrapper around the panes, while still keeping the attributes.
With replace:true the replacement process migrates all of the attributes / classes from the old element (<my-button ...>) to the new one (the root element in the template, <li ...> ). Transclude moves the content of the old element to the specified (ng-transclude) element. I'm not sure if there's a simple way to change which element in the template that will receive the migrated attributes.
To achieve what you want you could probably do some dom manipulation in a custom compile function in the my-button directive. However, I think it'd be a better idea to create a new isolate scope in the my-button directive:
<my-buttons>
<my-button click-fn="doOneThing()">abc</my-button>
<my-button click-fn="doAnotherThing()">def</my-button>
</my-buttons>
(notice I've changed ng-click to click-fn)
module.directive('myButtons', function () {
return {
restrict:'E',
transclude:true,
replace:true,
template:'<ul class="u" ng-transclude></ul>'
}
});
module.directive('myButton', function () {
return {
scope:{clickFn:'&'},
restrict:'E',
transclude:true,
replace:true,
template:'<li class="l"><button ng-click="clickFn()" ng-transclude></button></li>'
}
});
I've also made a working version of your fiddle.
To understand how the isolate scope works (scope:{clickFn:'&'}) I recommend you read the angular guide on directives.