Writing a directive to encapsulate multiple directives Angular - javascript

I'm using Angular 1.x and I have a section of code that I'm looking to repeat quite a bit, so I wanna throw it in a directive. The trouble is, it's somewhat complicated and I'm not sure how to begin writing it.
Essentially, it's a section of the page that displays various card directive and with infinite scrolling and perfect scrollbar.
<perfect-scrollbar refresh-on-change="myScope.data">
<div class="limit-columns">
<masonry masonry-options="{gutter: 30, isFitWidth: true}">
<user-card class="masonry-brick" ng-repeat="item in myScope.data"></user-card>
</masonry>
<div class="infinite-scroller" infinite-scroll="myScope.showMore()" infinite-scroll-tolerance="5"></div>
</div>
</perfect-scrollbar>
Perfect-scrollbar and masonry are both angular libraries on GitHub. Infinite-scroller is one I wrote myself, but works as you'd expect.
myScope contains a data attribute that is a list of objects containing a card's data. myScope.showMore is a function that adds items to that myScope.data list. Perfect-scrollbar also takes the refresh-on-change attribute which watches for changes on a particular object, in this case the list.
Ideally my directive would look something like this:
<card-scroller gutter="30" tolerance="5">
<some-card ng-repeat="achievements.data"></some-card>
</card-scroller>
But I'm not sure how feasible this is. Thanks!

Related

Using the same directive in a directive [angularjs]

I have a need to use the same directive within a directive, depending on a conditional param. However, when ever i try to do it, it seems to go to an endless loop. As i understand, it tries to pre-load the templates and that causes an endless recursion and at the end, it just throws me the following error:"RangeError: Maximum call stack size exceeded".
I have created an example in fiddle.. as you can see in the example, when the param's value is "1", it creates the error (even when the second level param is valued as "2" so it shouldn't have real recursion issues in the controller/app).
https://jsfiddle.net/qh9nh1gx/
"custom-directive"'s template:
<div>
<div ng-if='info==1'><div custom-directive info='2'></div></div>
<div ng-if='info==2'>DONE,single.</div>
</div>
Thanks
I have found 2 options to deal with the issue, the first one, is exactly what Jju described - creating a new "compiler" method (it can be grabbed from the url he sent).
The second option - always using an additional template for the "recursive" parts of the directive. For example, in my directive, i had a "ng-repeat" part that depending on the items value, it could request to display the directive again. while i used "ng-include" to have the other directive, it worked.
<div ng-repeat="item in items" ng-include="'inline-possibly-recursive-template"'></div>
in that template, you can call the directive again without any issues..
I hope that it will anyone else that will stumble into that issue.
You can look into https://stackoverflow.com/a/19065910/1680674 that describe a common approach to create directive that use himself inside

Toggling between templates displayed by an angular directive

I have an angular app that shows a list of things
<div ng-repeat="thing in things" regular-thing>
implemented with an ng-repeat that shows a regularThing directive for each thing in my list.
It's actually a little bit trickier than that though:
<div ng-repeat-start="thing in things">
<div ng-if="$first || thing.isSpecial" special-thing></div>
<div ng-if="!$first && !thing.isSpecial" regular-thing></div>
</div>
<div ng-repeat-end></div>
I use a different directive for the first thing in my list, and also for any "special" things. For our purposes, a thing becomes "special" when the user clicks on it - so, as they're scrolling through the list, they can click on a thing to have it displayed in a different (more extensive) template.
The way I have it now feels wrong to me. For one thing, I really don't need to have two different directives - just different templates. The logic is identical, specialThing just has a bit more of it. For another thing, I'm toggling a property on the data (namely thing.isSpecial) for purely view-related reasons, which makes me die a little bit inside.
So my question: Don't I deserve to die a little bit inside for this? Isn't there a cleaner, more "angular" way to handle this (i.e. to toggle between the directive templates)?
First, no one deserves do die. So the answer to your question is "no".
But you do raise some interesting points.
First, it's ok to have "view model" information in your scope (or controller, depending on if you are using the ControllerAs syntax or not). However, you definitely don't want to add view model information to your data models. Here's how I might do it (using your click-toggles-something-special example).
<div ng-repeat-start="thing in things">
<div ng-if="$first || isSpecial" ng-include="/specialtemplate.html" ng-click="isSpecial = !isSpecial"></div>
<div ng-if="!$first && !isSpecial" ng-include="/regulartemplate.html" ng-click="isSpecial = !isSpecial"></div>
</div>
<div ng-repeat-end></div>
The key difference is I'm adding the isSpecial property to the scope, not to thing, and isSpecial will be specific to that particular ng-repeat item's scope.
Also, unless you're planning on doing DOM manipulation, you can replace them with ng-include + ng-controller in the template html.
Some people prefer that pattern (include + controller) instead of directives, and other prefer to go ahead and others prefer to go ahead and write directives because it's more "componenty" (I made that word up). I think either is a valid way to go.

