Vue.js can't find element using querySelector - javascript

I am trying to create a chat style form. So a user inputs their data and then uses the button within my template with the class of continue-btn.
As you can see when the continue-btn is pressed it uses the nextStep method which adds 1 to the counter data property.
Within my template I then use v-if="counter >= 1" to display the next section of the chat dialog and input field.
I am then trying to use scrollTop to automatically scroll the page to the new section with the id of #conversation__tram-1. I originally tried running this block of code just after the counter had been given a value of 1:
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
This didn't work though because I'm guessing the #conversation__tram-1 element hadn't been added to the DOM yet.
So for the sake of testing I tried wrapping it in a timeout function:
setTimeout(function(){
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
However I am left with this error when trying this:
Uncaught TypeError: Cannot read property 'querySelector' of undefined
Here is my whole single vue file:
<template>
<div id="conversation-app">
<!-- <div v-for="item in items">
{{ item.text }}
</div> -->
<div class="conversation__track">
<div id="conversation__tram-0">
<div class="conversation__item agent">
<img src="/assets/cdn.annuityadvicecentre.dev/images/theme-f/michael-chat-agent.jpg" class="conversation__item-prof-img" alt="Michael Chat Agent" />
<div class="conversation__item-content">
<p>
Hello my name is {{ agent }}, we'll compare the whole annuity market to bring you back the best annuity rates from the top providers for you. Let's get started, what's your name?
</p>
</div>
</div>
<div class="conversation__item customer" id="title-fullname">
<div class="conversation__item-content">
<p>
Hi {{ agent }}, my name is...
</p>
<div class="row">
<div class="col-4">
<select id="title" class="field-title" name="payload[title]"><option value="mr">Mr</option><option value="mrs">Mrs</option><option value="miss">Miss</option><option value="ms">Ms</option></select>
</div>
<div class="col-8">
<input v-model="customerName" id="full_name" class="field-full_name" name="payload[full_name]" type="text">
</div>
</div>
</div>
</div>
</div>
<transition name="fade">
<div id="conversation__tram-1" v-if="counter >= 1">
<div class="conversation__item agent">
<img src="/assets/cdn.annuityadvicecentre.dev/images/theme-f/michael-chat-agent.jpg" class="conversation__item-prof-img" alt="Michael Chat Agent" />
<div class="conversation__item-content">
<p>
Thanks {{ firstName }}, nice to meet you. To process your instant quote please can I have your Pension Value?
</p>
</div>
</div>
<div class="conversation__item customer">
<div class="conversation__item-content">
<p>
Sure, my pension value is...
</p>
<input id="pension_value" class="field-pension_value" placeholder="£" pattern="\d*" name="payload[pension_value]" type="number">
<div class="error-wrap error_pension_value is-hidden" data-format="<div class="error-text">:message</div>"></div>
</div>
</div>
</div>
</transition>
<div id="conversation__buttons">
<button type="button" class="continue-btn"
v-on:click="nextStep"
>Continue <i class="fa fa-chevron-right" aria-hidden="true"></i></button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'conversation-app',
data () {
return {
agent: 'Brick',
counter: 0,
customerName: '',
}
},
methods: {
nextStep: function() {
this.counter += 1;
setTimeout(function(){
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
},
},
computed: {
firstName() {
return this.customerName.split(' ')[0];
}
}
}
</script>
Any idea why this isn't working? Thanks.

This is a good time to use arrow functions, as they preserve the context of this.
nextStep: function() {
this.counter += 1;
setTimeout(() => {
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
Altenatively, instead of the timeout you can use Vue.nextTick which is a more technically-correct way of doing this.
nextStep: function () {
this.counter += 1
this.$nextTick(() => { ... })

Related

Conditionally hide the nth element of a v-for loop without modifying the array. vue 3 composition api search function

I have a ref variable (foxArticles ), which holds a list that contains 100 items. In a v-for loop i loop over each value. As a result, i have 100 values rendered on the page.
<template>
<div class="news_container">
<div
v-for="article in foxArticles"
v-bind:key="article"
class="article_single_cell"
>
<div
class="news_box shadow hover:bg-red-100 "
v-if="containsKeyword(article, keywordInput)"
>
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const foxArticles = ref([]);
</script>
I also have a search function, which returns the value, if it includes the passed in keyword. The function is used in the child of the v-for loop.
<div class="search_input_container">
<input
type="text"
class="search_input"
v-model="keywordInput"
/>
</div>
<script>
const keywordInput = ref("");
function containsKeyword(article, keywordInput) {
if (article.title.toLowerCase().includes(keywordInput.toLowerCase())) {
return article;
}
}
</script>
The problem is, i can't use .slice() on the foxArticles array in the v-for loop, because that screws up the search functionality, as it returns only the values from the sliced range.
How can i have the access the all of the values of the array, while not rendering all 100 of returned articles on the initial load?
Any suggestions?
I think your approach will make it incredibly complex to achieve. It would be simpler to always iterate over some set, this set is either filtered based on a search-term, or it will be the first 100 items.
I'm not very familiar yet with the Vue 3 composition api so I'll demonstrate with a regular (vue 2) component.
<template>
<div class="news_container">
<div
v-for="article in matchingArticles"
v-bind:key="article"
class="article_single_cell"
>
... news_box ...
</div>
</div>
</template>
<script>
export default {
...
computed: {
matchingArticles() {
var articles = this.foxArticles;
if (this.keywordInput) {
articles = articles.filter(article => {
return this.containsKeyword(article, this.keywordInput)
})
} else {
// we will limit the result to 100
articles = articles.slice(0, 100);
}
// you may want to always limit results to 100
// but i'll leave that up to you.
return articles;
}
},
....
}
</script>
Another benefit is that the template does not need to worry about filtering results.
ok, so i kind of came up with another solution, for which you don't have to change the script part...
instead of having one v-for loop , you can make two of them, where each one is wrapped in a v-if statement div
The first v-if statement says, If the client has not used the search (keywordInput == ''), display articles in the range of (index, index)
The second one says = If the user has written something (keywordInput != ''), return those articles.
<template>
<div class="news_container">
<!-- if no search has been done -->
<div v-if="keywordInput == ''">
<div
v-for="article in foxArticles.slice(0, 4)"
v-bind:key="article"
class="article_single_cell"
>
<div class="news_box shadow hover:bg-red-100 ">
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
<!-- if searched something -->
<div v-else-if="keywordInput != ''">
<div
v-for="article in foxArticles"
v-bind:key="article"
class="article_single_cell"
>
<div
class="news_box shadow hover:bg-red-100 "
v-if="containsKeyword(article, keywordInput) && keywordInput != ''"
>
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
im not sure how this impacts performance tho, but that's a problem for another day

Vue js: load more data button not working properly

In my Vue.js code below I'm trying to add a Show More button to my data coming from API so initially it should show 10 data and whenever clicked load more 10 and so on. I tried answer from:
Load more button in vuejs
but it's not working since I'm looping over an array it gives me the error below can't read property of question title. Is there a way to do it?
<div class="search-askbutton">
<b-row>
<div class="search-wrapper">
<input
type="text"
v-model="search"
placeholder="Search something..."
class="fas fa-search"
/>
</div>
<div class="container vue">
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul
class="container-question"
v-for="(question, index) in filteredList"
:key="index"
>
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>
<button #click="commentsToShow += 10">show more</button>
</div>
<script>
export default {
data() {
return { commentsToShow: 10,
search: '',
questions: [],}
},
computed: {
filteredList() {
return this.questions.filter((question) => {
return (
question.questionTitle
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.owner.username
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.questionTitle
.toUpperCase()
.includes(this.search.toUpperCase()) ||
question.owner.username
.toUpperCase()
.includes(this.search.toUpperCase())
);
});
},
},
mounted: function() {
questionService.getAllQuestions().then((response) => {
this.questions = response.data.response;}
}
</script>
The problem is your subtraction
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
If commentIndex = 0 then you'll be saying 0-1 = -1 therefore it will not find the -1 index.
You could replace this line
<div v-if="commentIndex <= commentsToShow">
So that it can run only if the index is greater than 0
<div v-if="commentIndex > 0">
1)
v-for returns what's inside an array, not the array itself.
<div>{{question.questionTitle}} says:</div>
2)
also, you can remove the v-for loop.
note:- your reference question is also uses this way.
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul class="container-question">
<div>{{filteredList[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>

Vue : Accessing Nested Object Component's Values

I have problems accessing this "name" property on the component. I can only access it statically.
<template>
<div class="col-md-12">
<p
v-for="channel in channels"
:key="channel.id"
class="channel"
:class="{ 'active': channel.id == activeChannel }"
#click="setChannel(channel.id)">
{{ channel.users[0].name }}
</p>
</div>
</template>
Here is an Image of my Vue Devtools
So I have an v-for loop over channels, and I want to: Access the Usernames for each channel (if it is not my own preferably as "username" is set on my own i think its easy to exclude it right?) So that in the end In Channel 1 when there are 2 Users , I want to show the corresponding username, so the "other username", the one i am chatting with, and he should see my name that is the initial goal.
I thought of doing something like this:
<template>
<div class="col-md-12">
<p
v-for="channel in channels"
:key="channel.id"
class="channel"
:class="{ 'active': channel.id == activeChannel }"
#click="setChannel(channel.id)">
<!-- {{ channel.users[0].name }} -->
<span v-for="user,key in channel">{{key}}</span>
</p>
</div>
it at least displays the content of the channels object for each channel, but something like this isnt gonna work: key.user.name , unfortunately im stuck here. please help :)
edit: here is a dd() of the view
click
EDIT 2: Parent Data Provided:
//chat-app.blade.php
<div id="app">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Chats</div>
<vue-chat :channels="{{ $channels }}" ></vue-chat>
</div>
</div>
</div>
</div>
</div>
</div>
//<vue-chat> component
<template>
<div class="chat">
<div class="container">
<div class="row">
<div class="col-md-3">
<vue-chat-channels
:channels="channels"
:active-channel="activeChannel"
#channelChanged="onChannelChanged"
:username="sername"
></vue-chat-channels>
</div>
<div class="col-md-3">
<vue-chat-messages :messages="messages"></vue-chat-messages>
</div>
<div class="col-md-3">participants</div>
</div>
<div class="message-input-wrapper col-md-12"><vue-chat-new-message :active-channel="activeChannel"
:username="username"></vue-chat-new-message></div>
</div>
</div>
</template>
<script>
export default {
props: ["channels"],
data() {
return {
activeChannel: this.channels[0].id,
messages: [],
username: ''
};
},
methods: {
fetchMessages() {
let endpoint = `/channels/${this.activeChannel}/messages`;
axios.get(endpoint).then(({ data }) => {
this.messages = data;
});
},
onChannelChanged(id) {
this.activeChannel = id;
this.fetchMessages();
}
},
created() {
this.fetchMessages();
axios.get('/userfetch').then( ({data}) => {
console.log("Current User: "+data.name);
this.username = data.name;
});
console.log(this.channels[0].name);
// for (let channel of this.channels) {
this.channels.forEach(channel => {
// Channelname
window.Echo.channel('presence-'+channel.name)
.listen('MessageSent', (channel) => {
console.log(channel.data.message);
this.messages.push({ message: channel.data.message, author_username: channel.data.author_username});
if (this.activeChannel == channel.id) {
console.log("received message");
}
});
});
}
};
</script>
<style>
</style>
//ChatController.php
public function index()
{
$channels = Channel::with('users')->whereHas('users', function($q) {
$q->where('user_id',Auth::id());
})->get();
$user = Auth::user()->name;
return view('chat-app' , compact('channels','user'));
}
Short Explanation: ChatController returns the blade view, which has the data channels and user (my username) , and then vue comes into play which should pass down the prop of my username but i couldnt get it to work just yet
So you need to access users in every channel.
You can try like this:
<div class="col-md-12">
<p
v-for="channel in channels"
:key="channel.id"
class="channel"
:class="{ 'active': channel.id == activeChannel }"
#click="setChannel(channel.id)">
<span v-for="user in channel.users">
{{ user.name }}
</span>
</p>
</div>
This should work. If you have errors provide it here.
If you need to compare every user you can do it simply with v-if:
<span v-for="user in channel.users">
<span v-if="user.name === parentdata">
{{ user.name }}
</span>
</span>

Reset response to null after search input is cleared Vue.js

How do I reset a my response to NULL, as it is in my data upon the clearing/deleting of the query in my search bar?
I've vaguely achieved this with v-show and a query length, but I know its not really correct because it's hiding the results, not actually clearing them from the DOM. I also tried tying an ELSE statement to the query method with no luck.
<div class="searchBarContainer">
<div class="search">
<div class="searchBar">
<form v-on:submit="queryGitHub(query)">
<input type="search" placeholder="Search Repositories Ex. Hello
World" v-model="query" />
<button type="submit" v-on:click="isHidden =
!isHidden">Search</button>
</form>
</div>
<div class="results" id="results" v-if="response" v-show="query.length =
0">
<div class="notFound" v-if="response.length == 0">
<p>Sorry buddy, try another search!</p>
</div>
<div class="resultsHeadings" v-if="response.length >= 1">
<p>Name</p>
<p>Language</p>
</div>
<div class="items" v-if="response.length >= 1">
<div class="item" v-for="(item, index) in response, filteredList"
v-bind:id="item.id" :key="index">
<p>{{item.name}}</p>
<p>{{item.language}}</p>
<div class="expand">
<a #click="pushItem(index)">
<div class="itemButton">
<button v-on:click="addFave(item.id, item.forks)">Add to
Favorites</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
export default{
data () {
return {
query:'',
response: null,
items: [],
faves: [],
activeItems: [],
}
},
methods: {
queryGitHub(q) {
if (q.length >= 1){
fetch('https://api.github.com/search/repositories?q=' + q)
.then((j) => {
return j.json();
})
.then ((r) => {
console.log(r);
//this.response = r.items;
this.response = r.items.slice(0, 15)
})
}
}
}
};
I need my search input to remove the response by resetting it to NULL once the input has been cleared by the visitor. Presently if you clear the input, the results disappear which is great but if you type again, the results just reappear. So they are hidden, not removed. I believe I need a function, possibly via computed, to set the response in data back to null upon the clearing of the input.
You could attach an input event handler to your input element and inside it you'll check the length of the query string. If it's zero, then set response to null.
<input type="search" placeholder="Search Repositories Ex. Hello
World" v-model="query" #input="onQueryChange" />
The onQueryChange function should be under methods instead of computed since it's not returning any derived data.
methods: {
onQueryChange(event) {
// can be this.query.length === 0 as well
if(event.target.value.trim().length === 0) {
this.response = null;
}
}
}

How do I adjust marquee based on global variable? - Meteor

I'm developing an app using Meteor Framework.
One of the features I am looking to implement is having a marquee text (like a scrolling bottom text).
I have added the package meteor-jquery-marquee and it works great with a single string. But whenever I try to modify the string, nothing happens, and it stays the same.
It's worth mentioning that I did try sessions, and it changes the text, however, the marquee animation stops, which defeats the purpose.
I have been stuck for hours trying to get it to work, some help would really save my butt here.
I've initialized the global variable in the client/main.js as
globalMessage = "Welcome to my proJECT";
And it scrolls with the marquee just fine.
Thank you in advance!
My code:
My body template
<template name="App_Body">
{{> Header}}
{{>Template.dynamic template=main}}
{{> Footer}}
<div style="color: white;" class="ui center aligned container">
<div class='marquee'>{{globalMessage}}</div>
</div>
</template>
body.js
Template.App_Body.helpers({
globalMessage () {
return globalMessage;
},
});
where I'm trying to edit the marquee:
<template name="dailyMessageControl">
<div class="container">
<br>
<br>
<div class="info pull-right"> <!-- column div -->
<div class="panel panel-default">
<div class="panel-heading clearfix">
<h1 class="panel-title text-center panel-relative"> Modify Daily Message</h1>
</div>
<div class="list-group">
<div class="list-group-item">
<p style="font-size: 30px;">Current Message: <br>{{globalMessage}}</p>
</div>
<div class="panel-footer">
<form>
<div class="form-group">
<label for="exampleInputEmail1">Enter new messages</label>
<input type="text" name="newMsg" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="New Message">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div><!-- end column div -->
</div>
</template>
the .js
Template.dailyMessageControl.helpers({
globalMessage () {
return globalMessage;
},
});
Template.dailyMessageControl.events({
'submit form': function(){
event.preventDefault();
var newMsg = event.target.newMsg.value;
globalMessage = newMsg;
}
});
Your code clearly lacks reactivity, let's fix that.
Fist, initialize globalMessage as ReactiveVar instance (client/main.js):
globalMessage = new ReactiveVar('Welcome to my proJECT');
Next, code to react to its value change (body.js):
Remove globalMessage() helper
Add code that will track globalMessage variable and re-create $.marquee:
Template.App_Body.onRendered(function appBodyOnRendered() {
this.autorun(() => {
const value = globalMessage.get();
const $marquee = this.$('.marquee');
$marquee.marquee('destroy');
$marquee.html(value);
$marquee.marquee(); // add your marquee init options here
});
});
And, lastly, update code in dailyMessageControl template to work with ReactiveVar instance:
Template.dailyMessageControl.helpers({
globalMessage () {
return globalMessage.get(); // changed line
},
});
Template.dailyMessageControl.events({
'submit form': function(){
event.preventDefault();
var newMsg = event.target.newMsg.value;
globalMessage.set(newMsg); // changed line
}
});

Categories

Resources