I have a map function outputting a list of books to my Books component. On each li that is being returned by the map function i have an onClick event handler that calls a function openBook, this function simply displays the info of that clicked book to a div on screen.
My issue is that for some reason, when i pass parameters to the openBook function it only passes the last li element's title and author and it seems to run the function without even me clicking on a book.
How can i pass the title and author params to the created div when i click on a particular book?
Here's my code and what i've tried.
Books.js
function Books() {
const {books} = useContext(BookContext) //{books} here is the array (API) with all books inside but as a state in the BookContext file
function openBook(title, author) {
const bookview = document.createElement('DIV')
if(document.body.contains(document.querySelector('.bookview'))) {
document.body.removeChild(document.querySelector('.bookview'))
}
bookview.innerHTML = '<i class="fas fa-times close"></i> <h4>'+info+'</h4>'
bookview.classList.add('bookview')
document.body.appendChild(bookview)
document.querySelector('.close').addEventListener('click', function() {
document.body.removeChild(document.querySelector('.bookview'))
})
}
return (
<>
<div className="bookshelf">
<ul>
{
books.map(book => {
return <li onClick={openBook(book.title, book.author)}>{book.title} <span>{book.author}</span><i class="fas fa-bookmark"></i></li>
})
}
</ul>
</div>
</>
)
}
BookContext (just has some books info)
const [books, setBooks] = useState([
{
id: 1,
title: "Digital Fortress",
author: "Dan Brown"
},
{
id: 2,
title: "Origin",
author: "Dan Brown"
},
{
id: 3,
title: "The Lord Of The Rings",
author: "J.R Tolkien"
},
{
id: 4,
title: "The Bourne Identity",
author: "Robert Ludlum"
}
])
You have to modify the onClick like this
<li onClick={() => openBook(book.title, book.author)}><span>{book.author}</span><i class="fas fa-bookmark"></i></li>
Because in the way you have returned the function would get executed onLoad itself and the return value will be assigned to onClick.
But what you actually require is a function that needs to be executed onClick of the li
Related
i have this function which collect the image and display them in a gallery:
let gallery = document.getElementById("gallery");
let galleryItemsData = [
{
id: "UX",
img: "images/UX/BrandBoard.png",
title: "Brand Board",
},
{
id: "CMlogo",
img: "images/123CleanMe/mockup (12).png",
title: "123 Clean Me",
}
];
let generateGallery = () => {
return (gallery.innerHTML = galleryItemsData.map((x) => {
let { id, img, title, } = x;
return `
<div class="imgContainer" id="${id}" >
<p class="title">${title}</p>
<img class="galleryImg" id="${id}" src="${img}" alt="logo">
</div>
`
}).join(""));
};
generateGallery();
the problem.
how can I make a function that display the pictures in modal when the user clicks on it? I tried different options but doesn't seem to work.
what I'm trying to achieve:
I need a function that will collect the images from this function, and it will display in modal when the user clicks on the image.
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
I'm trying to make my website a little bit faster, and for that, I'm trying to make a button that on each click presents more images. For example: a user can see 5 images, and if the user wants to see 5 more he can, by clicking on the button.
So for now only got this, and i really think it's not the right way.
HTML ->
<ion-card *ngFor="let pic of photoList">
<h1>{{pic?.username}}</h1>
<h2>{{pic?.name}}</h2>
<img src={{pic?.picture}}>
</ion-card>
<button ion-button (click)="load()">Load More Images</button>
Js ->
load() {
firebase.database().ref('HomeList').limitToLast(5).on('value', snapshot => {
this.photoList = [];
snapshot.forEach(snap => {
this.photoList.push({
id: snap.key,
name: snap.val().name,
username: snap.val().username,
picture: snap.val().picture,
email: snap.val().email,
uid: snap.val().uid,
rating: snap.val().rating
});
console.log(this.photoList);
return false
});
return this.photoList.reverse();
});
}
so you need a pagination try to use .startAfter(number) and .limit(number); assuming this.current = 0; sets in constructor();
load() {
firebase.database().ref('HomeList').startAfter(this.current).limit(5).on('value', snapshot => {
this.photoList = [];
snapshot.forEach(snap => {
this.photoList.push({
id: snap.key,
name: snap.val().name,
username: snap.val().username,
picture: snap.val().picture,
email: snap.val().email,
uid: snap.val().uid,
rating: snap.val().rating
});
console.log(this.photoList);
this.current = this.current + photoList.length;
return false
});
return this.photoList.reverse();
});
}
I wrote this simple questionnaire app example: https://jsfiddle.net/neydmo34/ but I have problem with checkboxes loosing its state when user clicks buttons "Next" and "Back".
For example, if user performs this actions:
answer with "Lisp" on first question,
click "Next",
answer with "Bill Gates" on second question,
click "Back",
click "Next",
then you'll see that the "Bill Gates" checkbox will not be checked anymore, despite the fact that backing array userAnswers is correctly updated.
I cannot understand why that's happen and what I should change in my code to made it work correctly.
Here's the code:
<html>
<head>
<title>Questionnaire</title>
<script src="vue.js"></script>
</head>
<body>
<h1>Questionnaire</h1>
<div id="app">
<p><b>Question {{ currQuestionIndex + 1 }})</b> {{ currQuestion.text }}</p>
<div v-for="ans in currQuestion.answers">
<input type="radio"
:name="currQuestionIndex"
:value="ans"
v-model="userAnswers[currQuestionIndex]" />
<label :for="ans">{{ ans }}</label><br>
</div>
<p>
<button #click="goBack">Back</button>
<button #click="goNext">Next</button>
</p>
userAnswers = {{ userAnswers }}
</div>
</body>
<script>
var app = new Vue({
el: '#app',
data: {
currQuestionIndex: 0,
questions: [
{text: "What's the name of most powerful programming language?",
answers: ['Java', 'C#', 'Lisp', 'Haskell']
},
{text: 'Who is Microsoft founder?',
answers: ['Bill Gates', 'Richard Stallman', 'Steve Jobs']
},
{text: 'What type of software do you like most?',
answers: ['open source', 'closed source', 'public domain']
},
{text: 'The best computing company is:',
answers: ['IBM', 'Microsoft', 'Google']
},
],
userAnswers: [null, null, null, null]
},
computed: {
currQuestion: function () {
return this.questions[this.currQuestionIndex];
}
},
methods: {
goNext: function(e) {
var next = this.currQuestionIndex + 1;
if (next >= this.questions.length) {
alert("Ok, your answers are: " + this.userAnswers);
} else {
this.currQuestionIndex = next;
}
},
goBack: function(e) {
var previous = this.currQuestionIndex - 1;
if (previous >= 0) {
this.currQuestionIndex = previous;
}
}
}
});
</script>
</html>
var app = new Vue({
el: '#app',
data: {
currQuestionIndex: 0,
questions: [
{text: "What's the most powerful programming language?",
answers: ['Java', 'Scheme', 'Lisp', 'Haskell']
},
{text: 'Who is Microsoft founder?',
answers: ['Bill Gates', 'Richard Stallman', 'Steve Jobs']
},
{text: 'What type of software do you like most?',
answers: ['open source', 'closed source', 'public domain']
},
{text: 'The best computing company is:',
answers: ['IBM', 'Microsoft', 'Google']
},
],
userAnswers: [null, null, null, null]
},
computed: {
currQuestion: function () {
return this.questions[this.currQuestionIndex];
}
},
methods: {
goNext: function(e) {
var next = this.currQuestionIndex + 1;
if (next >= this.questions.length) {
alert("OK, your answers are: " + this.userAnswers);
} else {
this.currQuestionIndex = next;
}
},
goBack: function(e) {
var previous = this.currQuestionIndex - 1;
if (previous >= 0) {
this.currQuestionIndex = previous;
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<h1>Questionnaire</h1>
<div id="app">
<p><b>Question {{ currQuestionIndex + 1 }})</b> {{ currQuestion.text }}</p>
<div v-for="ans in currQuestion.answers" :key="ans">
<input type="radio" :name="currQuestionIndex" :value="ans" v-model="userAnswers[currQuestionIndex]" />
<label :for="ans">{{ ans }}</label><br>
</div>
<p>
<button #click="goBack">Back</button>
<button #click="goNext">Next</button>
</p>
userAnswers = {{ userAnswers }}
</div>
You need a key.
When Vue is updating a list of elements rendered with v-for, it by
default uses an “in-place patch” strategy. If the order of the data
items has changed, instead of moving the DOM elements to match the
order of the items, Vue will simply patch each element in-place and
make sure it reflects what should be rendered at that particular
index. This is similar to the behavior of track-by="$index" in Vue
1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM
state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus
reuse and reorder existing elements, you need to provide a unique key
attribute for each item.
Since each answer is unique in your form, you can use :key="ans".
Update: added snippet copied in from Cristy's fiddle.
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