I'm making an e-commerce type menu in Vue.js, with items which are divs that contain a fair amount of functionality and images. Performance is fairly good when rendering about 200 of these items, but when more than that many are added the site begins to perform sluggishly.
What's the best way to conditionally hide or remove Vue elements from the DOM if they are outside the current scrollable view (like ScrollViews in iOS)? Are there any plugins or libraries that can help with the performance of long lists of data items in Vue.js?
Thanks!
I've made a demo snippet using the package I mentioned in my comment.
I've made a "signal" item that acts as the watcher. When the "signal" item leaves the viewport, the "complex-stuff" is no longer rendered. I did it this way so you can see the "complex-stuff" disappear. When the "signal" scrolls back into view, the "complex-stuff" is rendered.
You could just put the watcher on the widget root element and things will only be hidden when the whole widget is out of view. You don't want to put the v-if on the root element, though, or it will never come back once it goes away.
const containerMonitor = scrollMonitor.createContainer(document.body);
new Vue({
el: '#app',
data: {
ids: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
},
components: {
widget: {
template: '#widget-template',
props: ['id'],
data() {
return {
visible: true
};
},
mounted() {
const elementWatcher = containerMonitor.create(this.$el.querySelector('.signal'));
elementWatcher.enterViewport(() => {
this.visible = true;
});
elementWatcher.exitViewport(() => {
this.visible = false;
});
}
}
}
});
.widget-container {
height: 200px;
margin: 10px;
background-color: #f0f0f0;
display: flex;
flex-flow: row wrap;
}
.signal {
height: 10px;
width: 10px;
margin: 30px;
background-color: red;
border: thin solid blue;
}
.complex-stuff {
flex-basis: 100%;
padding: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<script src="https://rawgit.com/stutrek/scrollMonitor/master/scrollMonitor.js"></script>
<template id="widget-template">
<div class="widget-container">
<div class="signal">
</div>
<div v-if="visible" class="complex-stuff">
This is widget {{id}}.
Blah blah blah.
</div>
</div>
</template>
<div id="app">
Widgets below:
<widget v-for="id in ids" :id="id"></widget>
:widgets above
</div>
Related
I have a quick app that im writing which contains a quick app page that references only a single custom element through the router.push API, the onShow lifecycle function for that page cannot be triggered.
<import name="listone" src="./aa.ux"></import>
<template>
<!-- The template can contain only one root node. -->
<listone></listone>
</template>
<script>
import prompt from '#system.prompt'
export default {
private: {
},
onInit: function() {
},
onShow() {
console.log('Enter a string whatever you like.');
prompt.showToast({
message: 'Enter a string whatever you like.'
})
}, // This function cannot be triggered.
}
</script>
<style>
.demo-page {
flex-direction: column;
justify-content: center;
align-items: center;
}
.title {
font-size: 40px;
text-align: center;
}
</style>
And my aa.ux file
<template>
<div class="container">
<text> Enter some words.</text>
<text>Enter some words.</text>
<text>Enter some words.</text>
<text>Enter some words.</text>
</div>
</template>
<style>
.container {
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #00fa9a;
}
</style>
<script>
module.exports = {
data: {
},
onInit() {
},
}
</script>
How can i get the onShow function to trigger in this situation?
I have gotten to the 'root' of the issue, the quick app loader does not trigger the onShow lifecycle function if the root node of the page is a custom element. However, the function can be triggered for a subelement. SO the way to get round this for me was to add an extra div element like:
<template>
<!-- The template can contain only one root node. -->
<div>
<listone></listone>
</div>
</template>
i copied the code from async part of the documentation, because that's the 'X' to remove value i want.
here is my general component that i use in other vue components
<template>
<div>
<multiselect
v-model="items"
:options="filteredList"
:multiple="multiple"
:close-on-select="multiple ? false : true"
:show-labels="false"
:placeholder="placeholder"
track-by="id"
:label="label"
#input="inputChanged"
:internal-search="false"
#search-change="searchItems"
>
<template slot="clear" slot-scope="props">
<div class="multiselect__clear" v-if="items.length" #mousedown.prevent.stop="clearAll(props.search)"></div>
</template>
</multiselect>
</div>
</template>
<script>
export default {
model: {
prop: 'parentItems',
event: 'change',
},
props: ['multiple', 'list', 'placeholder', 'label', 'parentItems'],
data() {
return {
items: this.parentItems,
filteredList: this.list,
}
},
methods: {
searchItems(query) {
let q = latinize(query.toLowerCase().replace(/\s/g,''))
this.filteredList = this.list.filter(li => latinize(li[this.label].toLowerCase().replace(/\s/g,'')).includes(q))
},
inputChanged() {
this.$emit('change', this.items);
},
clearAll() {
this.items = this.multiple ? [] : null
},
},
}
</script>
everything works as desired, except the X to clear selection is never displayed.
i found the clear element in console, it has width of 255 and height of 0. i tried to put X between the div tags, like this
<template slot="clear" slot-scope="props">
<div class="multiselect__clear" v-if="items.length"
#mousedown.prevent.stop="clearAll(props.search)"
>
X
</div>
</template>
but it would display above the select input field. also changing the height attribute in dev console just made clear space above input field.
what am i missing?
vue-multiselect does nothing special with the clear slot other than render it before the input tags. It leaves the styling and behavior/implementation entirely up to the end user.
In addition, the example from the docs you linked poorly implements the slot, as the provided slot has no contents, so the div won't be visible or clickable, making it effectively useless. Not to mention, it uses the obsolete (pre-2.6) slot syntax of Vue, which would cause warnings on the browser console if using the development build of Vue.
Solution
The clear slot should look like this:
<multiselect v-model="value"
:options="options"
multiple
taggable
#tag="addTag">
<template v-slot:clear>
<button v-if="value.length" class="multiselect__clear" #click="clearSelectedTags">
Ⓧ Clear selection
</button>
</template>
</multiselect>
demo
Thanks to tony19 I went and inspected the part of documentation I mentioned in the question.
I found out that they use different code for the example, so to attain desired effect, I need to add following code to my css.
.multiselect__clear {
position: absolute;
right: 41px;
height: 40px;
width: 40px;
display: block;
cursor: pointer;
/*z-index: 2;*/
}
.multiselect__clear:after, .multiselect__clear:before {
content: "";
display: block;
position: absolute;
width: 3px;
height: 16px;
background: #aaa;
top: 12px;
right: 4px;
cursor: pointer;
}
.multiselect__clear:before {
transform: rotate(45deg);
}
.multiselect__clear:after {
transform: rotate(-45deg);
}
I have created a webcomponent for a generic input boxes that i needed across multiple projects.
the design functionality remains same only i have to use switch themes on each projects.so i have decided to go on with webcomponents.One of the projects is based on Vue Js.In Vue js the DOM content is re-rendered while each update for enabling reactivity. That re-rendering of vue template is reinitializing my custom webcomponent which will result in loosing all my configurations i have assigned to the component using setters.
I know the below solutions. but i wanted to use a setter method.
pass data as Attributes
Event based passing of configurations.
Using Vue-directives.
using v-show instead of v-if
-- Above three solutions doesn't really match with what i am trying to create.
I have created a sample project in jsfiddle to display my issue.
Each time i an unchecking and checking the checkbox new instances of my component is creating. which causes loosing the theme i have selected. (please check he active boxes count)
For this particular example i want blue theme to be displayed. but it keep changing to red
JSFiddle direct Link
class InputBox extends HTMLElement {
constructor() {
super();
window.activeBoxes ? window.activeBoxes++ : window.activeBoxes = 1;
var shadow = this.attachShadow({
mode: 'open'
});
var template = `
<style>
.blue#iElem {
background: #00f !important;
color: #fff !important;
}
.green#iElem {
background: #0f0 !important;
color: #f00 !important;
}
#iElem {
background: #f00;
padding: 13px;
border-radius: 10px;
color: yellow;
border: 0;
outline: 0;
box-shadow: 0px 0px 14px -3px #000;
}
</style>
<input id="iElem" autocomplete="off" autocorrect="off" spellcheck="false" type="text" />
`;
shadow.innerHTML = template;
this._theme = 'red';
this.changeTheme = function(){
this.shadowRoot.querySelector('#iElem').className = '';
this.shadowRoot.querySelector('#iElem').classList.add(this._theme);
}
}
connectedCallback() {
this.changeTheme();
}
set theme(val){
this._theme = val;
this.changeTheme();
}
}
window.customElements.define('search-bar', InputBox);
<!DOCTYPE html>
<html>
<head>
<title>Wrapper Component</title>
<script src="https://unpkg.com/vue"></script>
<style>
html,
body {
font: 13px/18px sans-serif;
}
select {
min-width: 300px;
}
search-bar {
top: 100px;
position: absolute;
left: 300px;
}
input {
min-width: 20px;
padding: 25px;
top: 100px;
position: absolute;
}
</style>
</head>
<body>
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
</div>
</script>
<script type="text/x-template" id="select2-template">
<select>
<slot></slot>
</select>
</script>
<script>
var vm = new Vue({
el: "#el",
template: "#demo-template",
data: {
visible: true,
},
mounted(){
let self = this
setTimeout(()=>{
self.$refs.iBox.theme = 'blue';
} , 0)
}
});
</script>
</body>
</html>
<div class='parent' contentEditable='true' v-if='visible'>
<search-bar ref='iBox'></search-bar>
</div>
<input type='checkbox' v-model='visible'>
Vue's v-if will add/remove the whole DIV from the DOM
So <search-bar> is also added/removed on every checkbox click
If you want a state for <search-bar> you have to save it someplace outside the <search-bar> component:
JavaScript variable
localStorage
.getRootnode().host
CSS Properties I would go with this one, as they trickle into shadowDOM
...
...
Or change your checkbox code to not use v-if but hide the <div> with any CSS:
display: none
visibility: hidden
opacity: 0
move to off screen location
height: 0
...
and/or...
Managing multiple screen elements with Stylesheets
You can easily toggle styling using <style> elements:
<style id="SearchBox" onload="this.disabled=true">
... lots of CSS
... even more CSS
... and more CSS
</style>
The onload event makes sure the <style> is not applied on page load.
activate all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = false
remove all CSS styles:
(this.shadowRoot || document).getElementById("SearchBox").disabled = true
You do need CSS Properties for this to work in combo with shadowDOM Elements.
I prefer native over Frameworks. <style v-if='visible'/> will work.. by brutally removing/adding the stylesheet.
Thanks #zero298, the dup doesn't apply in important ways. I want to show all of the items in the object array, just conditionally add some UI to each based on user signal. Furthermore, v-if and v-show are very different (as noted here and elsewhere). vue q/a on this site seems to be pretty light -- because it's new -- seems like a mistake to aggressively close on such a new topic.
I can make show work this way...
(NOTE... Please run the snippets in "Expand Snippet" mode to see the behavior over the console stuff. Not sure how to suppress the vue messages in console)
const app = new Vue({
el: '#app',
data: {
show: false
},
methods: {
}
});
.demo{
width: 100px;
height: 30px;
background-color: green;
margin: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.14/vue.js"></script>
<div id="app">
<h3>Hi there</h3>
<div class="demo" #click="show = !show">Click me</div>
<div v-show="show">Show or hide me</div>
</div>
But why can't I make it work this way...
const app = new Vue({
el: '#app',
data: {
objects: [ { name: 'a' }, { name: 'b' }, { name: 'c' } ],
show: [false, false, false]
},
methods: {
}
});
.demo{
width: 100px;
height: 30px;
background-color: green;
margin: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.14/vue.js"></script>
<div id="app">
<div v-for="(object, i) in objects">
<h3>Hi there {{ object.name }} show status is {{ show[i] }}</h3>
<div class="demo" #click="show[i] = !show[i]">Click me</div>
<div v-show="show[i]">Show or hide me</div>
</div>
</div>
I've seen doc content saying not to use v-if in a loop, but what's wrong with v-show? There's evidence that the #click expression assigning to the show array isn't running (see the variable state next to the "hello" message), but why not?
I've tried moving that logic to a method, and moving the v-show check to a method, but with the same results.
Also, my array of objects will appear async and have an unknown (but small) length. I don't want to add a "show" property to those objects because user can save them back to the server. What's the right time and place to allocate a show array of bools that matches the objects array?
This because of vue's change detection. Vue cannot detect that the array is changing.
You have to do something like this to detect it:
<div class="demo" #click="$set(show, i, !show[i])">Click me</div>
Helpful: https://v2.vuejs.org/v2/guide/list.html#Array-Change-Detection
Using vue.js (and quasar framework), I have a card component. When an event is triggered a button at the bottom of the card is shown. When the button appears, the size of the card increases due to the height of the button which is added. I find this ugly and would prefer the size of the card to be the same before and after having a button.
I tried with some <br> before the button is added to compensate for the height difference, but this is clumsy and does not work properly when I animate the appearance of the button with a fade-in e.g.
As the card(s) will contain various content(size), making a fixed size for card will not really work.
How can I have the same size of my card before and after showing the button?
Quick fix: you must know button height before. Then nest it to element with same height achieved with min-height property :
<div id="button-container" style="min-height: /* your button height */">
<button>Hidden yet</button>
</div>
It is not very elegant way. Use it only if you are not able to use visibility: hidden on button, instead of display: none, as #musicformellons suggest in comment.
I think, this example demonstrates your problem:
new Vue({
el: '#app',
data: {
canShow: false
},
methods: {
toggleButton () {
this.canShow = !this.canShow
}
},
created () {
setInterval(function () {
this.toggleButton()
}.bind(this), 500)
}
})
.bordered {
position: absolute;
border: 2px solid black;
}
<div id="app">
<div class="bordered">
<p>Lorem Ipsum, bla, bla, bla...</p>
<button v-if="canShow">I am just troublemaker</button>
</div>
</div>
<script src="https://unpkg.com/vue"></script>
And I think this is the most elegant, "true Vue way" solution. Moreover, with this solution you need not to know button height before...
new Vue({
el: '#app',
data: {
visibility: false
},
methods: {
toggleButton () {
this.visibility = !this.visibility
},
logIt () {
console.log('button clicked')
}
},
created () {
setInterval(function () {
this.toggleButton()
}.bind(this), 1000)
}
})
.bordered {
position: absolute;
border: 2px solid black;
}
.animate-me {
transition: all .4s;
}
.is-hidden {
opacity: 0;
}
<div id="app">
<div class="bordered">
<p>Lorem Ipsum, bla, bla, bla...</p>
<!-- Render it always, but change visibility as needed instead -->
<button
class="animate-me"
:class="{'is-hidden': visibility}"
#click="logIt"
:disabled="visibility"
>
I am just troublemaker
</button>
</div>
</div>
<script src="https://unpkg.com/vue"></script>
You could give the css of the button position: absolute and the card position: relative, and then fiddle around with the bottom; left; top; right; position settings of the button.