Navigating through Vue.Js survey - javascript

I'm using Vue.Js for a survey, which is basically the main part and the purpose of the app. I have problem with the navigation. My prev button doesn't work and next keeps going in circles instead of only going forward to the next question. What I'm trying to accomplish is just to have only one question visible at a time and navigate through them in correct order using next and prev buttons and store the values of each input which I'll later use to calculate the output that will be on the result page, after the survey has been concluded. I've uploaded on fiddle a short sample of my code with only two questions just to showcase the problem. https://jsfiddle.net/cgrwe0u8/
new Vue({
el: '#quizz',
data: {
question1: 'How old are you?',
question2: 'How many times do you workout per week?',
show: true,
answer13: null,
answer10: null
}
})
document.querySelector('#answer13').getAttribute('value');
document.querySelector('#answer10').getAttribute('value');
HTML
<script src="https://unpkg.com/vue"></script>
<div id="quizz" class="question">
<h2 v-if=show>{{ question1 }}</h2>
<input v-if=show type="number" v-model="answer13">
<h2 v-if="!show">{{ question2 }}</h2>
<input v-if="!show" type="number" v-model="answer10">
<br>
<div class='button' id='next'>Next</div>
<div class='button' id='prev'>Prev
</div>
</div>
Thanks in advance!

You should look at making a Vue component that is for a survey question that way you can easily create multiple different questions.
Vue.component('survey-question', {
template: `<div><h2>{{question.text}}</h2><input type="number" v-model="question.answer" /></div>`,
props: ['question']
});
I've updated your code and implemented the next functionality so that you can try and create the prev functionality. Of course you should clean this up a little more. Maybe add a property on the question object so it can set what type the input should be. Stuff like that to make it more re-useable.
https://jsfiddle.net/9rsuwxvL/2/

If you ever have more than 1 of something, try to use an array, and process it with a loop. In this case you don't need a loop, but it's something to remember.
Since you only need to render one question at a time, just use a computed property to find the current question, based on some index. This index will be increased/decreased by the next/previous buttons.
With the code in this format, if you need to add a question, all you have to do is add it to the array.
https://jsfiddle.net/cgrwe0u8/1/
new Vue({
el: '#quizz',
data: {
questions:[
{question:'How old are you?', answer: ''},
{question:'How many times do you workout per week?', answer: ''},
],
index:0
},
computed:{
currentQuestion(){
return this.questions[this.index]
}
},
methods:{
next(){
if(this.index + 1 == this.questions.length)
this.index = 0;
else
this.index++;
},
previous(){
if(this.index - 1 < 0)
this.index = this.questions.length - 1;
else
this.index--;
}
}
})

Related

Make searched characters/words bold in results VueJS

