Iterate through array via ngRepeat with breaks - angularjs

I need your help.
I decided to make an attempt to create some kind of online market and I am somehow new to some parts of AngularJS and this is first time when I faced such a problem with ng-repeat.
I have the following html-structure (it is only a required part of it):
<div class="goods">
<div class="goods-row" ng-repeat="???">
<div class="good" ng-repeat="???">
<button id="add_to_cart">+</button>
<div class="descr">
<div class="descr-top">
<h5 class="g-name">
NAME IS HERE
</h5>
<span class="g-price">
PRICE IS HERE
</span>
</div>
<div class="descr-mid" ng-bind="good.description"></div>
</div>
</div>
</div>
</div>
"Goods"(goods-class) includes "rows of goods"(goods-row). Every row should include (by default) 4 goods (good-class), but, it should be also noticed that I will use filter, that could change number of visible goods in a row, because of that I need a flexible solution.
What should I enter in ng-repeat?
P.S. I created a blueprint with JS-code (posted below), that somehow shows what I want, but how to make it in AngularJS?
var arr = [];
for (i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) {
arr.push({
number: i
});
}
var divider = 4;
for (var i = 0; i < arr.length;) {
for (var j = 0; j < divider; j++) {
if ((j + i) < arr.length) {
console.log(arr[j + i]);
}
}
i = i + divider;
console.log(" ");
}

