Vue - loading div when data is re-rendered - javascript

I'm doing a list component with v-for and a function. Every time I change filters it re-renders the list. I did a div for loading message every time its changing, but I'm not sure exactly how to handle it.
This is an uncompleted code, but its easier to you read:
<template>
<div>
<Filters
:country-selected="countrySelected" :countries="countries"
#updateCountry="countrySelected = $event"
></Filters>
<section class="teams container-wrapper">
<!-- Loading Placeholder -->
<div class="loading-msg" :class="{'is-open': loading}">
<p>Loading...</p>
</div>
<!-- List -->
<div class="teams-list" :class="{'is-loading': loading}">
<team-list class="teams"
v-for="(team, index) in loadingTeams()" :key="index"
:lorem=team.lorem
></team-list>
</div>
</section>
</div>
</template>
<script>
export default {
data() {
return {
teams: json.teams,
countries: json.countries,
countrySelected: "",
teamList: undefined, //to watch
loading: true //variable to toggle
}
},
watch: {
teamList: function() {
this.loading = true // => THIS LINE EXECUTES AN INFINTE LOOP
setTimeout(() => this.loading = false, 1000)
}
},
methods: {
teamFilters: function() {
return this.teams.filter(
team => team.country = this.countrySelected
)
},
loadingTeams: function() {
this.teamList = this.teamFilters()
return this.teamFilters()
}
}
}
</script>
<style>
.is-open {
opacity: 1;
}
.is-loading {
opacity: 0;
}
</style>
Probably there is an easy way to handle list update and toogle loading div (i would like to know your opinions), but I'm not understading why the function in watch 'teamList' is doing a infinite loop. It should work, right?

Related

emit value from mounted in child.vue to parent.vue

