Create custom slider to control 'scrollLeft' of html element using Vue - javascript

I'm trying to create a custom slider to emulate horizontal scroll behavior of a specific element. the slider is implemented as a stand alone component and the selector for the element is passed to the slider component as a property:
<template>
<div class="slidercontainer">
<h1>{{contentWidth}}</h1>
<button class="right-scroll"></button>
<input
type="range"
min="1"
:max="contentWidth"
value="1"
class="slider"
v-on:input="handleScroll"
>
<button class="left-scroll"></button>
</div>
</template>
<script>
export default {
props: ["el"],
data() {
return {
scrollLeft: 0
};
},
methods: {
handleScroll($event) {
this.content.scrollLeft = this.contentWidth - $event.target.value;
}
},
computed: {
content() {
return document.querySelector(this.el);
},
contentWidth() {
return this.content.scrollWidth - this.content.clientWidth;
}
}
};
</script>
the problem though, is that content wont update and always return 0. when first loaded the app is waiting for some data from the server and thats why when mounted the contentWidth equals 0, but i thought returning content as a computed property should take care of that, but nothing happens after the content is injected with some new...well - content :)
any idea how to solve this?

Related

How to tell if the wheel event happens over an element with a scrollbar?

I have this vue template, with a number of child components, many of which use overflow-y to show a scrollbar when they get too much content.
<div #wheel="onWheel">
...
</div>
And the corresponding handler is:
onWheel: function (ev) {
//console.log(ev)
if (event.deltaY < 0) this.goto(-1)
else if (event.deltaY > 0) this.goto(+1)
},
I.e. the desired behaviour is for the mouse wheel to move up or down a database record, unless it has something more obvious to do. By that I mean, if it is over a component with a scroll bar, the user will expect the mouse wheel to scroll within that component, and not change the database record!
Is there a way to do this? I tried adding .self:
<div #wheel.self="onWheel">
...
</div>
But that stopped it working completely.
I wondered about trying to intercept the wheel event in each child component, but (apart from that being a maintenance nightmare) I am not sure this is possible as it is a special one (at least in Chrome) that you cannot call preventDefault() on.
I can see from looking at ev that I know what element the mouse is over from ev.target. Could I somehow go from that to finding out if it, or any parent, is showing a scrollbar?
Well, it's not so much that you want to preventDefault as you want the event to not bubble up to the parent if the child component has its own defined wheel event. You can do this using:
event.stopPropagation();
Code Sandbox:
I created a codesandbox demo here: https://codesandbox.io/s/wheel-propagation-demo-cocnx
Be sure to open the console in the demo to see the different events fire.
Code Example:
App.vue
<template>
<div #wheel="onWheel" id="app">
<img width="25%" src="./assets/logo.png">
<Base msg="Hello Vue in CodeSandbox!">this is a test</Base>
</div>
</template>
<script>
import Base from "./components/Base";
export default {
name: "App",
components: {
Base
},
methods: {
onWheel: function(e) {
console.log("wheeling over " + e.currentTarget.id);
}
}
};
</script>
Base.vue
<template>
<div class="hello" style="margin-top: 30px; padding: 30px; background: beige;">
<slot></slot>
<Wheeler>wheel me!</Wheeler>
</div>
</template>
<script>
import Wheeler from "./Wheeler.vue";
export default {
components: {
Wheeler
},
name: "Base",
props: {
msg: String
}
};
</script>
Wheeler.vue
<template>
<div #wheel="otherWheel" class="wheelMe">
<slot></slot>
</div>
</template>
<script>
export default {
methods: {
otherWheel: function(e) {
console.log("wheeling over " + e.currentTarget.className);
e.currentTarget.style["background-color"] =
"rgb(" +
[
Math.floor(Math.random() * 254),
Math.floor(Math.random() * 254),
Math.floor(Math.random() * 254)
].toString() +
")";
e.stopPropagation();
}
}
};
</script>
Note:
If you want to stop the wheel from causing a vertical scroll to occur you would handle that event separately.

Vue.js - Transfer focus to component

