Caret position in editable div after prop change using Vue.js - vue.js

I'm using Vue.js and have the following code.
When I type in div and this.content is updated, the caret is always reset to the beginning.
<template>
<div>
<div contenteditable="true"
v-html="content"
#input="onContentChange($event)">
</div>
</div>
</template>
<script>
export default {
props: ['content'],
methods: {
onContentChange: function(e) {
this.content = e.target.innerHTML;
},
},
}
</script>
<style>
</style>
How can I preserve the caret's position and update the content?
I've seen some other similar posts, but the solutions there either are not for Vue.js, or don't work in my case, or I might have failed to apply them correctly.

I've tested a few scenarios and I think what you actually need is plainly the Create a reusable editable component in this post.
However, if you want to have everything in one component, in chrome the following code works:
<template>
<div
ref="editable"
contenteditable
#input="onInput"
>
</div>
</template>
<script>
export default {
data () {
return {
content: 'hello world'
}
},
mounted () {
this.$refs.editable.innerText = this.content
},
methods: {
onInput (e) {
this.content = e.target.innerText
}
}
}
</script>
Note that the Vue plugin in Chrome doesn't seem to update correctly the value of content in this scenario, therefore you have to click on refresh on the top right of the vue plugin.

First we preserve current click on the contenteditable, then change HTML content, and set new selection.
const range = document.getSelection().getRangeAt(0)
const pos = range.endOffset
this.$el.innerHTML = this.content
const newRange = document.createRange()
const selection = window.getSelection()
const node = this.$el.childNodes[0]
newRange.setStart(node, node && pos > node.length ? 0 : pos)
newRange.collapse(true)
selection.removeAllRanges()
selection.addRange(newRange)

Related

Vue3js Cutom input component disappearing if value=null

I’m using the following component for custom clic-edit input
<template>
<el-input
v-show="edit"
ref="inputField"
type="text"
placeholder="place"
v-model='value'
#blur.native="
local = $event.target.value;
edit = false;
$emit('input', local);
"
#keyup.enter.native="
local = $event.target.value;
edit = false;
$emit('input', local);"
/>
<span v-show="!edit" #click="startEdit" > {{ local }}</span>
</template>
<script>
export default {
props: ["value"],
data() {
return {
edit: false,
local: this.value,
};
},
watch: {
value: function () {
this.local = this.value;
},
},
methods: {
startEdit() {
this.edit = true;
this.$refs.inputField.focus();
},
},
};
</script>
I am then using it, everything's seems ok.
Text switch to input as expected.
But If I pass a null value then the rendered component disappears.
Why ? How can I avoid this ?
<template>
<ClickEdit :value="'velue-test '" />
</template>
When passing or validating an empty value, the component disappears.
Actually your components is being rendered (you can see in devtools console). But because you pass empty or null as value prop to component there is nothing to show for editing. So you must set a default text to show in case of empty value (for example Edit here...). Then after user update the input replace it.
You can see this codesandbox to understand what I mean.

set property and retrieve HTML as string from Vue component

In order to separate my code and make it cleaner, I would like to use a Vue component as an HTML template, pass some parameters to the template and get the resulting HTML back as a string.
I have made a simple example that almost works, but for some reason the returned HTML is not up to date. When I hit "click me" I do get an HTML-string from the "MyDetails"-component, but it shows the value passed from the previous time, I hit the "click me"-button, instead of showing the actual value.
Main.vue
<template>
<div>
<p>
<myDetails ref="myDetails"/>
</p>
<button #click="handleClick">click me</button>
<p>{{message}}</p>
</div>
</template>
<script>
import MyDetails from "/components/MyDetails.vue";
export default {
name: "hello",
components: {
MyDetails
},
methods: {
handleClick() {
this.$refs.myDetails.setMessage(new Date().getTime());
this.message = this.$refs.myDetails.$el.outerHTML;
}
},
data() {
return {
message: ""
};
}
};
</script>
MyDetails.vue
<template>
<div style="background-color:red">
<h1>MyDetails component</h1>
<p>{{message}}</p>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
message: ""
};
},
methods: {
setMessage(value) {
this.message = value;
}
}
};
</script>
In the example above "MyDetails" is part of the template from the beginning. Is it possible to load it dynamically in the click-handler instead, so it doesn't show up, before I hit the "click me"-button?
Please see code here: https://codesandbox.io/s/vue-fullcalendar-example-50sv9?fontsize=14&hidenavigation=1&theme=dark
Updating the DOM takes time, you are immediately getting the myDetails outerHTML after you are changing its data, which doesn't give time for the change to propagate. Setting a slight delay as follows will give output as expected:
handleClick() {
this.$refs.myDetails.setMessage(new Date().getTime());
setTimeout(() => {
this.message = this.$refs.myDetails.$el.outerHTML;
}, 100)
}
For demo, see the sandbox here