I'm working with BootstrapVue.
I need to emit a value to my parent.vue - but my code line this.$emit('info', this.hide); doesn't work out.
If I console.log(this.hide) i get my value correct in this case false, otherwise if my if-statement is correct I get it true.
What is the mistake in here?
script of my child.vue:
data(){
return {
hide: true,
}
}
mounted() {
if (statement) {
if(some statement) {
//do something
} else {
this.hide = false;
console.log(this.hide); //HERE I GET CORRECT VALUE
this.$emit('info', this.hide); //THIS DOESNT WORK
}
}
}
How it should work in my parent.vue:
<template>
<div #info="info">
<div> //THIS DIV SHOULD BE SHOWN IF this.hide = false
</div>
<div> //THIS DIV SHOULD BE SHOWN IF this.hide = true
</div>
</div>
</template>
Try something like following snippet :
Vue.component('Child', {
template: `
<div class="">
child
<button #click="changeHide">change hide</button>
</div>
`,
data(){
return {
hide: true,
}
},
methods: {
changeHide() {
this.hide = !this.hide
this.sendInfo()
},
sendInfo() {
this.$emit('info', this.hide);
}
},
mounted() {
//if (statement) {
//if(some statement) {
//do something
//} else {
this.hide = false;
this.sendInfo()
//}
}
})
new Vue({
el: '#demo',
data(){
return {
info: null,
}
},
methods: {
setInfo(val) {
this.info = val
}
}
})
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div v-if="!info"> //THIS DIV SHOULD BE SHOWN IF this.hide = false
</div>
<div v-if="info"> //THIS DIV SHOULD BE SHOWN IF this.hide = true
</div>
<p>from child info is: {{ info }}</p>
<Child #info="setInfo" />
</div>
Ideally in App.vue (Parent)
you should have something like
<login #info="someHandler"></login>
there is no need to go for a child component if you gonna jus use to manage some logic. It is appropriate only if have some contents in the template.
Also placing emit handler on some div. Thats not how emit works
Below is a simple working example
App.vue (parent)
<template>
<h1>{{ title }}</h1>
<Child #changeTitle="ChangeT($event)" />
</template>
<script>
import Child from "./components/Child"
export default{
name:'App',
components: {
Child,
},
data()
{
return{
title:'Rick Grimes'
}
},
methods:{
ChangeT(title)
{
this.title=title;
},
}
</script>
<style></style>
Child.vue
<template lang="html">
<button type="button" #click='passEvent'> Update me</button>
</template>
<script>
export default {
name:'Child',
methods:{
passEvent()
{
this.$emit('changeTitle','Awesome ')
}
}
}
</script>
<style lang="css" scoped>
</style>

Display dropdowns dynamically in one component

I want to have multiple dropdowns in one component using one variable to display or not and also clicking away from their div to close them:
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
// second dropdown in same component
<div class="dropdown">
<button #click.prevent="isOpen = !isOpen"></button>
<div v-show="isOpen">Content</div>
</div>
data() {
return {
isOpen: false
}
},
watch: {
isOpen(isOpen) {
if(isOpen) {
document.addEventListener('click', this.closeIfClickedOutside)
}
}
},
methods: {
closeIfClickedOutside(event){
if(! event.target.closest('.dropdown')){
this.isOpen = false;
}
}
}
But now when I click one dropdown menu it displays both of them. I am kinda new to vue and cant find way to solve this
To use just one variable for this, the variable needs to identify which dropdown is open, so it can't be a Boolean. I suggest storing the index (e.g., a number) in the variable, and conditionally render the selected dropdown by the index:
Declare a data property to store the selected index:
export default {
data() {
return {
selectedIndex: null
}
}
}
Update closeIfClickedOutside() to clear the selected index, thereby closing the dropdowns:
export default {
methods: {
closeIfClickedOutside() {
this.selectedIndex = null
}
}
}
In the template, update the click-handlers to set the selected index:
<button #click.stop="selectedIndex = 1">Open 1</button>
<button #click.stop="selectedIndex = 2">Open 2</button>
Also, update the v-show condition to render based on the index:
<div v-show="selectedIndex === 1">Content 1</div>
<div v-show="selectedIndex === 2">Content 2</div>
Also, don't use a watcher to install a click-handler on the document because we want to know about the outside-clicks when this component is rendered. It would be more appropriate to add the handler in the mounted hook, and then remove in the beforeDestroy hook:
export default {
mounted() {
document.addEventListener('click', this.closeIfClickedOutside)
},
beforeDestroy() {
document.removeEventListener('click', this.closeIfClickedOutside)
},
}
demo
Make an array and loop through it, much easier that way.
<template>
<div id="app">
<div class="dropdown" v-for="(drop, index) in dropData" :key="index">
<button #click="openDropdown(index);">{{ drop.title }}</button>
<div v-show="isOpen === index">{{ drop.content }}</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: null,
dropData: [
{
title: "Hey",
content: "Hey it's content 1"
},
{
title: "Hey 2",
content: "Hey it's content 2"
},
{
title: "Hey 3",
content: "Hey it's content 3"
},
]
};
},
methods: {
openDropdown(idx){
if (this.isOpen === idx) {
this.isOpen = null;
} else {
this.isOpen = idx;
}
}
}
};
</script>

Render component template on invoked methods