I have a form fragment wrapped in a component that is hidden by v-if. When the user clicks a button, it toggles the boolean, revealing the hidden component, and when that happens I'd like to transfer focus to the first form input on the fragment.
I've tried using aria-live to no avail. I suspect the nature of the SPA interferes with the registration of those live regions (meaning my guess is that they must be registered when the page is rendered, as they don't seem responsive when injected into the DOM). I did not however, chase the answer down a rabbit hole so that is speculative. So then I added a class to the target input and tried to use HTMLElement.focus()
document.querySelector('.focus')[0].focus();
This also did not work. Does anyone know of a reason why I cannot seem to focus on an element that was recently injected into the DOM and is visible on the page at the time?
I think what's needed is for your form component to have something defined for when it's mounted:
Vue.config.productionTip = false;
const template = `<div>
<div>
<inner v-if="showInner" />
<button #click="toggle">Toggle inner component</button>
</div>
</div>`
const inner = {
name: 'inner',
template: '<form><input ref="textInput" type="text"/></form>',
mounted() {
this.$refs.textInput.focus()
}
};
new Vue({
template,
data: function() {
return {
showInner: false
};
},
methods: {
toggle() {
this.showInner = !this.showInner;
}
},
components: {
inner
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

How to control order of rendering in vue.js for sibling component

I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.

Basic onclick javascript jquery code to Vue

I'm used to jquery and I'm rewriting my code to Vue. Need a little help with this basic onclick event.
Original html:
<div id="main">
<div id="right-nav" onclick="closeRightNav();"></div>
<i onclick="openRightNav();"></i>
</div>
Original javascript:
function openRightNav() {
$("#right-nav").width("100%");
}
function closeRightNav() {
$("#right-nav").width("0");
}
This is where I'm currently at:
New html:
<div id="main">
<div id="right-nav" #click="closeRightNav"></div>
<i #click="openRightNav"></i>
</div>
New javascript using Vue:
var app = new Vue({
el: '#main',
data: {
width: '100%'
},
methods: {
openRightNav() {
$("#right-nav").width("100%"); // not sure how to write this part in Vue?
}
}
});
How do I write this correctly in Vue?
Ok. You can approach this from multiple angles. I like to use v-show or v-if directives like so:
<template>
<div>
<div v-if="rightOpen" id="right-div" style="width:100%">Right Div</div>
<a #click="rightOpen = ! rightOpen">Toggle</a> // this will actually toggle right sidebar open and close
</div>
</template>
<script>
export default {
mounted() {
},
data: function() {
return {
rightOpen: false
}
}
}
</script>
Now you could use v-show instead of v-if difference is v-show element will be rendered on the page but not shown and v-if will not render element.
You could also use v-class like so
...
<div :class="{ 'someClass': !rightOpen,'sidebar-opened': rightOpen}" id="right-div">Right Div</div>
....
In this example someClass will be the one loaded when component is rendered, lets say width: 0 will be in that class. sidebar-opened class should contain width: 100%. Everything else stays the same from previous example.
in openRightNav you change your data and don't manipulate the DOM directly. Just say in your method: this.width = 100.
in your html then you need to bind your width attribute like this:
<div id="right-nav" #click="closeRightNav" v-bind:style="{'width': width + '%'}"></div>
don't forget to set your default value for width to 0.
in closeRightNav you would only write: this.width = 0
Thanks to Yousef Kama here's how I ended up doing it:
New html:
<div id="main">
<div id="right-nav" #click="closeRightNav" v-bind:style="{'width': width + '%'}"></div>
<i #click="openRightNav"></i>
</div>
New Vue javascript:
var app = new Vue({
el: '#main',
data: {
width: 0
},
methods: {
openRightNav() {
this.width = 100;
},
closeRightNav() {
this.width = 0;
}
}
});

The content not shown properly in function callback in Vue.js

I've got two problems here. The first is that I can't get the star rendered properly. I can do it if I change the value in the data() function but if I want to do it in a function callback way, it doesn't work (see comments below). What's going wrong here? Does it have something to do with Vue's lifecycle?
The second one is that I want to submit the star-rate and the content of the textarea and when I refresh the page, the content should be rendered on the page and replace the <textarea></textarea> what can I do?
I want to make a JSFiddle here but I don't know how to make it in Vue's single-file component, really appreciate your help.
<div class="order-comment">
<ul class="list-wrap">
<li>
<span class="comment-label">rateA</span>
<star-rating :data="dimensionA"></star-rating>
</li>
</ul>
<div>
<h4 class="title">comment</h4>
<textarea class="content" v-model="content">
</textarea>
</div>
<mt-button type="primary" class="mt-button">submit</mt-button>
</div>
</template>
<script>
import starRating from 'components/starRating'
import dataService from 'services/dataService'
export default {
data () {
return {
dimensionA: '' //if I changed the value here the star rendered just fine.
}
},
components: {
starRating
},
methods: {
getComment (id) {
return dataService.getOrderCommentList(id).then(data => {
this.dimensionA = 1
})
}
},
created () {
this.getComment(1) // not working
}
}
</script>
What it seems is scope of this is not correct in your getComment method, you need changes like following:
methods: {
getComment (id) {
var self = this;
dataService.getOrderCommentList(id).then(data => {
self.dimensionA = 1
})
}
},
As you want to replace the <textarea> and render the content if present, you can use v-if for this, if content if available- show content else show <textarea>
<div>
<h4 class="title">comment</h4>
<span v-if="content> {{content}} </span>
<textarea v-else class="content" v-model="content">
</textarea>
</div>
See working fiddle here.
one more problem I have observed in your code is you are using dynamic props, but you have assigned the prop initially to the data variable value in star-rating component, but you are not checking future changes in the prop. One way to solve this, assuming you have some other usage of value variable is putting following watch:
watch:{
data: function(newVal){
this.value = newVal
}
}
see updated fiddle.

Categories