How to fix overlapping bricks with Angular + Masonry?

I'm using passy's angular-masonry directives to render tiled elements in my app. Unlike a lot of the masonry examples, my bricks don't include images, just static text and layout content rendered through a custom directive. My setup looks like:
<div data-masonry
data-column-width="250"
data-load-images="false"
data-preserve-order
data-reload-on-show
data-masonry-options="{ gutter: 15, isFitWidth: true, transitionDuration: 0 }">
<div class="masonry-brick"
data-ng-repeat="event in vm.events | orderBy: 'startTime.toDate()' | filter: (vm.showOnlyRegistered && { going: true })">
<div data-event-item="event"></div>
</div>
</div>
data-event-item is my directive that renders something like:
<div class="panel panel-default panel-thin light-shadow bgcolor-override event-item-card">
<div class="panel-body" data-ng-class="{'bg-success': event.going}">
<div>
<p class="text-medium text-thin">{{event.name}}</p>
</div>
<p>
<strong>{{event.computed.locationName}}</strong><br />
<span data-ng-if="!event.virtual">{{event.city}}, {{event.state}} {{event.zipCode}}</span>
</p>
<span class="center-block">
<span data-discover-pill data-type="default">
<span class="text-thin">{{event.computed.registrationLabel}}</span>
</span>
</span>
<button type="button"
class="btn push-to-bottom bottom-center"
data-ng-class="{'btn-default': !event.going, 'btn-success': event.going}"
data-ng-click="toggleGoing(event.id)">
I'm Going <i class="fa fa-check"></i>
</button>
</div>
In my CSS, I have a defined width and height for the elements that go into the bricks, so that (plus the fact that I'm explicitly setting masonry column-width) should let masonry know how big all my bricks are.
Everything works fine except that sometimes (randomly?) on pageload all the bricks are rendered stacked on top of one another in a big pile on the left edge, as if the layout routine didn't trigger. If the window is manually resized, everything snaps to normal and stays that way. This seems to be a problem that some others have run into: https://github.com/passy/angular-masonry/issues/82
I've tried all the combinations of preserve-order and load-images="false" and so on. I think I need to manually trigger a reload/relayout, but as far as I know with the passy directive, you can't directly call masonry methods.
I kept running into issues with Passy's directive. Perhaps it was due to the number of bricks I was loading or my styling but I kept getting a lot of sporadic overlaps, delayed reloading of bricks and so on.
I switched to klederson's angular-masonry-directive and have been extremely happy with the decision. I haven't benchmarked to confirm but it seemed to have sped up load times.
angular-masonry-directive
A very simple and 100% compatible masonry directive for AngularJS ... do you know how to use masonry? Good! You know how to use this.
This directive is meant for the raw masonry lib and not the jQuery one.
For anyone running into a similar problem, I figured it out with some help from these answers. As a bonus, this helps me with refreshing after using Angular filters to modify which bricks are shown (the second linked answer deals with this).
Here's my working code, which I trigger when my async data has loaded on pageload, and also anytime the data is filtered or resorted:
function refresh() {
// We need to give Masonry a little jump-start, otherwise the bricks
// will render in one big overlapped stack sometimes
common.$timeout(function () { $scope.$broadcast('masonry.reload'); }, 100);
}
Without $timeout, it looks (to my poorly-trained eyes) like the reload message is being consumed before the digest cycle completes, so the filtering or sorting may not have already taken place. I'm not sure that this is the best solution, but it seems to work in my testing so far. If anyone has a better one, I'd love to see it.
Tried 3 framework and only this one works :
https://github.com/s-yadav/angulargrid
No 3rd party lib required !! and smootly with twitter bootstrap. Hope it may help someone in the future
Note on others :
https://github.com/passy/angular-masonry -> work quite smooth on dev but failed on PROD.
https://github.com/klederson/angular-masonry-directive -> works but all the bricks are invisible !!?

Directive rendering an HTML output which contains rendered HTML in an attribute value

I'm trying to render an HTML like follows:
<div class="aClass" ng-controller="aCertainCtrl">
<span class="aSubclass" certain-attribute="<ul class='menu'><li class='selected'><a href='/'>Home</a></li><li><a href='/about'>About</a></li></ul>">
</span>
How is it possible to combine two directives (from my understanding it should be done using two), one for the element itself and the other to be compiled once the first has already been compiled, giving as an output an HTML markup embedded into the certain-atribute value?
Of course the aim of achieving this is allowing to render a certain HTML based on some model array, say the items of a menu, templating those items on how they will look like in terms of HTML, and embedding the ouput into the certain-attribute value.
I'm kind of newbie in AngularJS, so that still not controlling well the whole Directives process and its possibilities.
I know this is not the way to go, but I would like to see how to implement such a crappy thing in angular :D
Thanks in advance.
EDITING: Rewriting a bit for those ones who didn't understand well the issue
My goal is to have some HTML markup as a value inside an inline attribute of a certain element (even though is crap and nasty). Say <span data-some-markup="<p>Hello</p>"></span>.
I would like to use an attribute directive like this:
<div ng-controller="somecontroller"><my-custom-element></my-custom-element></div>
<my-custom-element> directive rendition will depend on someController $scope.data = [{name: Home, path: '/'}, { name: 'About', path: '/about' }];
The desired output will be something like:
<div class="aClass" ng-controller="aCertainCtrl">
<span class="aSubclass" certain-attribute="<ul class='menu'><li class='selected'><a href='/'>Home</a></li><li><a href='/about'>About</a></li></ul>">
</span>
</div>
Question is, how can you do this entirely with AngularJS?

Angular directive creation - using multiple templates

I'm fairly new to Angular JS and I'm trying to create a datepicker directive. It will not work like the Angular UI Bootstrap datepicker as it won't use a textbox but a full page calendar which you will be able to swipe through and click a day (which will update ngModel).
My plan was to have a single directive which has a renderMonth() function. This function would accept a month as a parameter and generate all the rows/days in an array which would then be bound to a template for the month.
My problem is that I can specify a template for the datepicker in my directive declaration but I don't know how to specify templates for the rows/days etc and load them in and bind them. I could do it using jQuery and lots of string concetenation, but that seems all wrong.
I have been reading the source code for the Angular UI Datepicker but as a newbie it makes very little sense to me. They have decoupled everything into many sub-directives (month directive, year directive etc.) and they have their own templates, but that's not what I want to do because my Angular skills aren't going to stretch to creating directives which communicate with each other. The Angular UI code is just way too complicated for me.
One thing I do like is that they use existing directives like ng-repeat in their templates and bind them to the array or rows/days. For me to do that I'd need to load the template in each time the renderMonth() is called and compile the template because it has existing directives in it.
So, basically my question is, does anybody have any examples of how I could write a import a template into my render function, compile it and ten bind it to the row/day data which is in my directives scope.
I'll be honest. I have no idea if I'm even speaking any sense. I'm just typing words which sound vaguely right.
ALl I need is for somebody to point me in the right direction. Thanks.
As discussed in the comments, I personally wouldn't worry too much about importing templates, the $compile function, or anything that complex for what is relatively simple markup.
I've created a (very) basic barebones plunker here, to demonstrate what my approach would be. I've used moment.js to deal with dates, because I am terrible with vanilla javascript date manipulation and find it very clumsy.
This is the template:
<div class="controls">
<button ng-click="prevMonth()"><-</button>
<span>{{selected.format('MMMM')}}</span>
<button ng-click="nextMonth()">-></button>
</div>
<div class="month">
<span class="day" ng-repeat="day in selected.days">
{{day.number}}
</span>
</div>
The template relies on the scope having a selected property which is a moment object. All it does is creates some buttons change the month and displays the month name at the top, then creates a span for each day in that month.
A tiny bit of CSS puts the days in rows of 7:
span.day{
float: left;
width: 25px;
}
span.day:nth-child(7n+1){
clear:left;
}
This is the directive's link function:
link: function(scope, element, attributes){
// Set the selection to now initially.
scope.selected = moment();
generateDaysArray = function(){
// -- REMOVED FOR BREVITY -- //
return days;
}
// Watch the month for changes and update the days array
scope.$watch(
function(){
return scope.selected.month();
},
function(newVal, oldVal){
scope.selected.days = generateDaysArray();
}
)
// Control button actions
scope.nextMonth = function(){
scope.selected.add('month', 1)
}
scope.prevMonth = function(){
scope.selected.subtract('month', 1)
}
}
There is really nothing special there, but do note that the $watch command used takes advantage of the fact that you can pass a function instead of a string (though looking at it now you could just use 'selected.month()' as a string).
Generating the days array is slightly more complex if you want to align the dates with days of the week, which I started to do, but didn't finish. My plan was for each object in the array to have a isPreviousMonth property which would conditionally apply a different style using ng-class, and would look up the correct number as well, rather than just using a ~ for the offset days!
There is a lot more to do to finish this off - not least making the dates selectable and integrating with ngModel, but hopefully this example has given you an idea of one way to approach the problem of templating. There are many other ways you could do this, but I find this the most intuitive.
Extension
To address your question regarding having multiple months, I've expanded the plunker a bit.
I added an attribute to the directive:
<date-picker num-months="3"></date-picker>
And modified the template to ng-repeat certain parts depending on the number of months you specify in the attribute:
<div class="controls">
<button ng-click="prevMonth()"><-</button>
<span ng-repeat="month in months" class="month-title">
{{month.moment.format('MMMM')}}
</span>
<button ng-click="nextMonth()">-></button>
</div>
<div class="month" ng-repeat="month in months">
<span class="day" ng-repeat="day in month.days">
{{day.number}}
</span>
</div>
So this instead requires an array of months, each with 2 properties: a moment, and an array of days. I added some quick and dirty javascript to generate that, but I won't paste it here as there are almost certainly much neater ways of doing it!
Note that at present we're still watching scope.selected for changes in the month, which would lead to the (probably unwanted) behaviour that if you selected a date not in the first month, the calendar would rearrange itself. I think instead of using a $watch in this case, I'd just add to the click events to handle redrawing the calendar properly.
PS This addresses the cloning part but not the scrolling part. For scrolling, I would recommend adding one extra month on either end of the months array, then having an ng-show condition that means only the middle month(s) is shown. You can then take advantage of angular's inbuilt animations (this is a good reference) to scroll them in/out using css. If you want more info about that contact me through the chat I opened for this question.

Categories

Resources