With lodash library you can use chunk function to split an array into chunks and then build a list that suits your needs.
So basically just iterate as you would do for a normal list of arrays (which represent your rows containing goods) and implement a function that build this list according to the divider. That way you can invoke the function when you need to rebuild your list (on keyup of the input in the example bellow) and let AngularJS do the rest.
(function(angular) {
'use strict';
angular.module('ngRepeat', [])
.controller('repeatController', function($scope) {
// the goods
$scope.goods = [
{ name: "name-1", price: 1.01, description: 'desc-1' },
{ name: "name-2", price: 2.02, description: 'desc-2' },
{ name: "name-3", price: 3.03, description: 'desc-3' },
{ name: "name-4", price: 4.04, description: 'desc-4' },
{ name: "name-5", price: 5.05, description: 'desc-5' },
{ name: "name-6", price: 6.06, description: 'desc-6' },
{ name: "name-7", price: 7.07, description: 'desc-7' },
{ name: "name-8", price: 8.08, description: 'desc-8' },
{ name: "name-9", price: 9.09, description: 'desc-9' },
{ name: "name-10", price: 10.10, description: 'desc-10' },
{ name: "name-11", price: 11.11, description: 'desc-11' },
{ name: "name-12", price: 12.12, description: 'desc-12' }
];
// divider determines how many goods per row (defaulted to 4)
$scope.divider = 4;
// function that build the rows of goods
$scope.dividerChanged = function() {
$scope.rows = _.chunk($scope.goods, $scope.divider);
};
// initialize rows on first load
$scope.dividerChanged();
});
})(window.angular);
.divider {
margin-bottom: 10px;
}
.goods-row {
border: 1px solid blue;
padding: 10px;
text-align: center;
}
.good {
border: 1px solid red;
display: inline-block;
padding: 10px;
margin: 10px;
width: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<body ng-app="ngRepeat">
<div ng-controller="repeatController">
divider: <input type="text" ng-model="divider" ng-keyup="dividerChanged()" class="divider">
<div class="goods">
<div class="goods-row" ng-repeat="row in rows">
<div class="good" ng-repeat="good in row">
<button id="add_to_cart">+</button>
<div class="descr">
<div class="descr-top">
<h5 class="g-name">{{ good.name }}</h5>
<span class="g-price">{{ good.price | currency }}</span>
</div>
<div class="descr-mid">{{ good.description }}</div>
</div>
</div>
</div>
</div>
</div>
</body>

I can't fully understand your question, are you trying to build a filtered array but always want to display the top 4 entries?
If yes, then it can be done with ng-repeat filter (custom or not) and limitTo filter
<div ng-repeat="data in test.data | customFilter:test.selectedFilter | limitTo:4">{{data}}</div>
https://jsbin.com/wajapabaro/1/edit?html,js,output

Related

Vue.js - show more objects from array if they have same title

I have an array of objects:
[{
title: 'foo'
id: 1,
name: 'anne'
},
{
title: 'example',
id: 2,
name: 'anne'
},
{
title: 'ex',
id: 3,
name: 'deb'
}]
The page is already showing the first object:
{
title: 'foo'
id: 1,
name: 'anne'
},
<div class="essay">
<p class="title" >{{ selectedItem.title }}</p>
<p class="id">{{ selectedItem.id }}</p>
<p class="name"> {{ selectedItem.name }} </p>
</div>
selectedItem comes from the created() life cycle
What is the best way to display the object with the same name?
I want to have a button which if clicked, will show another title from the person name 'anne'.
Try to get the filtered array.
created() {
this.selectedItem = arr.filter(item => item.name === 'anne');
// arr is the array source variable.
}
<div v-for="item in selectedItem" :key="item.id" class="essay">
<p class="title" >{{ selectedItem.title }}</p>
<p class="id">{{ selectedItem.id }}</p>
<p class="name"> {{ selectedItem.name }} </p>
</div>
I've made a snippet, maybe it would help You :-)
We need to store state somewhere, so I've created owners variable with shown counter.
There is also a computed property that groups items by owners.
Maybe there is an easier method but...
new Vue({
el: '#app',
data: {
items: [
{ owner: "John", name: "Box" },
{ owner: "John", name: "Keyboard" },
{ owner: "John", name: "Plate" },
{ owner: "Ann", name: "Flower" },
{ owner: "Ann", name: "Cup" }
],
owners: {},
},
methods: {
more(owner) {
if (this.owners[owner].shown < this.items.filter(i => i.owner == owner).length) {
this.owners[owner].shown++;
}
}
},
computed: {
ownersItems() {
let map = {};
this.items.forEach(i => {
map[i.owner] = map[i.owner] || [];
if (this.owners[i.owner].shown > map[i.owner].length) {
map[i.owner].push(i);
}
});
return map;
},
},
created() {
let owners = {};
this.items.forEach(i => {
owners[i.owner] = owners[i.owner] || {};
owners[i.owner].shown = 1;
});
this.owners = owners;
}
})
#app {
font-family: sans-serif;
padding: 1rem;
}
.header {
font-weight: bold;
margin-bottom: .5rem;
}
sup {
background: #000;
color: #fff;
border-radius: .15rem;
padding: 1px 3px;
font-size: 75%;
}
.btn {
padding: .25rem .5rem;
border: 1px solid #ddd;
background: #eee;
border-radius: .15rem;
margin: 2px;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header">Owners</div>
<template v-for="(cfg, owner) in owners">
<strong>{{ owner }} <sup>{{cfg.shown}}</sup>:</strong>
<a v-for="(item, i) in ownersItems[owner]" :key="`${owner}_${i}`" #click="more(owner)" class="btn">{{ item.name }}</a>
|
</template>
</div>

v-for render or dont render <div> on {x} iteration

I did not find solution to my question anywhere and I can't figure it out. I have divs like this
<div class="columns">
<div class="column">
{looping content here}
</div>
</div
Data is something like this:
{
title: 'blabla'
body: 'blabla'
msg: 'blabla"
}
For responsive purposes I need 3 columns max side by side and then start another columns container that will stack columns underneath. So 3 column divs inside columns container and then create another columns div with 3 column divs inside and go until the array is empty.
I have tried computed count property but don't know how to iterate it inside of v-for. Also tried v-if but it didnt work as planned :(
Is it even possible in v-for? I dont know what approach to take to be honest.
Why not just insert each column inside a single columns container and then use CSS to wrap to a new row every 3 columns. The added benefit of this is that you can adjust the number of columns that appear in each row with media queries.
Try running the snippet below in full screen and the resize the browser to less than 576px wide to see the responsive columns.
new Vue({
el: '#app',
data () {
return {
columns: [
{
title: 'blabla',
body: 'blabla',
msg: 'blabla'
},
{
title: 'blabla',
body: 'blabla',
msg: 'blabla'
},
{
title: 'blabla',
body: 'blabla',
msg: 'blabla'
},
{
title: 'blabla',
body: 'blabla',
msg: 'blabla'
},
{
title: 'blabla',
body: 'blabla',
msg: 'blabla'
}
]
}
}
})
.columns {
display: flex;
flex-wrap: wrap;
}
.column {
box-sizing: border-box;
padding: 1em;
width: 33.3%;
}
/* on devices smaller than 576px, stack columns */
#media (max-width: 576px) {
.column {
width: 100%;
}
}
<script src="https://unpkg.com/vue#2.6.8/dist/vue.min.js"></script>
<div id="app">
<div class="columns">
<div v-for="(column, index) in columns" class="column" :key="index">
<h2>{{ column.title }}</h2>
<p>{{ column.body }}</p>
<strong>{{ column.msg }}</strong>
</div>
</div>
</div>
It's possible to nest v-for loops if the data is correctly formatted.
For example, an array (for the first v-for) of objects (for the second loop):
new Vue({
el: "#app",
data() {
return {
items: [
{
title: 'Title 1',
body: 'body 1',
msg: 'message 1'
},
{
title: 'Title 2',
body: 'body 2',
msg: 'message 2'
}
]
}
}
})
.columns {
align-items: center;
display: flex;
height: 40px;
justify-content: space-around;
width: 50%;
}
<script src="https://unpkg.com/vue#2.6.8/dist/vue.min.js"></script>
<div id="app">
<div class="columns" v-for="item in items">
<div class="column" v-for="(value, key) in item">
<div>{{ value }}</div>
</div>
</div>
</div>