How to reference text that's in '<slot></slot>' in Vue.js

How to reference text that's in in Vue.js?
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
// i would like to access the text in slot here
}
});
Note: This answer applies to Vue v2 only.
The content inside the default slot, which is what you are describing, is exposed as this.$slots.default in the Vue. So the most naive way to get the text inside your button would be to use this.$slots.default[0].text.
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
const buttonText = this.$slots.default[0].text;
}
});
The problem is that there may be more than one node inside the slot, and the nodes may not necessarily be text. Consider this button:
<button><i class="fa fa-check"></i> OK</button>
In this case, using the first solution will result in undefined because the first node in the slot is not a text node.
To fix that we can borrow a function from the Vue documentation for render functions.
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
And write
Vue.component("mybutton", {
template:"<button><slot></slot></button>",
created(){
const text = getChildrenTextContent(this.$slots.default);
console.log(text)
}
})
Which will return all the text in the slot joined together. Assuming the above example with the icon, it would return, "OK".
For Vue 3.
The answer from #bert works well on Vue 2, but Vue 3 slots have a more complex structure.
Here is one way to get the slots text contents (from default slot) on Vue 3.
const getSlotChildrenText = children => children.map(node => {
if (!node.children || typeof node.children === 'string') return node.children || ''
else if (Array.isArray(node.children)) return getSlotChildrenText(node.children)
else if (node.children.default) return getSlotChildrenText(node.children.default())
}).join('')
const slotTexts = this.$slots.default && getSlotChildrenText(this.$slots.default()) || ''
console.log(slotTexts)
Run the code snippet below that get the slot text passed by parent :
I'm using "ref" :
<span ref="mySlot">
this.$refs.mySlot.innerHTML
Careful : <slot ref="refName"></slot> don't works because <slot> are not render on html.
You have to wrap the <slot></slot> with <div></div> or <span></span>
The code :
Vue.component('component', {
template: '<button>' +
'<span ref="mySlot">' +
'Text before<br />' +
'<slot name="slot1">' +
'Text by default' +
'</slot>' +
'<br />Text after' +
'</span>' +
'</button>',
mounted: function() {
console.log( this.$refs.mySlot.innerHTML);
}
});
new Vue({
el: '#app'
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<component>
<span slot="slot1">I'm overriding the slot and text appear in this.$refs.mySlot.innerHTML !</span>
</component>
</div>
You can access the slot text by joining the innerText of all the children inside the slot.
getSlotText() {
return this.$slots.default.map(vnode => (vnode.text || vnode.elm.innerText)).join('');
},
My use case was pretty simple, I had a default slot with only text.
Here's how I accessed the text in vue3 with script setup:
<script setup lang="ts">
import { computed, useSlots } from "vue";
const slots = useSlots();
const slotText = computed(() => {
return slots.default()[0].children; // This is the interesting line
});
</script>

VueJS - trigger Modal from materializecss

I am trying to trigger a modal from the materializecss framework within a VueJS-instance.
Both, VueJS and Materializecss, are implemented correct. On their own both frameworks work fine.
Clicking the open-button results in an error:
Uncaught TypeError: data[option] is not a function
at HTMLDivElement. (adminarea.js:24562)
at Function.each (adminarea.js:10567)
at jQuery.fn.init.each (adminarea.js:10356)
at jQuery.fn.init.Plugin [as modal] (adminarea.js:24556)
at Vue$3.showLoader (adminarea.js:21396)
at boundFn (adminarea.js:54956)
at HTMLButtonElement.invoker (adminarea.js:56467)
This is my Vue-Instance:
const app = new Vue({
el: '#app',
data: {
activeUser: {
username: '',
email: ''
},
},
methods: {
showLoader(){
$('#loaderModal').modal('open');
},
closeLoader(){
$('#loaderModal').modal('close');
}
},
mounted() {
// Get current User
axios.get('/api/currentUser')
.then(response => {
this.activeUser.username = response.data.username;
this.activeUser.email = response.data.email;
});
},
components: {
Admindashboard
}
});
And here is the part of my html-file with the modal structure:
<!-- Modal Structure -->
<div id="loaderModal" class="modal">
<div class="modal-content">
<h4>Fetching data..</h4>
<div class="progress">
<div class="indeterminate"></div>
</div>
</div>
</div>
<button class="btn cyan waves-effect waves-cyan" v-on:click="showLoader">Open</button>
Any ideas? Thanks!
It seems I found an solution:
Nice to know for Laravel-users: for my current project I use Laravel 5.5 with Materializecss, VueJS and VueRouter but I think the solution is universal. Materializecss was installed via npm and has to be included into your application. I've required the css-framework within my ressources/assets/js/bootstrap.js:
...// more code
try {
window.$ = window.jQuery = require('jquery');
window.materialize = require('materialize-css');
} catch (e) {
console.log(e);
}
...// more code
Now you have to initialize the Modal-function on the mounted-event of your wrapping Vue-instance:
const app = new Vue({
router,
data: {
...
},
methods: {
testClick: function(){
console.log('Testklick-Call');
$('#modal1').modal('open');
}
},
mounted: function(){
console.log('Instance mounted');
$('.modal').modal();
}
}).$mount('#app');
The code above is placed within my ressources/assets/js/app.js and is packed by default by Laravel Mix but I think this is universal and also usable without Laravel Mix/Webpack etc.
Now you can call every modal programmatically from where ever you want. I've tested it in my main instance on a click-event. Function is placed in my Vue-instance (see above). HTML-Code see below:
<button v-on:click="testClick">Open Modal</button>
But you can also make use of the modal within a mounted-function or any other function of any component:
<template>
<div>
<p>I am an component!</p>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted!');
$('#modal1').modal('open');
}
}
</script>
This also works, if the component becomes only visible after clicked on a link (using VueRouter).
Hopefully this helps someone except me :)
As suggested here, you need to add following code in the mounted block:
mounted() {
$('#loaderModal').modal(); //New line to be added
// Get current User
axios.get('/api/currentUser')
.then(response => {
this.activeUser.username = response.data.username;
this.activeUser.email = response.data.email;
});
},