I am currently creating a search box in VueJS.
Now my question is how to make letters that are searched bold in the results.
This is the part of code I have now:
<div v-for="result in searchResults.merk" :key="result.uid">
<inertia-link :href="result.url">
<strong v-if="result.name.toLowerCase().includes(searchInput.toLowerCase())">{{result.name}}</strong>
<span v-else>{{result.name}}</span>
</inertia-link>
</div>
This sort of does what I want, but not really, now it makes the whole word bold if it contains these letters. I would like to only give those specific letters bold, and the rest of the word normal styling.
See example below. It contains 2 solutions
Very simple but with the BIG warning as it is using v-html. v-html should not be used with user input for safety reasons (see the link). Luckily in this case if user enters some html into search box, it will be rendered only if it is also contained in searched text. So if searched text (source in example) is safe (produced by trusted source), it is perfectly save
Second solution is little bit involved - simply split the text into multiple segments, mark segments which should be highlighted and then render it using v-for and v-if. Advantage of this solution is you can also render Vue components (for example Chips) and use other Vue features (wanna bind a click handler on highlighted text?) which is not possible with v-html solution above...
const vm = new Vue({
el: '#app',
data() {
return {
source: 'foo bar baz baba',
search: 'ba',
}
},
computed: {
formatedHTML() {
const regexp = new RegExp(this.search, "ig")
const highlights = this.source.replace(regexp, '<strong>$&</strong>')
return `<span>${highlights}</span>`
},
highlights() {
const results = []
if (this.search && this.search.length > 0) {
const regexp = new RegExp(this.search, "ig")
let start = 0
for (let match of this.source.matchAll(regexp)) {
results.push({
text: this.source.substring(start, match.index),
match: false
})
start = match.index
results.push({
text: this.source.substr(start, this.search.length),
match: true
})
start += this.search.length
}
if(start < this.source.length)
results.push({ text: this.source.substring(start), match: false})
}
if (results.length === 0) {
results.push({
text: this.source,
match: false
})
}
return results
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="search" />
<div>As HTML:
<span v-html="formatedHTML"></span>
</div>
<div>Safe:
<span>
<template v-for="result in highlights">
<template v-if="result.match"><strong>{{result.text}}</strong></template>
<template v-else>{{result.text}}</template>
</template>
</span>
</div>
</div>
You can try splitting the word into a collection of in-line spans.
So you'd first iterate over the results. Then you'd iterate over each letter of the result.name, putting each individual letter in its own span.
Lastly, you'd toggle a bold class on the spam using the object class syntax.
Consider the following example:
<span
v-for="letter in result.name.toLowerCase()"
:key="letter"
:class={ isBold: searchInput.toLowerCase().includes(letter) }
>
{{letter}}
</span>
The object syntax for classes will toggle the class on or off if the condition to the right of the class name is true.
This isn't an ideal solution as it contains a reasonable amount of computational complexity (2x lowercase, and searching the search string for each letter). However, the use case is very simple so modern devices shouldn't struggle.
If you find it worth optimising, you can debounce the input and generate a map of the different letters lowercase search letters then search that map as map access is an O(1) operation.

Two arrays cloned with Lodash, separate but the same. Why?

I know for a fact there is something pretty obvious here that I am completely missing, so your help is greatly appreciated.
I have a feature that provides two dropdowns. They contain the same data (the feature allows a trade between two people, the people is the data), but I want them each to get their own copy of said data.
Another part of this feature is that by picking Person A in the first dropdown, I want to disable Person A in the second dropdown, and vice versa, so I have the ng-options tag paying attention to a disabled property on the object.
The issue I have is that even with using a method such as Lodash's clone to properly create a "new" array upon first time assignment, every time I access Person A in ONE array (and specifically do NOT access the other array) invariably I am seeing that when I touch Person A, that object is updated in BOTH arrays, which has me flustered.
This feels like a down-to-the-metal, barebones Javascript issue (standard PEBCAK, I feel like I'm clearly misunderstanding or straight up missing something fundamental), maybe with a bit of AngularJS rendering-related fun-ness involved, but... What gives?
angular.module('myApp', [])
.controller('weirdDataController', function($scope) {
$scope.$watch('manager1_id', () => {
if (angular.isDefined($scope.manager1_id) && parseInt($scope.manager1_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(false, $scope.manager1_id);
}
});
$scope.$watch('manager2_id', () => {
if (angular.isDefined($scope.manager2_id) && parseInt($scope.manager2_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(true, $scope.manager2_id);
}
});
$scope._gimmeFakeData = () => {
return [{
manager_id: 1,
manager_name: 'Bill',
disabled: false
},
{
manager_id: 2,
manager_name: 'Bob',
disabled: false
},
{
manager_id: 3,
manager_name: 'Beano',
disabled: false
},
{
manager_id: 4,
manager_name: 'Barf',
disabled: false
},
{
manager_id: 5,
manager_name: 'Biff',
disabled: false
},
];
};
const data = $scope._gimmeFakeData();
$scope.firstManagers = _.clone(data);
$scope.secondManagers = _.clone(data);
$scope._disableManagerInOtherDropdown = (otherIsFirstArray, managerId) => {
const disableManagers = manager => {
manager.disabled = manager.manager_id === managerId;
};
if (otherIsFirstArray) {
$scope.firstManagers.forEach(disableManagers);
} else {
$scope.secondManagers.forEach(disableManagers);
}
console.log('Is the first item the same??', $scope.firstManagers[0].disabled === $scope.secondManagers[0].disabled);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="myApp" ng-controller="weirdDataController">
<div class="col-xs-12 col-sm-6">
<select class="form-control" ng-model="manager1_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in firstManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose one manager</option>
</select>
<select class="form-control" ng-model="manager2_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in secondManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose another manager</option>
</select>
</div>
</div>
<br /><br />
I threw everything relevant on $scope just for the sake of getting it working and illustrating the issue, but here's how it goes:
On init, I grab the array, then clone a copy for each dropdown
When each dropdown changes the model property (the object ID), I have a scope listener then call a method to handle disabling the selected object/person in the OPPOSITE list
Within this method, I determine which of the two lists/arrays to iterate through and mark the disabled object
At the end of this method, I do a simple console.log call to check the value of a given object. For quick-and-dirty simplicity, I just grab item at index 0 .
What I expected: one object have a disabled value of true, and the opposite object to have false. What I see: they both have true (assuming you select the first "real" item in the dropdown)
What's the deal? How big of an idiot am I being?
The answer to my question was: clone() does not perform a "deep" clone by default, therefore I was dealing with the same array despite making the flawed attempt that I was not. Using Lodash's cloneDeep() method solved my issue, but as Patrick suggested I reevaluated how I wrote the method in question and refactored it, which I removed the need to use any cloning at all.

Adding element to array that has v-model causes duplicate

I've got a list of text input-fields created through a v-for with a v-model to an array. I want to add elements to the array, and thus creating another input-field.
So far all works. The problem is that the new input-fields are somehow all assigned the same index (?) or something else is happening to cause them to display the same value.
I've made this jsfiddle to showcase what I mean. If you press the button twice and then try to edit one of the new input-boxes, then all the new input-boxes will get the edited value. I'd want only the edited input-box to show the input value.
I guess there is something I am overlooking here. Is there anyone who can help with this please?
Javascript:
new Vue({
el: '#app',
data: {
items: [{name: "one", id: 0}],
template: {
name: "two",
id: 2,
},
},
methods: {
addRow: function(){
this.items.push(this.template);
this.items[this.items.length - 1].id = Math.random();
}
}
})
HTML:
<script src="https://unpkg.com/vue"></script>
<div id="app">
<div v-for="(item,index) in items" :key="item.id">
<input v-model="item.name">
</div>
<button v-on:click="addRow">
Add row
</button>
<div>Array content: {{items}}</div>
</div>
Usage:
screenshot of what i'm getting
The problem here is that with array.push(declaredObject) you are adding a reference of template so every change will be reflected in all its references.
You must add a new object with the same properties, you can achieve that in many ways, the more common is Object.assign({}, this.template) and the newest one is Destructuring objects {...this.template}. so in your case It should be this.items.push({...this.template})
try
this.items.push({
name: "two",
id: 2,
});
instead of this.items.push(this.template) because template property is reactive and it will affect other properties that use it
check this fiddle

Vue.js "track-by $index", how to render list items individually

Until recently I was using v-show to display each element in an array, one at a time, in my Vue instance. My html had the following line: <li v-for="tweet in tweets" v-show="showing == $index">{{{ tweet }}}</li>". My root Vue instance was constructed this way (thanks #Jeff!):
new Vue({
el: '#latest-tweets',
data: function(){
return {
tweets: [],
showing: 0
};
},
methods:{
fetch:function(){
var LatestTweets = {
"id": '706642968506146818',
"maxTweets": 5,
"showTime": false,
"enableLinks": true,
"customCallback":this.setTweets,
"showInteraction": false,
"showUser": false
};
twitterFetcher.fetch(LatestTweets);
},
setTweets: function(tweets){
this.tweets = tweets;
console.log(tweets);
},
rotate: function(){
if(this.showing == this.tweets.length - 1){
this.showing = -1;
}
this.showing += .5;
setTimeout(function(){
this.showing += .5;
}.bind(this), 1000);
}
},
ready:function() {
this.fetch();
setInterval(this.rotate, 10000);
}
It was all good until I came accross duplicate values. In order to handle these, I replaced v-show with track-by $index, as specified here. I now have this on my html: <li v-for="tweet in tweets" track-by="$index">{{{ tweet }}}</li>. The problem is that, instead of rendering each list item individually, the whole list is rendered at once.
As to the above rotate method, since I cannot do track-by="showing == $index", it is now useless. As far as I understand, this is due to Vue not being able to detect changes in the length of the Array. There seems to be a workaround, as detailed here, which is to "replace items with an empty array instead", which I did at no avail. I cannot figure out what am I missing.
Here're a couple of JsFiddles, with v-show and track-by $index.
The solution was after all rather simple and the resulting code leaner. Doing away with the v-for and track-by $index directives altogether and using a computed property instead did the trick:
computed: {
currentTweet: function () {
return this.tweets[this.showing]
}
}
On the html file, it is just a question of adding the computed property currentTweet as you normally would, with a mustache tag, here interpreted as raw html:
<li>{{{ currentTweet }}}<li>
No need therefore for anything like this:
<li v-for="tweet in tweets" track-by="$index">{{{ tweet }}}</li>
JsFiddle here

Looping over the map function in protractor

I have an angular application where I have a timeline with list event dates and the respective event description. This is the Html source code.
<!-- timeline -->
<h4 class="font-thin m-t-lg m-b-lg text-primary-lt">Historical Timeline</h4>
<p></p>
<div id="timeline"class="timeline m-l-sm m-r-sm b-info b-l">
<div ng-repeat = "timeline in formattedTimelineData | orderBy : '-eventDate'">
<div class = "tl-item">
<i class="pull-left timeline-badge {{timeline.class}} "></i>
<div class="m-l-lg">
<div id="eventDate{{$index}}" class="timeline-title">{{timeline.eventDate}}</div>
<p id="eventDescription{{$index}}" class="timeline-body">{{timeline.description}}</p>
</div>
</div>
</div>
</div>
<!-- / timeline -->
Now I am basically trying to make use protractor to ensure that the correct event date matches the event description. So i decided to use a map function. The issue is I would have a variable x which would tell me how many events there are . For example there can be 2 events, 6 events, etc. Events are dynamically
generated dynamically as you can tell by looking at html code also. Here is the code for my test I wrote.
it('FOO TEST', function(){
var x = 0;
while(x<4){
var timeline = element.all(by.css('#timeline')).map(function (timeline) {
return {
date: timeline.element(by.css('#eventDate'+x)).getText(),
events: timeline.element(by.css('#eventDescription'+x)).getText()
}
});
x++
}
timeline.then(function (Value) {
console.log(Value);
});
});
The issue is that for some reason in command line it only prints the last event out of 5 events. It does not print other events. I am definitely doing something wrong. I am brand new to promises so any suggestion here is appreciated. And yes i want to do like a individual test for each event in the timeline.
The problem is in the timeline locator: #timeline matches the timeline container while you need the inner repetative timeline blocks. Here is how you can match them:
var timeline = element.all(by.repeater('timeline in formattedTimelineData')).map(function (timeline) {
return {
date: timeline.element(by.binding('timeline.eventDate')).getText(),
events: timeline.element(by.binding('timeline.description')).getText()
}
});
timeline.then(function (timeline) {
console.log(timeline);
});
You can then loop over items like this:
timeline.then(function (timeline) {
for (var i = 0; i < timeline.length; ++i) {
// do smth with timeline[i]
}
});
Or, you can assert the complete timeline variable which is a promise and can be implicitly resolved by expect into an array of objects, for instance:
expect(timeline).toEqual([
{
date: "First date",
events: "Nothing happened"
},
{
date: "Second date",
events: "First base"
},
{
date: "Third date",
events: "Second base"
},
]);
I recommend against putting logic in your test - http://googletesting.blogspot.com/2014/07/testing-on-toilet-dont-put-logic-in.html.
A while loop is logic.
You should know ahead of time how many events will be in your timeline. In the example case 4. Then your specs should look like
element.all(by.css("#timeline")).then(function(events){
expect(events.count).toEqual(4);
expect(events[0].someThing()).toEqual(expectedValue0);
expect(events[1].someThing()).toEqual(expectedValue1);
...
expect(events[3].someThing()).toEqual(expectedValue3);
})
The repeater in my case has a lot of elements. I dont want to repeat through all elementsas my protractor spec times out while looping through so many elements. How can I just restrict the loop to run only for 1st 10 elements of the repeater? I have tried many things but I am just not able to get this working. How to loop only through first 10 elements of a repeater when using map()
The timeline variable in above example returns data for all elements in the repeater. How can i get the timeline variable to just have data for first 10 elements of the repeater as the looping through 1000 of entries of the repeater is time consuming that causes my protractor spec to timeout.

Categories