This question already has answers here:
Iterate through nested json array in angular controller and get unique values
(5 answers)
Closed 4 years ago.
{
name: 'Product One',
visibility: 1,
weight: '0.5',
price: '19.99'
custom_attributes: [
{
attribute_code: 'image',
value: '.img'
},
{
attribute_code: 'special_price',
value: '13.99'
}
]
},
{
name: 'Product One',
visibility: 1,
weight: '0.5',
price: '19.99'
custom_attributes: [
{
attribute_code: 'image',
value: '.img'
},
{
attribute_code: 'special_price',
value: '13.99'
}
]
}
How do I get access to 'special_price' value on ng-repeat or javascript?
There are few ways you could approach this.
Prepare the data in the controller
function ExampleCtrl (products) {
this.$onChanges = (changes) => {
if (changes.products) {
this.specialProductPrices = this.products.reduce((map, product) => {
map[product.id] = product.custom_attributes
// #TODO: Account for a case when there is no special price.
.find(({attribute_code}) => attribute_code === 'special_price').value;
return map;
}, {})
}
}
}
angular.module('foo').component('example', {
bindings: {
products: '<'
},
controller: [
ExampleCtrl
],
template: `
<div ng-repeat="product in $ctrl.products track by product.id">
Name: <span ng-bind="product.name"></span>
Price: <span ng-bind="product.price"></span>
Special Price: <span ng-bind="$ctrl.specialProductPrices[product.id]"></span>
</div>
`
})
And then the component can simply be used as <example products="products"></example>. This is mostly an idiomatic approach in angularjs land,
and overall encouraged due to the use of components + reducers which prepare the data once rather than looping multiple times during every $digest cycle.
Access it within the loop in template
If you have to do this in the template, you could do something like this:
<div ng-repeat="product in $ctrl.products track by product.id">
Name: <span ng-bind="product.name"></span>
Price: <span ng-bind="product.price"></span>
Special Price:
<span>
<span ng-repeat="customAttribute in product.custom_attributes track by customAttribute.attribute_code"
ng-show="customAttributeattribute_code === 'special_price'"
ng-bind="customAttribute.value">
</span>
</span>
</div>
However this method isn't encouraged, since it creates DOM elements which will never be shown (ng-if can't be used in this case due to the repeater.). Additionally if there are many custome attributes,
this will become very inefficient.
Other options
One could also potentially create components which take in product.custom_attributes and show the attribute if present, or create a filter which would pick the attribute out as well.
These methods are left as an exercise to the reader.
Related
Is it possible to access an element within the nested v-for loop by using the refs index of the element? I mean, I'm trying to focus a textbox that is within the nested v-for loop which I used to access by its refs index. It works fine for a single v-for loop but not with nested.
For more details here's my loop structure:
This works
<div v-for="(comItem, index) in commentItems" :key="comItem.commentId">
<textarea ref="addRep" ></textarea>
</div>
this.$nextTick(() => {
this.$refs.addRep[index].focus()
});
This won't work
<div v-for="(cont, i) in contentItems" :key="cont.contentId">
...
<div v-for="(comItem, index) in commentItems" :key="comItem.commentId">
<textarea ref="addRep" ></textarea>
</div>
</div>
this.$nextTick(() => {
this.$refs.addRep[index].focus()
});
Or
this.$nextTick(() => {
this.$refs.addRep[i].focus()
});
With the nested html v-for loop structure. The focus will just jump around anywhere. To anyone who encountered this kind of scenario. Please assist me if you know the solutions. Thanks.
Trying to calculate the appropriate index within addRep is a little tricky. You'd need the values of both i and index and then count up through the relevant arrays to work out the appropriate index.
A simpler way to do this is to use a dynamic ref name. We still need i and index to find the relevant element but there's no calculation required.
The core trick here is to set the ref to :ref="`addRep${i}`", or equivalently :ref="'addRep' + i" if you prefer. So you'll end up with multiple named refs, addRep0, addRep1, etc., each with its own array of elements. The value of i tells you the ref name and the index tells you the index within that array.
Here's an example:
new Vue({
el: '#app',
data () {
return {
contentItems: [
{
contentId: 1,
comments: [
{
commentId: 1,
text: 'A'
}, {
commentId: 2,
text: 'B'
}, {
commentId: 3,
text: 'C'
}
]
}, {
contentId: 2,
comments: [
{
commentId: 1,
text: 'D'
}
]
}, {
contentId: 3,
comments: [
{
commentId: 1,
text: 'E'
}, {
commentId: 2,
text: 'F'
}
]
}
]
}
},
methods: {
onButtonClick (i, index) {
this.$refs[`addRep${i}`][index].focus()
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<div v-for="(cont, i) in contentItems" :key="cont.contentId">
<h4>{{ cont.contentId }}</h4>
<div v-for="(comItem, index) in cont.comments" :key="comItem.commentId">
<textarea :ref="`addRep${i}`" v-model="comItem.text"></textarea>
<button #click="onButtonClick(i, index)">Focus</button>
</div>
</div>
</div>
Code below.
I think I'm missing a crucial piece here. I've been through the docs and watched the entire vue2 step by step. Everything is making sense so far but I'm stuck on what seems to be a core piece. Any help would be appreciated. If this is totally wrong, please let me know, I'm not married to any of this stuff.
Desired functionality: There is an order Vue instance and it has line items.
On order.mounted() we hit an api endpoint for the order's data, including possible existing line items. If there are existing line items, we set that order data (this.lineitems = request.body.lineitems or similar). This part works fine and I can get the order total since the orders' line items are up to date at this point.
Each line item is an editable form with a quantity and a product . If I change the quantity or product of any line item, I want the child line-item component to notify the parent component that it changed, then the parent will update its own lineitems data array with the new value, and preform a POST request with all current line item data so the server side can calculate the new line item totals (many specials, discounts, etc). This will return a full replacement array for the order's line item data, which in turn would passed down to the line items to re-render.
Problems:
The line-items components "update..." methods are feeling obviously wrong, but my biggest issue is understanding how to get the parent to update its own line items data array with the new data. for instance
lineitems = [
{id: 1000, quantity: 3, product: 555, total: 30.00},
{id: 1001, quantity: 2, product: 777, total: 10.00}
]
If the second line item is changed to quantity 1, how do I get the parent's lineitems data to change to this? My main problem is that I don't know how the parent is suppose to know which of its own lineitems data array need to be modified, and how to grab the data from the changed child. I assume it came in via an event, via emit, but do I now need to pass around the primary key everywhere so I can do loops and compare? What if its a new line item and there is no primary key yet?
Mentioned above, I'm using the existing line item's DB primary key as the v-for key. What if I need a "new lineitem" that appends a blank lineitem below the existing ones, or if its a new order with no primary keys. How is this normally handled.
Is there a best practice to use for props instead of my "initial..." style? I assume just using $emit directly on the v-on, but I'm not sure how to get the relevant information to get passed that way.
This seems like the exact task that VueJS is suited for and I just feel like I keep chasing my tail in the wrong direction. Thanks for the help!
LineItem
Vue.component('line-item', {
props: ["initialQuantity", "initialProduct", "total"],
data () {
return {
// There are more but limiting for example
quantity: initialQuantity,
product: initialProduct,
productOptions = [
{ id: 333, text: "Product A"},
{ id: 555, text: "Product B"},
{ id: 777, text: "Product C"},
]
}
},
updateQuantity(event) {
item = {
quantity: event.target.value,
product: this.product
}
this.$emit('update-item', item)
},
updateProduct(event) {
item = {
quantity: this.quantity,
product: event.target.value
}
this.$emit('update-item', item)
}
template: `
<input :value="quantity" type="number" #input="updateQuantity">
<select :value="product" #input="updateProduct">
<option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
</select>
Line Item Price: {{ total }}
<hr />
`
})
Order/App
var order = new Vue({
el: '#app',
data: {
orderPK: orderPK,
lineitems: []
},
mounted() {
this.fetchLineItems()
},
computed: {
total() {
// This should sum the line items, like (li.total for li in this.lineitems)
return 0.0
},
methods: {
updateOrder(item) {
// First, somehow update this.lineitems with the passed in item, then
fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems)
.then(resp => resp.json())
.then(data => {
this.lineitems = data.lineitems;
})
},
fetchLineItems() {
fetch(`domain.com/api/orders/${this.orderPK}`)
.then(resp => resp.json())
.then(data => {
this.lineitems = data.lineitems;
})
},
},
template: `
<div>
<h2 id="total">Order total: {{ total }}</h2>
<line-item v-for="item in lineitems"
#update-item="updateOrder"
:key="item.id"
:quantity="item.quantity"
:product="item.product"
:total="item.total"
></line-item>
</div>
`
})
Here's a list of problems in your attempt that would prevent it from displaying anything at all i.e.
quantity: initialQuantity, - surely you meant quantity: this.initialQuantity, ... etc for all the other such data
missing } for computed total
your line-item template is invalid - you have multiple "root" elements
And then there's some minor issues:
you want the #change handler for the select, not #input, if your code ran, you'd see the difference,
Similarly you want #change for input otherwise you'll be making fetch requests to change the items every keystroke, probably not what you'd want
So, despite all that, I've produced some working code that does all you need - mainly for my own "learning" though, to be fair :p
// ******** some dummy data and functions to emulate fetches
const products = [
{ id: 333, text: "Product A", unitPrice: 10},
{ id: 555, text: "Product B", unitPrice: 11},
{ id: 777, text: "Product C", unitPrice: 12},
];
let dummy = [
{id: 1, quantity:2, product: 333, total: 20},
{id: 2, quantity:3, product: 777, total: 36},
];
const getLineItems = () => new Promise(resolve => setTimeout(resolve, 1000, JSON.stringify({lineitems: dummy})));
const update = items => {
return new Promise(resolve => setTimeout(() => {
dummy = JSON.parse(items);
dummy.forEach(item =>
item.total = parseFloat(
(
item.quantity *
(products.find(p => p.id === item.product) || {unitPrice: 0}).unitPrice *
(item.quantity > 4 ? 0.9 : 1.0)
).toFixed(2)
)
);
let res = JSON.stringify({lineitems: dummy});
resolve(res);
}, 50));
}
//********* lineItem component
Vue.component('line-item', {
props: ["value"],
data () {
return {
productOptions: [
{ id: 333, text: "Product A"},
{ id: 555, text: "Product B"},
{ id: 777, text: "Product C"},
]
}
},
methods: {
doupdate() {
this.$emit('update-item', this.value.product);
}
},
template: `
<p>
<input v-model="value.quantity" type="number" #change="doupdate()"/>
<select v-model="value.product" #change="doupdate()">
<option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
</select>
Line Item Price: {{ '$' + value.total.toFixed(2) }}
</p>
`
})
//********* Order/App
const orderPK = '';
var order = new Vue({
el: '#app',
data: {
orderPK: orderPK,
lineitems: []
},
mounted() {
// initial load
this.fetchLineItems();
},
computed: {
carttotal() {
return this.lineitems.reduce((a, {total}) => a + total, 0)
}
},
methods: {
updateOrder(productCode) {
// only call update if the updated item has a product code
if (productCode) {
// real code would be
// fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems).then(resp => resp.json())
// dummy code is
update(JSON.stringify(this.lineitems)).then(data => JSON.parse(data))
.then(data => this.lineitems = data.lineitems);
}
},
fetchLineItems() {
// real code would be
//fetch(`domain.com/api/orders/${this.orderPK}`).then(resp => resp.json())
// dummy code is
getLineItems().then(data => JSON.parse(data))
.then(data => this.lineitems = data.lineitems);
},
addLine() {
this.lineitems.push({
id: Math.max([this.lineitems.map(({id}) => id)]) + 1,
quantity:0,
product: 0,
total: 0
});
}
},
template: `
<div>
<h2 id="total">Order: {{lineitems.length}} items, total: {{'$'+carttotal.toFixed(2)}}</h2>
<line-item v-for="(item, index) in lineitems"
:key="item.id"
v-model="lineitems[index]"
#update-item="updateOrder"
/>
<button #click="addLine()">
Add item
</button>
</div>
`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
</div>
note: there may be some inefficient code in there, please don't judge too harshly, I've only been using vuejs for a week
This must be simple and Angular probably has an inbuilt directive to do this but I cant think of how to do without looping through the Array.
I have a array of options i.e.
$scope.colors=[
{id:"0",label:"blue"},
{id:"1",label:"red"},
{id:"2",label:"green"}
]
And then my data object that stores the id of a color option i.e.
$scope.data={
color:"1",
otherproperty:""
}
But when I display the data to the user I want to show the label rather than the id, so is there a easy(angular) way to do this?:
{{data.color.label}}
The Angular way would be using ng-repeat & filter, your still essentially looping over the Array but all options would require some sort of loop i.e.
<div ng-repeat="color in colors | filter:{ 'id': data.color}:true">
{{ color.label }}
</div>
Setting the Filter strict comparison to 'true' as above will only select the id with an exact match
https://jsfiddle.net/sjmcpherso/wztunyr5/
The following will return the object where the id matches $scope.data.color:
var pickedColor = $scope.colors.filter(function( obj ) {
return obj.id === $scope.data.color;
});
pickedColor.label will be the label string.
Look at other way, hope it will help you.
https://jsfiddle.net/kkdvvkxk/.
We can also use $filter under controller.
Controller :
var myApp = angular.module('myApp', []);
function MyCtrl($scope, $filter) {
$scope.colors = [{
id: "0",
label: "blue"
}, {
id: "1",
label: "red"
}, {
id: "2",
label: "green"
}]
$scope.data = {
color: "1",
otherproperty: ""
}
$scope.getLabel = function(colorId) {
return $filter('filter')($scope.colors, { id: colorId }[0].label;
}
}
HTML :
{{ getLabel(data.color)}}
<div>
<ul id="teachers">
<li ng-repeat></li>
</ul>
<ul id="students">
<li ng-repeat></li>
</ul>
</div>
I have two ul elements and dynamic data. For example:
[
{
name: 'Jack'
status: 'teacher'
},
{
name: 'Angelina'
status: 'teacher'
},
{
name: 'Maya'
status: 'student'
},
{
name: 'Kate'
status: 'teacher'
},
{
name: 'Margaret'
status: 'student'
}
]
I want to write some custom directive for ng-repeat, which will generates lists, for students and for teachers, for different ul's.
How can I write directive, with some condition, which will repeat li's in the right ul?
Yes, I can, filter My data and generate two Arrays, for students and teachers and than repeat those Independently.
But, I don't like this way. How it is possible to write one custom directive which will determines, where to repeat current Object?
UPDATE
Okey, I'm new in angular, so I've thought, that there will be something simple trick, something like this:
if(status === 'something')
use this template
else
use this template
So, with your answers I could write conditions which I wanted. Sorry about this, this was stupid decision.
So my actual case is:
I have Breadcrumbs data and main container, which width is equal to 500px.
I want to repeat li in this container and I want to my li's were always always inline.
If my data will be big, or some title will be big and my ul width will be more, than my container, some li elements will be dropped bellow.
because of this, I have two ul elements and lis which won't have there space I want to insert in second ul, which will be hidden and after click on something I will show this ul
Options:
Use in built angular filters. For example:
<ul id="teachers">
<li ng-repeat="person in people | filter: { status: 'teacher' }"></li>
</ul>
plnkr
Split the array in your controller. Both split arrays should still point to the original object (in the original array), so manipulation should be ok.
You can definitely create your own directive, but you will end up encapsulating one of the options above.
Better than write a directive, filter your array javascript with the built-in functions for array.
Example:
HTML
<div ng-controller="ClassroomController as classroom">
<ul id="teachers">
<li ng-repeat="teacher in classroom.teachers track by $index"></li>
</ul>
<ul id="students">
<li ng-repeat="student in classroom.students track by $index"></li>
</ul>
</div>
JAVASCRIPT
function Controller() {
var vm = this;
vm.data = [
{
name: 'Jack'
status: 'teacher'
},
{
name: 'Angelina'
status: 'teacher'
},
{
name: 'Maya'
status: 'student'
},
{
name: 'Kate'
status: 'teacher'
},
{
name: 'Margaret'
status: 'student'
}
];
vm.teachers = vm.data.filter(function(item){return item.status === 'teacher'});
vm.students = vm.data.filter(function(item){return item.status === 'student'});
}
I also think that filtering is the best as already answered. But according to your update you can do something like this in yuor directive controller:
$scope.getTemplateUrl = function() {
if (status == something) {
return '/partials/template1.html';
} else {
return '/partials/template2.html';
}
}
Then define your directive template as follows:
template: '<ng-include src="getTemplateUrl()"/>',
Of course status has to be defined before the directive is rendered.
directive('info', function()
{
return {
restrict : 'E',
template : '<ul> <li ng-repeat="l in list"><div ng-if="check(l)">{{l.name}}</div></li></ul></br><ul><li ng-repeat="l in list"><div ng-if="!check(l)">{{l.name}}</div></li></ul>',
controller : function($scope)
{
$scope.check = function(l)
{
if(l.status=="student")
return true;
else if(l.status=="teacher")
return false;
}
}
};
});
I want to create an app that work like this : https://ionic-songhop.herokuapp.com
As you can see, when we click favorite button, the item will store in factory and we can invoke in another page (favorite page)
In my case : i use service to store the item data and create factory to store the pushed item.
Here's my code : (I store data in service)
.service('dataService',function(){
var service=this;
this.playerlist = [
{ name: 'Leonel Messi', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" },
{ name: 'Cristiano Ronaldo', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" },
{ name: 'Zlatan Ibrahimovic', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" },
{ name: 'Wayne Rooney', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" },
{ name: 'Michael Carrick', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" },
{ name: 'Phil Jones', ava:"https://pbs.twimg.com/profile_images/473469725981155329/E24vfxa3_400x400.jpeg" },
{ name: 'Angel di Maria', ava:"https://i.scdn.co/image/d1f58701179fe768cff26a77a46c56f291343d68" }
];
})
.factory('User', function() {
var play = { favorites: []}
play.addToFavorites = function(song) {
play.favorites.unshift(song);
}
play.removeFromFavorites = function(player, index) {
play.favorites.splice(index, 1);
}
return play;
})
Controller :
.controller('ChooseTabCtrl', function($scope, dataService, User) {
$scope.dataService=dataService;
$scope.addToFavorite = function (item) {
User.favorites.unshift(dataService.playerList.indexOf(), 1);
}
})
But when i click the favorite button on each item, the list dont show in favorite page.
Is it possible to do like this in Ionic app?
Here's my codepen : http://codepen.io/harked/pen/WvJQWp
There are a few issues with the code in your codepen...
In the controller you are referencing dataService.playerList.indexOf() when the player object is actually playerlist (all lowercase). Also, I assume you want to actually get the indexOf the player so that line needs to change to:
User.favorites.unshift(dataService.playerlist.indexOf(item));
// remove the `, 1` otherwise you'll be adding a `1` to the array everytime
and in your view, you need to change the following:
// wrong
ng-click="addToFavorite(item)"
// right
ng-click="addToFavorite(player)"
Next, in your ListTabCtrl change the following:
$scope.players=dataService;
// to
$scope.players=dataService.playerlist;
Then in the view:
<ion-item ng-repeat="player in favorites" class="item item-avatar" href="#">
<img ng-src="{{players[player].ava}}">
<h2>{{players[player].name}}</h2>
<p>Back off, man. I'm a scientist.</p>
<ion-option-button class="button-assertive" ng-click="removePlayer(player, $index)">
<i class="ion-minus-circled"></i>
</ion-option-button>
</ion-item>
I have posted a working example of your code on jsbin: http://jsbin.com/lukodukacu/edit?html,css,js,output