How to set keyup on whole page in Vue.js

Is it possible to set a v-on:keyup.enter on the whole page, not only for an input element in javascript framework Vue.js ?
Perhaps a better way to do this is with a Vue component. This would allow you to control when you listen to events by including or not including the component. Then you could attach event listeners to Nuxt using the no-ssr component.
Here is how you create the component:
<template>
<div></div>
</template>
<script>
export default {
created() {
const component = this;
this.handler = function (e) {
component.$emit('keyup', e);
}
window.addEventListener('keyup', this.handler);
},
beforeDestroy() {
window.removeEventListener('keyup', this.handler);
}
}
</script>
<style lang="stylus" scoped>
div {
display: none;
}
</style>
Then on the page you want to use that component you'd add this HTML:
<keyboard-events v-on:keyup="keyboardEvent"></keyboard-events>
And then you'll have to add your event handler method:
methods: {
keyboardEvent (e) {
if (e.which === 13) {
// run your code
}
}
}
Short answer is yes, but how depends on your context. If you are using vue-router as I am on a current project, you would want to add to the outer-most element you want that applied to. In my case I'm using the actual app.vue entry point's initial div element.
There is one catch that I believe is a hard requirement, the element has to be within the potentially focusable elements. The way I'm dealing with that is setting a -1 tabindex and just declaring my super-hotkeys (mostly for debug purposes right now) on the parent element in my app.
<template>
<div
id="app-main"
tabindex="-1"
#keyup.enter="doSomething"
>
<everything-else></everything-else>
</div>
</template>
EDIT:
As a side note, I also added a touch of additional configuration to my vue-router to make sure the right element is focused when I transition pages. This allows the pageup/pagedown scrolling to already be in the right section based on the content area being the only scrollable section. You'd also have to add the tabindex="-1" to the app-content element as well.
router.afterEach(function (transition) {
document.getElementById('app-content').focus();
});
and the basis of my app-content component:
<template>
<div id="app-content" tabindex="-1">
<router-view
id="app-view"
transition="app-view__transition"
transition-mode="out-in"
></router-view>
</div>
</template>
I created a small npm module that takes care of global keypress events in Vue, hope it makes someone's life easier:
https://github.com/lupas/vue-keypress
My simplest approach:
Add into your root Vue component (or any other component):
new Vue({
//...
created() {
window.addEventListener('keypress', this.onKeyPress);
},
beforeDestroy() {
window.removeEventListener('keypress', this.onKeyPress);
},
methods: {
onKeyPress(e) {
console.log('KEYPRESS EVENT', e)
//... your code
}
}
//...
})
In Vue 3 composition API, you can do it with a composable:
import { onMounted, onUnmounted } from "vue";
export function useKeyupEvent(handler) {
onMounted(() => document.addEventListener("keyup", handler));
onUnmounted(() => document.removeEventListener("keyup", handler));
}
and then in your component setup:
useKeyupEvent( event => console.log(event))