I'm working on a university project and I'm stuck trying to implement a searchbar that takes Game names from an API (https://api.rawg.io/api/games) and, after clicking on a game, redirects the user to a page with the specific game URL. Now, I wish I was at least able to do the first part.
I have created a searchbar.vue component and copy-pasted the code from the official Vuetify library:
(https://github.com/vuetifyjs/vuetify/blob/master/packages/docs/src/examples/autocompletes/simple/api.vue)
However, if I change the API from the one they provide as an example to the one I need (rawg), it does not work anymore and I am not sure why.
How can I make my searchbar work?
I'm providing a link to codesandbox where I'm currently working, in case it's needed:
(https://codesandbox.io/s/searchbar-mtjr2?file=/src/components/searchbar.vue)
I will do my best to give more information about the issue if needed.
Thank you very much in advance for even considering this post.
I believe all the problems with the search bar have been solved: Demo here
I added comments to the code so it should be clear how it works.
In searchbar.vue:
<template>
<v-autocomplete
clearable
:loading="isLoading"
:items="games"
:search-input.sync="search"
hide-details
item-text="name"
item-value="id"
label="Search for a Game..."
solo-inverted
></v-autocomplete>
</template>
<script>
import _ from "lodash";
export default {
data: () => ({
games: [], // this is where all the game data will be stored
isLoading: true, // variable to determine if the results are still being fetched from the API
search: null // this is where the query will be stored
}),
methods: {
getGames(params = "") {
// this function will fetch game data from https://api.rawg.io/api/games with whatever api parameters you pass to the parameter `params`
this.axios
.get("https://api.rawg.io/api/games" + params)
.then(resp => {
// fetch data
let tempGames = [...this.games.slice(0), ...resp.data.results]; //copy of this.games + new data
this.games = _.uniqBy(tempGames, "id"); // remove any duplicates
this.isLoading = false; // now that the data is in the array `games`, we can set isLoading to false
})
.catch(e => {
// code to run if there was an error
console.error(e); // display error message
});
},
searchGames(query) {
// this function will call getGames() with the search query formatted as an API parameter (?search=YOUR_QUERY)
let searchQuery = encodeURI("?search=" + query); // URI encode the query so it is able to be fetched properly
this.getGames(searchQuery);
}
},
watch: {
search: _.debounce(function(query) {
// debounce with a a value of 250 will allow this function to be every 250 milliseconds at most. So if the user is typing continually, it won't run until the user stops.
this.searchGames(query);
}, 250)
},
created() {
this.getGames(); // load the first 20 games initally
}
};
</script>
This will:
Load the first 20 games
When you enter a query and stop typing, it will perform a request to https://api.rawg.io/api/games?search=YOUR_QUERY, and add the results to the games array. (So each time you search for something, the saved games array increases. That means that if you search for the same game twice, while it will still search online with https://api.rawg.io/api/games?search=YOUR_QUERY, the game will have been already in the games array from the first time, so it will be loaded immediately.)
v-autocomplete filters and displays the results.
This method will work better on faster connections, as the results are loaded faster. So if you have a slow connection, it may take time for the results to load. Unfortunately that's something that can't be worked around (although you could load a lot of data beforehand, but there's no guarantee that what you load is what the user will search for)
The graphical problem with the navbar was resolved by changing the parent <div> in App.vue to <v-app>, like so:
Before:
<template>
<div id="app">
<nav-bar></nav-bar>
...
</div>
</template>
After:
<template>
<v-app id="app">
<nav-bar></nav-bar>
...
</v-app>
</template>
I also added lodash as a dependency for debouncing (waiting until the user stops typing) and removing duplicates.
Here's the codepen I was working on.
Do let me know if you have any further questions.
Related
As a newbie I’m trying to get my head around Vue and I’m having difficulty with the functionality of my Github jobs api app. The full project can be viewed here https://codesandbox.io/s/modest-banach-u6fzg
I’m having issues with the filtering of the api results, in particular the ‘load more’ button which, ideally, will filter the results of the api into batches of 10. The issues are:
The load more function works initially, but once there are, say, 30 results displayed on the app, the search function at the top does not work.
The search function works on the initial render of the page with 10 results being displayed, but the ‘load more’ button/function does not work on the returned results. As an example, if you search for ‘UK’ in location you get an initial 10 results, but a console.log reveals that there are 50 results returned from the api, which come back as undefined so are not displayed.
I’m not sure if these two problems are linked to a single issue.
Any advice would be much appreciated, as well as any feedback on how I’ve implemented the app.
Thanks!
Mike
You need to use computed prop instead of a function and you also you should reset loaded items counter if jobs loaded again.
See modified code
<template>
<div v-if="jobs.length">
<div v-for="job in filterJobs" v-bind:key="job.id">
<!-- <div v-for="job in jobs" v-bind:key="job.id"> -->
<Job v-bind:job="job" />
</div>
<button v-on:click="loadMore">Load More</button>
</div>
</template>
<script>
import Job from "./Job";
export default {
name: "Jobs",
components: {
Job,
},
data() {
return {
jobCount: 10,
};
},
props: ["jobs"],
watch: {
// watcher to reset a counter
jobs(newValue, oldValue) {
this.jobCount = 10;
},
},
computed: {
// computed prop instead of function, that way it would be reactive
filterJobs() {
return this.jobs.slice(0, this.jobCount);
},
},
methods: {
loadMore() {
// we need to check if we are exceeding a length of jobs array or not
if (this.jobCount + 10 <= this.jobs.length) {
this.jobCount += 10;
} else {
this.jobCount = this.jobs.length;
}
},
},
};
</script>
<style lang="stylus" scoped></style>
I have following simple html and Vuejs code. When I ran this, I was surprised to find out that the output was displayed as following.
3 times num = 4.6383976865880985e+49
I was expecting to see 30.
If I make a variable and return that temporally variable, I do see the expected value of 30. What is going on here?
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="test">
<p>3 times num = {{ mulBy3() }}</p>
</div>
new Vue({
el: '#test',
data: {
num: 10
},
methods: {
mulBy3: function() {
this.num = this.num * 3;
return this.num;
}
}
});
There's a vital lesson in this! Vue updates your page to reflect changes in your model. If rendering your page causes a change in your model (that causes Vue to re-render the page), then you have created an infinite loop - hence your very big number.
The moral of the story is that you don't know and shouldn't care when or how often your templates are rendered. You create the bindings such that your page reflects your model as you wish, then you leave Vue to take care of it. In practice, never call methods from a render function. Render functions should use data, injects, props, computed, perhaps watch: anything that's reactive. Methods should be used for responding to user activity and processing it back into the model.
You're running in to an infinite loop...
You call mulBy3() in your template
This mutates the num data property
This triggers a redraw
Goto #1
What you should do instead is use a computed property, eg
computed: {
mulBy3 () {
return this.num * 3
}
}
<p>3 times num = {{ mulBy3 }}</p>
This will react to changes to num.
For more reasons why you should not call methods in your templates, see https://v2.vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods
Currently, I am using the Vuex store for my authentication logic and I understand why being able to access stuff like user token, user id and username from anywhere in my application is extremely useful. I'm 100% sure that the authentication logic belongs to the store.
However, I'm currently playing around with Google Maps and have made a component which has a Google map in it and 3 functions for displaying, adding and removing Google markers from the map. Now I can't figure out if I should move this logic to the Vuex store. I think I will might have to use what is inside the markers: [] property in some other components in the future but everything else seems like it won't be used in any other components.
One of the pros of using Vuex store in this case is that it would make this component more readable and all my logic will be in one place but I'm not sure if that warrants moving the logic.
<template>
<div class="container">
<div class="map">
<GmapMap
#click='addMarker'
:center="center"
:zoom="zoom"
:map-type-id="map"
style="width: 100%; height: 800px"
>
<GmapMarker
:key="index"
v-for="(marker, index) in markers"
:position="marker"
:clickable="true"
:draggable="false"
#click="removeMarker(index)"
/>
</GmapMap>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data: function() {
return {
markers: [],
center: {lat: 42.150527, lng: 24.746477},
zoom: 15,
map: 'roadmap'
}
},
methods: {
addMarker(event){
axios.post('/location', {
userId: this.$store.state.auth.userId,
marker: event.latLng
}).then(response => {
this.markers.push({ 'id': response.data.locations.id,
'lat': response.data.locations.lat,
'lng': response.data.locations.lng})
}).catch((error) => console.log(error));
},
removeMarker(index){
const markerId = this.markers[index].id
this.markers.splice(index, 1);
axios.post('/deleteLocation', {
userId: this.$store.state.auth.userId,
markerId: markerId
})
}
},
mounted(){
axios.get('/location', {
userId: 1
}).then(response => {
this.markers = response.data.locations
}).catch((error) => console.log(error));
}
}
</script>
TLDR: Use local state.
Precursor:
This decision can always get tricky. Before you attempt to answer this question, think about UI as a function of state i.e. UI = F(state). This is how Elm is built. What it means is that some data X should be part of your central store (Vuex/Redux) when it determines the present view/UI of your overall application and the data itself can stand on its own.
Solution:
In this case, think about markers. Should data markers really exist on its own? Is there some UI component (besides the three components in question) of your entire application that can use marker independently on its own? Is data markers complete in itself? Probably yes or no. If you think more as yes, then you can put it inside Vuex otherwise use local component state.
To be more specific, markers together with three functions make a more cohesive unit. So instead of exposing markers as data, expose three functions keeping markers hidden. There are two ways you can do this. First, create one UI-less Vue component (simplified Vuex) that has three functions exposed.
Or as a second approach, Vue.js has provided a better solution for exactly this type of problem - Dependency injection With dependency injection, you can allow children to access ancestor component's data which is exactly what you need in this case. Of course, it also has its own pro and cons but that is a topic for another day.
Note: Alternately, you can also think that these three components GmapMap, GmapMarker, and their parent will always exist together. GmapMarker doesn't make sense on its own without GmapMap, so it is better to keep that state locally. You can then create a separate reusable module of these components, publish to NPM and use it in other applications.
I am working on a vuejs SPA.
I have a view that shows a list of items and another view that shows details for a specific Item.
when I click the item I switch views using:
this.$router.push('/item/' + event.ItemId );
The data is managed using vuex modules.
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
Where would be the appropriate place (code/lifecycle) to trigger the (async) retrieval of the data required for rendering the item details view?
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
One way to achieve this, is to define a state variable, named e.g. isLoading, in the data context of the Vue component. This variable would then be true while the data is retrieved asynchronously. In the template, you can use v-if to display a spinner while loading, and displaying the content after that.
If you are retrieving the data multiple times (refreshing the view), I would move the retrieving code into a method, e.g. called loadData. In the mounted section of the Vue component you then can just initially call this method once.
Here is some example code:
<template>
<div>
<button #click="loadData" :disabled="isLoading">Refresh</button>
<div class="item" v-if="!isLoading">
{{ item }}
</div>
<div class="spinner" v-else>
Loading...
</div>
</div>
</template>
<script>
import HttpService from '#/services/HttpService';
export default {
name: 'item-details',
data () {
return {
isLoading: false,
item: {}
};
},
methods: {
loadData () {
this.isLoading = true;
HttpService.loadData().then(response => {
this.item = response.data;
this.isLoading = false;
}, () => {
this.item = {};
this.isLoading = false;
});
}
},
mounted () {
this.loadData();
}
};
</script>
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
This issue you mentioned occurs if you are not using the HTML5 history mode, but an anchor (#) in the URL instead. If you are just changing the part after the anchor in the URL, the page is not actually refreshed by the browser. The Vue component won't be reloaded in this case and the state is still old. There are basically two ways around this:
You are switching from anchors in the URL to a real URL with the HTML5 history mode, supported by the Vue Router. This requires some back-end configuration, though. The browser then does not have this faulty behavior, because there is no anchor. It will reload the page on every manual URL change.
You can watch the $route object to get notified on every route change. Depending on if the user is changing the part after the anchor, or before, the behavior is different (it also depends where the cursor is, when you hit enter). If the part after the anchor is changed (your actual Vue route), only the component is notified. Otherwise, a full page refresh is made. Here's some example code:
// ...inside a Vue component
watch: {
'$route' (to, from) {
this.loadData();
}
}
I'd like to insert new vuejs components on the fly, at arbitrary points within a block of not-necessarily-predefined HTML.
Here's a slightly contrived example that demonstrates the sort of thing I'm trying to do:
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
This inserts the child component but doesn't render it
or the HTML:
<div>{{output}}</div>
<hr>
This renders the HTML but of course strips out the child component:
<div v-html="output"></div>
<hr>
(This is the child component, just to show that it's usable here:
<child>hello</child>)
<hr>
This is the goal: it renders both the input html
and the inserted child components:
TODO ¯\_(ツ)_/¯
</div>`,
computed: {
output() {
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
In the above snippet, assume data.text is sanitized HTML. <child> is some sub-component that does something useful, which I want to wrap around chunks of data.text that aren't known ahead of time. (input is just for demo here. This MCVE doesn't really resemble the code I'm building, it's just an example that shows the sort of situation I'm stuck on.)
So: how would I change either the output function or the parent component's template, such that both the HTML from input and the inserted <child> templates are rendered properly?
What I've tried
In Vue 1, the answer to this would be a straightforward $compile. I'm using vuejs2 which removed $compile (out of justifiable concern that it made it too easy to naively introduce XSS vulnerabilities.)
v-html sanitizes what you feed it, which strips the child component out. Obviously this is not the way to do this. (That page suggests using partials instead, but I'm not sure how that could be applied to this situation; in any case partials have also been removed from vue2.)
I've tried passing the results of output() into another component which would then use it as its template. This seems like a promising approach, but I can't figure out how to change that secondary component's template. template only accepts a string, not a function like many of the other component properties, so I can't pass the template html in, say, a prop. Something like rewriting this.template inside beforeMount() or bind() would have been nice, but no joy there either. Is there some other way to replace a component's template string before it's mounted?
Unlike template, I can pass data to a component's render() function... but then I'm still stuck having to parse that html string into nested createElement functions. Which is exactly what Vue is doing internally in the first place; is there some way to hook into that here short of reinventing it myself?
Vue.component('foo', {
props: ['myInput'],
render(createElement) {
console.log(this.myInput); // this works...
// ...but how to parse the html in this.myInput into a usable render function?
// return createElement('div', this.myInput);
},
})
I wasn't able to cheat my around this with inline-template, either: <foo inline-template>{{$parent.output}}</foo> does exactly the same thing as a plain old {{output}}. In retrospect that should have been obvious, but it was worth a shot.
Maybe constructing an async component on the fly is the answer? This could clearly generate a component with an arbitrary template, but how would I reasonably call that from the parent component, and feed output to the constructor? (It would need to be reusable with different input, with multiple instances potentially visible simultaneously; no globals or singletons.)
I've even considered ridiculous stuff like having output() split the input into an array at the points where it would have inserted <child>, and then doing something like this in the main template:
...
<template v-for="chunk in output">
<span v-html="chunk"></span>
<child>...</child>
</template>
....
That would be doable, if laborious -- I'd have to split out what goes in the child's slot into a separate array too and get it by index during the v-for, but that could be done... if input were plain text instead of HTML. In splitting HTML I'll often wind up with unbalanced tags in each chunk, which can mess up the formatting when v-html rebalances it for me. And anyway this whole strategy feels like a bad hack; there must be a better way.
Maybe I just drop the whole input into a v-html and then (somehow) insert the child components at the proper positions through after-the-fact DOM manipulation? I haven't explored this option too deeply because it, too, feels like a hack, and the reverse of the data-driven strategy, but maybe it's a way to go if all else fails?
A couple of pre-emptive disclaimers
I'm very well aware of the XSS risks involved in $compile-like operations. Please be assured that none of what I'm doing involves unsanitized user input in any way; the user isn't inserting arbitrary component code, instead a component needs to insert child components at user-defined positions.
I'm reasonably confident that this is not an XY problem, that I really do need to insert components on the fly. (I hope it's obvious from the number of failed attempts and blind alleys I've run down that I've put more than a little thought into this one!) That said, if there's a different approach that leads to similar results, I'm all ears. The salient point is that I know which component I need to add, but I can't know ahead of time where to add it; that decision happens at run time.
If it's relevant, in real life I'm using the single-file component structure from vue-cli webpack template, not Vue.component() as in the samples above. Answers that don't stray too far from that structure are preferred, though anything that works will work.
Progress!
#BertEvans points out in comments that Vue.compile() is a thing that exists, which is an I-can't-believe-I-missed-that if ever there was one.
But I'm still having trouble using it without resorting to global variables as in that documentation. This renders, but hardcodes the template in a global:
var precompiled = Vue.compile('<span><child>test</child></span>');
Vue.component('test', {
render: precompiled.render,
staticRenderFns: precompiled.staticRenderFns
});
But various attempts to rejigger that into something that can accept an input property have been unsuccessful (the following for example throws "Error in render function: ReferenceError: _c is not defined", I assume because the staticRenderFns aren't ready to go when render needs them?
Vue.component('test', {
props: ['input'],
render() { return Vue.compile(this.input).render()},
staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}
});
(It's not because there are two separate compile()s -- doing the precompile inside beforeMount() and then returning its render and staticRenderFns throws the same error.)
This really feels like it's on the right track but I'm just stuck on a dumb syntax error or the like...
As mentioned in the my comment above, $compile was removed, but Vue.compile is available in certain builds. Using that below works as I believe you intend except in a couple cases.
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<div><p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p></div>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
<div><component :is="output"></component></div>
</div>`,
computed: {
output() {
if (!this.input)
return Vue.compile(this.text)
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
out = Vue.compile(out)
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
You mentioned you are building with webpack and I believe the default for that build is Vue without the compiler, so you would need to modify it to use a different build.
I added a dynamic component to accept the results of the compiled output.
The sample text is not a valid template because it has more than one root. I added a wrapping div to make it a valid template.
One note: this will fail if the search term matches all or part of any of the HTML tags in the text. For example, if you enter "i", or "di" or "p" the results will not be what you expect and certain combinations will throw an error on compilation.
I'm posting this as a supplement to Bert Evans's answer, for the benefit of vue-cli webpack users who want to use .vue files instead of Vue.component(). (Which is to say, I'm mostly posting this so I'll be able to find this information when I inevitably forget it...)
Getting the right Vue build
In vue-cli 2 (and possibly 1?), to ensure Vue.compile will be available in the distribution build, confirm webpack.base.conf.js contains this line:
'vue$': 'vue/dist/vue.esm.js' // or vue/dist/vue.common.js for webpack1
instead of 'vue/dist/vue.runtime.esm.js'. (If you accepted the defaults when running vue init webpack you will already have the full standalone build. The "webpack-simple" template also sets the full standalone build.)
Vue-cli 3 works somewhat differently, and does not have Vue.compile available by default; here you'll need to add the runtimeCompiler rule to vue.config.js:
module.exports = {
/* (other config here) */
runtimeCompiler: true
};
The component
The "child" component can be a normal .vue file, nothing special about that.
A bare-bones version of the "parent" component would be:
<template>
<component :is="output"></component>
</template>
<script>
import Vue from 'vue';
import Child from './Child'; // normal .vue component import
export default {
name: 'Parent',
computed: {
output() {
var input = "<span>Arbitrary single-root HTML string that depends on <child></child>. This can come from anywhere; don't use unsanitized user input though...</span>";
var ret = Vue.compile(input);
ret.components = { Child }; // add any other necessary properties similarly
ret.methods = { /* ... */ } // like so
return ret;
}
}
};
</script>
(The only significant difference between this and the non-webpack version is importing the child, then declaring the component dependencies as ret.components: {Child} before returning it.)