=================
First of I just want to say that I'm new to this AngularJs business (started earlier today) and I probably over complicate things.
With that being said let's get down to business, shall we?
I have the following jsFiddle and the html output is as follows.
<div ng-app="test" ng-controller="About" class="hex-grid clearfix ng-scope">
<div class="hex odd" ng-repeat="person in ledning">
<a href="">
<div class="inner">
<h3 class="ng-binding">Just a name</h3>
<hr class="grid-1 center">
<p class="ng-binding">a title</p>
</div>
<div class="hex-1">
<span class="after"></span>
</div>
<div class="hex-2">
<span class="after"></span>
</div>
<span class="after"></span>
</a>
</div>
<!-- and 5 times more -->
</div>
Now, What I want to achieve is this -->http://jsfiddle.net/engstrumpan/yCv79/4/embedded/result/.
This is just plain html (no AngularJs) just to demonstrate what I want to achieve. This particular layout was achieved by inserting <br /> after hexagon 3 and 5
Imagine now if I want a layout like so
1 1 1 1
1 1 1 1 or even 1 1
1 1 1 1 1 1
1 1 1 1
1 1 1
1 1
1
How would one go about implementing this?
This directive is used multiple times so I want it to be as generic as possible.
What I've tried so far is is the following
var app = angular.module('test', []);
app.controller('About', function ($scope) {
$scope.ledning = [
{
...
shouldBreak: true
}
});
app.directive('hexagon', function () {
var tmpl = '<div>
<!-- directive template snippet -->
</div>
<br ng-show="{{data.shouldBreak}}" />';
// rest of code
});
This <br ng-show="{{data.shouldBreak}}" /> won't work since AngularJs throws an exception with the following message Template must have exactly one root element.
Also messing with compile on the directive like so but that results only in the <br /> being inserted after the very last iteration (after the repeater is done working)
var app = angular.module('test', []);
app.directive('hexagon', function () {
return {
...
compile: function($scope, el, attrs){
return function ($scope, el, attrs){
if($scope.data.shouldBreak !== undefined){
el.after('<br />');
}
};
}
}
});
I've read the documentation when it comes to directives and either I'm just stupid or I'm missing the obvious so please help me out.
End of walloftext and maybe some sleep will do the trick. Jump back on the horse tomorrow so to speak.
So I managed to come to a solution after being away for a couple of days and then when I returned to town I tackled my problem again.
The solution
The view
<hexagon ng-repeat="person in ledning" data="person" layout="layout"></hexagon>
The controller:
var app = angular.module('test', []);
app.controller('About', function ($scope) {
...
$scope.layout = [2,4]; // what index (inside ngRepeat) should we break on
}
});
and the directive
app.directive('hexagon', function ($timeout) {
...
return {
...
scope: {
data: '=',
layout: '=' // grab the data bound to 'layout'
},
link: function ($scope, el, attrs) {
$timeout(function(){
for (var i = 0; i < $scope.layout.length; i++) {
if ($scope.layout[i] == $scope.$parent.$index)
el.after('<br />');
}
}, 0);
}
};
});
A working example can be seen here http://jsfiddle.net/engstrumpan/yCv79/5/.
So if I for instance were to work with 10 items I could set $scope.layout = [2,6] to get the following output in this jsFiddle
So how did I end up with this solution? Well I have to thank this guy (Lorenz Merdian) and his blog for it. At least when it comes down tho the use of $timeout to give the browser enough time to render the DOM for the element so I could insert my <br />
It might not be the most elegant solution but it works for me in this particular scenario
You could use pseudoclasses to insert a carriage return "\a" either before :before or after :after the element in question (although if you are supporting legacy browsers, this ain't for you).
CSS:
.last:before {
content: "\A";
white-space:pre;
}
And a conditional ng-class to set the line-breaking class, where item.lineBreak contained true or false depending on its order in the group:
ng-class="{last:item.lineBreak}"
That is the basic answer, and here's the working plunk with the ng-class on the element.
Fork of the plunk with the ng-class on the root element in the template. Same result.
..
..
The tricky/fun part is being able to dynamically insert that class/pseudoclass into the correct elements without hardcoding them. It looked like you wanted a pyramid (Fibonacci wannabe) design (which is what looked interesting about this question).
What I did was use a pretty basic looping process to insert the true property - (from the above example item.lineBreak) - into the items which were the 1st, 3rd, 6th, 10th, (and on) items in the ng-repeat. I put this in a $watch of a select-box model so you could choose the number of items in the group.
Here's the $watch and
var findFib = function(index){
if(index==increment){
rowLength += 1;
increment = rowLength + increment;
return true;
}
return false;
}
$scope.$watch('collectionAmount.value', function(val){
$scope.collection = [];
rowLength = 1;
increment = 1;
var i = 1;
//loop through the items, using the Fibonacci-like
//method to determine which items will recieve the
//lineBreak property of true;
while(i<=val){
var f = findFib(i);
$scope.collection.push({number: i, lineBreak: f});
console.log($scope.collection[i-1]);
i++;
}
//reverse the array if using :before pseudoclass to get upside-down pyramid:
$scope.collection.reverse();
})
From there, the ng-class conditionally applied the .last class which contained the :before pseudoclass that inserted the return. This could easily be scaled to fit your more complex markup.
Related
When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview
I just can't find a good source that explains to me how to manipulate DOM elements with angular:
How do I select specific elements on the page?
<span>This is a span txt1</span>
<span>This is a span txt2</span>
<span>This is a span txt3</span>
<span>This is a span txt4</span>
<p>This is a p txt 1</p>
<div class="aDiv">This is a div txt</div>
Exp: With jQuery, if we wanted to get the text from the clicked span, we could simple write:
$('span').click(function(){
var clickedSpanTxt = $(this).html();
console.log(clickedSpanTxt);
});
How do I do that in Angular?
I understand that using 'directives' is the right way to manipulate DOM and so I am trying:
var myApp = angular.module("myApp", []);
myApp.directive("drctv", function(){
return {
restrict: 'E',
scope: {},
link: function(scope, element, attrs){
var c = element('p');
c.addClass('box');
}
};
});
html:
<drctv>
<div class="txt">This is a div Txt</div>
<p>This is a p Txt</p>
<span>This is a span Txt </span>
</drctv>
How do I select only 'p' element here in 'drctv'?
Since element is a jQuery-lite element (or a jQuery element if you've included the jQuery library in your app), you can use the find method to get all the paragraphs inside : element.find('p')
To Answer your first question, in Angular you can hook into click events with the build in directive ng-click. So each of your span elements would have an ng-click attribute that calls your click function:
<span ng-click="myHandler()">This is a span txt1</span>
<span ng-click="myHandler()">This is a span txt2</span>
<span ng-click="myHandler()">This is a span txt3</span>
<span ng-click="myHandler()">This is a span txt4</span>
However, that's not very nice, as there is a lot of repetition, so you'd probably move on to another Angular directive, ng-repeat to handle repeating your span elements. Now your html looks like this:
<span ng-repeat="elem in myCollection" ng-click="myHandler($index)">This is a span txt{{$index+1}}</span>
For the second part of your question, I could probably offer an 'Angular' way of doing things if we knew what it was you wanted to do with the 'p' element - otherwise you can still perform jQuery selections using jQuery lite that Angular provides (See Jamie Dixon's answer).
If you use Angular in the way it was intended to be used, you will likely find you have no need to use jQuery directly!
You should avoid DOM manipulation in the first place. AngularJS is an MVC framework. You get data from the model, not from the view. Your example would look like this in AngularJS:
controller:
// this, in reality, typically come from the backend
$scope.spans = [
{
text: 'this is a span'
},
{
text: 'this is a span'
},
{
text: 'this is a span'
}
];
$scope.clickSpan = function(span) {
console.log(span.text);
}
view:
<span ng=repeat="span in spans" ng-click="clickSpan(span)">{{ span.text }}</span>
ng-click is the simpler solution for that, as long as I do not really understand what you want to do I will only try to explain how to perform the same thing as the one you have shown with jquery.
So, to display the content of the item which as been clicked, you can use ng-click directive and ask for the event object through the $event parameter, see https://docs.angularjs.org/api/ng/directive/ngClick
so here is the html:
<div ng-controller="foo">
<span ng-click="display($event)" >This is a span txt1</span>
<span ng-click="display($event)" >This is a span txt2</span>
<span ng-click="display($event)" >This is a span txt3</span>
<span ng-click="display($event)" >This is a span txt4</span>
<p>This is a p txt 1</p>
<div class="aDiv">This is a div txt</div>
</div>
and here is the javascript
var myApp = angular.module("myApp", []);
myApp.controller(['$scope', function($scope) {
$scope.display = function (event) {
console.log(event.srcElement.innerHtml);
//if you prefer having the angular wrapping around the element
var elt = angular.element(event.srcElement);
console.log(elt.html());
}
}]);
If you want to dig further in angular here is a simplification of what ng-click do
.directive('myNgClick', ['$parse', function ($parse) {
return {
link: function (scope, elt, attr) {
/*
Gets the function you have passed to ng-click directive, for us it is
display
Parse returns a function which has a context and extra params which
overrides the context
*/
var fn = $parse(attr['myNgClick']);
/*
here you bind on click event you can look at the documentation
https://docs.angularjs.org/api/ng/function/angular.element
*/
elt.on('click', function (event) {
//callback is here for the explanation
var callback = function () {
/*
Here fn will do the following, it will call the display function
and fill the arguments with the elements found in the scope (if
possible), the second argument will override the $event attribute in
the scope and provide the event element of the click
*/
fn(scope, {$event: event});
}
//$apply force angular to run a digest cycle in order to propagate the
//changes
scope.$apply(callback);
});
}
}
}]);
plunkr here: http://plnkr.co/edit/MI3qRtEkGSW7l6EsvZQV?p=preview
if you want to test things
I have a JSON object that contains an int property x, I would like to repeat following code x times
<span class="glyphicon glyphicon-star"/>
ng-repeat does not seem to be indicated as it's working with collection.
Any suggestion (angular newbie)
I would use custom filter with ng-repeat:
HTML
<div ng-app='myApp' ng-controller="Main">
<li ng-repeat="n in [] | range:20">
<span class="glyphicon glyphicon-star" >{{n}}</span>
</li>
</div>
filter
app.filter('range', function() {
return function(val, range) {
range = parseInt(range);
for (var i=0; i<range; i++)
val.push(i);
return val;
};
});
Demo Fiddle
Shortest answer: 2 lines of code
JS (in your AngularJS controller)
$scope.range = new Array(MAX_STARS); // MAX_STARS should be the most stars you will ever need in a single ng-repeat
HTML
<span class="glyphicon glyphicon-star" ng-repeat="i in range.slice(0,starCount) track by $index"/>
...where starCount is the number of stars that should appear in this location.
you can write filter range:
'use strict';
angular.module('app.Filter')
.filter('range', function() {
return function(input, total) {
total = parseInt(total);
for (var i=0; i < total; ++i) {
input.push(i);
}
return input;
};
});
then use it
<span class="glyphicon glyphicon-star" ng-repeat="i in [] | range:5"/>
5 its your x
When I was first starting to play with AngularJS, I found a decent tutorial that walks you through making a custom directive for doing a "Rating" widget in Angularjs.
http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/
They don't do anything special other than create a collection of empty objects based on the value of a two-way bound scope variable defined in the directive.
directive('fundooRating', function () {
return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" class="filled">' +
'\u2605' +
'</li>' +
'</ul>',
scope: {
ratingValue: '='
},
link: function (scope, elem, attrs) {
scope.stars = [];
for (var i = 0; i < scope.ratingValue; i++) {
scope.stars.push({});
}
}
}
});
The nice thing is that the collection messiness is at least encapsulated inside of the directive, and all the controller has to do is deal with the the numerical rating value. The tutorial also walks you through doing two way linking between the directive and the controller on the rating value scope variable.
IMHO, this is the cleanest solution since AngularJS doesn't directly support what you want to do. At least here, it is easy to understand what you are trying to do in your controller and view (which is where you want to avoid unnecessary complexity) and you move the hackish-ness into the directive (which you will probably write once and forget).
Angular (V1.2.9 and up) includes a filter limitTo:n that will do this out of the box. For example to limit ng-repeat to the first 20 elements use the following syntax:
<div ng-app='myApp' ng-controller="Main">
<li ng-repeat="n in [] | limitTo:20">
<span class="glyphicon glyphicon-star" >{{n}}</span>
</li>
</div>
Documentation for limitTo is here
To be fair the limitTo filter didn't exist at the time the original question was asked.
I answered a similar (duplicate?) question https://stackoverflow.com/a/31864879/1740079. I'll just repost here as I also ended up here searching for an answer.
(function () {
angular
.module('app')
.directive('repeatTimes', repeatTimes);
function repeatTimes ($window, $compile) {
return { link: link };
function link (scope, element, attrs) {
var times = scope.$eval(attrs.repeatTimes),
template = element.clone().removeAttr('repeat-times');
$window._(times).times(function (i) {
var _scope = angular.extend(scope.$new(), { '$index': i });
var html = $compile(template.clone())(_scope);
html.insertBefore(element);
});
element.remove();
}
}
})();
... and the html:
<div repeat-times="4">{{ $index }}</div>
LIVE EXAMPLE
I can't get two-way-data-binding to work in an Angular js ng-repeat.
I have an ng-controller specified on the same element that has the ng-repeat specified -
I just learnt that by doing this, I can get a hold of each item that is being iterated over by ng-repeat. Here is the HTML:
<div ng-controller="OtherController">
<div id="hoverMe" ng-controller="ItemController" ng-mouseenter="addCaption()"
ng-mouseleave="saveCaption()" ng-repeat="image in images">
<div class="imgMarker" style="{{image.css}}">
<div ng-show="image.captionExists" class="carousel-caption">
<p class="lead" contenteditable="true">{{image.caption}}</p>
</div>
</div>
</div>
</div>
And here is the ItemController:
function ItemController($scope){
$scope.addCaption = function(){
if($scope.image.captionExists === false){
$scope.image.captionExists = true;
}
}
$scope.saveCaption = function(){
console.log($scope.image.caption);
}
}
And the OtherController:
function OtherController($scope){
$scope.images = ..
}
When I hover the mouse over the #hoverMe-div - the caption-div is added correctly. But when I input some text in the paragraph and then move the mouse away from the #hoveMe-div, the $scope.image-variables caption value is not updated in the saveCaption-method. I understand I'm missing something. But what is it?
You don't need a ng-controller specified on the same element that has the ng-repeat to be able to get each item.
You can get the item like this:
<div ng-repeat="image in images" ng-mouseenter="addCaption(image)" ng-mouseleave="saveCaption(image)" class="image">
And in your controller code:
$scope.addCaption = function (image) {
if(!image.captionExists){
image.captionExists = true;
}
};
To get contenteditable to work you need to use ng-model and a directive that updates the model correctly.
Here is a simple example based on the documentation:
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
element.on('blur', function() {
scope.$apply(function() {
controller.$setViewValue(element.html());
});
});
controller.$render = function(value) {
element.html(value);
};
}
};
});
Note that the directive probably needs more logic to be able to handle for example line breaks.
Here is a working Plunker: http://plnkr.co/edit/0L3NKS?p=preview
I assume you are editing the content in p contenteditable and are expecting that the model image.caption is update. To make it work you need to setup 2 way binding.
2 way binding is available for element that support ng-model or else data needs to be synced manually. Check the ngModelController documentation and the sample available there. It should serve your purpose.
I'm trying to change the input size from 20 to 100 each time it ng-focus. Then, when it ng-blur, it should convert from 100 to 20. Here is my html code:
<div ng-controller="MenuCtrl" class="navbar-collapse collapse bs-navbar-collapse navbar-ex1-collapse">
<input type="text" class="form-control" ng-blur="{[{ change_text_width() }]}" ng-focus="{[{ change_text_width() }]}" size="{[{search_text_width}]}" placeholder="Search">
</div>
Before my angular module, I put a parent module:
var JobMaps = angular.module('JobMaps', ['MenuCtrl']);
Here is my angular code:
var MenuCtrl = angular.module('MenuCtrl',[]);
MenuCtrl.controller = angular.module('MenuCtrl',function($scope) {
$scope.search_text_width = 20;
$scope.change_text_width = function() {
if ($scope.search_text_width==20) {
$scope.search_text_width = 100;
//alert($scope.search_text_width);
} else {
$scope.search_text_width = 20;
//alert($scope.search_text_width);
}
}
});
Observation:
I tried to alert it, and it seems like it runs several times.
I think it may be converting from 20 to 100 and 100 to 20 back so fast, I could not see it... Well, it could be a possibility because of several alerts that popup.
In the Angular world you use directives for DOM manipulation. One advantage is that you can reuse the directives without much extra code and seperate the UI stuff from the controller. I created a plunker for the resizing input: Plunker
I changed the attribute size because width can affect the responsiveness when using Bootstrap.
MenuCtrl.directive('changeSize', function () {
return {
restrict: 'A',
link: function($scope, $element, $attributes) {
var onFocusSize = $attributes['changeSize'] || $element.attr('size');
var onBlurSize = $element.attr('size');
$element.focus(function() { $element.attr('size',onFocusSize); });
$element.blur(function() { $element.attr('size',onBlurSize); });
}
}
});