How to access only Vue.js components elements using a global mixin? - vue.js

Trying to create a plugin with global mixin which would automatically look for specific element and change its attributes.
export default {
// called by Vue.use(ThisPlugin)
install(Vue, options) {
Vue.mixin({
created() {
console.log($("div").length); // get rid of jQuery and global content
},
});
},
};
As this is called on every vue component I want to limit content mixin accesses with similar like el parameter in directives or like components have element querySelector (this.$el.querySelector("div")) and to replace jquery usage. Is my approach correct and how would I access only components contents in a mixin?
Want to skip directives as those would need to modify tons of existing components, rather introduce a plugin for a component.

Related

VueJS & Vuetify - How to use JS variables in Sass

Using VueJS, I need to display different colors for each user. The color depends on the user settings.
In my vuetify.js, I have:
export default new Vuetify({
theme: {
themes: {
light: {
primary: user.colorMain ? user.colorMain : '#F39200',
It works when I use:
$vuetify.theme.themes.light.primary
in my components.
But I would need to override the Sass variables too, in my variable.scss file:
$primary-color: #f39200;
Is there a way to override my sass variables dynamically from a JS variable?
tl:dr; no, it's not possible to change Sass variable values at runtime, because they no longer exist at runtime. They have been translated into plain (static) CSS.
However, like with any CSS values, you can override them.
Sass variables are only used to pre-process SCSS into CSS at compile time. The result of compilation is static CSS, loaded when the app is mounted. In simpler terms, the app doesn't know that CSS was preprocessed from an SCSS source. For it, it's static CSS.
Example:
$primary-color: #f39200;
.my-button { color: $primary-color; }
will output the following CSS code:
.my-button { color: #f39200; }
If you want runtime dynamic values, you have two options:
Use CSS variables.
Produce the following CSS, via your preferred method (from SCSS/CSS/Stylus, doesn't matter, as long as this is the output):
.my-button { color: var(--primary-color); }
... and, anywhere in the chain of parents or on the element itself:
<div :style="{'--primary-color': someDynamicColor }" />
With the above in place, when you change someDynamicColor, at runtime, the color changes in DOM.
Use Vue3's "reactive styles" feature:
<script>
export default {
data: () => ({ someDynamicColor: 'red' })
}
</script>
<style>
.my-button {
color: v-bind('someDynamicColor');
}
</style>
Again, this is dynamic. If you change/animate the value of someDynamicColor on the element, the CSS value will be applied in DOM. It doesn't have to be a data prop, it can be a prop, computed, ...
Important notes:
when using CSS variables (1.), the value of var(--primary-color) doesn't have to be set in the same component, but it has to be set on a direct ancestor of the current DOM element.
when using reactive styles (2.), the prop/computed referenced in CSS/SCSS has to be set in the current component's scope.
Under the hood, reactive styles also use CSS variables: they're uniquely named at compile time.
CSS variables don't use specificity. If you override the value set by some grand-parent at parent level, the child has no way of reading the grand-parent's value, regardless of specificity. If you have such a case, you probably want to manage the grandparent value in external state and provide it to both grand-parent and child.

Is it possible to access a HTML element using a ref in the data section of a component with Vue 2?

Is it possible to access $refs from the data section of a Vue (2.6.12) component e.g.
flickingPlugins: [new Arrow({
prevElSelector: '.overview-arrow-prev',
nextElSelector: '.overview-arrow-next',
parentEl: this.$refs['overview-arrows'], // _this is undefined
})],
The reason I'm asking is because of the requirements of the Flicking slider/carousel component. This component has an additional arrow plugin, for arrows to appear on either end to control sliding content left and right. The plugin docs (3rd example) provide a Vue example that looks like this:
data() {
return {
plugins: [new Arrow({ parentEl: document.body })]
}
}
The parentEl attribute is a way to specify where your custom arrow control elements will be located, and requires a HTMLElement value. I've always been told that element access should be done using refs, but how would you do that in this case?
As far as I'm aware and the lifecycle documentation reads, this is not possible due to the design of Vue.js.
Created
[...] At this stage, the instance has finished processing the options which means the following have been set up: data observation, computed properties, [...]. However, the mounting phase has not been started, and the $el property will not be available yet.
So you will have to wait until your component has been mounted in order to access the element and its references.
Why not try an approach like this:
{
// ...
data () {
// no access to $el and $refs
return {
plugins: []
}
},
mounted () {
// here you have access to $el and $refs
this.plugins.push(new Arrow({ parentEl: /* ... */ }))
},
}

Dynamically instantiating a component in Vue.js

Following this tutorial, I'm trying to programmatically create instances of a component on my page.
The main snippet is this:
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount()
this.$refs.container.appendChild(instance.$el)
However I get two errors:
The component I'm trying to instantiate contains references to the store, and these don't work: "TypeError: Cannot read property 'state' of undefined".
For the last line of the snippet (this.$refs.container.appendChild(instance.$el)) I get this error: "Uncaught TypeError: Cannot read property 'container' of undefined"
I'm really not sure how to troubleshoot this, if anyone strong in Vue.js could give me some hint as to why I'm getting these errors and to solve them that would be terrific.
1) Since you're manually instantiating that component and it doesn't belong to your main app's component tree, the store won't be automatically injected into it from your root component. You'll have to manually provide the store to the constructor when you instantiate the component ..
import ProjectRow from "./ProjectRow.vue";
import Vue from "vue";
import store from "../store";
let ProjectRowClass = Vue.extend(ProjectRow);
let ProjectRowInstance = new ProjectRowClass({ store });
2) In a Vue Single File Component (SFC), outside of the default export this doesn't refer to the Vue instance, so you don't have access to $refs or any other Vue instance property/method. To gain access to the Vue instance you'll need to move this line this.$refs.container.appendChild(instance.$el) somewhere inside the default export, for example in the mounted hook or inside one of your methods.
See this CodeSandbox for an example of how you may go about this.
This is another way to instantiate a component in Vue.js, you can use two different root elements.
// Instantiate you main app
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
//
// Then instantiate your component dynamically
//
// Create a component or import it.
const Hello = {
props: ['text'],
template: '<div class="hello">{{ text }}</div>',
};
// Create a componentClass by Vue.
const HelloCtor = Vue.extend(Hello);
// Use componentClass to instantiate your component.
const vm = new HelloCtor({
propsData: {
text: 'HI :)'
}
})
// then mount it to an element.
.$mount('#mount');
It works by assigning "this" to the property "parent". By setting the parent you also have access to the $store in the new instance. (Provided that "this" is another Vue instance/Component and already has access to the store, of course)
new (Vue.extend(YourNewComponent))({
parent: this,
propsData: {
whatever: 'some value',
},
}).$mount(el.querySelector('.some-id'))
If you don't need the reference to the parent, you can just leave "parent: this," out.
Important note: When mounting many (like 500+) items on the page this way you will get a huge performance hit. It is better to only give the new Component the necessary stuff via props instead of giving it the entire "this" object.
I went down this path, following all the examples above, and even this one: https://css-tricks.com/creating-vue-js-component-instances-programmatically/
While I got far, and it works (I made a lot of components this way), at least for my case, it came with drawbacks. For example I'm using Vuetify at the same time, and the dynamically added components didn't belong to the outer form, which meant that while local (per component) validation worked, the form didn't receive the overall status. Another thing that did not work was to disable the form. With more work, passing the form as parent property, some of that got working, but what about removing components. That didn't go well. While they were invisible, they were not really removed (memory leak).
So I changed to use render functions. It is actually much easier, well documented (both Vue 2 and Vue 3), and everything just works. I also had good help from this project: https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/
Basically, to add a function dynamically, just implement the render() function instead of using a template. Works a bit like React. You can implement any logic in here to choose the tag, the options, everything. Just return that, and Vue will build the shadow-DOM and keep the real DOM up to date.
The methods in here seems to manipulate the DOM directly, which I'm glad I no longer have to do.

Access the component name from a Vue directive

I want to create a custom Vue directive that lets me select components on my page which I want to hydrate. In other words, this is what I want to archive
I render my Vue app on the server (ssr)
I attach a directive to some components, like this:
<template>
<div v-hydrate #click="do-something"> I will be hydrated</div>
</template>
I send my code to the client and only those components that have the v-hydrate property will be hydrated (as root elements) on the client.
I want to achieve this roughly this way:
I will create a directives that marks and remembers components:
import Vue from "vue";
Vue.directive("hydrate", {
inserted: function(el, binding, vnode) {
el.setAttribute("data-hydration-component", vnode.component.name);
}
});
My idea is that in my inserted method write a data-attribute to the server-rendered element that I can read out in the client and then hydrate my component with.
Now I have 2 questions:
Is that a feasible approach
How do I get the component name in el.setAttribute? vnode.component.name is just dummy code and does not exist this way.
PS: If you want to know why I only want to hydrate parts of my website: It's ads. They mess with the DOM which breaks Vue.
I could figure it out:
import Vue from "vue";
Vue.directive("hydrate", {
inserted: function(el, binding, vnode) {
console.log(vnode.context.$options.name); // the component's name
}
});
I couldn't get the name of my single file components using the previously posted solution, so I had a look at the source code of vue devtools that always manages to find the name. Here's how they do it:
export function getComponentName (options) {
const name = options.name || options._componentTag
if (name) {
return name
}
const file = options.__file // injected by vue-loader
if (file) {
return classify(basename(file, '.vue'))
}
}
where options === $vm.$options

VUE.JS - Using the vNode argument to add unknown quantity of items to a dynamic navigation

I'm currently trying to create a component which manages several linked dropdowns and elements on a page. In addition, this element supplies a rather fancy navigation element, containing anchor links which automatically scroll to the desired element in this component.
The problem is that the actual contents of the component are completely dynamic and partially determined by the content manager in the CMS. There are several sub components that are always present, but apart from that the content manager can add any number of sections, (using various named and an unnamed ) and each of these sections should be added to the navigation of the component.
I see 3 options:
For every component added, it's title and unique id is added to a property array on the parent component. This, however, is a bit prone to errors. (Unfortunately I will have no control over the eventual backend implementation, so I'm trying to create a foolproof system to avoid to much wasted time.)
Due to other components, I'm already using Vuex to manage most of the data in the app. I figured I use a simple directive, to be added on each item in the parent component. This directive is responsible for adding that element to the Vuex store. The parent component simply reads the contents of the store and generates the navigation based on this.
The problem here is that, as far as I can tell, I have to use the vNode argument in the bind hook of my directive to access the Vuex store. This seems... hacky. Is there anything wrong with this approach?
In the mounted hook of my parent component, I traverse the DOM, searching for elements with a particular data-property, and add a link to each of these elements to my navigation. Seems prone to failure and fragile.
What is the preferred approach for this situation?
As a follow up question - What is the correct use case for the vNode argument in a vue directive? The documentation seems rather sparse on this subject.
I would steer away from using a directive in this case. In Vue 2, the primary use case for directives is for low level DOM manipulation.
Note that in Vue 2.0, the primary form of code reuse and abstraction
is components - however there may be cases where you just need some
low-level DOM access on plain elements, and this is where custom
directives would still be useful.
Instead I would suggest a mixin approach, where your mixin essentially registers your components that should be included in navigation with Vuex.
Consider the following code.
const NavMixin = {
computed:{
navElement(){
return this.$el
},
title(){
return this.$vnode.tag.split("-").pop()
}
},
mounted(){
this.$store.commit('addLink', {
element: this.navElement,
title: this.title
})
}
}
This mixin defines a couple of computed values that determine the element that should be used for navigation and the title of the component. Obviously the title is a placeholder and you should modify it to suit your needs. The mounted hook registers the component with Vue. Should a component need a custom title or navElement, mixed in computed properties are overridden by the component's definition.
Next I define my components and use the mixin.
Vue.component("child1",{
mixins:[NavMixin],
template:`<h1>I am child1</h1>`
})
Vue.component("child2",{
mixins:[NavMixin],
template:`<h1>I am child2</h1>`
})
Vue.component("child3",{
template:`<h1>I am child3</h1>`
})
Note that here I am not adding the mixin to the third component, because I could conceive of a situation where you may not want all components included in navigation.
Here is a quick example of usage.
console.clear()
const store = new Vuex.Store({
state: {
links: []
},
mutations: {
addLink (state, link) {
state.links.push(link)
}
}
})
const NavMixin = {
computed:{
navElement(){
return this.$el
},
title(){
return this.$vnode.tag.split("-").pop()
}
},
mounted(){
this.$store.commit('addLink', {
element: this.navElement,
title: this.title
})
}
}
Vue.component("child1",{
mixins:[NavMixin],
template:`<h1>I am child1</h1>`,
})
Vue.component("child2",{
mixins:[NavMixin],
template:`<h1>I am child2</h1>`
})
Vue.component("child3",{
template:`<h1>I am child3</h1>`
})
Vue.component("container",{
template:`
<div>
<button v-for="link in $store.state.links" #click="scroll(link)">{{link.title}}</button>
<slot></slot>
</div>
`,
methods:{
scroll(link){
document.querySelector("body").scrollTop = link.element.offsetTop
}
},
})
new Vue({
el:"#app",
store
})
h1{
height:300px
}
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.js"></script>
<div id="app">
<container>
<child1></child1>
<child3></child3>
<child2></child2>
</container>
</div>
This solution is pretty robust. You do not need to parse anything. You have control over which components are added to the navigation. You handle potentially nested components. You said you don't know which types of components will be added, but you should have control over the definition of the components that will be used, which means its relatively simple to include the mixin.