How to add class to Vue component via $refs - javascript

I need to add class name to some Vue components using their ref names. The ref names are defined in a config file. I would like to do it dynamically, to avoid adding class manually on each Vue component.
I have tried to find each component using $refs and if found, add the class name to the element's class list. The class is added, but it is removed as soon as user interaction begins (e.g. the component is clicked, receives new value etc.)
Here is some sample code I've tried:
beforeCreate() {
let requiredFields = config.requiredFields
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}

You can use this:
this.$refs[field].$el.classList.value = this.$refs[field].$el.classList.value + 'my-class'

the only thing that you need to make sure of is that your config.requiredFields must include the ref name as a string and nothing more or less ... you can achieve that with :
//for each ref you have
for (let ref in this.$refs) {
config.requiredFields.push(ref)
}
// so config.requiredFields will look like this : ['one','two]
here is an example of a working sample :
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('one', {
template: '<p>component number one</p>'
})
Vue.component('two', {
template: '<p>component number two</p>'
})
new Vue({
el: "#app",
beforeCreate() {
let requiredFields = ['one','two'] // config.requiredFields should be like this
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}
})
.my-class {
color : red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<one ref="one" ></one>
<two ref="two" ></two>
</div>

I know this question was posted ages ago, but I was playing around with something similar and came across a much easier way to add a class to $refs.
When we reference this.$refs['some-ref'].$el.classList it becomes a DOMTokenList which has a bunch of methods and properties you can access.
In this instance, to add a class it is as simple as
this.$refs['some-ref'].$el.classList.add('some-class')

You've to make sure classList.value is an array. By default its a string.
methods: {
onClick(ref) {
const activeClass = 'active-submenu'
if (!this.$refs[ref].classList.length) {
this.$refs[ref].classList.value = [activeClass]
} else {
this.$refs[ref].classList.value = ''
}
},
},

this post helped me tremendously. I needed to target an element within a v-for loop and I ended up writing a little method for it (i'm using Quasar/Vue).
hopefully this will save someone else some time.
addStyleToRef: function(referEl, indexp, classToAdd) {
//will add a class to a $ref element (even within a v-for loop)
//supply $ref name (referEl - txt), index within list (indexp - int) & css class name (classToAdd txt)
if ( this.$refs[referEl][indexp].$el.classList.value.includes(classToAdd) ){
console.log('class already added')
} else {
this.$refs[referEl][indexp].$el.classList.value = this.$refs[referEl][indexp].$el.classList.value + ' ' + classToAdd
}
}

let tag = this.$refs[ref-key][0];
$(tag).addClass('d-none');
Simply get the tag with ref let tag = this.$refs[ref-key][0]; then put this tag into jquery object $(tag).addClass('d-none'); class will be added to required tag.

Related

Is there a way to reference a DOM element in Svelte component created using Client-side component API?

I am creating a svelte component in use:action function and I need to get the DOM element from the svelte component. I can't figure out how to bind or just get reference to DOM element in this scenario. One way I thought was to give the element a unique attribute, class or any identifier to use document.querySelect grab it but I feel like there should be way to get the element since I just create it right there. I am looking for api to equivalent of bind:this.
Example code
import ToolTip from '$lib/components/tooltips/Plain.svelte';
export default function (node: HTMLElement) {
const title = node.getAttribute('title');
let tooltipComponent = new ToolTip({
props: {
title,
},
target: node,
});
const tooltip = undefined; // ?? how to get the DOM element here
let arrow = tooltip.querySelector('#tooltip-arrow');
let update = () => {
compute(node, tooltip, arrow);
};
node.addEventListener('mouseenter', update);
node.addEventListener('focus', update);
return {
update() {
},
destroy() {
node.removeEventListener('mouseenter', update);
node.removeEventListener('focus', update);
tooltipComponent.$destroy();
},
};
}
One way I thought to do it
const uniqueId = generateUniqueId();
let tooltipComponent = new ToolTip({
props: {
title,
class: `tooltip-${uniqueId}`,
},
target: node,
});
const tooltip = tooltip.querySelector(`.tooltip-${uniqueId}`);
Alternatively to querying the element a reference inside the component could be set with bind:this and either made accessible via <svelte:options accessors={true}/> or a function REPL
<script>
import Tooltip from './Tooltip.svelte'
function addTooltip(node) {
const title = node.getAttribute('title');
const tooltipComp = new Tooltip({ target: node , props: {title}});
const tooltipElem = tooltipComp.tooltip
tooltipElem.style.background = 'lightblue'
const tooltipElemFn = tooltipComp.getTooltipElem()
tooltipElemFn.style.border = '2px solid purple'
}
</script>
<div use:addTooltip title="tooltip">
I have a tooltip
</div>
<svelte:options accessors={true}/>
<script>
export let title
// export variable with option accessors={true} OR make accessible via function
export let tooltip
export function getTooltipElem() {
return tooltip
}
</script>
<div bind:this={tooltip}>
{title}
</div>
There is no guaranteed way of finding any elements because components do not necessarily create any. Using some unique identifier or the DOM hierarchy can work if you know the structure of the component you create.
Just make sure to look within the target that you mount the component on. So use something like node.querySelector(...).
REPL example

React checkbox doesn't work as expected

I had and idea to ad some object style to React project. That is why I have written that class
export class Tagi {
constructor() {
this.members = [{
tag: "No matter",
color: "Aquamarine",
isMarked: true
}];
this.handleTagMarking = this.handleTagMarking.bind(this);
}
add(x) {
this.members.push(x);
}
static fromTable(table) {
let tags = new Tagi();
let shortened = unique(table);
for (let value of shortened) {
let record = {
tag: value,
color: colors[shortened.indexOf(value)],
isMarked: false
}
tags.add(record)
}
return tags;
}
get getAllTags() {
return this.members;
}
handleTagMarking(event){
let x = event.target.value;
const index = this.members.findIndex((element)=>element.tag==x);
const currentMarkStatus = this.members[index].isMarked;
if (currentMarkStatus) this.UnMarkTag(index); else this.MarkTag(index)
console.log(this.members)
}
The last part thereof is event handler, more about it later.
That class is implemented in Parent component like this
let Taggs =Tagi.fromTable(d);
console.log (Taggs.getMarkedTags);
Please note that this is implemented in its render function. It has nothing to do with its state.
Later in this Parent component I send it as props
render() {
const label = this.props.labels;
const SingleCheckbox =()=>{return(label.map((element)=>
<label key={element.tag} ><input type="checkbox" value={element.tag} checked={element.isMarked} onChange={this.props.fn} />{element.tag}</label>))}
return (
<div className="checkbox">
<SingleCheckbox />
</div>);
The problem is that checkbox doesn't react to checking items properly. What I mean by this is that data is changed ( I send to console the data within evenhandler so that is clear) but it does not check the fields what is expected behaviour. I suspect it happens because in Parent, Taggs are not state-linked (but only suspect) Is that correct or could there be other reason? If so, how could I easily reshape the code keeping its object oriented style?
I have just chcecked my initial idea of being-not-a-state.
That is not correct I guess. Now in constructor Taggs are initiated like this:
Taggs:Tagi.fromTable(props.disciplines.map((d) => d.tags)),
and later in render{return()} are called like this
<CheckBox labels ={this.state.Taggs.getAllTags} fn={this.state.Taggs.handleTagMarking}/>
Does not change anything at all

global update event for Vue.js

In Vue.js, is there a way to register an event if any component updates its data?
My usecase: I am modeling a RPG character via a set of Javascript classes. The TCharacter class has several attributes that can be modified: name, level, HP, magic. While "name" is a simple string, "HP" and "magic" is a custom class TResource which has its own consumption and refill rules.
Instance of the TCharacter class is a source of truth, and I created some Vue components that are views of it.
I created a character component and a resource component in Vue, vaguely like this:
<div class=template id=character>
<input v-model="ch.name">
<resource :attr="ch.magic"></resource>
<resource :attr="ch.hp"></resource>
</div>
<div class="template" id="resource">
you have {{ attr.available }} points
<button #click="attr.consume">X</button>
</div>
<div id="main">
<character :ch="lancelot"></character>
</div>
and the javascript:
class TCharacter {
constructor() {
this.name = "Lancelot"
this.hp = new Resource(20)
this.magic = new Resource(10)
}
}
class TResource {
constructor(limit) {
this.available = limit
this.limit = limit
}
consume() {
if (this.available > 0) this.available--;
}
}
let lancelot = new TCharacter()
Vue.component('character', {
template: '#character',
props: ['ch'],
})
Vue.component('resource', {
template: '#resource',
props: ['attr'],
})
new Vue({
el: "#main",
data() { return { lancelot } }
})
(I'm not sure the code works exactly as written, but hopefully the intent is clear. Something very similar to this is already working for me.)
Now, I'd like to save the character object to localstorage every time the user makes a modification: changes its name, clicks on a button that consumes a point of magic, etc.
So for instance, I want to be notified that the value of ch.name changed because the user typed something into the input box. Or that a magic point was lost because the user clicked a button for that.
I could detect changes to the character component by installing an updated() handler, which notifies me whenever a DOM is modified (viz). However, this won't trigger when the child component resource is modified. I'd need to add a separate updated() handler to all other components. This gets tedious very fast.
I'm imagining something like a global updated() handler that would fire any time any component has registered a change. Or better, a way to specify that update should fire on component's children changes as well.
edit: I have reworded parts of the question to clarify what I'm trying to accomplish.
Some of you already suggested Vuex. But, from what I understood, Vuex enforces being the single source of truth -- I already have a single source of truth. How is Vuex different / better?
You're going to need a serialized version of lancelot to write out. You can do that with a computed. Then you can watch the computed to see when anything changes.
Alternatively, you could watch each individual trait, and write it out as it changes.
class TCharacter {
constructor() {
this.name = "Lancelot"
this.hp = new TResource(20)
this.magic = new TResource(10)
}
}
class TResource {
constructor(limit) {
this.available = limit
this.limit = limit
}
consume() {
if (this.available > 0) this.available--;
}
}
let lancelot = new TCharacter()
Vue.component('character', {
template: '#character',
props: ['ch'],
})
Vue.component('resource', {
template: '#resource',
props: ['attr'],
})
const vm = new Vue({
el: "#main",
data() {
return {
lancelot
}
},
computed: {
serializedLancelot() {
return JSON.stringify(this.lancelot);
}
},
watch: {
serializedLancelot(newValue) {
console.log("Save update:", newValue);
}
}
});
setTimeout(() => {
vm.lancelot.hp.consume();
}, 500);
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="main">
</div>
Am not sure I understand the use case in entirety, but if my assumption is right, you need to update components based on an object's update (updates to properties of an object), for that you could use Vuex . Although am not sure if you are restricted to use an additional library
Here as an example, you could add a state value named character which is an object, something along the lines of
const state = {
character = {};
}
And now you can mutate this using vuex mutations.
commit('set_character', your_new_value)
Now since you said you need to update all or some components based on any mutation to character, use vuex plugins to listen to any mutation to that object, and update the state of the components.
store.subscribe(mutation => {
if (mutation.type === 'set_character') {
// do whatever you want here
}
})
All of the above is just an outline based on what you mentioned, but this is just a starter, you may or may not want to add character into the store's state but simply the properties such as magic or hp.

Is there a way to access a Vue Component's index — or some other unique identifier — from within its template?

If I'm creating, maybe dynamically, n number of components where each needs to refer to a unique index/id/reference from within its own template is there such a thing? I'm not using a for in loop to generate the components, so I don't think I have access to (read: already tried to use…) the $index.
I hacked together (poorly) a very bad illustration of what I'm trying to do, but I suspect there's a prebuilt method.
https://jsfiddle.net/itopizarro/dw070yyL/
Well I made a whole example before I read the part about you don't want to use $index so here's example if it helps: https://jsfiddle.net/dw070yyL/1/
But you CAN use v-ref to give a child component a unique identifier, see http://vuejs.org/api/#v-ref
<bar v-ref:bar-one></bar>
Then in the parent component you can use this.$refs.barOne to reference that child component.
This also can work with lists as seen in the docs:
<bar v-ref:bar-list v-for="item in list"></bar>
Would make this.$refs.barList an array of child components
I'll also say that the way you're doing it isn't too bad, for instance this Wizard component uses a similar method by iterating through this.$children in the ready() function and setting each of their index accordingly:
ready() {
this.countItems = this.$children.length
this.$children.forEach((item, index) => {
item.index = index
})
}
You can use a method to assign the index in the template without using a ready function.
Vue.component('task', {
methods: {
onStartGetIndex() {
this.$index = this.$root.$children.length;
return this.$index;
}
},
template: '<li>{{ onStartGetIndex() }}</li>'
});
Or save the index in an attribute to be referenced more in your template.
Vue.component('task', {
methods: {
onStartGetIndex() {
this.$index = this.$root.$children.length;
return this.$index;
}
},
template: '<li :index="onStartGetIndex()">{{ this.$index }}</li>'
});
The above two only work until a change is made and then the components refresh.
You can add a method that finds the index, and uses the length if it's not found.
Vue.component('task', {
methods: {
getIndex() {
if (this.$root.$children.indexOf(this) == -1) {
this.$index = this.$root.$children.length;
} else {
return this.$root.$children.indexOf(this);
}
},
},
template: '<li>{{ getIndex() }}</li>'
});

ReactJS: access child method

I'm trying to make a simple form system for a project I'm working on, and the idea is simple. I have a FormStore where forms can be registered, components can be registered to those forms and all the data can be retrieved.
I want to make this as automated as possible, so when the <Form> element is mounted, it should search any children for a isFormItem property, and automatically register that input field with some callback to get the values, defined within the input element.
The issue I'm facing is that I don't know how I would access the child component from the higher level component. I don't think there's a way to use refs, since those have to be manually assigned, and that's what I'm trying to avoid.
This is the relevant snippet from my Form.react.jsx. This is where I try to attach each Input field to the form.
componentDidMount() {
const name = this.props.name ||
'form_' + Math.floor(Math.random() * 100000000);
FormStore.registerForm(name);
React.Children.forEach(this.props.children, (child) => {
if (selectn('props.isFormItem', child)) {
// I need to call child.attachToForm somehow...
}
});
};
And these are the relevant parts of Input.react.jsx.
constructor(props) {
this.attachToForm = this.attachToForm.bind(this);
}
attachToForm(formName) {
const name = this.props.name ||
'formItem_' + Math.floor(Math.random() * 100000000);
FormStore.attachToForm(formName, name, () => this.state.value);
}
I have tried accessing the child component method like this:
React.Children.forEach(this.props.children, (child) => {
if (selectn('props.isFormItem', child)) {
child.type.prototype.attachToForm =
child.type.prototype.attachToForm.bind(child);
child.type.prototype.attachToForm(name);
}
});
But calling the prototype doesn't bind the actual object instance anywhere, so that ended up in having a whole bunch of binds everywhere, and in the end it didn't evne work in addition to being terribly messy all around.
Is there some way of doing this? I am just not seeing it... Any help would be appreciated.
You can pass the formName as a prop to each Input component and then attach each item in it‘s own componentDidMount method instead.
Also, the getDefaultProps method seems handy in your case.
Something like:
getDefaultProps() {
return {
name: 'form_' + Math.floor(Math.random() * 100000000)
}
}
render() {
return <Input formName={this.props.name} />
}
And then in the Input component:
getDefaultProps() {
return {
name: 'formItem_' + Math.floor(Math.random() * 100000000)
}
}
componentDidMount() {
FormStore.attachToForm(this.props.formName, this.props.name, () => this.state.value)
}

Categories

Resources