I'm building an app with Bootstrap and AngularJS. At some point I have an ng-repeat on a col-md-3, listing products. My problem is that I want to be able to insert a collapse into the grid, but as the columns are automatically generated, I don't really know how to do it.
Here's a diagram to understand it better:
First, the grid of .col-md-3 is populated from the ng-repeat.
And what I'm trying to achieve, is to add a .col-md-12 that appears right under the row of the .col-md-3 that gets clicked on.
My initial thought was to add an empty .col-md-12 dynamically after each group of 4 .col-md-3, but I wouldn't know how to do so, and it kinda seems to be that it would be a rather dull approach. Any ideas?
Here's the relevant html:
<div class="infinite" infinite-scroll="loadDetails()">
<div class="col-xs-3 col-md-3" ng-repeat="release in main.releases | filter:main.album">
<release release="release" artist="main.artist" class="dropdown"></release>
</div>
</div>
EDIT: Here's a working Plunker including tasseKATTs solution.
Place a custom directive on your inner element together with a position counter that starts with 1 and a marker describing if it's the last element:
<div ng-repeat="item in items" class="col-xs-3">
<div class="item" the-directive position="{{ $index + 1 }}" last="{{ $last }}">
</div>
</div>
Create the directive with an isolated scope, bind scope properties to the values of the position and last attributes and attach a click event handler to the element:
app.directive('theDirective', function() {
return {
restrict: 'A',
scope: { position: '#', last: '#' },
link: function(scope, element, attrs) {
element.bind('click', function() {
...
});
}
};
});
In the click handler first create the collapse element or select it if it already exists:
var collapseQuery = document.querySelector('#collapse');
var collapse = collapseQuery === null ?
angular.element('<div id="collapse" class="col-xs-12"><div class="twelve"></div></div>') :
angular.element(collapseQuery);
Based on the position of the clicked element calculate the rounded number up to the nearest multiple of four:
var calculatedPosition = Math.ceil(scope.position / 4) * 4;
Get the element at the calculated position or the last one if the position is out of range:
var calculatedQuery = document.querySelector('[position="' + calculatedPosition + '"]');
if (calculatedQuery === null) calculatedQuery = document.querySelector('[last="true"]');;
var calculatedElement = angular.element(calculatedQuery);
Insert the collapse element after the element at the calculated position:
calculatedElement.parent().after(collapse);
Could use some optimizations, but hopefully puts you on the right track.
Demo with some extra visuals: http://plnkr.co/edit/fsC51vS7Ily3X3CVmxSZ?p=preview
This question is easier to answer in an angular way if you follow the bootstrap convention using 12 columns per a row:
Grid columns are created by specifying the number of twelve available columns you wish to span. For example, three equal columns would use three .col-xs-4.
In your case, this means each row can have up to 4 .col-xs-3 columns, or just 1 .col-xs-12. You can prep your data to be displayed this way by splitting it into an array of smaller arrays.
$scope.getRows = function(array) {
var rows = [];
//https://stackoverflow.com/questions/8495687/split-array-into-chunks
var i,j,temparray,chunk = 4;
for (i=0,j=array.length; i<j; i+=chunk) {
temparray = array.slice(i,i+chunk);
rows.push(temparray);
}
return rows;
};
$scope.rows = $scope.getRows($scope.main.releases);
Then you can nest ngRepeat to achieve the desired layout, using ng-if to only create a col-xs-12 when a corresponding .col-xs-3 is clicked.
<div ng-repeat="row in rows">
<div class="row">
<div class="col-xs-3" ng-repeat="release in row" ng-click="main.releaseClicked=release">
<div class="release">{{release}}</div>
</div>
</div>
<div class="row" ng-repeat="release in row" ng-if="main.releaseClicked==release">
<div class="col-xs-12">
<div class="detail">Release detail: {{release}}</div>
</div>
</div>
</div>
This leaves you with a more declarative view that describes how the app works, and doesn't require jQuery to do DOM manipulation.
Here is a working demo: http://plnkr.co/ujlpq5iaX413fThbocSj
Related
I have an an example page containing several categories. Each category is wrapped in a .items class which contains an h2 title tag and several links. My goal is to sort each of those categories alphabetically based on the h2 tag.
I found several examples on how to do this, but they were in jquery. I want to do this only in javascript. I found some code that will sort divs but not by the divs's h2 tag.
HTML
<div id="mainContainer" class="column-container row">
<div class="item column">
<h2>Testimonials</h2>
Testimonial slider
</div>
<div class="item column">
<h2>Directories</h2>
Staff Directory
</div>
<div class="item column">
<h2>FAQ</h2>
</div>
<div class="item column">
<h2>Forms</h2>
Simple contact form - WIP
Online payment form using Network Merchants - WIP
Form with attachment
</div>
</div>
JavaScript
sortCategory('#mainContainer');
function sortCategory(s) {
Array.prototype.slice.call(document.body.querySelectorAll(s)).sort(function sort (ea, eb) {
var a = ea.textContent.trim();
var b = eb.textContent.trim();
if (a < b) return -1;
if (a > b) return 1;
return 0;
}).forEach(function(div) {
div.parentElement.appendChild(div);
});
}
How can I modify the javascipt code to sort each .item by the h2 tag?
Solution
With the help of others I figured it out and wanted to share the code. I also formatted the code to be easily read.
//****************************************
// Sort Categories Alphabetically
//****************************************
function sortCategory(elementContainer)
{
var allElements = document.body.querySelectorAll(elementContainer);
Array.prototype.slice.call(allElements).sort(byAlphabet).forEach(function(div)
{
div.parentElement.appendChild(div);
});
}
function byAlphabet(first, second)
{
var order = 0;
var first = first.querySelector('h2').textContent.trim();
var second = second.querySelector('h2').textContent.trim();
first > second ? order = 1 : order = -1;
return order;
}
//Call sortCategory function and pass in the container you want sorted
sortCategory('#mainContainer>.item');
Change ea.textContent.trim() to ea.querySelector('h2').textContent.trim()
and
change eb.textContent.trim() to eb.querySelector('h2').textContent.trim()
This will basically say check each div's first H2 element, rather than the div.
Hope I was helpful!
Hello people I have a question regarding drag and drop . I am creating an application for creating websites only by using drag and drop . I am kinda stuck implementing a feature in which the user can drop elements at a particular positions and the elements below that dropped element get shifted below . Now the issue is that element is generated dynamically
for eg . In the right panel there are different components in the form of images and when i drag those elements I pass some meta-data and when the element gets dropped , based on the meta-data an html element is created .
<div class="drop-zone">
<div>
<!--container 1-->
</div>
<!-- now i want to drop that element in between which is generated dynamically -->
<div>
<!--container 2-->
</div>
</div>
*****************EDIT*****************************
In the below code header drop is the directive which will be dragged , JSONfn is same as JSON object used to stringify the functions as well
(function(){
define(['../../../../app','./../service/header.factory.js'],function(app){
app.directive('headerDrop',['HeaderFactory',function(HeaderFactory){
return {
restrict: "E",
replace: false,
scope: {},
link: function(scope,element,attrs) {
element.on('dragstart',function(e){
e.originalEvent.dataTransfer.setData("data",JSONfn.stringify(HeaderFactory));
});
},
template: "<img id='header' draggable='true' src='/src/create_template/images/noimage.jpg' width='100' height='100'> </img>"
}
}]);
app.directive('dragContainer',function(){
return {
restrict: "E",
replace: false,
scope: {},
template: "<div id='elements-container'> <h1 style='text-align:center;'> All </h1><header-drop> </header-drop> </div>"
}
});
});
})()
Inside the controller
element.on('drop',function(event){
console.log(event);
if(event.target.className !== "drop-zone"){
event.preventDefault();
return false;
}
var data = JSONfn.parse(event.originalEvent.dataTransfer.getData("data"));
if(data.type=="header"){
var heading = document.createElement("h1");
console.log("client height" , heading.clientWidth);
heading.innerHTML = data.textValue;
//console.log("pageY",event.pageY);
//console.log("pageX",event.pageX);
heading.style.position = "relative";
heading.style.top = ((event.pageY/window.innerHeight)*100)+ "%";
heading.className = "editable";
event.target.appendChild(heading);
heading.style.top = event.clientY;
addingEvents();
}
});
Well i actually tackled that problem with a naive approach though i found the answer later .
I was making a template generator so this approach worked for me
Naive Approach
What i did was , whenever i was dropping the element , i dropped one more block inside the designer of height 5px just above the element kinda like
<div class="adjacent-block"> </div> inside css .adjacent-block {min-height: 5px; }
<div class="dropped-block"> </div>
Then drop that element inside the adjacent block .
Disadvantage : You have also keep track of the adjacent block
THE ABOVE METHOD WAS PRETTY PATHETIC AND I WOULDN'T RECOMMEND ANYONE TO DO THAT
EASIER AND BETTER APPROACH:
Inside the dragover event just check if the element is above another element and drop the block before or after the element according to the position of the mouse from the mid of that element .
I haven't implemented this approach but I am pretty sure this would work
I have a simple array of text objects that I want to display in a grid. This is using the Ionic framework.
The grid must be:
Fixed height
Exactly 3 rows
Horizontal scroll (infinite columns)
2 visible columns with screen width < 768px (phones), 4 visible columns 768px and above (tablets)
I have the media query for the column widths and am using for the horizontal scrolling functionality.
How can I use ng-repeat (maybe with ngif ??) to create the grid, starting in the first column and filling that column with 3 rows before moving to the next column etc etc)
JS
var items = [
{text: "This is item1"},
{text: "This is item2"},
{text: "This is item3"},
.
.
.
]
CSS:
.myGridRow {
height: 40%;
}
.myGridCol {
width: 50%;
}
#media only screen and (min-width : 768px) {
.myGridCol {
width: 25%;
}
}
HTML
<ion-scroll direction="x">
<div class="row myGridRow"> <!-- The single container for the whole grid -->
<div ng-repeat="item in items"> <!-- only do this 3 times before starting a new row -->
<div class="row"><div class="col myGridCol">{{item.text}}</div></div>
</div>
</div>
</ion-scroll>
Desired Output
I am stuck here trying to determine how to move to the next column after each 3 rows, or if this is even the right way to do this.
ng-repeat is constrained to iterate over a collection and will not work with an integer as input. However, you can create a placeholder collection for the number of repetitions required:
$scope.items = [/* The base collection */];
$scope.iterCount = 3; // The number of iterations before a new row is created
$scope.itemCount = Math.floor($scope.items.length / $scope.iterCount);
// Get a collection of items equally spaced from each other. For
// example: getSpacedItems([1, 2, 3, 4], 0, 2) == [1, 3]
$scope.getSpacedItems = function(collection, start, space) {
var subColl = [];
for(var i = start; i < collection.length; i += space) {
result.push(collection[i]);
}
return result;
};
These values can be applied to the view in this manner:
<div>
<div class="row" ng-repeat="n in [0, 1, 2] track by $index">
<div class="col myGridCol" ng-repeat="item in getSpacedItems(items, $index, iterCount)">{{item.text}}</div>
</div>
</div>
I suppose the correct solution here - transform array with data on the controller side. So you "rotate" your 2D array and rows become columns, columns become rows. Then you perform just regular output. It is not the task for view side.
How can I go to the next item by clicking on the next button and previous by clicking the previous button?
Repeat to Show Data from Database:
idSelectedShipment is the selected div or shipment id
<div ng-repeat="shipment in shipments | orderBy:predicate:reverse">
<div ng-click="show_shipment($index,shipment.shipment_id)" ng-class="{selected_trip: shipment.shipment_id == idSelectedShipment}">
<div> From {{shipment.from_location}}</div>
</div>
</div>
Next and Previous Buttons:
<a class="" ng-click="next($event)" href="#">next</a>
<a class="" ng-click="previous($event)" href="#">previous</a>
I am having trouble in this part. The next button and the previous buttons are outside the ng-repeat and I can not seems to pass the index on the click.
$scope.next= function(index){
[index + 1]
};
$scope.previous= function(index){
[index - 1]
};
It looks like your goal is to render the selected_trip class on the "current" repeated element, and your back/next buttons change this?
Based on what you've currently got, what you need to do in your next and back functions is change the value of idSelectedShipment accordingly, but I think that may not be the best way forward.
The tricky part is that your underlying data structure, shipments, is sorted for the view. Your controller and the scope outside of your ngRepeat block won't be aware of this. For that reason, you can't really use $index meaningfully.
What I would recommend is pre-sorting your array in the controller and then keeping track of current index position. Your code would probably look something like the following:
function MyController ($scope, $filter) {
$scope.sortedShipments = $filter('orderBy')($scope.shipments, 'predicate', true);
$scope.currentShipment = 0;
$scope.back = function () {
if ($scope.currentShipment > 0) {
$scope.currentShipment--;
}
};
$scope.next = function () {
if ($scope.currentShipment < $scope.sortedShipments.length - 1) {
$scope.currentShipment++;
}
};
}
Then change your HTML to...
<div ng-repeat="shipment in sortedShipments">
<div ng-click="foo()" ng-class="{selected_trip: $index === currentShipment}">
<div> From {{shipment.from_location}}</div>
</div>
</div>
Working with Bootstrap and JavaScript and I am using it as an accordion format - once clicked on the collapsed div it will open and show the items within the div based on the id.
Problem:
If the div doesn't contain any items i want it to open and show a message to the user:
"no items here"
How do I go about doing that? In the JavaScript ?
This is what I have:
View
<div class="accordion-body collapse state-loading" data-group-id="13" data-bind="attr: { 'id': 'GroupMember_' + Id(), 'data-type-id': ModelId() }" id="GroupMember_15" data-type-id="15">
<div class="accordion-inner no_border" data-bind="foreach: Children"></div><!--END: accordion-inner--></div>
</div>
If the Children are 0 i want it to open and have this text show: No items here
Javascript:
OnSuccess: function (data) {
var _groups = linq.From(options.groupData);
var _groupsToUpdate = _groups .Where(function (x) { return x.Id == options.groupId; });
if (_groupsToUpdate.Any()) {
_groupsToUpdate.First().Children = data.Items;
}
Not sure if i am missing anything else to share - let me know.
UPDATE
Div Layout:
<div class='accordion-group'>
<div class='accordion-heading'> Group 1 </div>
<div class='accordion-body'>
<div class='accordion-inner'>
<div class='element'>No items here</div>
</div>
</div>
</div>
I have to click on the 'accordion-heading' class in order to display the 'accordion-body' and get into the accordion-inner items
You'd need to bind to the show event on the accordion elements and perform your check there, from your classes I'm assuming your using Bootstrap v2.3.2:
$('.accordion .collapse').on('show', function () {
var $inner = $(this).find('.accordion-inner');
if($inner.is(':empty')){
$inner.html('No items here');
}
});
Demo fiddle
Note that the :empty selector is very picky, it will not work if there's any white space between the opening and closing tags of .accordion-inner.
You may also use if(!$.trim($inner.html())) to check if the element is empty or as #JL suggested check the length of the children elements just beware that text nodes are not treated like children, so a div with only text would be considered empty
Do you have jQuery installed? You can check if a <div> has children like this:
if ($('#divId').children().length == 0) {
$('#divId').append("no items here");
}
If you don't have jQuery:
if (!document.getElementById('divId').hasChildNodes()) {
document.getElementById('divId').innerHTML = "no items here";
}
Based on your edit, I think we're inspecting accordian-inner for children. If so, give it an ID and substitute that into our code. Note: You don't need a <div> to contain our "no items" message...the message will get printed with javascript (Plus, if you have a <div> there, then you've in effect added a child and the message no longer applies). Change your HTML to this:
<div class='accordion-group'>
<div class='accordion-heading'> Group 1 </div>
<div class='accordion-body'>
<div id='innerId' class='accordion-inner'>
<!-- Remove the 'element' div -->
</div>
</div>
</div>
Then:
if (!document.getElementById('innerId').hasChildNodes()) {
document.getElementById('innerId').innerHTML = "no items here";
}