So while I'm learning vue, I wanted to double check if someone can show me what I'm doing wrong or lead me in the right answer. Below, I will show the code and then explain what I'm attempting to do.
Here is my Vue.js app:
Vue.component('o365_apps_notifications', {
template:
`
<div class="notification is-success is-light">
// Call the name here and if added/removed.
</div>
`,
});
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
components: 'o365_apps_notifications',
data() {
return {
list: {},
movable: true,
editable: true,
isDragging: false,
delayedDragging: false,
options: {
group: 'o365apps',
disabled: true,
handle: '.o365_app_handle',
}
}
},
methods: {
add(index, obj) {
console.log(obj.name);
this.$data.list.selected.push(...this.$data.list.available.splice(index, 1));
this.changed();
},
remove(index, obj) {
console.log(obj.name);
this.$data.list.available.push(...this.$data.list.selected.splice(index, 1));
this.changed();
},
checkMove(evt) {
console.log(evt.draggedContext.element.name);
},
},
});
Here is my modal:
<div id="o365-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Applications</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="o365-modal-edit-wrapper">
<div class="columns">
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
<div class="column is-half-desktop is-full-mobile buttons">
// Empty
</div>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<o365-apps-notifications></o365-apps-notifications>
</footer>
</div>
</div>
Here is what I'm attempting to do:
Inside my modal, I have my o365_apps_notifications html tag called, my add() and remove() methods output a name on each add/remove using console.log(obj.name); and my checkMove method also drags the same name on drag as shown below:
How could I get my component to render and output the name inside the modal footer? I've tried all methods, but I can't seem to figure out how to trigger the component.
Also, would I have to do something special to make the component fade out after a set timeframe?
All help is appreciated!
A couple issues:
You've declared the notification component with underscores (o365_apps_notifications), but used hyphens in the modal's template. They should be consistent (the convention is hyphens).
The notification component is declared globally (with Vue.component), but it looks like you're trying to add it to the modal's components, which is intended for local components. Only one registration is needed (the global component registration should do).
<o365-apps-notifications>
The notification component should have public props that take the item name and state:
Vue.component('o365-apps-notifications', {
props: {
item: String,
isAdded: Boolean
},
})
Then, its template could use data binding to display these props.
Vue.component('o365-apps-notifications', {
template:
`<div>
{{ item }} {{ isAdded ? 'added' : 'removed '}}
</div>`
})
For the fade transition, we want to conditionally render this data based on a local Boolean data property (e.g., named show):
Vue.component('o365-apps-notifications', {
template:
`<div v-if="show">
...
</div>`,
data() {
return {
show: false
}
}
})
...and add the <transition> element along with CSS to style the fade:
Vue.component('o365-apps-notifications', {
template:
`<transition name="fade">
<div v-if="show">
...
</div>
</transition>`,
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
To automatically fade out the data, add a watch on item, which sets show=true and then show=false after a delay:
Vue.component('o365-apps-notifications', {
watch: {
item(item) {
if (!item) {
return;
}
this.show = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.show = false, 1000);
}
}
})
Usage
In the modal component, declare local data properties that hold the currently added/removed item:
new Vue({
el: '#o365-modal-edit',
data() {
return {
changedItem: null,
changedItemIsAdded: false,
}
},
})
Also update add() and remove() to set these properties:
new Vue({
methods: {
add(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = true;
},
remove(index, obj) {
this.changedItem = obj.name;
this.changedItemIsAdded = false;
},
},
})
Then in the modal component's template, bind these properties to the notification component's props:
<o365-apps-notifications :item="changedItem" :is-added="changedItemIsAdded"></o365-apps-notifications>
demo

Vue.js: How to rerun your code whenever you change slides in your vue carousel

I am very new to vue.js and fumbling my way though it, forgive me if my terms are incorrect. I am creating a touchscreen application that needs to be ADA compliant (only the bottom part of the screen is accessible, so i have to use buttons for interaction).
I have a parent component with a carousel creating an array of slides, pulling data from my child component.
parent component HTML
<carousel :navigateTo="selectedListIndex" #pageChange="OnPageChange">
<slide v-for="(member, index) in selectedList" :key="index">
<MemberBioPage :member="member"/>
</slide>
</carousel>
parent component SCRIPT:
export default {
data () {
return {
currentPage: 0
}
},
components: {
MemberBioPage,
Carousel,
Slide
},
computed: {
selectedList () {
return this.$store.state.selectedList
},
selectedListIndex () {
return this.$store.state.selectedListIndex
}
},
methods: {
OnPageChange (newPageIndex) {
console.log(newPageIndex)
this.currentPage = newPageIndex
}
}
}
within my child component, i have bio copy being pulled from my data and arrow buttons that allow you to scroll the text. There is an outer container and an inner container to allow the scrolling and based on the height that the content takes up in the container will determine when the arrows disable or not.
child component HTML:
<div class="member-bio-page">
<div class="bio">
<div class="portrait-image">
<img :src="member.imgSrc" />
</div>
<div class="bio-container">
<div class="inner-scroll" v-bind:style="{top: scrollVar + 'px'}">
<h1>{{ member.name }}</h1>
<div class="description-container">
<div class="para">
<p v-html="member.shortBio"></p>
</div>
</div>
</div>
</div>
<div class="scroll-buttons">
<div>
<!-- set the class of active is the scroll variable is less than 0-->
<img class="btn-scroll" v-bind:class="{ 'active': scrollVar < 0 }" #click="scrollUp" src="#/assets/arrow-up.png">
</div>
<div>
<!-- set the class of active is the scroll variable is greater than the height of the scrollable inner container-->
<img class="btn-scroll" v-bind:class="{ 'active': scrollVar > newHeight }" #click="scrollDown" src="#/assets/arrow-down.png">
</div>
</div>
</div>
</div>
child component SCRIPT:
<script>
export default {
props: [
'member', 'currentPage'
],
data () {
return {
scrollVar: 0,
outerHeight: 0,
innerHeight: 0,
newHeight: -10
}
},
mounted () {
this.outerHeight = document.getElementsByClassName('bio-container')[0].clientHeight
this.innerHeight = document.getElementsByClassName('inner-scroll')[0].clientHeight
this.newHeight = this.outerHeight - this.innerHeight
return this.newHeight
},
methods: {
scrollUp () {
console.log(this.scrollVar)
this.scrollVar += 40
},
scrollDown () {
console.log(this.scrollVar)
this.scrollVar -= 40
},
showVideo () {
this.$emit('showContent')
}
}
}
</script>
I am able to get the height of the first bio i look at, but on page change it keeps that set height. I basically want the code in mounted to be able to rerun based on the index of the slide i am on. I need 'newHeight' to update on each page change. I tried grabbing the 'currentPage' from my parent component using props, but it pulls undefined.
here is all a snippet from my data to show you what data i currently have:
{
index: 12,
name: 'Name of Person',
carouselImage: require('#/assets/carousel-images/image.jpg'),
imgSrc: require('#/assets/bio-page-image-placeholder.jpg'),
shortBio: '<p>a bunch of text being pulled</p>',
pin: require('#/assets/image-of-pin.png')
}
this is also my store just in case
const store = new Vuex.Store({
state: {
foundersList: founders,
chairmanList: chairmans,
selectedList: founders,
selectedListIndex: -1
},
mutations: {
setSelectedState (state, list) {
state.selectedList = list
},
setSelectedListIndex (state, idx) {
state.selectedListIndex = idx
}
}
})
Alright, so this is a good start. Here's a few things I would try:
Move the code you currently have in mounted to a new method called calculateHeight or something similar.
Call the method from your scrollUp and scrollDown methods.
So your final code would look something like this:
export default {
props: [
'member', 'currentPage'
],
data () {
return {
scrollVar: 0,
outerHeight: 0,
innerHeight: 0,
newHeight: -10
}
},
mounted () {
this.calculateHeight();
},
methods: {
calculateHeight() {
this.outerHeight = document.getElementsByClassName('bio-container')[0].clientHeight
this.innerHeight = document.getElementsByClassName('inner-scroll')[0].clientHeight
this.newHeight = this.outerHeight - this.innerHeight
},
scrollUp () {
console.log(this.scrollVar)
this.scrollVar += 40
this.calculateHeight()
},
scrollDown () {
console.log(this.scrollVar)
this.scrollVar -= 40
this.calculateHeight()
},
showVideo () {
this.$emit('showContent')
}
}
}

Update array items asynchronously - watches not firing

I have created a component that displays blog article previews. This component has pagination and upon selecting a new page I refresh the array of article previews. The list of articles is fetched from a JSON api from server1. The response contains information to fetch each article from server 2. Then I fire x asynchronous fetches to server 2, as many as items in the first response. In those responses I update the items in the array.
I am new to vue but after some struggling I got this to work. Now I'm trying to add a spinner in the article previews while the separate articles are loading. My idea was to watch in the previewcomponent for an article update and show the spinner depending on that. Unfortunately it doesn't work and now I'm starting to doubt my implementation. I notice that the watch in the preview is not called for every previewcomponent but still every preview is updated and shown correctly. I assume this is because of the messaging system but I don't manage to fix it.
My question is twofold:
Is my implementation a correct way of handling this problem? To get this to work I nicely I need to 'erase' the array because otherwise new articles were 'overwriting' old ones and this was visible.
How can I handle the spinners. Why are the watches not triggered and how can I fix this? In the code below I have some console writes. I see 10 times 'async' and each time a different amount of 'watch', never 10.
The complete code is on github here: Home and ArticlePreview. These are the most relevant parts:
Home:
<template>
<div class="container article">
<div class="row" v-for="(article, index) in articles" :key="index">
<ArticlePreview v-bind:blogEntry="article"></ArticlePreview>
</div>
<b-pagination-nav :use-router="true" :link-gen="generateLink" align="center" :number-of-pages="nofPages" v-model="pageIndex" />
</div>
</template>
data: function ()
{
return {
articles: <BlogEntry[]> [],
nofPages: 1
}
},
loadContent()
{
fetch("./api/v1/articles.php?page=" + this.pageIndex)
.then(response => response.json())
.then((data) =>
{
this.nofPages = Math.ceil(data.nofItems/10);
this.articles.splice(0);
this.articles.splice(data.data.length);
let index :number;
for(index = 0; index < data.data.length; index++)
{
createArticleAsync(data.data[index].name, data.data[index].permlink).then(function(this: any, index: number, article: BlogEntry)
{
console.log('async');
Vue.set(this.articles, index, article);
}.bind(this, index));
}
})
},
ArticlePreview:
<template>
<div class="container-fluid">
<div class="row" v-if="blogEntry">
<template v-if="blogEntry">
<div class="imageframe col-md-3">
<div class="blog-image">
<img :src="blogEntry.previewImage" style="border-radius: 5px;">
</div>
</div>
<div class="col-md-9">
<h5 class="font-weight-bold" style="margin-top:5px;"><router-link :to="{ name: 'Article', params: {author: blogEntry.author, permlink: blogEntry.permlink } }">{{blogEntry.title}}</router-link></h5>
<div class="multiline-ellipsis">
<p>{{blogEntry.previewBody}}</p>
</div>
<span class="metadata"><i>by <a :href="AuthorBlogLink">{{blogEntry.author}}</a> on {{blogEntry.created | formatDate}}</i></span>
</div>
</template>
<template v-else>
<p>Loading</p>
</template>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import VueRouter from 'vue-router';
import {formatDate} from "../utils/utils";
export default Vue.extend({
props: [
'blogEntry'
],
data: function ()
{
return {
loading: true
}
},
watch:
{
blogEntry(newValue)
{
console.log('watch');
if(newValue)
this.loading = false;
else
this.loading = true;
}
}
});
</script>
I think the method of getting the detailed data of the article should be encapsulated inside the component, and the loading state is also maintained internally.just like the code below:(It doesn't work properly because Mockjs cannot execute correctly in snippet)
Mock.setup({timeout: 2000})
const URL_ARTICLE_LIST = '/api/v1/articles.php'
const URL_ARTICLE_DETAIL = '/api/v1/article_detail.php'
Mock.mock(/\/api\/v1\/articles\.php.*/,function(options){
return {
nofItems: 33,
data: Mock.mock({
'list|10': [{
'title': '#title',
'url': URL_ARTICLE_DETAIL
}]
}).list
}
})
Mock.mock(URL_ARTICLE_DETAIL,function(options){
return Mock.mock({
content: '#paragraph'
})
})
Vue.component('article-card',{
template: `
<div>
<template v-if="!loading">
<div class="article-title">{{articleTitle}}</div>
<div class="article-content">{{article.content}}</div>
</template>
<template v-else>
<div>loading...</div>
</template>
</div>`,
data () {
return {
loading: false,
article: {}
}
},
props: {
articleTitle: {
required: true,
type: String
},
articleUrl: {
required: true,
type: String
}
},
watch: {
articleUrl (url,oldUrl) {
if(url && url!=oldUrl){
this.loadContent()
}
}
},
methods: {
loadContent () {
this.loading = true;
//or your own async functions
axios.get(this.articleUrl).then(res=>{
this.article = res.data
this.loading = false;
})
}
},
created () {
this.loadContent()
}
});
var app = new Vue({
el: '#app',
data () {
return {
articles: [],
nofPages: 1,
page: 1 //you should get page param from vue-router just like this.$route.query.page
}
},
created () {
//you can also use fetch here
axios.get(URL_ARTICLE_LIST+'?page='+this.page).then(res=>{
console.log(res.data)
this.nofPages = Math.ceil(res.data.nofItems/10);
this.articles = res.data.data
})
}
})
ul,li{
list-style: none;
}
.article_list{
display: flex;
flex-wrap: wrap;
}
.article_list>li{
width: 300px;
background: skyblue;
color: white;
margin: 10px;
}
.article-content{
text-indent: 2em;
}
.pagination-wrapper>li{
display:inline-block;
padding: 5px;
border: 1px solid skyblue;
margin: 3px;
}
.pagination-wrapper>li.active{
background: skyblue;
color: #fff;
}
<script src="https://cdn.bootcss.com/Mock.js/1.0.1-beta3/mock-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="app">
<ul class="article_list">
<li v-for="article of articles">
<article-card :article-title="article.title" :article-url="article.url"></article-card>
</li>
</ul>
<ul class="pagination-wrapper">
<li v-for="x in nofPages" :class="{active: page==x}">{{x}}</li>
</ul>
</div>

Categories

Resources