Save the click on a method

How can i save the titles selecteds of a ng-click in a method in controller?
<div ng-click="selectItem(dimension, $event)">
<span text="{{dimension.title}}" title="{{dimension.title}}">Item 1</span>
</div>
<div ng-click="selectItem(dimension, $event)">
<span text="{{dimension.title}}" title="{{dimension.title}}">Item 2</span>
</div>
<div ng-click="selectItem(dimension, $event)">
<span text="{{dimension.title}}" title="{{dimension.title}}">Item 3</span>
</div>
I am editing a qlik sense extension and need to save the selections in the app.
A better idea would be to identify objects with a unique identifier rather than a title that can be repeated. If the dimension object has an ID(or some other unique value) use it.
Adapt code below to your needs:
var app = angular.module("myModule", []);
app.controller("myCtrl", function($scope) {
var vm = this;
vm.dimensions = [
{
title: 'Title 1'
},{
title: 'Title 2'
},{
title: 'Title 3'
},{
title: 'Title 4'
},{
title: 'Title 5'
},{
title: 'Title 6'
},
];
vm.selected = [];
vm.selectItem = function(item, event) {
if(!vm.isSelected(item)) {
vm.addItem(item);
} else {
vm.removeItem(item);
}
// Show selected list
console.log(vm.selected);
}
vm.isSelected = function(item) {
return vm.selected.indexOf(item.title) != -1;
}
vm.addItem = function(item) {
vm.selected.push(item.title);
}
vm.removeItem = function(item) {
var index = vm.selected.indexOf(item.title);
vm.selected.splice(index, 1);
}
});
.element {
margin: 20px;
line-height: 10px;
border-bottom: 1px solid black;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="myModule">
<div ng-controller="myCtrl as ctrl">
<div ng-repeat="dimension in ctrl.dimensions" ng-click="ctrl.selectItem(dimension, $event)">
<span text="{{dimension.title}}" title="{{dimension.title}}" class="element">
{{ dimension.title }}
<span ng-if="ctrl.isSelected(dimension)">- checked</span>
</span>
</div>
</div>
</div>

VueJS v-for: remove duplicates

Is there a simple solution I can use to remove the duplicates from my v-for loop which is looping through the JSON data.
I'm using this to populate a Select option and would rather not import https://lodash.com/docs#uniq
Codepen with issue: https://codepen.io/anon/pen/JaZJmP?editors=1010
Thanks
Create a computed property that returns only those items from your array which you want/need. To remove duplicates from info, you can do new Set(this.info.map(i=>i.title.rendered) and destructure that back into an array using [...new Set(this.map(i=>i.title.rendered))]:
var vm = new Vue({
el: "#wp-vue-app",
data() {
return {
info: [{
id: 1,
status: "publish",
link: "",
title: {
rendered: "Test Name One"
},
acf: {
employee_details: {
employee_name: "Test Name",
employee_email: "Test-Email#email.co.uk",
employee_number: "123",
cost_centre_manager: "Manager Name",
manager_email: "Manager-Email#email.co.uk"
}
}
},
{
id: 2,
status: "publish",
link: "",
title: {
rendered: "Test Name"
},
acf: {
employee_details: {
employee_name: "Test Two Name",
employee_email: "Test-Two-Email#email.co.uk",
employee_number: "1234",
cost_centre_manager: "Manager Two Name",
manager_email: "Manager-Two-Email#email.co.uk"
}
}
},
{
id: 3,
status: "publish",
link: "",
title: {
rendered: "Test Name"
},
acf: {
employee_details: {
employee_name: "Test Three Name",
employee_email: "Test-Three-Email#email.co.uk",
employee_number: "12345",
cost_centre_manager: "Manager Three Name",
manager_email: "Manager-Three-Email#email.co.uk"
}
}
}
],
loading: true,
errored: false,
emp_manager: "All",
emp_cost_centre: "All"
};
},
computed: {
info_title: function() {
return [...new Set(this.info.map(i => i.title.rendered))]
},
info_employee_name: function() {
return [...new Set(this.info.map(i => i.acf.employee_details.employee_name))]
},
},
});
.container {
padding: 20px;
width: 90%;
max-width: 400px;
margin: 0 auto;
}
label {
display: block;
line-height: 1.5em;
}
ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
li {
padding: 8px 16px;
border-bottom: 1px solid #eee;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js"></script>
<div id="wp-vue-app">
<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>
<section v-else>
-
<div class="row">
<div class="col">
<select class="form-control" v-model="emp_manager">
<option>All</option>
<option v-for="item in info_title" :value="item">{{ item }}</option>
</select>
<span>Selected: {{ emp_manager }}</span>
</div>
<div class="col">
<select class="form-control" v-model="emp_cost_centre">
<option>All</option>
<option v-for="item in info_employee_name" :value="item">{{ item }}</option>
</select>
<span>Selected: {{ emp_cost_centre }}</span>
</div>
</div>
<br />
</section>
</div>
https://codepen.io/anon/pen/bxjpKG
You can use a computed property to filter your info (let's say filteredInfo). Then use v-for on the filteredInfo property.

Twitter Bootstrap Columns and Angular Order By

I keep running into this problem and haven't found a good solution that doesn't cause layout issues. What's the best way to display an array of items, sorted alphabetically, in columns? I'm using ng-repeat to iterate over an array and display a checkbox for each item. I want the data to be displayed in n columns, alphabetically i.e., not alphabetically in rows.
alphabetically in colums
|item a| |item d| |item g|
|item b| |item e| ...
|item c| |item f| ...
current implementation - alphabetically in rows
<div class="checkbox col-xs-12 col-sm-12 col-md-3 col-lg-3" ng-repeat="user in user.results | orderBy:'lastName' track by user.id">
<input id="{{ user.id }}" type="checkbox">
{{ user.lastName }}, {{ user.firstName }}
<label for="{{ user.id }}" class="pull-left checkbox-label"></label>
</div>
Edit
I originally went with the dynamic bootstrap method but this actually screwed up the checkbox behavior i.e., clicking a checkbox resulted in the incorrect checkbox being checked. I'm trying fix this using flexbox but I haven't used it before and don't understand how to dynamically change the column count without having to set a fixed height on the flex container. I would like to have one column on small/extra small screens and three columns for medium/large screens.
.flex-box {
display: flex;
flex-direction: column;
flex-wrap: wrap;
}
.flex-item {
background: green;
width: 33%;
}
/* Small screens */
#media all and (max-width: #container-tablet) {
.flex-item {
width: 100%;
}
}
<div class="flex-box">
<div class="flex-item" ng-repeat="country in region.countries | orderBy:'name' track by $index">
<input id="{{ country.isoCode }}" checklist-model="vm.selectedCountries.value[region.name]" checklist-value="country" type="checkbox" ng-change="vm.setRegionCountry(region, country, checked)">
<label for="{{ country.isoCode }}" class="pull-left checkbox-label"></label>
<span>{{ country.name | limitTo:17 }}{{country.name.length > 17 ? '...' : ''}}</span>
</div>
</div>
Solution
.flex-box {
display: flex;
flex-flow: column wrap;
}
/* Small screens */
#media all and (min-width: #screen-sm-min) {
.flex-box {
max-height: 375px;
}
}
/* Medium screens */
#media all and (min-width: #screen-md-min) {
.flex-box {
max-height: 550px;
}
}
/* Large screens */
#media all and (min-width: #screen-lg-min) {
.flex-box {
max-height: 375px;
}
}
You can tweak little bit as below if you want to use with bootstrap. Otherwise you can use either flex-box or column-count.
var app = angular.module('app', []);
app.controller('TestController', function($scope){
$scope.fixedColumn = 3;
$scope.getColumns = function(){
return new Array($scope.fixedColumn);
};
$scope.getColumnWidth = function(){
return Math.floor(12 / $scope.fixedColumn);
};
$scope.getRowCount = function(){
return Math.ceil($scope.user.results.length / $scope.fixedColumn);
};
$scope.user = {
results: [
{
id: 1,
firstName: 'FirstName1',
lastName: 'LastName1'
},
{
id: 2,
firstName: 'FirstName2',
lastName: 'LastName2'
},
{
id: 3,
firstName: 'FirstName3',
lastName: 'LastName3'
},
{
id: 4,
firstName: 'FirstName4',
lastName: 'LastName4'
},
{
id: 5,
firstName: 'FirstName5',
lastName: 'LastName5'
},
{
id: 6,
firstName: 'FirstName6',
lastName: 'LastName6'
},
{
id: 7,
firstName: 'FirstName7',
lastName: 'LastName7'
},
{
id: 8,
firstName: 'FirstName8',
lastName: 'LastName8'
}
]
};
});
angular.bootstrap(document, ['app']);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="container" ng-controller="TestController">
<div class="row" ng-repeat="u in user.results | orderBy:'lastName' track by u.id" ng-init="pIndex=$index" ng-if="$index < getRowCount()">
<div ng-repeat="col in getColumns() track by $index" ng-init="usr = user.results[pIndex + ($index * getRowCount())]" ng-class="'col-xs-'+ getColumnWidth() + ' col-sm-'+ getColumnWidth() + ' col-md-'+ getColumnWidth()" ng-if="user.results.length > (pIndex + ($index * getRowCount()))">
<input id="{{ usr.id }}" type="checkbox">
{{ usr.lastName }}, {{ usr.firstName }}
<label for="{{ usr.id }}" class="pull-left checkbox-label"></label>
</div>
</div>
</div>
<style>
.flex-box {
height: 100px;
overflow: hidden;
display: flex;
flex-direction: column;
flex-wrap: wrap;
}
.flex-item {
background: green;
text-align: center;
}
</style>
<div class="container">
<div class="row">
<div class="col-xs-12">
<div class="flex-box">
<div class="flex-item" ng-repeat="user in user.results | orderBy:'lastName'">{{user.id}}</div>
</div>
</div>
</div>
</div>
Using CSS flexbox you can dynamically add as many column as required for your data set https://css-tricks.com/snippets/css/a-guide-to-flexbox/
You could do this without bootstrap using columns. See this fiddle: https://jsfiddle.net/ojzdxpt1/1/
#wrapper {
column-count:3;
-webkit-column-count:3;
-moz-column-count:3;
}
.col {
background:#ccc;
border:1px solid #000;
}
#wrapper contains each repeater. See more about css columns
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Columns/Using_multi-column_layouts
https://css-tricks.com/almanac/properties/c/columns/

Resources