Angular directive creation - using multiple templates - javascript

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.

Related

Angular2: Dynamically create view selector based on model property?

I've been a C/C++ coder for a long time, but am new to Angular2 and TypeScript. I have a TypeScript class (call it Node) which has a type: string property that specifies what sort of node it is. (The data comes as JSON from a server, and is basically being parsed out as a syntax tree.)
What I'm doing now in the view template in order to choose the view for each Node object is using an ngSwitch to check the value of node.type, and then inserting the appropriate selector using ngSwitchCase for each possible case, e.g.:
<div [ngSwitch]="node.type">
<node-type-a-view *ngSwitchCase="'a'" [(node)]="node"></node-type-a-view>
<node-type-b-view *ngSwitchCase="'b'" [(node)]="node"></node-type-a-view>
<node-type-c-view *ngSwitchCase="'c'" [(node)]="node"></node-type-a-view>
<div class="debug" *ngSwitchDefault>
TODO: View for type "{{node.type}}" is missing
</div>
</div>
The problem is that there are over a hundred different node types, and so this is going to be very unwieldy, very quickly.
What I'd like to do is something like this (but obviously this doesn't work in Angular2):
<node-type-{{node.type}}-view [(node)]="node"></node-type-{{node.type}}-view>
Or, using an attribute selector:
<div node-type-{{node.type}}-view [(node)]="node"></div>
Is there any functionality that comes close to what I'm looking for? I don't need to dynamically create the node-type-*-view views themselves, just the template that references them.
Also, apologies if I'm getting this architecture completely wrong. I'm still wrapping my head around the way Angular2 does things!
Thanks for any assistance you can give!

Writing a directive to encapsulate multiple directives Angular

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!

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

How to add html into DOM with Angularjs?

if it's jquery, we can do like $('table-row').clone().preprenTo($('table'));
I know it's just push a new value into an object in adding new data but I want to first insert the empty row and field first, how to add that html with angularjs?
John added a link to a famous question/answer regarding AngularJS. I would advice you to read that.
That said, to answer your question - in angular you do not tell it how to manipulate the dom. You tell it what data you have and how you want it presented.
I can only guess to what you are trying to do, but if you have a template (your 'table-row') and a 'destination' ('table') you would describe it in AngularJS like this:
<table ng-controller="PersonsController">
<tr ng-repeat="person in persons">
<td>{{person.Name}}</td>
<td>{{person.Address}}</td>
</tr>
</table>
That is great, but how do you add a row? Well you don't "add a row" - you add a person and AngularJS will do the adding of rows for you. You already explained how you want a person displayed.
To add a person you would have to add a person to a list/array of persons and that list would be in the scope of your view/application.
persons.push({Name: 'Bill', Address: 'Somewhere'});
You will need to attach the persons to your scope, which you will do in a controller. A controller would have to be associated with the code above with the ng-controller directive.
app.controller('PersonsController', function ($scope) {
$scope.persons = [];
});
In the above code i assume you have a variable app pointing to your angular application. There is a small learning curve you will have to overcome, going from jquery to angular. Mostly, the way i see it, is moving your mindset from a imperative coding style to a declarative coding style.
I suspect you need a paradigm shift. Read "Thinking in AngularJS" if I have a jQuery background.
Then, if you still think that your problem is best solved by adding a row to the DOM, consider using an AngularJS directive.

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.

Categories

Resources