I'm trying to use Vue on an existing page where there are some controls outside of my control. I wanted to wrap the controls with a Vue component, but all references to those controls are stale after Vue is initialized.
See the following codesandbox for repro: https://codesandbox.io/s/heuristic-fast-405up
The component wrapping the controls is super simply:
Component:
<div id="app">
<slot />
</div>
Page:
<div id="app">
<test-container>
<div id="some-element">Initial Text</div>
</test-container>
</div>
var element = document.getElementById("some-element");
new Vue({
components: {
TestContainer: App
}
}).$mount("#app");
console.log(element);
setTimeout(() => {
console.log("updating element...");
element.innerText = "Updated Text";
console.log(element.innerText);
console.log(document.getElementById("some-element").innerText);
}, 1000);
This will print:
updating element...
Updated Text
Initial Text // should be Updated Text
Is there a way to not break existing references to those controls when rendered in a slot? I want to keep rendering the existing controls, but wrap them with a Stepper component for example.
Related
I try to understand how the mounted hook works on Vue.js. If I understand correctly, the mounted function is called when all DOM elements are rendered. So in this scenario, I will have access to the DOM elements with document.getElementById or this.$el.somenthing.
Is this right?
But why I get the following behavior? Lets say I have the following structure:
mounted() {
this.buildTextTargetWidth();
},
methods: {
buildTextTargetWidth() {
console.log(document.getElementsByClassName('typographyTextPreview')); // Empty HTML-Collection. But, curiously, when I open the array in the console, there is the right element.
console.log(document.getElementsByClassName('typographyTextPreview')[0]); // Undefined
},
}
So far so good. The result is undefined. But for my understanding it should not!
Lets try to add some async stuff to wait 500ms:
mounted() {
this.buildTextTargetWidth();
},
methods: {
async buildTextTargetWidth() {
await new Promise(r => setTimeout(r, 500));
console.log(document.getElementById('test')); // The element is there
},
}
Now the element is there!
But why I have to wait other 500ms to get the DOM data if the mounted() is calling after the DOM is ready? It makes no sense for me.
By the way: The element typographyTextPreview which I try to call is embedded inside a v-for. Could this be the reason for this behavior? If yes, how could I wait until the v-for is ready?
--EDIT--
I wanted to create this topic as clean and simple as possible. But here I add additional data from the component, for example the entire for loop where the element typographyTextPreview is included:
<div ref="builderLayer" class="vconf-create-builder column" :class="getColumnLength">
<div class="vconf-create-builder-layers" :key="layersKey">
<div v-for="layer of getLayers" class="vconf-create-builder-layer" :key="layer.uid" :style="{ background: 'url(' + getLayerImage(layer) + ') center / contain no-repeat' }">
<template v-for="tile of layer.data">
<!-- <VueDragResize :key="tile.uid" v-if="tile.type=='typography'" :parentLimitation="true" :isActive="true" :x="calculatePositionX(tile)" :y="calculatePositionY(tile)" :w="tile.editBoxWidth" :h="tile.editBoxHeight" #resizestop="updateTypoBoxSize($event, layer, tile)" #dragstop="updateTypoBoxPosition($event, layer, tile)">
<h2 class="typographyTextPreview" :style="{'font-size':calculateFontSize(tile) + 'px', color: tile.fontColor}">{{tile.text ? tile.text : "Please enter an initial text in the settings"}}</h2>
</VueDragResize> -->
<div v-if="tile.type=='typography'" class="container" :key="tile.uid">
<div class="target" :style="buildTextTargetStyle(tile)">
<h2 id="test" class="typographyTextPreview" :style="{color: tile.fontColor}">{{tile.text ? tile.text : "Please enter an initial text in the settings"}}</h2>
</div>
<Moveable
className="moveable"
:target="['.target']"
:keepRatio="true"
:throttleResize="1"
:bounds=' {"left":0,"top":0, "right": clientWidth, "bottom": clientHeight}'
:origin="true"
:snappable="true"
:zoom="1"
:draggable="true"
:scalable="true"
:rotatable="true"
#drag="onDrag($event, tile)"
#scale="onScale($event, tile)"
#rotate="onRotate($event, tile)"
/>
</div>
</template>
</div>
</div>
</div>
From the docs:
A component is considered mounted after:
All of its synchronous child components have been mounted (does not include async components or components inside trees).
Its own DOM tree has been created and inserted into the parent container. Note it only guarantees that the component's DOM tree is in-document if the application's root container is also in-document.
If you need to wait for DOM changes, I suggest you to use the nextTick utility, which waits for the DOM to be updated. More details here
Is there a way to display a Vuetify loader component until all the elements are loaded? If not it displays for a moment the {{ and }} and is not very aesthetic.
I have a big page and a lot of items, applying v-cloak on each item don't consider to be cool either, I prefer to display a loader.
The Vuetify overlay component is not a bad choice in this kind of instance. You can chose an opaque overlay with an indeterminate loader that you then hide when everything has loaded:
<template>
<div>
<div id="the-content">
This is the stuff that gets hidden until it's loaded...
</div>
<v-overlay
:opacity="1"
:value="overlay"
>
<v-progress-circular indeterminate size="64">
Loading...
</v-progress-circular>
</v-overlay>
</div>
</template>
<script>
export default {
data: () => ({
overlay: true,
}),
mounted() {
// hide the overlay when everything has loaded
// you could choose some other event, e.g. if you're loading
// data asynchronously, you could wait until that process returns
this.overlay = false
},
}
</script>
Note: in this example, you're not likely to see the loading overlay since the content on this page takes such a short time to actually load.
OK, so I'm new to Vue ( basically, new to JS in general, but I'm playing with Vue right now ) and what I want to do, is to auto hide an element ( not on click ) inside the template tag of a component. In jQuery this would look like:
$(function() {
setTimeout(function() {
$(".hideElement").hide()
}, 1000);
});
but how this works in Vue? my component looks like this:
<template>
<div>
<h1 class="hideElement"> HELLO </h1>
</div>
</template>
<script> // nothing here
</script>
<style> // nothing here
</style>
I know how to toggle the element on click of a button, but I just want to auto hide it after 1 second without any click events everytime the users enter this component ( which is a new "page" )
You could just add a property in the data object and use v-show directive to determine whether the element should be visible or not. If the boolean is false the element is hidden, if true the element is visible.
Method Created called synchronously after the instance is created.
<template>
<div>
<h1 v-show="elementVisible" class="hideElement"> HELLO </h1>
</div>
</template>
<script>
export default {
data() {
return {
elementVisible: true
}
},
created() {
setTimeout(() => this.elementVisible = false, 1000)
}
}
</script>
I am very new to vue js. I am just learning to use it from laracasts. What I want to do is communicate between root class and subclass. Here, user will put a coupon code and when he changes focus it will show a text.
My html code is like this
<body>
<div id="root">
<coupon #applied="couponApplied">
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
<script src="https://unpkg.com/vue#2.5.21/dist/vue.js"></script>
<script src="main.js"></script>
</body>
My main.js is like this,
Vue.component('coupon', {
template: '<input #blur="applied">',
methods: {
applied()
{
this.$emit('applied');
}
}
});
new Vue({
el: '#root',
data: {
isCouponApplied:false,
},
methods:{
couponApplied()
{
this.isCouponApplied = true;
}
}
});
I am checking using vue devtools extension in chrome. There is no error. The blur event is triggered. isCouponApplied also changes to true. But the h1 is not showing. Can anyone show me where I made the mistake?
The problem is that you are not closing your <coupon> tag
<div id="root">
<coupon #applied="couponApplied"></coupon>
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
Should fix your issue. If you don't close your tag, the parser will auto-close it, but it will do so at the close of its wrapping container (the root div), so the h1 content will be seen as inside the <coupon> element, and will be replaced by your component's template.
I'd like to create a custom Vue directive to omit the tag but render the tag's contents when the directive is true.
So for example, if the data for my vue instance is defined as
data:{
omitIt: true
}
And if the markup looks like this:
<div v-omit="omitIt" class="someClass">
Hello world!
</div>
When omitIt is set to false as it is above, I'd like the following rendered into the dom:
<div class="someClass">
Hello world!
</div>
But when omitIt is true I'd like only the following rendered into the dom:
Hello world!
I initially tried to solve this by doing the following (admittedly not a custom vue directive):
<template v-if="!omitIt">
<div class="someClass">
</template>
Hello world!
<template v-if="!omitIt">
</div>
</template>
The above isn't pretty but I thought perhaps it would work. But alas what gets rendered into the dom when omitIt is false is:
<div class="someClass"></div>
Hello world!
Any suggestions on how to achieve the results I'm looking for?
I thought #Nit's answer was a great and simple one and upvoted it, but it does have one flaw: a slot may not be a root element so the component will fail when the wrapper needs to be omitted. This is because slots can contain more than one element and if the slot does contain more than one, there could end up being more than one root element, which is not allowed.
I have a partial solution that renders just the first element in the slot if the component should not wrap.
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
Here is an example of it working.
console.clear()
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// Log a warning if content is being omitted
const omissionMessage = "Wrapper component contains more than one root node with nowrap specified. Only the first node will be rendered."
if (this.$slots.default.length > 1 && this.nowrap)
console.warn(omissionMessage)
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
new Vue({
el: "#app"
})
.someClass{
color: blue
}
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<wrapper class="someClass">Hello World</wrapper>
<wrapper nowrap>No wrap, single root</wrapper> <br>
<wrapper nowrap>
No wrap, two roots. Paragraph is ommitted.
<p>Some other content</p>
</wrapper>
</div>
A couple notes: The component will always wrap unless you add nowrap as an attribute. Also, notice the class is added to the wrapped container without specifying it as a prop. This is because Vue automatically renders attributes that aren't specified as props on the root element of a component, unless you tell it not to.
This answer is wrong, slots cannot be used in this manner. Please see Bert's answer instead.
The easiest solution would be to create a wrapper component with slots for this purpose, passing the omitting argument as a prop.
The content distribution part becomes rather straightforward.
In the wrapper component template:
<slot v-if="omitIt"></slot>
<div v-else>
<slot></slot>
</div>
Wherever you want to use the wrapper:
<wrapper v-bind:omitIt="omitIt">
// Content
</wrapper>